From 37a36cfdef8cdbe3630087c98eedc2547de5dfb7 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 2 Aug 2024 22:51:42 -0700 Subject: [PATCH 1/9] Async nested chat --- autogen/agentchat/conversable_agent.py | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index a088c491082e..1094ee4a7f3d 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -410,6 +410,41 @@ def _summary_from_nested_chats( return True, None res = initiate_chats(chat_to_run) return True, res[-1].summary + + @staticmethod + async def _a_summary_from_nested_chats( + chat_queue: List[Dict[str, Any]], recipient: Agent, messages: Union[str, Callable], sender: Agent, config: Any + ) -> Tuple[bool, str]: + """A simple chat reply function. + This function initiate one or a sequence of chats between the "recipient" and the agents in the + chat_queue. + + It extracts and returns a summary from the nested chat based on the "summary_method" in each chat in chat_queue. + + Returns: + Tuple[bool, str]: A tuple where the first element indicates the completion of the chat, and the second element contains the summary of the last chat if any chats were initiated. + """ + last_msg = messages[-1].get("content") + chat_to_run = [] + for i, c in enumerate(chat_queue): + current_c = c.copy() + if current_c.get("sender") is None: + current_c["sender"] = recipient + message = current_c.get("message") + # If message is not provided in chat_queue, we by default use the last message from the original chat history as the first message in this nested chat (for the first chat in the chat queue). + # NOTE: This setting is prone to change. + if message is None and i == 0: + message = last_msg + if callable(message): + message = message(recipient, messages, sender, config) + # We only run chat that has a valid message. NOTE: This is prone to change dependin on applications. + if message: + current_c["message"] = message + chat_to_run.append(current_c) + if not chat_to_run: + return True, None + res = await a_initiate_chats(chat_to_run) + return True, res[-1].summary def register_nested_chats( self, @@ -457,6 +492,52 @@ def wrapped_reply_func(recipient, messages=None, sender=None, config=None): ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat"), ) + async def a_register_nested_chats( + self, + chat_queue: List[Dict[str, Any]], + trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List], + reply_func_from_nested_chats: Union[str, Callable] = "summary_from_nested_chats", + position: int = 2, + **kwargs, + ) -> None: + """Register a nested chat reply function. + Args: + chat_queue (list): a list of chat objects to be initiated. + trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. + reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. + The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. + Default to "summary_from_nested_chats", which corresponds to a built-in reply function that get summary from the nested chat_queue. + ```python + def reply_func_from_nested_chats( + chat_queue: List[Dict], + recipient: ConversableAgent, + messages: Optional[List[Dict]] = None, + sender: Optional[Agent] = None, + config: Optional[Any] = None, + ) -> Tuple[bool, Union[str, Dict, None]]: + ``` + position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. + kwargs: Ref to `register_reply` for details. + """ + if reply_func_from_nested_chats == "summary_from_nested_chats": + reply_func_from_nested_chats = self._a_summary_from_nested_chats + if not callable(reply_func_from_nested_chats): + raise ValueError("reply_func_from_nested_chats must be a callable") + + async def a_wrapped_reply_func(recipient, messages=None, sender=None, config=None): + return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) + + functools.update_wrapper(a_wrapped_reply_func, reply_func_from_nested_chats) + + self.register_reply( + trigger, + a_wrapped_reply_func, + position, + kwargs.get("config"), + kwargs.get("reset_config"), + ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat", True), + ) + @property def system_message(self) -> str: """Return the system message.""" From 85bddf4d09e7de64be250d642c0c09e5963b829b Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sun, 4 Aug 2024 14:12:55 -0700 Subject: [PATCH 2/9] Add some tests --- .devcontainer/README.md | 2 +- autogen/agentchat/conversable_agent.py | 9 ++- test/agentchat/test_nested_chat.py | 101 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 test/agentchat/test_nested_chat.py diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 8ae045f27d19..6c0cc2533298 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -8,7 +8,7 @@ These configurations can be used with Codespaces and locally. ### base -- **Purpose**: This Dockerfile, i.e., `./Dockerfile`, is designed for basic setups. It includes common Python libraries and essential dependencies required for general usage of AutoGen. +- **Purpose**: This fwfile, i.e., `./fwfile`, is designed for basic setups. It includes common Python libraries and essential dependencies required for general usage of AutoGen. - **Usage**: Ideal for those just starting with AutoGen or for general-purpose applications. - **Building the Image**: Run `docker build -f ./Dockerfile -t autogen_base_img .` in this directory. - **Using with Codespaces**: `Code > Codespaces > Click on +` By default + creates a Codespace on the current branch. diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 1094ee4a7f3d..b43c36139683 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -444,7 +444,8 @@ async def _a_summary_from_nested_chats( if not chat_to_run: return True, None res = await a_initiate_chats(chat_to_run) - return True, res[-1].summary + index_of_last_chat = chat_to_run[-1]["chat_id"] + return True, res[index_of_last_chat].summary def register_nested_chats( self, @@ -492,7 +493,7 @@ def wrapped_reply_func(recipient, messages=None, sender=None, config=None): ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat"), ) - async def a_register_nested_chats( + def a_register_nested_chats( self, chat_queue: List[Dict[str, Any]], trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List], @@ -525,7 +526,7 @@ def reply_func_from_nested_chats( raise ValueError("reply_func_from_nested_chats must be a callable") async def a_wrapped_reply_func(recipient, messages=None, sender=None, config=None): - return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) + return await reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) functools.update_wrapper(a_wrapped_reply_func, reply_func_from_nested_chats) @@ -535,7 +536,7 @@ async def a_wrapped_reply_func(recipient, messages=None, sender=None, config=Non position, kwargs.get("config"), kwargs.get("reset_config"), - ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat", True), + ignore_async_in_sync_chat=True, ) @property diff --git a/test/agentchat/test_nested_chat.py b/test/agentchat/test_nested_chat.py new file mode 100644 index 000000000000..9c398308f54a --- /dev/null +++ b/test/agentchat/test_nested_chat.py @@ -0,0 +1,101 @@ +import pytest +import autogen +from typing import List +from autogen.agentchat.contrib.capabilities.agent_capability import AgentCapability + +class MockAgentReplies(AgentCapability): + def __init__(self, mock_messages: List[str]): + self.mock_messages = mock_messages + self.mock_message_index = 0 + + def add_to_agent(self, agent: autogen.ConversableAgent): + def mock_reply(recipient, messages, sender, config): + if self.mock_message_index < len(self.mock_messages): + reply_msg = self.mock_messages[self.mock_message_index] + self.mock_message_index += 1 + return [True, reply_msg] + else: + raise ValueError(f"No more mock messages available for {sender.name} to reply to {recipient.name}") + agent.register_reply([autogen.Agent, None], mock_reply, position=2) + +def test_sync_nested_chat(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination, + + ) + assistant.register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], + trigger=user + ) + chat_result = user.initiate_chat(assistant, message="Start chat") + assert(len(chat_result.chat_history) == 2) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "FINAL_RESULT"]) + +@pytest.mark.asyncio +async def test_async_nested_chat(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination, + + ) + assistant.a_register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], + trigger=user + ) + chat_result = await user.a_initiate_chat(assistant, message="Start chat") + assert(len(chat_result.chat_history) == 2) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "FINAL_RESULT"]) + +def test_sync_nested_chat_in_group(): + pass + +def test_async_nested_chat_in_group(): + pass + +if __name__ == "__main__": + test_sync_nested_chat() \ No newline at end of file From e9c88535e0986de14a3fe59ae60a45bfdb7da47a Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 09:08:49 -0700 Subject: [PATCH 3/9] Revert --- .devcontainer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 6c0cc2533298..8ae045f27d19 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -8,7 +8,7 @@ These configurations can be used with Codespaces and locally. ### base -- **Purpose**: This fwfile, i.e., `./fwfile`, is designed for basic setups. It includes common Python libraries and essential dependencies required for general usage of AutoGen. +- **Purpose**: This Dockerfile, i.e., `./Dockerfile`, is designed for basic setups. It includes common Python libraries and essential dependencies required for general usage of AutoGen. - **Usage**: Ideal for those just starting with AutoGen or for general-purpose applications. - **Building the Image**: Run `docker build -f ./Dockerfile -t autogen_base_img .` in this directory. - **Using with Codespaces**: `Code > Codespaces > Click on +` By default + creates a Codespace on the current branch. From 2377d3b74f5960a314219c5ad2f4da7be9723424 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 09:44:44 -0700 Subject: [PATCH 4/9] Move tested chat test in test_nested --- test/agentchat/test_nested.py | 181 +++++++++++++++++++++++++++++ test/agentchat/test_nested_chat.py | 101 ---------------- 2 files changed, 181 insertions(+), 101 deletions(-) delete mode 100644 test/agentchat/test_nested_chat.py diff --git a/test/agentchat/test_nested.py b/test/agentchat/test_nested.py index ee8da793fdec..37c886ae3c86 100755 --- a/test/agentchat/test_nested.py +++ b/test/agentchat/test_nested.py @@ -2,16 +2,32 @@ import os import sys +from typing import List import pytest import autogen +from autogen.agentchat.contrib.capabilities.agent_capability import AgentCapability sys.path.append(os.path.join(os.path.dirname(__file__), "..")) sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) from conftest import reason, skip_openai # noqa: E402 from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST # noqa: E402 +class MockAgentReplies(AgentCapability): + def __init__(self, mock_messages: List[str]): + self.mock_messages = mock_messages + self.mock_message_index = 0 + + def add_to_agent(self, agent: autogen.ConversableAgent): + def mock_reply(recipient, messages, sender, config): + if self.mock_message_index < len(self.mock_messages): + reply_msg = self.mock_messages[self.mock_message_index] + self.mock_message_index += 1 + return [True, reply_msg] + else: + raise ValueError(f"No more mock messages available for {sender.name} to reply to {recipient.name}") + agent.register_reply([autogen.Agent, None], mock_reply, position=2) @pytest.mark.skipif(skip_openai, reason=reason) def test_nested(): @@ -141,6 +157,171 @@ def writing_message(recipient, messages, sender, config): ] ) +def test_sync_nested_chat(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination, + + ) + assistant.register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], + trigger=user + ) + chat_result = user.initiate_chat(assistant, message="Start chat") + assert(len(chat_result.chat_history) == 2) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "FINAL_RESULT"]) + +@pytest.mark.asyncio +async def test_async_nested_chat(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination, + + ) + assistant.a_register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], + trigger=user + ) + chat_result = await user.a_initiate_chat(assistant, message="Start chat") + assert(len(chat_result.chat_history) == 2) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "FINAL_RESULT"]) + +def test_sync_nested_chat_in_group(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant_In_Group_1", + ) + MockAgentReplies(["Assistant_In_Group_1 message 1"]).add_to_agent(assistant) + assistant2 = autogen.AssistantAgent( + "Assistant_In_Group_2", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination + ) + group = autogen.GroupChat( + agents=[assistant, assistant2, user], + messages=[], + speaker_selection_method="round_robin", + ) + group_manager = autogen.GroupChatManager(groupchat=group) + assistant2.register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], + trigger=group_manager + ) + + chat_result = user.initiate_chat(group_manager, message="Start chat", summary_method="last_msg") + assert(len(chat_result.chat_history) == 3) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"]) + +@pytest.mark.asyncio +async def test_async_nested_chat_in_group(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) + + assistant = autogen.AssistantAgent( + "Assistant_In_Group_1", + ) + MockAgentReplies(["Assistant_In_Group_1 message 1"]).add_to_agent(assistant) + assistant2 = autogen.AssistantAgent( + "Assistant_In_Group_2", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination + ) + group = autogen.GroupChat( + agents=[assistant, assistant2, user], + messages=[], + speaker_selection_method="round_robin", + ) + group_manager = autogen.GroupChatManager(groupchat=group) + assistant2.a_register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], + trigger=group_manager + ) + + chat_result = await user.a_initiate_chat(group_manager, message="Start chat", summary_method="last_msg") + assert(len(chat_result.chat_history) == 3) + chat_messages = [msg["content"] for msg in chat_result.chat_history] + assert(chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"]) if __name__ == "__main__": test_nested() diff --git a/test/agentchat/test_nested_chat.py b/test/agentchat/test_nested_chat.py deleted file mode 100644 index 9c398308f54a..000000000000 --- a/test/agentchat/test_nested_chat.py +++ /dev/null @@ -1,101 +0,0 @@ -import pytest -import autogen -from typing import List -from autogen.agentchat.contrib.capabilities.agent_capability import AgentCapability - -class MockAgentReplies(AgentCapability): - def __init__(self, mock_messages: List[str]): - self.mock_messages = mock_messages - self.mock_message_index = 0 - - def add_to_agent(self, agent: autogen.ConversableAgent): - def mock_reply(recipient, messages, sender, config): - if self.mock_message_index < len(self.mock_messages): - reply_msg = self.mock_messages[self.mock_message_index] - self.mock_message_index += 1 - return [True, reply_msg] - else: - raise ValueError(f"No more mock messages available for {sender.name} to reply to {recipient.name}") - agent.register_reply([autogen.Agent, None], mock_reply, position=2) - -def test_sync_nested_chat(): - def is_termination(msg): - if isinstance(msg, str) and msg == "FINAL_RESULT": - return True - elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": - return True - return False - inner_assistant = autogen.AssistantAgent( - "Inner-assistant", - is_termination_msg=is_termination, - ) - MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - - inner_assistant_2 = autogen.AssistantAgent( - "Inner-assistant-2", - ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - - assistant = autogen.AssistantAgent( - "Assistant", - ) - user = autogen.UserProxyAgent( - "User", - human_input_mode="NEVER", - is_termination_msg=is_termination, - - ) - assistant.register_nested_chats( - [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], - trigger=user - ) - chat_result = user.initiate_chat(assistant, message="Start chat") - assert(len(chat_result.chat_history) == 2) - chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "FINAL_RESULT"]) - -@pytest.mark.asyncio -async def test_async_nested_chat(): - def is_termination(msg): - if isinstance(msg, str) and msg == "FINAL_RESULT": - return True - elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": - return True - return False - inner_assistant = autogen.AssistantAgent( - "Inner-assistant", - is_termination_msg=is_termination, - ) - MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - - inner_assistant_2 = autogen.AssistantAgent( - "Inner-assistant-2", - ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - - assistant = autogen.AssistantAgent( - "Assistant", - ) - user = autogen.UserProxyAgent( - "User", - human_input_mode="NEVER", - is_termination_msg=is_termination, - - ) - assistant.a_register_nested_chats( - [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], - trigger=user - ) - chat_result = await user.a_initiate_chat(assistant, message="Start chat") - assert(len(chat_result.chat_history) == 2) - chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "FINAL_RESULT"]) - -def test_sync_nested_chat_in_group(): - pass - -def test_async_nested_chat_in_group(): - pass - -if __name__ == "__main__": - test_sync_nested_chat() \ No newline at end of file From 026bd3a4440f25b45ad8e97d4222f8ac9f76aade Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 10:01:47 -0700 Subject: [PATCH 5/9] Run precomit --- autogen/agentchat/conversable_agent.py | 2 +- test/agentchat/test_nested.py | 89 ++++++++++++++------------ 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index b43c36139683..bb2735950f7e 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -410,7 +410,7 @@ def _summary_from_nested_chats( return True, None res = initiate_chats(chat_to_run) return True, res[-1].summary - + @staticmethod async def _a_summary_from_nested_chats( chat_queue: List[Dict[str, Any]], recipient: Agent, messages: Union[str, Callable], sender: Agent, config: Any diff --git a/test/agentchat/test_nested.py b/test/agentchat/test_nested.py index 37c886ae3c86..01bfc9543b2e 100755 --- a/test/agentchat/test_nested.py +++ b/test/agentchat/test_nested.py @@ -14,6 +14,7 @@ from conftest import reason, skip_openai # noqa: E402 from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST # noqa: E402 + class MockAgentReplies(AgentCapability): def __init__(self, mock_messages: List[str]): self.mock_messages = mock_messages @@ -27,8 +28,10 @@ def mock_reply(recipient, messages, sender, config): return [True, reply_msg] else: raise ValueError(f"No more mock messages available for {sender.name} to reply to {recipient.name}") + agent.register_reply([autogen.Agent, None], mock_reply, position=2) + @pytest.mark.skipif(skip_openai, reason=reason) def test_nested(): config_list = autogen.config_list_from_json(env_or_file=OAI_CONFIG_LIST, file_location=KEY_LOC) @@ -157,6 +160,7 @@ def writing_message(recipient, messages, sender, config): ] ) + def test_sync_nested_chat(): def is_termination(msg): if isinstance(msg, str) and msg == "FINAL_RESULT": @@ -164,17 +168,20 @@ def is_termination(msg): elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": return True return False + inner_assistant = autogen.AssistantAgent( "Inner-assistant", is_termination_msg=is_termination, ) MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - + inner_assistant_2 = autogen.AssistantAgent( "Inner-assistant-2", ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent( + inner_assistant_2 + ) + assistant = autogen.AssistantAgent( "Assistant", ) @@ -182,17 +189,16 @@ def is_termination(msg): "User", human_input_mode="NEVER", is_termination_msg=is_termination, - ) assistant.register_nested_chats( - [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], - trigger=user + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], trigger=user ) chat_result = user.initiate_chat(assistant, message="Start chat") - assert(len(chat_result.chat_history) == 2) + assert len(chat_result.chat_history) == 2 chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "FINAL_RESULT"]) - + assert chat_messages == ["Start chat", "FINAL_RESULT"] + + @pytest.mark.asyncio async def test_async_nested_chat(): def is_termination(msg): @@ -201,17 +207,20 @@ def is_termination(msg): elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": return True return False + inner_assistant = autogen.AssistantAgent( "Inner-assistant", is_termination_msg=is_termination, ) MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - + inner_assistant_2 = autogen.AssistantAgent( "Inner-assistant-2", ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent( + inner_assistant_2 + ) + assistant = autogen.AssistantAgent( "Assistant", ) @@ -219,16 +228,16 @@ def is_termination(msg): "User", human_input_mode="NEVER", is_termination_msg=is_termination, - ) assistant.a_register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], - trigger=user + trigger=user, ) chat_result = await user.a_initiate_chat(assistant, message="Start chat") - assert(len(chat_result.chat_history) == 2) + assert len(chat_result.chat_history) == 2 chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "FINAL_RESULT"]) + assert chat_messages == ["Start chat", "FINAL_RESULT"] + def test_sync_nested_chat_in_group(): def is_termination(msg): @@ -237,17 +246,20 @@ def is_termination(msg): elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": return True return False + inner_assistant = autogen.AssistantAgent( "Inner-assistant", is_termination_msg=is_termination, ) MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - + inner_assistant_2 = autogen.AssistantAgent( "Inner-assistant-2", ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent( + inner_assistant_2 + ) + assistant = autogen.AssistantAgent( "Assistant_In_Group_1", ) @@ -255,11 +267,7 @@ def is_termination(msg): assistant2 = autogen.AssistantAgent( "Assistant_In_Group_2", ) - user = autogen.UserProxyAgent( - "User", - human_input_mode="NEVER", - is_termination_msg=is_termination - ) + user = autogen.UserProxyAgent("User", human_input_mode="NEVER", is_termination_msg=is_termination) group = autogen.GroupChat( agents=[assistant, assistant2, user], messages=[], @@ -268,13 +276,14 @@ def is_termination(msg): group_manager = autogen.GroupChatManager(groupchat=group) assistant2.register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], - trigger=group_manager + trigger=group_manager, ) - + chat_result = user.initiate_chat(group_manager, message="Start chat", summary_method="last_msg") - assert(len(chat_result.chat_history) == 3) + assert len(chat_result.chat_history) == 3 chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"]) + assert chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"] + @pytest.mark.asyncio async def test_async_nested_chat_in_group(): @@ -284,17 +293,20 @@ def is_termination(msg): elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": return True return False + inner_assistant = autogen.AssistantAgent( "Inner-assistant", is_termination_msg=is_termination, ) MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) - + inner_assistant_2 = autogen.AssistantAgent( "Inner-assistant-2", ) - MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent(inner_assistant_2) - + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent( + inner_assistant_2 + ) + assistant = autogen.AssistantAgent( "Assistant_In_Group_1", ) @@ -302,11 +314,7 @@ def is_termination(msg): assistant2 = autogen.AssistantAgent( "Assistant_In_Group_2", ) - user = autogen.UserProxyAgent( - "User", - human_input_mode="NEVER", - is_termination_msg=is_termination - ) + user = autogen.UserProxyAgent("User", human_input_mode="NEVER", is_termination_msg=is_termination) group = autogen.GroupChat( agents=[assistant, assistant2, user], messages=[], @@ -315,13 +323,14 @@ def is_termination(msg): group_manager = autogen.GroupChatManager(groupchat=group) assistant2.a_register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], - trigger=group_manager + trigger=group_manager, ) - + chat_result = await user.a_initiate_chat(group_manager, message="Start chat", summary_method="last_msg") - assert(len(chat_result.chat_history) == 3) + assert len(chat_result.chat_history) == 3 chat_messages = [msg["content"] for msg in chat_result.chat_history] - assert(chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"]) + assert chat_messages == ["Start chat", "Assistant_In_Group_1 message 1", "FINAL_RESULT"] + if __name__ == "__main__": test_nested() From acc2b90d8d354a2a2476cd530982ea0dd64ceb14 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 10:56:18 -0700 Subject: [PATCH 6/9] Add validation --- autogen/agentchat/conversable_agent.py | 6 ++++- test/agentchat/test_nested.py | 37 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index bb2735950f7e..8d0a46eb6247 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -503,7 +503,7 @@ def a_register_nested_chats( ) -> None: """Register a nested chat reply function. Args: - chat_queue (list): a list of chat objects to be initiated. + chat_queue (list): a list of chat objects to be initiated. For async messages in the chat_queue, please include a chat_id in the chat object. trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. @@ -520,6 +520,10 @@ def reply_func_from_nested_chats( position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. kwargs: Ref to `register_reply` for details. """ + for chat in chat_queue: + if chat.get("chat_id") is None: + raise ValueError("chat_id is required for async nested chats") + if reply_func_from_nested_chats == "summary_from_nested_chats": reply_func_from_nested_chats = self._a_summary_from_nested_chats if not callable(reply_func_from_nested_chats): diff --git a/test/agentchat/test_nested.py b/test/agentchat/test_nested.py index 01bfc9543b2e..3fdb058647c2 100755 --- a/test/agentchat/test_nested.py +++ b/test/agentchat/test_nested.py @@ -239,6 +239,43 @@ def is_termination(msg): assert chat_messages == ["Start chat", "FINAL_RESULT"] +@pytest.mark.asyncio +async def test_async_nested_chat_chat_id_validation(): + def is_termination(msg): + if isinstance(msg, str) and msg == "FINAL_RESULT": + return True + elif isinstance(msg, dict) and msg.get("content") == "FINAL_RESULT": + return True + return False + + inner_assistant = autogen.AssistantAgent( + "Inner-assistant", + is_termination_msg=is_termination, + ) + MockAgentReplies(["Inner-assistant message 1", "Inner-assistant message 2"]).add_to_agent(inner_assistant) + + inner_assistant_2 = autogen.AssistantAgent( + "Inner-assistant-2", + ) + MockAgentReplies(["Inner-assistant-2 message 1", "Inner-assistant-2 message 2", "FINAL_RESULT"]).add_to_agent( + inner_assistant_2 + ) + + assistant = autogen.AssistantAgent( + "Assistant", + ) + user = autogen.UserProxyAgent( + "User", + human_input_mode="NEVER", + is_termination_msg=is_termination, + ) + with pytest.raises(ValueError, match="chat_id is required for async nested chats"): + assistant.a_register_nested_chats( + [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], + trigger=user, + ) + + def test_sync_nested_chat_in_group(): def is_termination(msg): if isinstance(msg, str) and msg == "FINAL_RESULT": From fea88f55b440160851d542f980795c05613fb881 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 11:09:00 -0700 Subject: [PATCH 7/9] Remove a_register_nested_chats --- autogen/agentchat/conversable_agent.py | 88 +++++++++----------------- test/agentchat/test_nested.py | 9 ++- 2 files changed, 36 insertions(+), 61 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 8d0a46eb6247..fe224bdda7ff 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -453,11 +453,12 @@ def register_nested_chats( trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List], reply_func_from_nested_chats: Union[str, Callable] = "summary_from_nested_chats", position: int = 2, + use_async: Union[bool, None] = None, **kwargs, ) -> None: """Register a nested chat reply function. Args: - chat_queue (list): a list of chat objects to be initiated. + chat_queue (list): a list of chat objects to be initiated. If use_async is messages in the chat_queue, please include a chat_id in the chat object. trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. @@ -472,75 +473,46 @@ def reply_func_from_nested_chats( ) -> Tuple[bool, Union[str, Dict, None]]: ``` position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. + use_async: Uses a_initiate_chats internally to start nested chats. If the original chat is initiated with a_initiate_chats, set this to true so nested chats do not run in sync. kwargs: Ref to `register_reply` for details. """ - if reply_func_from_nested_chats == "summary_from_nested_chats": - reply_func_from_nested_chats = self._summary_from_nested_chats - if not callable(reply_func_from_nested_chats): - raise ValueError("reply_func_from_nested_chats must be a callable") - - def wrapped_reply_func(recipient, messages=None, sender=None, config=None): - return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) - - functools.update_wrapper(wrapped_reply_func, reply_func_from_nested_chats) + if use_async: + for chat in chat_queue: + if chat.get("chat_id") is None: + raise ValueError("chat_id is required for async nested chats") + + if use_async: + if reply_func_from_nested_chats == "summary_from_nested_chats": + reply_func_from_nested_chats = self._a_summary_from_nested_chats + if not callable(reply_func_from_nested_chats) or not inspect.iscoroutinefunction( + reply_func_from_nested_chats + ): + raise ValueError("reply_func_from_nested_chats must be a callable and a coroutine") - self.register_reply( - trigger, - wrapped_reply_func, - position, - kwargs.get("config"), - kwargs.get("reset_config"), - ignore_async_in_sync_chat=kwargs.get("ignore_async_in_sync_chat"), - ) + async def wrapped_reply_func(recipient, messages=None, sender=None, config=None): + return await reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) - def a_register_nested_chats( - self, - chat_queue: List[Dict[str, Any]], - trigger: Union[Type[Agent], str, Agent, Callable[[Agent], bool], List], - reply_func_from_nested_chats: Union[str, Callable] = "summary_from_nested_chats", - position: int = 2, - **kwargs, - ) -> None: - """Register a nested chat reply function. - Args: - chat_queue (list): a list of chat objects to be initiated. For async messages in the chat_queue, please include a chat_id in the chat object. - trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. - reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. - The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. - Default to "summary_from_nested_chats", which corresponds to a built-in reply function that get summary from the nested chat_queue. - ```python - def reply_func_from_nested_chats( - chat_queue: List[Dict], - recipient: ConversableAgent, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - config: Optional[Any] = None, - ) -> Tuple[bool, Union[str, Dict, None]]: - ``` - position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. - kwargs: Ref to `register_reply` for details. - """ - for chat in chat_queue: - if chat.get("chat_id") is None: - raise ValueError("chat_id is required for async nested chats") - - if reply_func_from_nested_chats == "summary_from_nested_chats": - reply_func_from_nested_chats = self._a_summary_from_nested_chats - if not callable(reply_func_from_nested_chats): - raise ValueError("reply_func_from_nested_chats must be a callable") + functools.update_wrapper(wrapped_reply_func, reply_func_from_nested_chats) + else: + if reply_func_from_nested_chats == "summary_from_nested_chats": + reply_func_from_nested_chats = self._summary_from_nested_chats + if not callable(reply_func_from_nested_chats): + raise ValueError("reply_func_from_nested_chats must be a callable") - async def a_wrapped_reply_func(recipient, messages=None, sender=None, config=None): - return await reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) + def wrapped_reply_func(recipient, messages=None, sender=None, config=None): + return reply_func_from_nested_chats(chat_queue, recipient, messages, sender, config) - functools.update_wrapper(a_wrapped_reply_func, reply_func_from_nested_chats) + functools.update_wrapper(wrapped_reply_func, reply_func_from_nested_chats) self.register_reply( trigger, - a_wrapped_reply_func, + wrapped_reply_func, position, kwargs.get("config"), kwargs.get("reset_config"), - ignore_async_in_sync_chat=True, + ignore_async_in_sync_chat=( + not use_async if use_async is not None else kwargs.get("ignore_async_in_sync_chat") + ), ) @property diff --git a/test/agentchat/test_nested.py b/test/agentchat/test_nested.py index 3fdb058647c2..04fc84b5b399 100755 --- a/test/agentchat/test_nested.py +++ b/test/agentchat/test_nested.py @@ -229,9 +229,10 @@ def is_termination(msg): human_input_mode="NEVER", is_termination_msg=is_termination, ) - assistant.a_register_nested_chats( + assistant.register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], trigger=user, + use_async=True, ) chat_result = await user.a_initiate_chat(assistant, message="Start chat") assert len(chat_result.chat_history) == 2 @@ -270,9 +271,10 @@ def is_termination(msg): is_termination_msg=is_termination, ) with pytest.raises(ValueError, match="chat_id is required for async nested chats"): - assistant.a_register_nested_chats( + assistant.register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg"}], trigger=user, + use_async=True, ) @@ -358,9 +360,10 @@ def is_termination(msg): speaker_selection_method="round_robin", ) group_manager = autogen.GroupChatManager(groupchat=group) - assistant2.a_register_nested_chats( + assistant2.register_nested_chats( [{"sender": inner_assistant, "recipient": inner_assistant_2, "summary_method": "last_msg", "chat_id": 1}], trigger=group_manager, + use_async=True, ) chat_result = await user.a_initiate_chat(group_manager, message="Start chat", summary_method="last_msg") From c4ebe406ac45afa3b58ecbbdd8421a0804a34129 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 12:34:27 -0700 Subject: [PATCH 8/9] Update doc --- autogen/agentchat/conversable_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index fe224bdda7ff..cf232697f706 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -458,7 +458,7 @@ def register_nested_chats( ) -> None: """Register a nested chat reply function. Args: - chat_queue (list): a list of chat objects to be initiated. If use_async is messages in the chat_queue, please include a chat_id in the chat object. + chat_queue (list): a list of chat objects to be initiated. If use_async is used, then all messages in chat_queue must have a chat-id associated with them. trigger (Agent class, str, Agent instance, callable, or list): refer to `register_reply` for details. reply_func_from_nested_chats (Callable, str): the reply function for the nested chat. The function takes a chat_queue for nested chat, recipient agent, a list of messages, a sender agent and a config as input and returns a reply message. From cd454e409bd29d15392c3432e41558d0a1e6724f Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 5 Aug 2024 12:35:51 -0700 Subject: [PATCH 9/9] Update doc --- autogen/agentchat/conversable_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index cf232697f706..9649f18b62bf 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -473,7 +473,7 @@ def reply_func_from_nested_chats( ) -> Tuple[bool, Union[str, Dict, None]]: ``` position (int): Ref to `register_reply` for details. Default to 2. It means we first check the termination and human reply, then check the registered nested chat reply. - use_async: Uses a_initiate_chats internally to start nested chats. If the original chat is initiated with a_initiate_chats, set this to true so nested chats do not run in sync. + use_async: Uses a_initiate_chats internally to start nested chats. If the original chat is initiated with a_initiate_chats, you may set this to true so nested chats do not run in sync. kwargs: Ref to `register_reply` for details. """ if use_async: