diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index d952fc35ef41..b503403d0258 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -1126,8 +1126,18 @@ def clear_history(self, recipient: Optional[Agent] = None, nr_messages_to_preser if recipient is None: if nr_messages_to_preserve: for key in self._oai_messages: + nr_messages_to_preserve_internal = nr_messages_to_preserve + # if breaking history between function call and function response, save function call message + # additionally, otherwise openai will return error + first_msg_to_save = self._oai_messages[key][-nr_messages_to_preserve_internal] + if "tool_responses" in first_msg_to_save: + nr_messages_to_preserve_internal += 1 + print( + f"Preserving one more message for {self.name} to not divide history between tool call and " + f"tool response." + ) # Remove messages from history except last `nr_messages_to_preserve` messages. - self._oai_messages[key] = self._oai_messages[key][-nr_messages_to_preserve:] + self._oai_messages[key] = self._oai_messages[key][-nr_messages_to_preserve_internal:] else: self._oai_messages.clear() else: diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index fbb7b88afee1..c9ebe82c32c1 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -596,9 +596,11 @@ def run_chat( if ( groupchat.enable_clear_history and isinstance(reply, dict) + and reply["content"] and "CLEAR HISTORY" in reply["content"].upper() ): - reply["content"] = self.clear_agents_history(reply["content"], groupchat) + reply["content"] = self.clear_agents_history(reply, groupchat) + # The speaker sends the message without requesting a reply speaker.send(reply, self, request_reply=False) message = self.last_message(speaker) @@ -684,7 +686,7 @@ def _raise_exception_on_async_reply_functions(self) -> None: for agent in self._groupchat.agents: agent._raise_exception_on_async_reply_functions() - def clear_agents_history(self, reply: str, groupchat: GroupChat) -> str: + def clear_agents_history(self, reply: dict, groupchat: GroupChat) -> str: """Clears history of messages for all agents or selected one. Can preserve selected number of last messages. That function is called when user manually provide "clear history" phrase in his reply. When "clear history" is provided, the history of messages for all agents is cleared. @@ -696,23 +698,27 @@ def clear_agents_history(self, reply: str, groupchat: GroupChat) -> str: Phrase "clear history" and optional arguments are cut out from the reply before it passed to the chat. Args: - reply (str): Admin reply to analyse. + reply (dict): reply message dict to analyze. groupchat (GroupChat): GroupChat object. """ + reply_content = reply["content"] # Split the reply into words - words = reply.split() + words = reply_content.split() # Find the position of "clear" to determine where to start processing clear_word_index = next(i for i in reversed(range(len(words))) if words[i].upper() == "CLEAR") # Extract potential agent name and steps words_to_check = words[clear_word_index + 2 : clear_word_index + 4] nr_messages_to_preserve = None + nr_messages_to_preserve_provided = False agent_to_memory_clear = None for word in words_to_check: if word.isdigit(): nr_messages_to_preserve = int(word) + nr_messages_to_preserve_provided = True elif word[:-1].isdigit(): # for the case when number of messages is followed by dot or other sign nr_messages_to_preserve = int(word[:-1]) + nr_messages_to_preserve_provided = True else: for agent in groupchat.agents: if agent.name == word: @@ -721,6 +727,12 @@ def clear_agents_history(self, reply: str, groupchat: GroupChat) -> str: elif agent.name == word[:-1]: # for the case when agent name is followed by dot or other sign agent_to_memory_clear = agent break + # preserve last tool call message if clear history called inside of tool response + if "tool_responses" in reply and not nr_messages_to_preserve: + nr_messages_to_preserve = 1 + logger.warning( + "The last tool call message will be saved to prevent errors caused by tool response without tool call." + ) # clear history if agent_to_memory_clear: if nr_messages_to_preserve: @@ -746,7 +758,7 @@ def clear_agents_history(self, reply: str, groupchat: GroupChat) -> str: agent.clear_history(nr_messages_to_preserve=nr_messages_to_preserve) # Reconstruct the reply without the "clear history" command and parameters - skip_words_number = 2 + int(bool(agent_to_memory_clear)) + int(bool(nr_messages_to_preserve)) - reply = " ".join(words[:clear_word_index] + words[clear_word_index + skip_words_number :]) + skip_words_number = 2 + int(bool(agent_to_memory_clear)) + int(nr_messages_to_preserve_provided) + reply_content = " ".join(words[:clear_word_index] + words[clear_word_index + skip_words_number :]) - return reply + return reply_content diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py index 1c16e6778609..d3d07c5b1e1d 100755 --- a/test/agentchat/test_groupchat.py +++ b/test/agentchat/test_groupchat.py @@ -779,6 +779,103 @@ def test_clear_agents_history(): {"content": "How you doing?", "name": "sam", "role": "user"}, ] + # testing saving tool_call message when clear history going to remove it leaving only tool_response message + agent1.reset() + agent2.reset() + agent3.reset() + # we want to broadcast the message only in the preparation. + groupchat = autogen.GroupChat(agents=[agent1, agent2, agent3], messages=[], max_round=1, enable_clear_history=True) + group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=False) + # We want to trigger the broadcast of group chat manager, which requires `request_reply` to be set to True. + agent1.send("dummy message", group_chat_manager, request_reply=True) + agent1.send( + { + "content": None, + "role": "assistant", + "function_call": None, + "tool_calls": [ + {"id": "call_test_id", "function": {"arguments": "", "name": "test_tool"}, "type": "function"} + ], + }, + group_chat_manager, + request_reply=True, + ) + agent1.send( + { + "role": "tool", + "tool_responses": [{"tool_call_id": "call_emulated", "role": "tool", "content": "example tool response"}], + "content": "example tool response", + }, + group_chat_manager, + request_reply=True, + ) + # increase max_round to 3 + groupchat.max_round = 3 + group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=False) + with mock.patch.object(builtins, "input", lambda _: "clear history alice 1. How you doing?"): + agent1.initiate_chat(group_chat_manager, message="hello", clear_history=False) + + agent1_history = list(agent1._oai_messages.values())[0] + assert agent1_history == [ + { + "tool_calls": [ + {"id": "call_test_id", "function": {"arguments": "", "name": "test_tool"}, "type": "function"}, + ], + "content": None, + "role": "assistant", + }, + { + "content": "example tool response", + "tool_responses": [{"tool_call_id": "call_emulated", "role": "tool", "content": "example tool response"}], + "role": "tool", + }, + ] + + # testing clear history called from tool response + agent1.reset() + agent2.reset() + agent3.reset() + agent2 = autogen.ConversableAgent( + "bob", + max_consecutive_auto_reply=10, + human_input_mode="NEVER", + llm_config=False, + default_auto_reply={ + "role": "tool", + "tool_responses": [{"tool_call_id": "call_emulated", "role": "tool", "content": "USER INTERRUPTED"}], + "content": "Clear history. How you doing?", + }, + ) + groupchat = autogen.GroupChat(agents=[agent1, agent2, agent3], messages=[], max_round=1, enable_clear_history=True) + group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=False) + agent1.send("dummy message", group_chat_manager, request_reply=True) + agent1.send( + { + "content": None, + "role": "assistant", + "function_call": None, + "tool_calls": [ + {"id": "call_test_id", "function": {"arguments": "", "name": "test_tool"}, "type": "function"} + ], + }, + group_chat_manager, + request_reply=True, + ) + groupchat.max_round = 2 + group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=False) + + agent1.initiate_chat(group_chat_manager, message="hello") + agent1_history = list(agent1._oai_messages.values())[0] + assert agent1_history == [ + { + "tool_calls": [ + {"id": "call_test_id", "function": {"arguments": "", "name": "test_tool"}, "type": "function"}, + ], + "content": None, + "role": "assistant", + }, + ] + def test_get_agent_by_name(): def agent(name: str) -> autogen.ConversableAgent: