From 7292a44f0b9d0943861df56032f09af8ad0423a8 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Mon, 6 Jan 2025 20:22:31 +0000 Subject: [PATCH 01/11] Add next_agent_selection_msg Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index ecffa40e74..4484027dd2 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -76,6 +76,7 @@ def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, """ agent: Union[AfterWorkOption, ConversableAgent, str, Callable] + next_agent_selection_msg: Optional[Union[str, Callable, UpdateCondition]] = None def __post_init__(self): if isinstance(self.agent, str): From 80e875815ad06eb79cfeef585ef2737d5c9114a1 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Mon, 6 Jan 2025 21:38:20 +0000 Subject: [PATCH 02/11] Updated next_agent_selection_msg types, group chat constants Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 55 ++++++++++++++++++++++-- autogen/agentchat/groupchat.py | 18 +++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index 23dc36ba9d..5a7efa8917 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -19,7 +19,7 @@ from ..agent import Agent from ..chat import ChatResult from ..conversable_agent import __CONTEXT_VARIABLES_PARAM_NAME__, ConversableAgent -from ..groupchat import GroupChat, GroupChatManager +from ..groupchat import __SELECT_SPEAKER_PROMPT_TEMPLATE__, GroupChat, GroupChatManager from ..user_proxy_agent import UserProxyAgent @@ -75,15 +75,30 @@ class AfterWork: agent: The agent to hand off to or the after work option. Can be a ConversableAgent, a string name of a ConversableAgent, an AfterWorkOption, or a Callable. The Callable signature is: def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, ConversableAgent, str]: + next_agent_selection_msg: Optional[Union[str, Callable, UpdateCondition]]: Optional message to use for the agent selection (in internal group chat), only valid for when agent is AfterWorkOption.SWARM_MANAGER. + If an UpdateCondition, that will take a string or a Callable, see the UpdateCondition class for more information. """ agent: Union[AfterWorkOption, ConversableAgent, str, Callable] - next_agent_selection_msg: Optional[Union[str, Callable, UpdateCondition]] = None + next_agent_selection_msg: Optional[Union[str, UpdateCondition]] = None def __post_init__(self): if isinstance(self.agent, str): self.agent = AfterWorkOption(self.agent.upper()) + # next_agent_selection_msg is only valid for when agent is AfterWorkOption.SWARM_MANAGER, but isn't mandatory + if self.next_agent_selection_msg is not None: + + if not isinstance(self.next_agent_selection_msg, (str, UpdateCondition)): + raise ValueError("next_agent_selection_msg must be a string or an UpdateCondition") + + if self.agent != AfterWorkOption.SWARM_MANAGER: + warnings.warn( + "next_agent_selection_msg is only valid for agent=AfterWorkOption.SWARM_MANAGER. Ignoring the value.", + UserWarning, + ) + self.next_agent_selection_msg = None + class AFTER_WORK(AfterWork): """Deprecated: Use AfterWork instead. This class will be removed in a future version (TBD).""" @@ -125,7 +140,7 @@ def __post_init__(self): if isinstance(self.condition, str): assert self.condition.strip(), "'condition' must be a non-empty string" else: - assert isinstance(self.condition, UpdateCondition), "'condition' must be a string or UpdateOnConditionStr" + assert isinstance(self.condition, UpdateCondition), "'condition' must be a string or UpdateOnCondition" if self.available is not None: assert isinstance(self.available, (Callable, str)), "'available' must be a callable or a string" @@ -155,6 +170,7 @@ def _swarm_agent_str(self: ConversableAgent) -> str: return f"Swarm agent --> {self.name}" agent._swarm_after_work = None + agent._swarm_after_work_selection_msg = None # Store nested chats hand offs as we'll establish these in the initiate_swarm_chat # List of Dictionaries containing the nested_chats and condition @@ -330,6 +346,31 @@ def _cleanup_temp_user_messages(chat_result: ChatResult) -> None: del message["name"] +def _update_groupchat_selection_message( + groupchat: GroupChat, + context_variables: dict[str, Any], + after_work_next_agent_selection_msg: Optional[Union[str, UpdateCondition]], +) -> None: + """Update or restore the groupchat speaker selection message (for 'auto' selection) + + Args: + groupchat: GroupChat instance. + after_work_next_agent_selection_msg: Optional message to use for the agent selection (in internal group chat). + """ + if after_work_next_agent_selection_msg is None: + # If there's no selection message, restore the default + groupchat.select_speaker_prompt_template = __SELECT_SPEAKER_PROMPT_TEMPLATE__ + else: + if isinstance(after_work_next_agent_selection_msg, str): + groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg + elif isinstance(after_work_next_agent_selection_msg, UpdateCondition): + groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( + template=after_work_next_agent_selection_msg.update_function, + context=context_variables, + allow_format_str_template=True, + ) + + def _determine_next_agent( last_speaker: ConversableAgent, groupchat: GroupChat, @@ -387,11 +428,14 @@ def _determine_next_agent( if (user_agent and last_speaker == user_agent) or groupchat.messages[-1]["role"] == "tool": return last_swarm_speaker + after_work_next_agent_selection_msg = None + # Resolve after_work condition (agent-level overrides global) after_work_condition = ( last_swarm_speaker._swarm_after_work if last_swarm_speaker._swarm_after_work is not None else swarm_after_work ) if isinstance(after_work_condition, AfterWork): + after_work_next_agent_selection_msg = after_work_condition.next_agent_selection_msg after_work_condition = after_work_condition.agent # Evaluate callable after_work @@ -413,6 +457,10 @@ def _determine_next_agent( elif after_work_condition == AfterWorkOption.STAY: return last_speaker elif after_work_condition == AfterWorkOption.SWARM_MANAGER: + groupchat.select_speaker_auto_llm_config = last_speaker.llm_config + _update_groupchat_selection_message( + groupchat, last_speaker._context_variables, after_work_next_agent_selection_msg + ) return "auto" else: raise ValueError("Invalid After Work condition or return value from callable") @@ -681,6 +729,7 @@ def transfer_to_agent_name() -> ConversableAgent: transit.agent, (AfterWorkOption, ConversableAgent, str, Callable) ), "Invalid After Work value" agent._swarm_after_work = transit + agent._swarm_after_work_selection_msg = transit.next_agent_selection_msg elif isinstance(transit, OnCondition): if isinstance(transit.target, ConversableAgent): diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 02c1203d70..f729af6bfa 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -33,6 +33,15 @@ logger = logging.getLogger(__name__) +__SELECT_SPEAKER_MESSAGE_TEMPLATE__ = """You are in a role play game. The following roles are available: +{roles}. +Read the following conversation. +Then select the next role from {agentlist} to play. Only return the role.""" + +__SELECT_SPEAKER_PROMPT_TEMPLATE__ = ( + "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." +) + @dataclass class GroupChat: @@ -130,13 +139,8 @@ def custom_speaker_selection_func( speaker_transitions_type: Literal["allowed", "disallowed", None] = None enable_clear_history: bool = False send_introductions: bool = False - select_speaker_message_template: str = """You are in a role play game. The following roles are available: - {roles}. - Read the following conversation. - Then select the next role from {agentlist} to play. Only return the role.""" - select_speaker_prompt_template: str = ( - "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." - ) + select_speaker_message_template: str = __SELECT_SPEAKER_MESSAGE_TEMPLATE__ + select_speaker_prompt_template: str = __SELECT_SPEAKER_PROMPT_TEMPLATE__ select_speaker_auto_multiple_template: str = """You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules: 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name 2. If it refers to the "next" speaker name, choose that name From a088fa0ee36307abc07cb34b430054b018e07020 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Mon, 6 Jan 2025 22:43:56 +0000 Subject: [PATCH 03/11] Prepare group chat for auto speaker selection Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 50 ++++++++++++++++++------ autogen/agentchat/groupchat.py | 2 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index 5a7efa8917..00e0045f14 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -346,17 +346,29 @@ def _cleanup_temp_user_messages(chat_result: ChatResult) -> None: del message["name"] -def _update_groupchat_selection_message( +def _prepare_groupchat_auto_speaker( groupchat: GroupChat, - context_variables: dict[str, Any], + last_swarm_agent: ConversableAgent, after_work_next_agent_selection_msg: Optional[Union[str, UpdateCondition]], ) -> None: - """Update or restore the groupchat speaker selection message (for 'auto' selection) + """Prepare the group chat for auto speaker selection, includes updating or restore the groupchat speaker selection message + and setting the LLM Config to use. + + Tool Executor and Nested Chat agents will be removed from the available agents list. Args: groupchat: GroupChat instance. + last_swarm_agent: The last swarm agent for which the LLM config is used after_work_next_agent_selection_msg: Optional message to use for the agent selection (in internal group chat). """ + + # LLM Config + if last_swarm_agent.llm_config is None or "config_list" not in last_swarm_agent.llm_config: + raise ValueError("LLM Config must be set for the from agent to use the SWARM_MANAGER after work option") + + groupchat.select_speaker_auto_llm_config = {"config_list": last_swarm_agent.llm_config["config_list"]} + + # Prompt template if after_work_next_agent_selection_msg is None: # If there's no selection message, restore the default groupchat.select_speaker_prompt_template = __SELECT_SPEAKER_PROMPT_TEMPLATE__ @@ -364,11 +376,28 @@ def _update_groupchat_selection_message( if isinstance(after_work_next_agent_selection_msg, str): groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg elif isinstance(after_work_next_agent_selection_msg, UpdateCondition): - groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( - template=after_work_next_agent_selection_msg.update_function, - context=context_variables, - allow_format_str_template=True, - ) + groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg.update_function + + # Run through group chat's string substitution first for {agentlist} + # We need to do this so that the next substitution doesn't fail with agentlist + # and we can remove the tool executor and nested chats from the available agents list + agent_list = [ + agent + for agent in groupchat.agents + if agent.name != __TOOL_EXECUTOR_NAME__ and not agent.name.startswith("nested_chat_") + ] + + _working_string = groupchat.select_speaker_prompt(agent_list) + + # Then substitute context variables + groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( + template=_working_string, + context=last_swarm_agent._context_variables, + allow_format_str_template=True, + ) + + # Temporary visibility into speaker selection + groupchat.select_speaker_auto_verbose = True def _determine_next_agent( @@ -457,10 +486,7 @@ def _determine_next_agent( elif after_work_condition == AfterWorkOption.STAY: return last_speaker elif after_work_condition == AfterWorkOption.SWARM_MANAGER: - groupchat.select_speaker_auto_llm_config = last_speaker.llm_config - _update_groupchat_selection_message( - groupchat, last_speaker._context_variables, after_work_next_agent_selection_msg - ) + _prepare_groupchat_auto_speaker(groupchat, last_swarm_speaker, after_work_next_agent_selection_msg) return "auto" else: raise ValueError("Invalid After Work condition or return value from callable") diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index f729af6bfa..bede7e2496 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -379,7 +379,7 @@ def select_speaker_prompt(self, agents: Optional[list[Agent]] = None) -> str: agentlist = f"{[agent.name for agent in agents]}" - return_prompt = self.select_speaker_prompt_template.format(agentlist=agentlist) + return_prompt = f"{self.select_speaker_prompt_template}".replace("{agentlist}", agentlist) return return_prompt def introductions_msg(self, agents: Optional[list[Agent]] = None) -> str: From cd8840dcef5ca48fdf96ed97d12401c581ffe1c4 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Mon, 6 Jan 2025 23:26:50 +0000 Subject: [PATCH 04/11] Documentation Signed-off-by: Mark Sze --- website/docs/topics/swarm.ipynb | 54 ++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index 60d7905ba0..1dacfe6727 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -177,6 +177,7 @@ "- `TERMINATE`: Terminate the chat \n", "- `STAY`: Stay at the current agent \n", "- `REVERT_TO_USER`: Revert to the user agent. Only if a user agent is passed in when initializing. (See below for more details)\n", + "- `SWARM_MANAGER`: Use the internal group chat's `auto` speaker selection method (see next section for more details)\n", "\n", "The callable function signature is:\n", "`def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, ConversableAgent, str]:`\n", @@ -210,9 +211,60 @@ " ...\n", " after_work=AfterWorkOption.TERMINATE # Or an agent or Callable\n", ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### AfterWorkOption.SWARM_MANAGER\n", + "\n", + "When using the AfterWorkOption.SWARM_MANAGER for an AfterWork, the underlying group chat is used to select the next speaker through the `auto` speaker selection mode, which involves using an LLM with a prompt and the swarm messages.\n", + "\n", + "There is a default prompt that is used for this auto speaker selection within a group chat, however, you can set the prompt through the `next_agent_selection_msg` AfterWork parameter. This parameter takes a string or a `Callable`, both allowing you to incorporate context variables into the prompt.\n", + "\n", + "The callable function signature is:\n", + "`def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str`\n", + "\n", + "The string provided, or returned from the `Callable`, will have the following string substitutions:\n", + "- `{agentlist}` will be replaced by a comma-delimited list of agents\n", + "- context variable keys will be replaced by their values\n", + "\n", + "Here's a partial code example of using the AfterWork `next_agent_selection_msg` parameter.\n", + "```python\n", + "context_variables = {\n", + " \"customer_status\": \"Active\"\n", + "}\n", + "\n", + "def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str:\n", + " return \"Review the above messages and return the name of the next agent from this list {agentlist}. The customer's status is: {customer_status}.\"\n", "\n", + "# Use the default `auto` speaker selection prompt\n", + "register_hand_off(agent=agent_2, hand_to=[AfterWork(AfterWorkOption.SWARM_MANAGER)])\n", + "\n", + "# Use a provided selection prompt\n", + "register_hand_off(\n", + " agent=agent_2,\n", + " hand_to=[\n", + " AfterWork(\n", + " agent=AfterWorkOption.SWARM_MANAGER,\n", + " next_agent_selection_msg=\"Review the above messages and return the name of the next agent from this list {agentlist}. The customer's status is: {customer_status}.\")\n", + " ]\n", + ")\n", + "\n", + "# Same as the previous example but using a Callable\n", + "register_hand_off(\n", + " agent=agent_2,\n", + " hand_to=[\n", + " AfterWork(\n", + " agent=AfterWorkOption.SWARM_MANAGER,\n", + " next_agent_selection_msg=my_selection_message)\n", + " ]\n", + ")\n", "```\n", - "```" + "\n", + "**Important:** As this requires the use of an LLM, the LLM configuration of the agent the hand off is registered with." ] }, { From b7bfe700a9a092d75f68d97b56d1b929875cc2e4 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 00:26:38 +0000 Subject: [PATCH 05/11] Add swarm_manager_args Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 71 +++++++++++++++++------- website/docs/topics/swarm.ipynb | 6 +- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index 00e0045f14..8bcd43caf2 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -75,12 +75,14 @@ class AfterWork: agent: The agent to hand off to or the after work option. Can be a ConversableAgent, a string name of a ConversableAgent, an AfterWorkOption, or a Callable. The Callable signature is: def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, ConversableAgent, str]: - next_agent_selection_msg: Optional[Union[str, Callable, UpdateCondition]]: Optional message to use for the agent selection (in internal group chat), only valid for when agent is AfterWorkOption.SWARM_MANAGER. - If an UpdateCondition, that will take a string or a Callable, see the UpdateCondition class for more information. + next_agent_selection_msg: Optional[Union[str, Callable]]: Optional message to use for the agent selection (in internal group chat), only valid for when agent is AfterWorkOption.SWARM_MANAGER. + If a string, it will be used as a template and substitute the context variables. + If a Callable, it should have the signature: + def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str """ agent: Union[AfterWorkOption, ConversableAgent, str, Callable] - next_agent_selection_msg: Optional[Union[str, UpdateCondition]] = None + next_agent_selection_msg: Optional[Union[str, Callable]] = None def __post_init__(self): if isinstance(self.agent, str): @@ -89,8 +91,8 @@ def __post_init__(self): # next_agent_selection_msg is only valid for when agent is AfterWorkOption.SWARM_MANAGER, but isn't mandatory if self.next_agent_selection_msg is not None: - if not isinstance(self.next_agent_selection_msg, (str, UpdateCondition)): - raise ValueError("next_agent_selection_msg must be a string or an UpdateCondition") + if not isinstance(self.next_agent_selection_msg, (str, Callable)): + raise ValueError("next_agent_selection_msg must be a string or a Callable") if self.agent != AfterWorkOption.SWARM_MANAGER: warnings.warn( @@ -349,10 +351,9 @@ def _cleanup_temp_user_messages(chat_result: ChatResult) -> None: def _prepare_groupchat_auto_speaker( groupchat: GroupChat, last_swarm_agent: ConversableAgent, - after_work_next_agent_selection_msg: Optional[Union[str, UpdateCondition]], + after_work_next_agent_selection_msg: Optional[Union[str, Callable]], ) -> None: - """Prepare the group chat for auto speaker selection, includes updating or restore the groupchat speaker selection message - and setting the LLM Config to use. + """Prepare the group chat for auto speaker selection, includes updating or restore the groupchat speaker selection message. Tool Executor and Nested Chat agents will be removed from the available agents list. @@ -361,22 +362,16 @@ def _prepare_groupchat_auto_speaker( last_swarm_agent: The last swarm agent for which the LLM config is used after_work_next_agent_selection_msg: Optional message to use for the agent selection (in internal group chat). """ - - # LLM Config - if last_swarm_agent.llm_config is None or "config_list" not in last_swarm_agent.llm_config: - raise ValueError("LLM Config must be set for the from agent to use the SWARM_MANAGER after work option") - - groupchat.select_speaker_auto_llm_config = {"config_list": last_swarm_agent.llm_config["config_list"]} - - # Prompt template if after_work_next_agent_selection_msg is None: # If there's no selection message, restore the default groupchat.select_speaker_prompt_template = __SELECT_SPEAKER_PROMPT_TEMPLATE__ else: if isinstance(after_work_next_agent_selection_msg, str): groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg - elif isinstance(after_work_next_agent_selection_msg, UpdateCondition): - groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg.update_function + elif isinstance(after_work_next_agent_selection_msg, Callable): + groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg( + last_swarm_agent, groupchat.messages + ) # Run through group chat's string substitution first for {agentlist} # We need to do this so that the next substitution doesn't fail with agentlist @@ -532,11 +527,44 @@ def swarm_transition(last_speaker: ConversableAgent, groupchat: GroupChat) -> Op return swarm_transition +def _create_swarm_manager( + groupchat: GroupChat, swarm_manager_args: dict[str, Any], agents: list[ConversableAgent] +) -> GroupChatManager: + """Create a GroupChatManager for the swarm chat utilising any arguments passed in and ensure an LLM Config exists if needed + + Args: + groupchat (GroupChat): Swarm groupchat. + swarm_manager_args (dict[str, Any]): Swarm manager arguments to create the GroupChatManager. + + Returns: + GroupChatManager: GroupChatManager instance. + """ + manager_args = (swarm_manager_args or {}).copy() + if "groupchat" in manager_args: + raise ValueError("'groupchat' cannot be specified in swarm_manager_args as it is set by initiate_swarm_chat") + manager = GroupChatManager(groupchat, **manager_args) + + # Ensure that our manager has an LLM Config if we have any AfterWorkOption.SWARM_MANAGER after works + if manager.llm_config is False: + for agent in agents: + if ( + agent._swarm_after_work + and isinstance(agent._swarm_after_work.agent, AfterWorkOption) + and agent._swarm_after_work.agent == AfterWorkOption.SWARM_MANAGER + ): + raise ValueError( + "The swarm manager doesn't have an LLM Config and it is required for AfterWorkOption.SWARM_MANAGER. Use the swarm_manager_args to specify the LLM Config for the swarm manager." + ) + + return manager + + def initiate_swarm_chat( initial_agent: ConversableAgent, messages: Union[list[dict[str, Any]], str], agents: list[ConversableAgent], user_agent: Optional[UserProxyAgent] = None, + swarm_manager_args: Optional[dict[str, Any]] = None, max_rounds: int = 20, context_variables: Optional[dict[str, Any]] = None, after_work: Optional[Union[AfterWorkOption, Callable]] = AfterWork(AfterWorkOption.TERMINATE), @@ -548,6 +576,7 @@ def initiate_swarm_chat( messages: Initial message(s). agents: List of swarm agents. user_agent: Optional user proxy agent for falling back to. + swarm_manager_args: Optional group chat manager arguments used to establish the swarm's groupchat manager, required when AfterWorkOption.SWARM_MANAGER is used. max_rounds: Maximum number of conversation rounds. context_variables: Starting context variables. after_work: Method to handle conversation continuation when an agent doesn't select the next agent. If no agent is selected and no tool calls are output, we will use this method to determine the next agent. @@ -588,7 +617,7 @@ def custom_afterwork_func(last_speaker: ConversableAgent, messages: List[Dict[st speaker_selection_method=swarm_transition, ) - manager = GroupChatManager(groupchat) + manager = _create_swarm_manager(groupchat, swarm_manager_args, agents) # Point all ConversableAgent's context variables to this function's context_variables _setup_context_variables(tool_execution, agents, manager, context_variables or {}) @@ -616,6 +645,7 @@ async def a_initiate_swarm_chat( messages: Union[list[dict[str, Any]], str], agents: list[ConversableAgent], user_agent: Optional[UserProxyAgent] = None, + swarm_manager_args: Optional[dict[str, Any]] = None, max_rounds: int = 20, context_variables: Optional[dict[str, Any]] = None, after_work: Optional[Union[AfterWorkOption, Callable]] = AfterWork(AfterWorkOption.TERMINATE), @@ -627,6 +657,7 @@ async def a_initiate_swarm_chat( messages: Initial message(s). agents: List of swarm agents. user_agent: Optional user proxy agent for falling back to. + swarm_manager_args: Optional group chat manager arguments used to establish the swarm's groupchat manager, required when AfterWorkOption.SWARM_MANAGER is used. max_rounds: Maximum number of conversation rounds. context_variables: Starting context variables. after_work: Method to handle conversation continuation when an agent doesn't select the next agent. If no agent is selected and no tool calls are output, we will use this method to determine the next agent. @@ -667,7 +698,7 @@ def custom_afterwork_func(last_speaker: ConversableAgent, messages: List[Dict[st speaker_selection_method=swarm_transition, ) - manager = GroupChatManager(groupchat) + manager = _create_swarm_manager(groupchat, swarm_manager_args, agents) # Point all ConversableAgent's context variables to this function's context_variables _setup_context_variables(tool_execution, agents, manager, context_variables or {}) diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index 1dacfe6727..5966031bc4 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -231,6 +231,8 @@ "- `{agentlist}` will be replaced by a comma-delimited list of agents\n", "- context variable keys will be replaced by their values\n", "\n", + "**Important:** The swarm manager's LLM configuration will be used, so be sure to pass in `llm_config` through the `swarm_manager_args` in `initiate_swarm_chat`.\n", + "\n", "Here's a partial code example of using the AfterWork `next_agent_selection_msg` parameter.\n", "```python\n", "context_variables = {\n", @@ -262,9 +264,7 @@ " next_agent_selection_msg=my_selection_message)\n", " ]\n", ")\n", - "```\n", - "\n", - "**Important:** As this requires the use of an LLM, the LLM configuration of the agent the hand off is registered with." + "```" ] }, { From c267c65039aa176ea2a62160bc352afe602e0323 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 00:34:01 +0000 Subject: [PATCH 06/11] Updated initiate_swarm_chat in documentation Signed-off-by: Mark Sze --- website/docs/topics/swarm.ipynb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index 5966031bc4..3661be49a2 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -328,7 +328,10 @@ " agents=[agent_1, agent_2, agent_3], # a list of agents\n", " messages=[{\"role\": \"user\", \"content\": \"Hello\"}], # a list of messages to start the chat, you can also pass in one string\n", " user_agent=user_agent, # optional, if you want to revert to the user agent\n", + " swarm_manager_args={\"llm_config\", llm_config} # optional configuration for underlying GroupChatManager\n", " context_variables={\"key\": \"value\"} # optional, initial context variables\n", + " max_rounds=20, # maximum number of conversation rounds, defaults to 20\n", + " after_work=AfterWorkOption.TERMINATE # optional swarm-level after work, agents revert to this if they do not have one\n", ")\n", "```\n", "\n", From 84cc7495f4c4e2fc02bd87b086d3386cdb6702dc Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 03:03:49 +0000 Subject: [PATCH 07/11] Restore groupchat prompt template Signed-off-by: Mark Sze --- autogen/agentchat/groupchat.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index bede7e2496..ad37df6e5d 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -33,11 +33,6 @@ logger = logging.getLogger(__name__) -__SELECT_SPEAKER_MESSAGE_TEMPLATE__ = """You are in a role play game. The following roles are available: -{roles}. -Read the following conversation. -Then select the next role from {agentlist} to play. Only return the role.""" - __SELECT_SPEAKER_PROMPT_TEMPLATE__ = ( "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." ) @@ -139,7 +134,10 @@ def custom_speaker_selection_func( speaker_transitions_type: Literal["allowed", "disallowed", None] = None enable_clear_history: bool = False send_introductions: bool = False - select_speaker_message_template: str = __SELECT_SPEAKER_MESSAGE_TEMPLATE__ + select_speaker_message_template: str = """You are in a role play game. The following roles are available: + {roles}. + Read the following conversation. + Then select the next role from {agentlist} to play. Only return the role.""" select_speaker_prompt_template: str = __SELECT_SPEAKER_PROMPT_TEMPLATE__ select_speaker_auto_multiple_template: str = """You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules: 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name From 796a6dc2073f62d137e5f08f1fea4c49e014ba1e Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 20:21:23 +0000 Subject: [PATCH 08/11] constant name change, callable updated to be final string, documentation Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 63 ++++++++++++------------ autogen/agentchat/groupchat.py | 4 +- website/docs/topics/swarm.ipynb | 13 +++-- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index 8bcd43caf2..b75fd4b865 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -19,7 +19,7 @@ from ..agent import Agent from ..chat import ChatResult from ..conversable_agent import __CONTEXT_VARIABLES_PARAM_NAME__, ConversableAgent -from ..groupchat import __SELECT_SPEAKER_PROMPT_TEMPLATE__, GroupChat, GroupChatManager +from ..groupchat import SELECT_SPEAKER_PROMPT_TEMPLATE, GroupChat, GroupChatManager from ..user_proxy_agent import UserProxyAgent @@ -142,7 +142,7 @@ def __post_init__(self): if isinstance(self.condition, str): assert self.condition.strip(), "'condition' must be a non-empty string" else: - assert isinstance(self.condition, UpdateCondition), "'condition' must be a string or UpdateOnCondition" + assert isinstance(self.condition, UpdateCondition), "'condition' must be a string or UpdateCondition" if self.available is not None: assert isinstance(self.available, (Callable, str)), "'available' must be a callable or a string" @@ -362,37 +362,38 @@ def _prepare_groupchat_auto_speaker( last_swarm_agent: The last swarm agent for which the LLM config is used after_work_next_agent_selection_msg: Optional message to use for the agent selection (in internal group chat). """ + + def run_select_speaker_prompt_filtered(template: str) -> str: + # Run through group chat's string substitution first for {agentlist} + # We need to do this so that the next substitution doesn't fail with agentlist + # and we can remove the tool executor and nested chats from the available agents list + agent_list = [ + agent + for agent in groupchat.agents + if agent.name != __TOOL_EXECUTOR_NAME__ and not agent.name.startswith("nested_chat_") + ] + + groupchat.select_speaker_prompt_template = template + return groupchat.select_speaker_prompt(agent_list) + if after_work_next_agent_selection_msg is None: - # If there's no selection message, restore the default - groupchat.select_speaker_prompt_template = __SELECT_SPEAKER_PROMPT_TEMPLATE__ - else: - if isinstance(after_work_next_agent_selection_msg, str): - groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg - elif isinstance(after_work_next_agent_selection_msg, Callable): - groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg( - last_swarm_agent, groupchat.messages - ) - - # Run through group chat's string substitution first for {agentlist} - # We need to do this so that the next substitution doesn't fail with agentlist - # and we can remove the tool executor and nested chats from the available agents list - agent_list = [ - agent - for agent in groupchat.agents - if agent.name != __TOOL_EXECUTOR_NAME__ and not agent.name.startswith("nested_chat_") - ] - - _working_string = groupchat.select_speaker_prompt(agent_list) - - # Then substitute context variables - groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( - template=_working_string, - context=last_swarm_agent._context_variables, - allow_format_str_template=True, - ) + # If there's no selection message, restore the default and filter out the tool executor and nested chat agents + groupchat.select_speaker_prompt_template = run_select_speaker_prompt_filtered(SELECT_SPEAKER_PROMPT_TEMPLATE) + elif isinstance(after_work_next_agent_selection_msg, str): + groupchat.select_speaker_prompt_template = run_select_speaker_prompt_filtered( + after_work_next_agent_selection_msg + ) - # Temporary visibility into speaker selection - groupchat.select_speaker_auto_verbose = True + # Substitute context variables + groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( + template=groupchat.select_speaker_prompt_template, + context=last_swarm_agent._context_variables, + allow_format_str_template=True, + ) + elif isinstance(after_work_next_agent_selection_msg, Callable): + groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg( + last_swarm_agent, groupchat.messages + ) def _determine_next_agent( diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index ad37df6e5d..98176fe34b 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) -__SELECT_SPEAKER_PROMPT_TEMPLATE__ = ( +SELECT_SPEAKER_PROMPT_TEMPLATE = ( "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." ) @@ -138,7 +138,7 @@ def custom_speaker_selection_func( {roles}. Read the following conversation. Then select the next role from {agentlist} to play. Only return the role.""" - select_speaker_prompt_template: str = __SELECT_SPEAKER_PROMPT_TEMPLATE__ + select_speaker_prompt_template: str = SELECT_SPEAKER_PROMPT_TEMPLATE select_speaker_auto_multiple_template: str = """You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules: 1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name 2. If it refers to the "next" speaker name, choose that name diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index 3661be49a2..a15b099ae0 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -222,17 +222,19 @@ "\n", "When using the AfterWorkOption.SWARM_MANAGER for an AfterWork, the underlying group chat is used to select the next speaker through the `auto` speaker selection mode, which involves using an LLM with a prompt and the swarm messages.\n", "\n", - "There is a default prompt that is used for this auto speaker selection within a group chat, however, you can set the prompt through the `next_agent_selection_msg` AfterWork parameter. This parameter takes a string or a `Callable`, both allowing you to incorporate context variables into the prompt.\n", + "There is a default prompt that is used for this auto speaker selection within a group chat, however, you can set the prompt through the `next_agent_selection_msg` AfterWork parameter. This parameter takes a string or a `Callable`.\n", "\n", "The callable function signature is:\n", "`def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str`\n", "\n", - "The string provided, or returned from the `Callable`, will have the following string substitutions:\n", + "If using a string you can incorporate context variables into your string, with the following string substitutions occurring:\n", "- `{agentlist}` will be replaced by a comma-delimited list of agents\n", - "- context variable keys will be replaced by their values\n", + "- context variable keys will be replaced by their values, e.g. `{my_context_variable_key}` will be replaced by its respective value.\n", "\n", "**Important:** The swarm manager's LLM configuration will be used, so be sure to pass in `llm_config` through the `swarm_manager_args` in `initiate_swarm_chat`.\n", "\n", + "If not set, the default prompt of \"Read the above conversation. Then select the next role from {agentlist} to play. Only return the role.\" is used.\n", + "\n", "Here's a partial code example of using the AfterWork `next_agent_selection_msg` parameter.\n", "```python\n", "context_variables = {\n", @@ -240,7 +242,8 @@ "}\n", "\n", "def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str:\n", - " return \"Review the above messages and return the name of the next agent from this list {agentlist}. The customer's status is: {customer_status}.\"\n", + " # Returns the final selection message\n", + " return \"Review the above messages and return the name of the next agent from this list ['agent_1', 'agent_3']. The customer's status is: 'Active'.\"\n", "\n", "# Use the default `auto` speaker selection prompt\n", "register_hand_off(agent=agent_2, hand_to=[AfterWork(AfterWorkOption.SWARM_MANAGER)])\n", @@ -255,7 +258,7 @@ " ]\n", ")\n", "\n", - "# Same as the previous example but using a Callable\n", + "# Similar to the previous example but using a Callable\n", "register_hand_off(\n", " agent=agent_2,\n", " hand_to=[\n", From 1bb7236a798070a91efa4627227cdda50e913677 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 23:28:33 +0000 Subject: [PATCH 09/11] Introduce ContextStr, remove UpdateCondition, add documentation Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 123 ++++++++++++----------- website/docs/topics/swarm.ipynb | 38 ++++--- 2 files changed, 88 insertions(+), 73 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index b75fd4b865..c08e978e62 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -24,36 +24,35 @@ @dataclass -class UpdateCondition: - """Update the condition string before they reply +class ContextStr: + """A string that requires context variable substitution. + + Use the format method to substitute context variables into the string. Args: - update_function: The string or function to update the condition string. Can be a string or a Callable. - If a string, it will be used as a template and substitute the context variables. - If a Callable, it should have the signature: - def my_update_function(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str + template: The string to be substituted with context variables. It is expected that the string will contain {var} placeholders + and that string format will be able to replace all values. """ - update_function: Union[Callable, str] + template: str - def __post_init__(self): - if isinstance(self.update_function, str): - assert self.update_function.strip(), " please provide a non-empty string or a callable" - # find all {var} in the string - vars = re.findall(r"\{(\w+)\}", self.update_function) - if len(vars) == 0: - warnings.warn("Update function string contains no variables. This is probably unintended.") - - elif isinstance(self.update_function, Callable): - sig = signature(self.update_function) - if len(sig.parameters) != 2: - raise ValueError( - "Update function must accept two parameters of type ConversableAgent and List[Dict[str, Any]], respectively" - ) - if sig.return_annotation != str: - raise ValueError("Update function must return a string") - else: - raise ValueError("Update function must be either a string or a callable") + def __init__(self, template: str): + self.template = template + + def format(self, context_variables: dict[str, Any]) -> str: + """Substitute context variables into the string. + + Args: + context_variables: The context variables to substitute into the string. + """ + return OpenAIWrapper.instantiate( + template=self.template, + context=context_variables, + allow_format_str_template=True, + ) + + def __str__(self) -> str: + return f"ContextStr, unformatted: {self.template}" # Created tool executor's name @@ -82,7 +81,7 @@ def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]] """ agent: Union[AfterWorkOption, ConversableAgent, str, Callable] - next_agent_selection_msg: Optional[Union[str, Callable]] = None + next_agent_selection_msg: Optional[Union[str, ContextStr, Callable]] = None def __post_init__(self): if isinstance(self.agent, str): @@ -91,8 +90,8 @@ def __post_init__(self): # next_agent_selection_msg is only valid for when agent is AfterWorkOption.SWARM_MANAGER, but isn't mandatory if self.next_agent_selection_msg is not None: - if not isinstance(self.next_agent_selection_msg, (str, Callable)): - raise ValueError("next_agent_selection_msg must be a string or a Callable") + if not isinstance(self.next_agent_selection_msg, (str, ContextStr, Callable)): + raise ValueError("next_agent_selection_msg must be a string, ContextStr, or a Callable") if self.agent != AfterWorkOption.SWARM_MANAGER: warnings.warn( @@ -122,13 +121,18 @@ class OnCondition: target: The agent to hand off to or the nested chat configuration. Can be a ConversableAgent or a Dict. If a Dict, it should follow the convention of the nested chat configuration, with the exception of a carryover configuration which is unique to Swarms. Swarm Nested chat documentation: https://docs.ag2.ai/docs/topics/swarm#registering-handoffs-to-a-nested-chat - condition (str): The condition for transitioning to the target agent, evaluated by the LLM to determine whether to call the underlying function/tool which does the transition. - available (Union[Callable, str]): Optional condition to determine if this OnCondition is available. Can be a Callable or a string. - If a string, it will look up the value of the context variable with that name, which should be a bool. + condition (Union[str, ContextStr, Callable]): The condition for transitioning to the target agent, evaluated by the LLM. + If a string or Callable, no automatic context variable substitution occurs. + If a ContextStr, context variable substitution occurs. + available (Union[Callable, str]): Optional condition to determine if this OnCondition is included for the LLM to evaluate. Can be a Callable or a string. + If a string, it will look up the value of the context variable with that name, which should be a bool, to determine whether it should include this condition. + The Callable signature is: + def my_available_func(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> bool + """ target: Union[ConversableAgent, dict[str, Any]] = None - condition: Union[str, UpdateCondition] = "" + condition: Union[str, ContextStr, Callable] = "" available: Optional[Union[Callable, str]] = None def __post_init__(self): @@ -142,7 +146,9 @@ def __post_init__(self): if isinstance(self.condition, str): assert self.condition.strip(), "'condition' must be a non-empty string" else: - assert isinstance(self.condition, UpdateCondition), "'condition' must be a string or UpdateCondition" + assert isinstance( + self.condition, (ContextStr, Callable) + ), "'condition' must be a string, ContextStr, or callable" if self.available is not None: assert isinstance(self.available, (Callable, str)), "'available' must be a callable or a string" @@ -351,19 +357,23 @@ def _cleanup_temp_user_messages(chat_result: ChatResult) -> None: def _prepare_groupchat_auto_speaker( groupchat: GroupChat, last_swarm_agent: ConversableAgent, - after_work_next_agent_selection_msg: Optional[Union[str, Callable]], + after_work_next_agent_selection_msg: Optional[Union[str, ContextStr, Callable]], ) -> None: """Prepare the group chat for auto speaker selection, includes updating or restore the groupchat speaker selection message. Tool Executor and Nested Chat agents will be removed from the available agents list. Args: - groupchat: GroupChat instance. - last_swarm_agent: The last swarm agent for which the LLM config is used - after_work_next_agent_selection_msg: Optional message to use for the agent selection (in internal group chat). + groupchat (GroupChat): GroupChat instance. + last_swarm_agent (ConversableAgent): The last swarm agent for which the LLM config is used + after_work_next_agent_selection_msg (Union[str, ContextStr, Callable]): Optional message to use for the agent selection (in internal group chat). + if a string, it will be use the string a the prompt template, no context variable substitution however '{agentlist}' will be substituted for a list of agents. + if a ContextStr, it will substitute the agentlist first and then the context variables + if a Callable, it will not substitute the agentlist or context variables, signature: + def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str """ - def run_select_speaker_prompt_filtered(template: str) -> str: + def substitute_agentlist(template: str) -> str: # Run through group chat's string substitution first for {agentlist} # We need to do this so that the next substitution doesn't fail with agentlist # and we can remove the tool executor and nested chats from the available agents list @@ -378,21 +388,21 @@ def run_select_speaker_prompt_filtered(template: str) -> str: if after_work_next_agent_selection_msg is None: # If there's no selection message, restore the default and filter out the tool executor and nested chat agents - groupchat.select_speaker_prompt_template = run_select_speaker_prompt_filtered(SELECT_SPEAKER_PROMPT_TEMPLATE) + groupchat.select_speaker_prompt_template = substitute_agentlist(SELECT_SPEAKER_PROMPT_TEMPLATE) elif isinstance(after_work_next_agent_selection_msg, str): - groupchat.select_speaker_prompt_template = run_select_speaker_prompt_filtered( - after_work_next_agent_selection_msg - ) - - # Substitute context variables - groupchat.select_speaker_prompt_template = OpenAIWrapper.instantiate( - template=groupchat.select_speaker_prompt_template, - context=last_swarm_agent._context_variables, - allow_format_str_template=True, + # No context variable substitution for string, but agentlist will be substituted + groupchat.select_speaker_prompt_template = substitute_agentlist(after_work_next_agent_selection_msg) + elif isinstance(after_work_next_agent_selection_msg, ContextStr): + # Replace the agentlist in the string first, putting it into a new ContextStr + agent_list_replaced_string = ContextStr(substitute_agentlist(after_work_next_agent_selection_msg.template)) + + # Then replace the context variables + groupchat.select_speaker_prompt_template = agent_list_replaced_string.format( + last_swarm_agent._context_variables ) elif isinstance(after_work_next_agent_selection_msg, Callable): - groupchat.select_speaker_prompt_template = after_work_next_agent_selection_msg( - last_swarm_agent, groupchat.messages + groupchat.select_speaker_prompt_template = substitute_agentlist( + after_work_next_agent_selection_msg(last_swarm_agent, groupchat.messages) ) @@ -845,15 +855,10 @@ def _update_conditional_functions(agent: ConversableAgent, messages: Optional[li # then add the function if it is available, so that the function signature is updated if is_available: condition = on_condition.condition - if isinstance(condition, UpdateCondition): - if isinstance(condition.update_function, str): - condition = OpenAIWrapper.instantiate( - template=condition.update_function, - context=agent._context_variables, - allow_format_str_template=True, - ) - else: - condition = condition.update_function(agent, messages) + if isinstance(condition, ContextStr): + condition = condition.format(context_variables=agent._context_variables) + elif isinstance(condition, Callable): + condition = condition(agent, messages) agent._add_single_function(func, func_name, condition) diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index a15b099ae0..acdc19ea25 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -222,18 +222,12 @@ "\n", "When using the AfterWorkOption.SWARM_MANAGER for an AfterWork, the underlying group chat is used to select the next speaker through the `auto` speaker selection mode, which involves using an LLM with a prompt and the swarm messages.\n", "\n", - "There is a default prompt that is used for this auto speaker selection within a group chat, however, you can set the prompt through the `next_agent_selection_msg` AfterWork parameter. This parameter takes a string or a `Callable`.\n", + "There is a default prompt that is used for this auto speaker selection within a group chat, however, you can set the prompt through the `next_agent_selection_msg` AfterWork parameter. This parameter takes a string, `ContextStr`, or a `Callable`.\n", "\n", "The callable function signature is:\n", "`def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str`\n", "\n", - "If using a string you can incorporate context variables into your string, with the following string substitutions occurring:\n", - "- `{agentlist}` will be replaced by a comma-delimited list of agents\n", - "- context variable keys will be replaced by their values, e.g. `{my_context_variable_key}` will be replaced by its respective value.\n", - "\n", - "**Important:** The swarm manager's LLM configuration will be used, so be sure to pass in `llm_config` through the `swarm_manager_args` in `initiate_swarm_chat`.\n", - "\n", - "If not set, the default prompt of \"Read the above conversation. Then select the next role from {agentlist} to play. Only return the role.\" is used.\n", + "By using a `ContextStr` you can incorporate context variables into your string, by incorporating the context variables with f-string syntax: `\"Select the SupportAgent if the sentiment of this note is negative: {customer_note}\"`\n", "\n", "Here's a partial code example of using the AfterWork `next_agent_selection_msg` parameter.\n", "```python\n", @@ -243,22 +237,33 @@ "\n", "def my_selection_message(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str:\n", " # Returns the final selection message\n", - " return \"Review the above messages and return the name of the next agent from this list ['agent_1', 'agent_3']. The customer's status is: 'Active'.\"\n", + " return f\"Review the above messages and return the name of the next agent from this list ['agent_1', 'agent_3']. Take into account the customer's status: {agent.get_context('customer_status')}\"\n", "\n", - "# Use the default `auto` speaker selection prompt\n", + "# 1. Use the default `auto` speaker selection prompt\n", "register_hand_off(agent=agent_2, hand_to=[AfterWork(AfterWorkOption.SWARM_MANAGER)])\n", "\n", - "# Use a provided selection prompt\n", + "# 2. Use a specific prompt\n", + "register_hand_off(\n", + " agent=agent_2,\n", + " hand_to=[\n", + " AfterWork(\n", + " agent=AfterWorkOption.SWARM_MANAGER,\n", + " next_agent_selection_msg=\"Review the above messages and return the name of the next agent.\")\n", + " ]\n", + ")\n", + "\n", + "# 3. Using a ContextStr for context variable substitution\n", "register_hand_off(\n", " agent=agent_2,\n", " hand_to=[\n", " AfterWork(\n", " agent=AfterWorkOption.SWARM_MANAGER,\n", - " next_agent_selection_msg=\"Review the above messages and return the name of the next agent from this list {agentlist}. The customer's status is: {customer_status}.\")\n", + " next_agent_selection_msg=ContextStr(\"Review the above messages and return the name of the next agent. Take into account the customer's status: {customer_status}\"))\n", " ]\n", ")\n", "\n", - "# Similar to the previous example but using a Callable\n", + "\n", + "# 4. Using a Callable where we substitute the context_variables manually\n", "register_hand_off(\n", " agent=agent_2,\n", " hand_to=[\n", @@ -267,7 +272,12 @@ " next_agent_selection_msg=my_selection_message)\n", " ]\n", ")\n", - "```" + "```\n", + "\n", + "Notes:\n", + "- The swarm manager's LLM configuration will be used, so be sure to pass in `llm_config` through the `swarm_manager_args` in `initiate_swarm_chat`.\n", + "- If the resulting string of `next_agent_selection_msg` includes `{agentlist}` this will be substituted with a list of agents.\n", + "- If `next_agent_selection_msg` is not set, the default prompt of \"Read the above conversation. Then select the next role from {agentlist} to play. Only return the role.\" is used." ] }, { From b6dc63ec2825ab677dc95c7fad44845bcc6673bc Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Tue, 7 Jan 2025 23:35:08 +0000 Subject: [PATCH 10/11] Update init Signed-off-by: Mark Sze --- autogen/agentchat/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogen/agentchat/__init__.py b/autogen/agentchat/__init__.py index ad1cfceb78..0e745a77ab 100644 --- a/autogen/agentchat/__init__.py +++ b/autogen/agentchat/__init__.py @@ -19,9 +19,9 @@ ON_CONDITION, AfterWork, AfterWorkOption, + ContextStr, OnCondition, SwarmResult, - UpdateCondition, a_initiate_swarm_chat, initiate_swarm_chat, register_hand_off, @@ -47,7 +47,7 @@ "SwarmResult", "ON_CONDITION", "OnCondition", - "UpdateCondition", + "ContextStr", "AFTER_WORK", "AfterWork", "AfterWorkOption", From 0ced1dfefc58b3f5e218d41222ea907b57516661 Mon Sep 17 00:00:00 2001 From: Mark Sze Date: Wed, 8 Jan 2025 00:17:59 +0000 Subject: [PATCH 11/11] Documentation Signed-off-by: Mark Sze --- autogen/agentchat/contrib/swarm_agent.py | 2 + website/docs/topics/swarm.ipynb | 79 ++++++++++++++++++++---- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/autogen/agentchat/contrib/swarm_agent.py b/autogen/agentchat/contrib/swarm_agent.py index c08e978e62..6c57979f4f 100644 --- a/autogen/agentchat/contrib/swarm_agent.py +++ b/autogen/agentchat/contrib/swarm_agent.py @@ -124,6 +124,8 @@ class OnCondition: condition (Union[str, ContextStr, Callable]): The condition for transitioning to the target agent, evaluated by the LLM. If a string or Callable, no automatic context variable substitution occurs. If a ContextStr, context variable substitution occurs. + The Callable signature is: + def my_condition_string(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str available (Union[Callable, str]): Optional condition to determine if this OnCondition is included for the LLM to evaluate. Can be a Callable or a string. If a string, it will look up the value of the context variable with that name, which should be a bool, to determine whether it should include this condition. The Callable signature is: diff --git a/website/docs/topics/swarm.ipynb b/website/docs/topics/swarm.ipynb index acdc19ea25..f26d6b733d 100644 --- a/website/docs/topics/swarm.ipynb +++ b/website/docs/topics/swarm.ipynb @@ -44,7 +44,13 @@ "\n", "# Register the handoff using register_hand_off\n", "agent_1 = ConversableAgent(...)\n", - "register_hand_off(agent=agent_1, hand_to=[OnCondition(agent_2, \"condition_1\"), OnCondition(agent_3, \"condition_2\")])\n", + "register_hand_off(\n", + " agent=agent_1,\n", + " hand_to=[\n", + " OnCondition(target=agent_2, condition=\"condition_1\"),\n", + " OnCondition(target=agent_3, condition=\"condition_2\")\n", + " ]\n", + ")\n", "\n", "# This is equivalent to:\n", "def transfer_to_agent_2():\n", @@ -59,23 +65,33 @@ "# You can also use agent_1._add_functions to add more functions after initialization\n", "```\n", "\n", - "### UpdateCondition\n", - "`UpdateCondition` offers a simple way to set up a boolean expression using context variables within `OnCondition`. Its functionality and implementation are quite similar to `UpdateSystemMessage` in that it will substitute in the context variables, allowing you to make use of them in the condition's string.\n", + "### OnCondition condition\n", "\n", - "The following code realizes the following logic:\n", - "- if context_variables['condition'] == 1, transfer to agent_1 \n", - "- if context_variables['condition'] == 3, transfer to agent_3\n", + "The `condition` parameter of `OnCondition` can be a string, `ContextStr`, or Callable that returns a string.\n", + "\n", + "The callable function signature is:\n", + "`def my_condition_string(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str`\n", + "\n", + "By using a `ContextStr` you can incorporate context variables into your condition with f-string syntax.\n", + "\n", + "See partial examples below of using a `ContextStr` and Callable for the `condition` parameter.\n", "\n", "```python\n", + "def my_condition_func(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> str:\n", + " if agent.get_context(\"outstanding_issues\") == 0:\n", + " return \"Call this only if the customer has raised a new issue.\"\n", + " else:\n", + " return \"Call this if the customer wants to close an issue.\"\n", + "\n", "register_hand_off(\n", - " agent=agent_2,\n", + " agent=agent_1,\n", " hand_to=[\n", - " OnCondition(agent_1, \"transfer back to agent 1 if {condition} == 1\"),\n", - " OnCondition(agent_3, \"transfer back to agent 3 if {condition} == 3\")\n", + " OnCondition(target=agent_2, condition=ContextStr(\"Call this if the sentiment of this note is negative: {customer_note}\")),\n", + " OnCondition(target=agent_3, condition=my_condition_func)\n", " ]\n", ")\n", - "```\n", "\n", + "```\n", "\n", "### Registering Handoffs to a nested chat\n", "In addition to transferring to an agent, you can also trigger a nested chat by doing a handoff and using `OnCondition`. This is a useful way to perform sub-tasks without that work becoming part of the broader swarm's messages.\n", @@ -159,8 +175,43 @@ "\n", "See the documentation on [registering a nested chat](https://docs.ag2.ai/docs/reference/agentchat/conversable_agent#register-nested-chats) for further information on the parameters `reply_func_from_nested_chats`, `use_async`, and `config`.\n", "\n", - "Once a nested chat is complete, the resulting output from the last chat in the nested chats will be returned as the agent that triggered the nested chat's response.\n", - "\n" + "Once a nested chat is complete, the resulting output from the last chat in the nested chats will be returned as the agent that triggered the nested chat's response." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Enable/disable an OnCondition\n", + "\n", + "To help control the options available to the LLM when deciding the next agent, you can enable or disable `OnCondition`'s using the `available` parameter. This is useful when you have conditions that you don't want the LLM to consider because of the current state, such as if a user isn't yet authenticated, or an order hasn't been chosen. If the `available` parameter is not provided, the condition will always be available.\n", + "\n", + "This parameter takes either a context variable's key or a Callable that returns a boolean.\n", + "\n", + "If a context variable key is used, the context variable needs to contain a boolean and if it is `True` the condition will be made available.\n", + "\n", + "The callable function signature is:\n", + "```python\n", + "def my_available_func(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> bool\n", + "```\n", + "\n", + "Here are partial examples showing the use of a context variable key and a callable:\n", + "```python\n", + "context_variables = {\n", + " \"user_authenticated\": True\n", + "}\n", + "\n", + "def user_logged_in(agent: ConversableAgent, messages: List[Dict[str, Any]]) -> bool\n", + " return mock_external_logged_in()\n", + "\n", + "register_hand_off(\n", + " agent=agent_1,\n", + " hand_to=[\n", + " OnCondition(target=agent_3, condition=\"condition_2\", available=\"user_authenticated\"),\n", + " OnCondition(target=agent_2, condition=\"condition_1\", available=user_logged_in),\n", + " ]\n", + ")\n", + "```" ] }, { @@ -180,7 +231,9 @@ "- `SWARM_MANAGER`: Use the internal group chat's `auto` speaker selection method (see next section for more details)\n", "\n", "The callable function signature is:\n", - "`def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, ConversableAgent, str]:`\n", + "```python\n", + "def my_after_work_func(last_speaker: ConversableAgent, messages: List[Dict[str, Any]], groupchat: GroupChat) -> Union[AfterWorkOption, ConversableAgent, str]:\n", + "```\n", "\n", "Note: there should only be one `AfterWork`, if your requirement is more complex, use the callable function parameter.\n", "\n",