From c853cf20a05c241798abc18feb8009c555bd7dec Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Apr 2024 10:08:28 +0000 Subject: [PATCH 01/14] Added requery_on_multiple_speaker_names to GroupChat and updated _finalize_speaker to requery on multiple speaker names (if enabled) --- autogen/agentchat/groupchat.py | 60 +++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 05ac06e290a1..f338dad869c2 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -51,6 +51,10 @@ def custom_speaker_selection_func( last_speaker: Agent, groupchat: GroupChat ) -> Union[Agent, str, None]: ``` + - requery_on_multiple_speaker_names: whether to requery the LLM if multiple speaker names are returned during speaker selection. + Applies only to "auto" speaker selection method. + Default is False, in which case if the LLM returns multiple speaker names it will not requery the LLM. + If set to True and the LLM returns multiple speaker names, a message will be sent to the LLM with that response asking the LLM to return just one name based on it. - allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is True, in which case all speakers are allowed to speak consecutively. If `allow_repeat_speaker` is a list of Agents, then only those listed agents are allowed to repeat. @@ -77,6 +81,7 @@ def custom_speaker_selection_func( admin_name: Optional[str] = "Admin" func_call_filter: Optional[bool] = True speaker_selection_method: Union[Literal["auto", "manual", "random", "round_robin"], Callable] = "auto" + requery_on_multiple_speaker_names: bool = False allow_repeat_speaker: Optional[Union[bool, List[Agent]]] = None allowed_or_disallowed_speaker_transitions: Optional[Dict] = None speaker_transitions_type: Literal["allowed", "disallowed", None] = None @@ -466,7 +471,7 @@ def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Age # auto speaker selection selector.update_system_message(self.select_speaker_msg(agents)) final, name = selector.generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) + return self._finalize_speaker(last_speaker, final, name, selector, agents) async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: """Select the next speaker.""" @@ -476,9 +481,11 @@ async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent # auto speaker selection selector.update_system_message(self.select_speaker_msg(agents)) final, name = await selector.a_generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) + return self._finalize_speaker(last_speaker, final, name, selector, agents) - def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: Optional[List[Agent]]) -> Agent: + def _finalize_speaker( + self, last_speaker: Agent, final: bool, name: str, selector: ConversableAgent, agents: Optional[List[Agent]] + ) -> Agent: if not final: # the LLM client is None, thus no reply is generated. Use round robin instead. return self.next_agent(last_speaker, agents) @@ -487,6 +494,43 @@ def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: mentions = self._mentioned_agents(name, agents) if len(mentions) == 1: name = next(iter(mentions)) + elif self.requery_on_multiple_speaker_names and len(mentions) > 1: + # We have more than one name mentioned in the response, requery and + # ask the LLM to choose one from the response + select_name_message = [ + { + #'content': f'Your role is to select the current speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}, respond with just the name of the current speaker in the following context.\nContext:\n{name}', + #'content': f'Your role is to select the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. If the context refers to the next speaker name, choose that name, otherwise choose the current speaker. Respond with just the name of the speaker in the following context.\nContext:\n{name}', + "content": f"""Your role is to identify the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. 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 + 3. Otherwise, choose the first provided speaker's name in the context + + Respond with just the name of the speaker and do not provide a reason.\nContext:\n{name}""", + "role": "system", + } + ] + + # Requery the LLM + # final_single, name_single = selector.generate_oai_reply(select_name_message) + + response = selector.client.create( + context=None, + messages=select_name_message, + cache=None, + ) + name_single = selector.client.extract_text_or_completion_object(response)[0] + + # Evaluate the response for agent names + mentions = self._mentioned_agents(name_single, agents) + if len(mentions) == 1: + # Successfully identified just the one agent name on the requery + name = next(iter(mentions)) + else: + # Requery failed to identify just one agent name + logger.warning( + f"GroupChat select_speaker failed to resolve the next speaker's name (even with requery). Requery speaker selection returned:\n{name_single}" + ) else: logger.warning( f"GroupChat select_speaker failed to resolve the next speaker's name. This is because the speaker selection OAI call returned:\n{name}" @@ -531,8 +575,14 @@ def _mentioned_agents(self, message_content: Union[str, List], agents: Optional[ mentions = dict() for agent in agents: regex = ( - r"(?<=\W)" + re.escape(agent.name) + r"(?=\W)" - ) # Finds agent mentions, taking word boundaries into account + r"(?<=\W)(" + + re.escape(agent.name) + + r"|" + + re.escape(agent.name.replace("_", " ")) + + r"|" + + re.escape(agent.name.replace("_", r"\_")) + + r")(?=\W)" + ) count = len(re.findall(regex, f" {message_content} ")) # Pad the message to help with matching if count > 0: mentions[agent.name] = count From 9357265653bb2980d22ead7c9414b00d388e0b4b Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Apr 2024 10:11:26 +0000 Subject: [PATCH 02/14] Removed unnecessary comments --- autogen/agentchat/groupchat.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index f338dad869c2..71a3ee7f8198 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -499,8 +499,6 @@ def _finalize_speaker( # ask the LLM to choose one from the response select_name_message = [ { - #'content': f'Your role is to select the current speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}, respond with just the name of the current speaker in the following context.\nContext:\n{name}', - #'content': f'Your role is to select the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. If the context refers to the next speaker name, choose that name, otherwise choose the current speaker. Respond with just the name of the speaker in the following context.\nContext:\n{name}', "content": f"""Your role is to identify the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. 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 @@ -511,9 +509,7 @@ def _finalize_speaker( } ] - # Requery the LLM - # final_single, name_single = selector.generate_oai_reply(select_name_message) - + # Send to LLM for a response response = selector.client.create( context=None, messages=select_name_message, From 3b453e0ad9c8d295b2e1e84376cbac78dbb5a8b9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Apr 2024 03:42:08 +0000 Subject: [PATCH 03/14] Update to current main --- .../static/img/gallery/composio-autogen.png | Bin 109905 -> 131 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/static/img/gallery/composio-autogen.png b/website/static/img/gallery/composio-autogen.png index 0c7b8eb7867de01ac4d18958886051b07c60417b..72bd5f5f2e5595ca8375af6b21a0fe86800f77eb 100644 GIT binary patch literal 131 zcmWN?OA^8$3;@tQr{DsXPXaZ)4FMv|sO<>t!qe;9ysLgj-`~2;d5q1NN1wN+jOBkj zdCB|BjH70AIkoqqM@v4$%?rCwkQgutNEBZLfvdBE%uK|oCXco{z%0D3_mm3<3zQ)d NM>YDlYG;XTP(MklDA@o2 literal 109905 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 Date: Fri, 5 Apr 2024 03:26:11 +0000 Subject: [PATCH 04/14] Tweak error message. --- autogen/agentchat/groupchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index dc11778c0fc5..68e3f5d35488 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -525,7 +525,7 @@ def _finalize_speaker( else: # Requery failed to identify just one agent name logger.warning( - f"GroupChat select_speaker failed to resolve the next speaker's name (even with requery). Requery speaker selection returned:\n{name_single}" + f"GroupChat select_speaker failed to resolve the next speaker's name (including with requery). Requery speaker selection returned:\n{name_single}" ) else: logger.warning( From 86de80a32e302345a47fd0ed2e1b06584f2e328d Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Apr 2024 03:32:08 +0000 Subject: [PATCH 05/14] Comment clarity --- autogen/agentchat/groupchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 68e3f5d35488..d6720411deeb 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -496,7 +496,7 @@ def _finalize_speaker( name = next(iter(mentions)) elif self.requery_on_multiple_speaker_names and len(mentions) > 1: # We have more than one name mentioned in the response, requery and - # ask the LLM to choose one from the response + # ask the LLM to choose one name from that response select_name_message = [ { "content": f"""Your role is to identify the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. To determine the speaker use these prioritised rules: From c464f4524b123834f549361a175a0d4ded3a566a Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Apr 2024 04:01:25 +0000 Subject: [PATCH 06/14] Expanded description of Group Chat requery_on_multiple_speaker_names --- autogen/agentchat/groupchat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 4eaac208ec16..e05e4ad2c33a 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -52,6 +52,7 @@ def custom_speaker_selection_func( ) -> Union[Agent, str, None]: ``` - requery_on_multiple_speaker_names: whether to requery the LLM if multiple speaker names are returned during speaker selection. + The response with multiple names will be fed back to the LLM with a prompt asking it to select the primary speaker's name. Applies only to "auto" speaker selection method. Default is False, in which case if the LLM returns multiple speaker names it will not requery the LLM. If set to True and the LLM returns multiple speaker names, a message will be sent to the LLM with that response asking the LLM to return just one name based on it. @@ -515,6 +516,8 @@ def _finalize_speaker( messages=select_name_message, cache=None, ) + + # Returned name name_single = selector.client.extract_text_or_completion_object(response)[0] # Evaluate the response for agent names From 16c547d76a1a3ec58c752e17e3ef7e3a27a57592 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Apr 2024 00:39:56 +0000 Subject: [PATCH 07/14] Reworked to two-way nested chat for speaker selection with default of 2 retries. --- autogen/agentchat/groupchat.py | 425 +++++++++++++++++++++++++++---- test/agentchat/test_groupchat.py | 233 ++++++++++++++++- 2 files changed, 605 insertions(+), 53 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index e05e4ad2c33a..0b3fb953ac9b 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -7,6 +7,7 @@ from ..code_utils import content_str from ..exception_utils import AgentNameConflict, NoEligibleSpeaker, UndefinedNextAgent +from ..formatting_utils import colored from ..graph_utils import check_graph_validity, invert_disallowed_to_allowed from ..io.base import IOStream from ..runtime_logging import log_new_agent, logging_enabled @@ -51,11 +52,15 @@ def custom_speaker_selection_func( last_speaker: Agent, groupchat: GroupChat ) -> Union[Agent, str, None]: ``` - - requery_on_multiple_speaker_names: whether to requery the LLM if multiple speaker names are returned during speaker selection. - The response with multiple names will be fed back to the LLM with a prompt asking it to select the primary speaker's name. + - max_retries_for_selecting_speaker: the maximum number of times the speaker selection requery process will run. + If, during speaker selection, multiple agent names or no agent names are returned by the LLM as the next agent, it will be queried again up to the maximum number + of times until a single agent is returned or it exhausts the maximum attempts. + Applies only to "auto" speaker selection method. + Default is 2. + - select_speaker_auto_verbose: whether to output the select speaker responses and selections + If set to True, the response from selecting an agent will be displayed if it is not just an agent name and + it will output the selected agent's name. Applies only to "auto" speaker selection method. - Default is False, in which case if the LLM returns multiple speaker names it will not requery the LLM. - If set to True and the LLM returns multiple speaker names, a message will be sent to the LLM with that response asking the LLM to return just one name based on it. - allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is True, in which case all speakers are allowed to speak consecutively. If `allow_repeat_speaker` is a list of Agents, then only those listed agents are allowed to repeat. @@ -82,7 +87,7 @@ def custom_speaker_selection_func( admin_name: Optional[str] = "Admin" func_call_filter: Optional[bool] = True speaker_selection_method: Union[Literal["auto", "manual", "random", "round_robin"], Callable] = "auto" - requery_on_multiple_speaker_names: bool = False + max_retries_for_selecting_speaker: Optional[int] = 2 allow_repeat_speaker: Optional[Union[bool, List[Agent]]] = None allowed_or_disallowed_speaker_transitions: Optional[Dict] = None speaker_transitions_type: Literal["allowed", "disallowed", None] = None @@ -95,6 +100,7 @@ def custom_speaker_selection_func( select_speaker_prompt_template: str = ( "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." ) + select_speaker_auto_verbose: Optional[bool] = False role_for_select_speaker_messages: Optional[str] = "system" _VALID_SPEAKER_SELECTION_METHODS = ["auto", "manual", "random", "round_robin"] @@ -465,28 +471,37 @@ def _prepare_and_select_agents( return selected_agent, graph_eligible_agents, select_speaker_messages def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: - """Select the next speaker.""" + """Select the next speaker (with requery).""" + + # Prepare the list of available agents and select an agent if selection method allows (non-auto) selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent - # auto speaker selection - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = selector.generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, selector, agents) + + if self.speaker_selection_method == "manual": + selector.update_system_message(self.select_speaker_msg(agents)) + final, name = selector.generate_oai_reply(messages) + return self._finalize_speaker(last_speaker, final, name, agents) + else: + # auto speaker selection with 2-agent chat + return self._auto_select_speaker(last_speaker, selector, messages, agents) async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: - """Select the next speaker.""" + """Select the next speaker (with requery), asynchronously.""" + selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent - # auto speaker selection - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = await selector.a_generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, selector, agents) - def _finalize_speaker( - self, last_speaker: Agent, final: bool, name: str, selector: ConversableAgent, agents: Optional[List[Agent]] - ) -> Agent: + if self.speaker_selection_method == "manual": + selector.update_system_message(self.select_speaker_msg(agents)) + final, name = await selector.a_generate_oai_reply(messages) + return self._finalize_speaker(last_speaker, final, name, agents) + else: + # auto speaker selection with 2-agent chat + return await self.a_auto_select_speaker(last_speaker, selector, messages, agents) + + def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: Optional[List[Agent]]) -> Agent: if not final: # the LLM client is None, thus no reply is generated. Use round robin instead. return self.next_agent(last_speaker, agents) @@ -495,49 +510,355 @@ def _finalize_speaker( mentions = self._mentioned_agents(name, agents) if len(mentions) == 1: name = next(iter(mentions)) - elif self.requery_on_multiple_speaker_names and len(mentions) > 1: - # We have more than one name mentioned in the response, requery and - # ask the LLM to choose one name from that response - select_name_message = [ - { - "content": f"""Your role is to identify the current or next speaker based on the provided context. The valid speaker names are {[agent.name for agent in agents]}. 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 - 3. Otherwise, choose the first provided speaker's name in the context + else: + logger.warning( + f"GroupChat select_speaker failed to resolve the next speaker's name. This is because the speaker selection OAI call returned:\n{name}" + ) - Respond with just the name of the speaker and do not provide a reason.\nContext:\n{name}""", - "role": "system", - } - ] + # Return the result + agent = self.agent_by_name(name) + return agent if agent else self.next_agent(last_speaker, agents) + + def _auto_select_speaker( + self, + last_speaker: Agent, + selector: ConversableAgent, + messages: Optional[List[Dict]], + agents: Optional[List[Agent]], + ) -> Agent: + """Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying. + + Speaker selection for "auto" speaker selection method: + 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat + 2. Copy the current group messages and use the speaker selection message + 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: + - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response + 4. Chat continues until a single agent is nominated or there are no more attempts left + 5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned + + Args: + last_speaker Agent: The previous speaker in the group chat + selector ConversableAgent: + messages Optional[List[Dict]]: Current chat messages + agents Optional[List[Agent]]: Valid list of agents for speaker selection + + Returns: + Dict: a counter for mentioned agents. + """ + + # If no agents are passed in, assign all the group chat's agents + if agents is None: + agents = self.agents + + # The maximum number of speaker selection attempts (including requeries) + # We track these and use them in the validation function as we can't + # access the max_turns from within validate_speaker_name + max_attempts = 1 + self.max_retries_for_selecting_speaker + attempts_left = max_attempts + attempt = 0 + + # Registered reply function for checking_agent, checks the result of the response for agent names + def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Union[str, Dict, None]]: + + # The number of retries left, starting at max_retries_for_selecting_speaker + nonlocal attempts_left + nonlocal attempt + + attempt = attempt + 1 + attempts_left = attempts_left - 1 + + return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents) + + # Two-agent chat for speaker selection + + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_prompt_template.format(agentlist=f"{[agent.name for agent in agents]}"), + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + + # Agent for checking the response from the speaker_select_agent + checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) + + # Register the speaker validation function with the checking agent + checking_agent.register_reply( + [ConversableAgent, None], + reply_func=validate_speaker_name, # Validate each response + remove_other_reply_funcs=True, + ) + + # Copy the current group chat messages into this internal chat + # Exclude the last item which is the speaker selection prompt + for msg in messages[:-1]: + if ( + "content" in msg + and "role" in msg + and msg["content"] is not None + and msg["role"] is not None + and len(msg["content"]) != 0 + and len(msg["role"]) != 0 + ): + # if msg["role"] == "user": + if msg["role"] == "user": + msg_name = msg["name"] if "name" in msg else "" + checking_agent.send( + {"content": msg["content"], "role": msg["role"], "name": msg_name}, + speaker_selection_agent, + request_reply=False, + silent=True, + ) + + result = checking_agent.initiate_chat( + speaker_selection_agent, + cache=None, # don't use caching for the speaker selection chat + message="You are an expert at finding the next speaker.", + max_turns=2 + * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one + clear_history=False, + silent=True, # suppress output, we're using terminal output + ) + + return self._process_speaker_selection_result(result, last_speaker, agents) + + async def a_auto_select_speaker( + self, + last_speaker: Agent, + selector: ConversableAgent, + messages: Optional[List[Dict]], + agents: Optional[List[Agent]], + ) -> Agent: + """(Asynchronous) Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying. + + Speaker selection for "auto" speaker selection method: + 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat + 2. Copy the current group messages and use the speaker selection message + 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: + - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response + 4. Chat continues until a single agent is nominated or there are no more attempts left + 5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned + + Args: + last_speaker Agent: The previous speaker in the group chat + selector ConversableAgent: + messages Optional[List[Dict]]: Current chat messages + agents Optional[List[Agent]]: Valid list of agents for speaker selection + + Returns: + Dict: a counter for mentioned agents. + """ + + # If no agents are passed in, assign all the group chat's agents + if agents is None: + agents = self.agents + + # The maximum number of speaker selection attempts (including requeries) + # We track these and use them in the validation function as we can't + # access the max_turns from within validate_speaker_name + max_attempts = 1 + self.max_retries_for_selecting_speaker + attempts_left = max_attempts + attempt = 0 + + # Registered reply function for checking_agent, checks the result of the response for agent names + def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Union[str, Dict, None]]: + + # The number of retries left, starting at max_retries_for_selecting_speaker + nonlocal attempts_left + nonlocal attempt + + attempt = attempt + 1 + attempts_left = attempts_left - 1 + + return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents) + + # Two-agent chat for speaker selection + + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_prompt_template.format(agentlist=f"{[agent.name for agent in agents]}"), + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) - # Send to LLM for a response - response = selector.client.create( - context=None, - messages=select_name_message, - cache=None, + # Agent for checking the response from the speaker_select_agent + checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) + + # Register the speaker validation function with the checking agent + checking_agent.register_reply( + [ConversableAgent, None], + reply_func=validate_speaker_name, # Validate each response + remove_other_reply_funcs=True, + ) + + # Copy the current group chat messages into this internal chat + # Exclude the last item which is the speaker selection prompt + for msg in messages[:-1]: + if ( + "content" in msg + and "role" in msg + and msg["content"] is not None + and msg["role"] is not None + and len(msg["content"]) != 0 + and len(msg["role"]) != 0 + ): + # if msg["role"] == "user": + if msg["role"] == "user": + msg_name = msg["name"] if "name" in msg else "" + checking_agent.send( + {"content": msg["content"], "role": msg["role"], "name": msg_name}, + speaker_selection_agent, + request_reply=False, + silent=True, + ) + + result = await checking_agent.a_initiate_chat( + speaker_selection_agent, + cache=None, # don't use caching for the speaker selection chat + message="You are an expert at finding the next speaker.", + max_turns=2 + * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one + clear_history=False, + silent=True, # suppress output, we're using terminal output + ) + + return self._process_speaker_selection_result(result, last_speaker, agents) + + def _validate_speaker_name( + self, recipient, messages, sender, config, attempts_left, attempt, agents + ) -> Tuple[bool, Union[str, Dict, None]]: + """Validates the speaker response for each round in the internal 2-agent + chat within the auto select speaker method. + + Used by auto_select_speaker and a_auto_select_speaker. + """ + + # Output the query and requery results + if self.select_speaker_auto_verbose: + iostream = IOStream.get_default() + + # Validate the speaker name selected + select_name = messages[-1]["content"].strip() + + mentions = self._mentioned_agents(select_name, agents) + + # Display the response if it's not the exact speaker name + if self.select_speaker_auto_verbose and (len(mentions) != 1 or (select_name.strip() != next(iter(mentions)))): + iostream.print( + colored( + f"\n>>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} response:\n{'[BLANK]' if not select_name.strip() else select_name.strip()}", + "magenta", + ), + flush=False, ) - # Returned name - name_single = selector.client.extract_text_or_completion_object(response)[0] + if len(mentions) == 1: + + # Success on retry, we have just one name mentioned + selected_agent_name = next(iter(mentions)) + + # Add the selected agent to the response so we can return it + messages.append({"role": "user", "content": f"[AGENT SELECTED]{selected_agent_name}"}) + + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} successfully selected: {selected_agent_name}", + "green", + ), + flush=True, + ) + + elif len(mentions) > 1: + # More than one name on requery so add additional reminder prompt for next retry + + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} failed as it included multiple agent names.", + "red", + ), + flush=True, + ) - # Evaluate the response for agent names - mentions = self._mentioned_agents(name_single, agents) - if len(mentions) == 1: - # Successfully identified just the one agent name on the requery - name = next(iter(mentions)) + if attempts_left: + # Message to return to the chat for the next attempt + return ( + True, + """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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + ) else: - # Requery failed to identify just one agent name - logger.warning( - f"GroupChat select_speaker failed to resolve the next speaker's name (including with requery). Requery speaker selection returned:\n{name_single}" + # Final failure, no attempts left + messages.append( + { + "role": "user", + "content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it returned multiple names.", + } ) + else: - logger.warning( - f"GroupChat select_speaker failed to resolve the next speaker's name. This is because the speaker selection OAI call returned:\n{name}" - ) + # No names at all on requery so add additional reminder prompt for next retry - # Return the result - agent = self.agent_by_name(name) - return agent if agent else self.next_agent(last_speaker, agents) + if self.select_speaker_auto_verbose: + iostream.print( + colored( + f">>>>>>>> Select speaker attempt #{attempt} failed as it did not include any agent names.", + "red", + ), + flush=True, + ) + + if attempts_left: + # Message to return to the chat for the next attempt + return ( + True, + f"""You didn't choose a speaker. As a reminder, 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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {[agent.name for agent in agents]}. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + ) + else: + # Final failure, no attempts left + messages.append( + { + "role": "user", + "content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it did not include any agent names.", + } + ) + + return True, None + + def _process_speaker_selection_result(self, result, last_speaker: ConversableAgent, agents: Optional[List[Agent]]): + """Checks the result of the auto_select_speaker function, returning the + agent to speak. + + Used by auto_select_speaker and a_auto_select_speaker.""" + if len(result.chat_history) > 0: + + # Use the final message, which will have the selected agent or reason for failure + final_message = result.chat_history[-1]["content"] + + if "[AGENT SELECTED]" in final_message: + + # Have successfully selected an agent, return it + return self.agent_by_name(final_message.replace("[AGENT SELECTED]", "")) + + else: # "[AGENT SELECTION FAILED]" + + # Failed to select an agent, so we'll select the next agent in the list + next_agent = self.next_agent(last_speaker, agents) + + # No agent, return the failed reason + return next_agent def _participant_roles(self, agents: List[Agent] = None) -> str: # Default to all agents registered diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py index 8a4758d2d37b..3a8eb158c6d9 100755 --- a/test/agentchat/test_groupchat.py +++ b/test/agentchat/test_groupchat.py @@ -1426,6 +1426,235 @@ def test_speaker_selection_agent_name_match(): assert result == {} +def test_speaker_selection_auto_process_result(): + """ + Tests the return result of the 2-agent chat used for speaker selection for the auto method. + The last message of the messages passed in will contain a pass or fail. + If passed, the message will contain the name of the correct agent and that agent will be returned. + If failed, the message will contain the reason for failure for the last attempt and the next + agent in the sequence will be returned. + """ + cmo = autogen.ConversableAgent( + name="Chief_Marketing_Officer", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is alice speaking.", + ) + pm = autogen.ConversableAgent( + name="Product_Manager", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob speaking.", + function_map={"test_func": lambda x: x}, + ) + + agent_list = [cmo, pm] + groupchat = autogen.GroupChat(agents=agent_list, messages=[], max_round=3) + + chat_result = autogen.ChatResult( + chat_id=None, + chat_history=[ + { + "content": "Let's get this meeting started. First the Product_Manager will create 3 new product ideas.", + "name": "Chairperson", + "role": "assistant", + }, + {"content": "You are an expert at finding the next speaker.", "role": "assistant"}, + {"content": "Product_Manager", "role": "user"}, + {"content": "UPDATED_BELOW", "role": "user"}, + ], + ) + + ### Agent selected successfully + chat_result.chat_history[3]["content"] = "[AGENT SELECTED]Product_Manager" + + # Product_Manager should be returned + assert groupchat._process_speaker_selection_result(chat_result, cmo, agent_list) == pm + + ### Agent not selected successfully + chat_result.chat_history[3][ + "content" + ] = "[AGENT SELECTION FAILED]Select speaker attempt #3 of 3 failed as it did not include any agent names." + + # The next speaker in the list will be selected, which will be the Product_Manager (as the last speaker is the Chief_Marketing_Officer) + assert groupchat._process_speaker_selection_result(chat_result, cmo, agent_list) == pm + + ### Invalid result messages, will return the next agent + chat_result.chat_history[3]["content"] = "This text should not be here." + + # The next speaker in the list will be selected, which will be the Chief_Marketing_Officer (as the last speaker is the Product_Maanger) + assert groupchat._process_speaker_selection_result(chat_result, pm, agent_list) == cmo + + +def test_speaker_selection_validate_speaker_name(): + """ + Tests the speaker name validation function used to evaluate the return result of the LLM + during speaker selection in 'auto' mode. + + Function: _validate_speaker_name + + If a single agent name is returned by the LLM, it will add a relevant message to the chat messages and return True, None + If multiple agent names are returned and there are attempts left, it will return a message to be used to prompt the LLM to try again + If multiple agent names are return and there are no attempts left, it will add a relevant message to the chat messages and return True, None + If no agent names are returned and there are attempts left, it will return a message to be used to prompt the LLM to try again + If no agent names are returned and there are no attempts left, it will add a relevant message to the chat messages and return True, None + """ + + # Group Chat setup + cmo = autogen.ConversableAgent( + name="Chief_Marketing_Officer", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is alice speaking.", + ) + pm = autogen.ConversableAgent( + name="Product_Manager", + human_input_mode="NEVER", + llm_config=False, + default_auto_reply="This is bob speaking.", + function_map={"test_func": lambda x: x}, + ) + + agent_list = [cmo, pm] + groupchat = autogen.GroupChat(agents=agent_list, messages=[], max_round=3) + + # Speaker Selection 2-agent chat setup + + # Agent for selecting a single agent name from the response + speaker_selection_agent = autogen.ConversableAgent( + "speaker_selection_agent", + ) + + # Agent for checking the response from the speaker_select_agent + checking_agent = autogen.ConversableAgent("checking_agent") + + # Select speaker messages + select_speaker_messages = [ + { + "content": "Let's get this meeting started. First the Product_Manager will create 3 new product ideas.", + "name": "Chairperson", + "role": "assistant", + }, + {"content": "You are an expert at finding the next speaker.", "role": "assistant"}, + {"content": "UPDATED_BELOW", "role": "user"}, + ] + + ### Single agent name returned + attempts_left = 2 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager is the next to speak" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert select_speaker_messages[-1]["content"] == "[AGENT SELECTED]Product_Manager" + + select_speaker_messages.pop(-1) # Remove the last message before the next test + + ### Multiple agent names returned with attempts left + attempts_left = 2 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager must speak after the Chief_Marketing_Officer" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == ( + True, + """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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + ) + + ### Multiple agent names returned with no attempts left + attempts_left = 0 + attempt = 1 + select_speaker_messages[-1]["content"] = "Product_Manager must speak after the Chief_Marketing_Officer" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert ( + select_speaker_messages[-1]["content"] + == f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it returned multiple names." + ) + + select_speaker_messages.pop(-1) # Remove the last message before the next test + + ### No agent names returned with attempts left + attempts_left = 3 + attempt = 2 + select_speaker_messages[-1]["content"] = "The PM must speak after the CMO" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == ( + True, + f"""You didn't choose a speaker. As a reminder, 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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {[agent.name for agent in agent_list]}. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + ) + + ### Multiple agents returned with no attempts left + attempts_left = 0 + attempt = 3 + select_speaker_messages[-1]["content"] = "The PM must speak after the CMO" + + result = groupchat._validate_speaker_name( + recipient=checking_agent, + messages=select_speaker_messages, + sender=speaker_selection_agent, + config=None, + attempts_left=attempts_left, + attempt=attempt, + agents=agent_list, + ) + + assert result == (True, None) + assert ( + select_speaker_messages[-1]["content"] + == f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it did not include any agent names." + ) + + if __name__ == "__main__": # test_func_call_groupchat() # test_broadcast() @@ -1443,5 +1672,7 @@ def test_speaker_selection_agent_name_match(): # test_custom_speaker_selection_overrides_transition_graph() # test_role_for_select_speaker_messages() # test_select_speaker_message_and_prompt_templates() - test_speaker_selection_agent_name_match() + # test_speaker_selection_agent_name_match() + test_speaker_selection_auto_process_result() + test_speaker_selection_validate_speaker_name() # pass From 4fbfbbe22a91652ed9a526cf843d5eb4fa9e4d85 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Apr 2024 01:38:44 +0000 Subject: [PATCH 08/14] Adding validation of new GroupChat attributes --- autogen/agentchat/groupchat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 0b3fb953ac9b..279fd22fd7cc 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -200,6 +200,21 @@ def __post_init__(self): if self.role_for_select_speaker_messages is None or len(self.role_for_select_speaker_messages) == 0: raise ValueError("role_for_select_speaker_messages cannot be empty or None.") + if self.max_retries_for_selecting_speaker is None or len(self.role_for_select_speaker_messages) == 0: + raise ValueError("role_for_select_speaker_messages cannot be empty or None.") + + # Validate max select speakers retries + if self.max_retries_for_selecting_speaker is None or not isinstance( + self.max_retries_for_selecting_speaker, int + ): + raise ValueError("max_retries_for_selecting_speaker cannot be None or non-int") + elif self.max_retries_for_selecting_speaker < 0: + raise ValueError("max_retries_for_selecting_speaker must be greater than or equal to zero") + + # Validate select_speaker_auto_verbose + if self.select_speaker_auto_verbose is None or not isinstance(self.select_speaker_auto_verbose, bool): + raise ValueError("select_speaker_auto_verbose cannot be None or non-bool") + @property def agent_names(self) -> List[str]: """Return the names of the agents in the group chat.""" From 21c5af065ac18885fa178281490a21b22f2b8d5a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Apr 2024 04:28:22 +0000 Subject: [PATCH 09/14] Updates as per @ekzhu's suggestions --- autogen/agentchat/groupchat.py | 106 ++++++++------------------------- 1 file changed, 26 insertions(+), 80 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 279fd22fd7cc..3b3dda4e9cea 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -58,8 +58,8 @@ def custom_speaker_selection_func( Applies only to "auto" speaker selection method. Default is 2. - select_speaker_auto_verbose: whether to output the select speaker responses and selections - If set to True, the response from selecting an agent will be displayed if it is not just an agent name and - it will output the selected agent's name. + If set to True, the outputs from the two agents in the nested select speaker chat will be output, along with + whether the responses were successful, or not, in selecting an agent Applies only to "auto" speaker selection method. - allow_repeat_speaker: whether to allow the same speaker to speak consecutively. Default is True, in which case all speakers are allowed to speak consecutively. @@ -477,12 +477,6 @@ def _prepare_and_select_agents( select_speaker_messages[-1] = dict(select_speaker_messages[-1], function_call=None) if select_speaker_messages[-1].get("tool_calls", False): select_speaker_messages[-1] = dict(select_speaker_messages[-1], tool_calls=None) - select_speaker_messages = select_speaker_messages + [ - { - "role": self.role_for_select_speaker_messages, - "content": self.select_speaker_prompt(graph_eligible_agents), - } - ] return selected_agent, graph_eligible_agents, select_speaker_messages def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: @@ -545,7 +539,7 @@ def _auto_select_speaker( Speaker selection for "auto" speaker selection method: 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat - 2. Copy the current group messages and use the speaker selection message + 2. Inject the group messages into the new chat 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response 4. Chat continues until a single agent is nominated or there are no more attempts left @@ -586,14 +580,6 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un # Two-agent chat for speaker selection - # Agent for selecting a single agent name from the response - speaker_selection_agent = ConversableAgent( - "speaker_selection_agent", - system_message=self.select_speaker_prompt_template.format(agentlist=f"{[agent.name for agent in agents]}"), - llm_config=selector.llm_config, - human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose - ) - # Agent for checking the response from the speaker_select_agent checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) @@ -604,35 +590,24 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un remove_other_reply_funcs=True, ) - # Copy the current group chat messages into this internal chat - # Exclude the last item which is the speaker selection prompt - for msg in messages[:-1]: - if ( - "content" in msg - and "role" in msg - and msg["content"] is not None - and msg["role"] is not None - and len(msg["content"]) != 0 - and len(msg["role"]) != 0 - ): - # if msg["role"] == "user": - if msg["role"] == "user": - msg_name = msg["name"] if "name" in msg else "" - checking_agent.send( - {"content": msg["content"], "role": msg["role"], "name": msg_name}, - speaker_selection_agent, - request_reply=False, - silent=True, - ) + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_msg(agents), + chat_messages={checking_agent: messages}, + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + # Run the speaker selection chat result = checking_agent.initiate_chat( speaker_selection_agent, cache=None, # don't use caching for the speaker selection chat - message="You are an expert at finding the next speaker.", + message=self.select_speaker_prompt(agents), max_turns=2 * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one clear_history=False, - silent=True, # suppress output, we're using terminal output + silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute ) return self._process_speaker_selection_result(result, last_speaker, agents) @@ -648,7 +623,7 @@ async def a_auto_select_speaker( Speaker selection for "auto" speaker selection method: 1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat - 2. Copy the current group messages and use the speaker selection message + 2. Inject the group messages into the new chat 3. Run the two-agent chat, evaluating the result of response from the speaker selector agent: - If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response 4. Chat continues until a single agent is nominated or there are no more attempts left @@ -689,14 +664,6 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un # Two-agent chat for speaker selection - # Agent for selecting a single agent name from the response - speaker_selection_agent = ConversableAgent( - "speaker_selection_agent", - system_message=self.select_speaker_prompt_template.format(agentlist=f"{[agent.name for agent in agents]}"), - llm_config=selector.llm_config, - human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose - ) - # Agent for checking the response from the speaker_select_agent checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts) @@ -707,35 +674,24 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un remove_other_reply_funcs=True, ) - # Copy the current group chat messages into this internal chat - # Exclude the last item which is the speaker selection prompt - for msg in messages[:-1]: - if ( - "content" in msg - and "role" in msg - and msg["content"] is not None - and msg["role"] is not None - and len(msg["content"]) != 0 - and len(msg["role"]) != 0 - ): - # if msg["role"] == "user": - if msg["role"] == "user": - msg_name = msg["name"] if "name" in msg else "" - checking_agent.send( - {"content": msg["content"], "role": msg["role"], "name": msg_name}, - speaker_selection_agent, - request_reply=False, - silent=True, - ) + # Agent for selecting a single agent name from the response + speaker_selection_agent = ConversableAgent( + "speaker_selection_agent", + system_message=self.select_speaker_msg(agents), + chat_messages={checking_agent: messages}, + llm_config=selector.llm_config, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + # Run the speaker selection chat result = await checking_agent.a_initiate_chat( speaker_selection_agent, cache=None, # don't use caching for the speaker selection chat - message="You are an expert at finding the next speaker.", + message=self.select_speaker_prompt(agents), max_turns=2 * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one clear_history=False, - silent=True, # suppress output, we're using terminal output + silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute ) return self._process_speaker_selection_result(result, last_speaker, agents) @@ -758,16 +714,6 @@ def _validate_speaker_name( mentions = self._mentioned_agents(select_name, agents) - # Display the response if it's not the exact speaker name - if self.select_speaker_auto_verbose and (len(mentions) != 1 or (select_name.strip() != next(iter(mentions)))): - iostream.print( - colored( - f"\n>>>>>>>> Select speaker attempt {attempt} of {attempt + attempts_left} response:\n{'[BLANK]' if not select_name.strip() else select_name.strip()}", - "magenta", - ), - flush=False, - ) - if len(mentions) == 1: # Success on retry, we have just one name mentioned From 9af50e575dbf1b53183d063d7434f6928b0ef8e9 Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:49:05 +1000 Subject: [PATCH 10/14] Update groupchat - Added select_speaker_auto_multiple_template and select_speaker_auto_none_template - Added max_attempts comment - Re-instated support for role_for_select_speaker_messages - --- autogen/agentchat/groupchat.py | 79 +++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 3b3dda4e9cea..a1b6e55aefb5 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -29,13 +29,28 @@ class GroupChat: When set to True and when a message is a function call suggestion, the next speaker will be chosen from an agent which contains the corresponding function name in its `function_map`. - - select_speaker_message_template: customize the select speaker message (used in "auto" speaker selection), which appears first in the message context and generally includes the agent descriptions and list of agents. The string value will be converted to an f-string, use "{roles}" to output the agent's and their role descriptions and "{agentlist}" for a comma-separated list of agent names in square brackets. The default value is: + - select_speaker_message_template: customize the select speaker message (used in "auto" speaker selection), which appears first in the message context and generally includes the agent descriptions and list of agents. If the string contains "{roles}" it will replaced with the agent's and their role descriptions. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: "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: customize the select speaker prompt (used in "auto" speaker selection), which appears last in the message context and generally includes the list of agents and guidance for the LLM to select the next agent. The string value will be converted to an f-string, use "{agentlist}" for a comma-separated list of agent names in square brackets. The default value is: + - select_speaker_prompt_template: customize the select speaker prompt (used in "auto" speaker selection), which appears last in the message context and generally includes the list of agents and guidance for the LLM to select the next agent. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." + - select_speaker_auto_multiple_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains multiple agent names. This prompt guides the LLM to return just one agent name. Applies only to "auto" speaker selection method. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: + "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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason." + - select_speaker_auto_none_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains no agent names. This prompt guides the LLM to return an agent name and provides a list of agent names. Applies only to "auto" speaker selection method. If the string contains "{agentlist}" it will be replaced with a comma-separated list of agent names in square brackets. The default value is: + "You didn't choose a speaker. As a reminder, 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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {agentlist}. + Respond with ONLY the name of the speaker and DO NOT provide a reason." - speaker_selection_method: the method for selecting the next speaker. Default is "auto". Could be any of the following (case insensitive), will raise ValueError if not recognized: - "auto": the next speaker is selected automatically by LLM. @@ -100,6 +115,19 @@ def custom_speaker_selection_func( select_speaker_prompt_template: str = ( "Read the above conversation. Then select the next role from {agentlist} to play. Only return the role." ) + 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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""" + select_speaker_auto_none_template: str = """You didn't choose a speaker. As a reminder, 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 + 3. Otherwise, choose the first provided speaker's name in the context + The names are case-sensitive and should not be abbreviated or changed. + The only names that are accepted are {agentlist}. + Respond with ONLY the name of the speaker and DO NOT provide a reason.""" select_speaker_auto_verbose: Optional[bool] = False role_for_select_speaker_messages: Optional[str] = "system" @@ -190,7 +218,7 @@ def __post_init__(self): agents=self.agents, ) - # Check select_speaker_message_template and select_speaker_prompt_template have values + # Check select speaker messages, prompts, roles, and retries have values if self.select_speaker_message_template is None or len(self.select_speaker_message_template) == 0: raise ValueError("select_speaker_message_template cannot be empty or None.") @@ -200,6 +228,12 @@ def __post_init__(self): if self.role_for_select_speaker_messages is None or len(self.role_for_select_speaker_messages) == 0: raise ValueError("role_for_select_speaker_messages cannot be empty or None.") + if self.select_speaker_auto_multiple_template is None or len(self.select_speaker_auto_multiple_template) == 0: + raise ValueError("select_speaker_auto_multiple_template cannot be empty or None.") + + if self.select_speaker_auto_none_template is None or len(self.select_speaker_auto_none_template) == 0: + raise ValueError("select_speaker_auto_none_template cannot be empty or None.") + if self.max_retries_for_selecting_speaker is None or len(self.role_for_select_speaker_messages) == 0: raise ValueError("role_for_select_speaker_messages cannot be empty or None.") @@ -560,8 +594,9 @@ def _auto_select_speaker( agents = self.agents # The maximum number of speaker selection attempts (including requeries) + # is the initial speaker selection attempt plus the maximum number of retries. # We track these and use them in the validation function as we can't - # access the max_turns from within validate_speaker_name + # access the max_turns from within validate_speaker_name. max_attempts = 1 + self.max_retries_for_selecting_speaker attempts_left = max_attempts attempt = 0 @@ -603,7 +638,10 @@ def validate_speaker_name(recipient, messages, sender, config) -> Tuple[bool, Un result = checking_agent.initiate_chat( speaker_selection_agent, cache=None, # don't use caching for the speaker selection chat - message=self.select_speaker_prompt(agents), + message={ + "content": self.select_speaker_prompt(agents), + "override_role": self.role_for_select_speaker_messages, + }, max_turns=2 * max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one clear_history=False, @@ -745,15 +783,12 @@ def _validate_speaker_name( if attempts_left: # Message to return to the chat for the next attempt - return ( - True, - """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 - 3. Otherwise, choose the first provided speaker's name in the context - The names are case-sensitive and should not be abbreviated or changed. - Respond with ONLY the name of the speaker and DO NOT provide a reason.""", - ) + agentlist = f"{[agent.name for agent in agents]}" + + return True, { + "content": self.select_speaker_auto_multiple_template.format(agentlist=agentlist), + "override_role": self.role_for_select_speaker_messages, + } else: # Final failure, no attempts left messages.append( @@ -777,16 +812,12 @@ def _validate_speaker_name( if attempts_left: # Message to return to the chat for the next attempt - return ( - True, - f"""You didn't choose a speaker. As a reminder, 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 - 3. Otherwise, choose the first provided speaker's name in the context - The names are case-sensitive and should not be abbreviated or changed. - The only names that are accepted are {[agent.name for agent in agents]}. - Respond with ONLY the name of the speaker and DO NOT provide a reason.""", - ) + agentlist = f"{[agent.name for agent in agents]}" + + return True, { + "content": self.select_speaker_auto_none_template.format(agentlist=agentlist), + "override_role": self.role_for_select_speaker_messages, + } else: # Final failure, no attempts left messages.append( From 6550143be9c5924b93b697c0f92a64b282b17930 Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:50:02 +1000 Subject: [PATCH 11/14] Update conversable_agent.py Added ability to force override role for a message to support select speaker prompt. --- autogen/agentchat/conversable_agent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 262fc513d235..b04222f514e2 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -576,6 +576,11 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: if message.get("role") in ["function", "tool"]: oai_message["role"] = message.get("role") + elif "override_role" in message: + # If we have a direction to override the role then set the + # role accordingly. Used to customise the role for the + # select speaker prompt. + oai_message["role"] = message.get("override_role") else: oai_message["role"] = role From f83ffc5dba9bc5e9f0d3713b33ff638191978dfb Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:51:08 +1000 Subject: [PATCH 12/14] Update test_groupchat.py Updated existing select_speaker test functions as underlying approach has changed, added necessary tests for new functionality. --- test/agentchat/test_groupchat.py | 162 ++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 25 deletions(-) diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py index 3a8eb158c6d9..a4689bd539f7 100755 --- a/test/agentchat/test_groupchat.py +++ b/test/agentchat/test_groupchat.py @@ -1196,28 +1196,46 @@ def test_role_for_select_speaker_messages(): agents=[agent1, agent2], messages=[{"role": "user", "content": "Let's have a chat!"}], max_round=3, + role_for_select_speaker_messages="system", ) - # Run the select agents function to get the select speaker messages - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + # Replicate the _auto_select_speaker nested chat. + + # Agent for checking the response from the speaker_select_agent + checking_agent = autogen.ConversableAgent("checking_agent") + + # Agent for selecting a single agent name from the response + speaker_selection_agent = autogen.ConversableAgent( + "speaker_selection_agent", + llm_config=None, + human_input_mode="NEVER", # Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose + ) + + # The role_for_select_speaker_message is put into the initiate_chat of the nested two-way chat + # into a message attribute called 'override_role'. This is evaluated in Conversable Agent's _append_oai_message function + # e.g.: message={'content':self.select_speaker_prompt(agents),'override_role':self.role_for_select_speaker_messages}, + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) # Test default is "system" - assert len(messages) == 2 - assert messages[-1]["role"] == "system" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "system" # Test as "user" groupchat.role_for_select_speaker_messages = "user" - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) - assert len(messages) == 2 - assert messages[-1]["role"] == "user" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "user" # Test as something unusual groupchat.role_for_select_speaker_messages = "SockS" - selected_agent, agents, messages = groupchat._prepare_and_select_agents(agent1) + message = {"content": "A prompt goes here.", "override_role": groupchat.role_for_select_speaker_messages} + checking_agent._append_oai_message(message, "assistant", speaker_selection_agent) - assert len(messages) == 2 - assert messages[-1]["role"] == "SockS" + assert len(checking_agent.chat_messages) == 1 + assert checking_agent.chat_messages[speaker_selection_agent][-1]["role"] == "SockS" # Test empty string and None isn't accepted @@ -1307,7 +1325,7 @@ def test_select_speaker_message_and_prompt_templates(): speaker_selection_method="auto", max_round=10, select_speaker_message_template="Not empty.", - select_speaker_prompt_template=None, + select_speaker_prompt_template="", ) # Test with None @@ -1328,7 +1346,7 @@ def test_select_speaker_message_and_prompt_templates(): speaker_selection_method="auto", max_round=10, select_speaker_message_template="Not empty.", - select_speaker_prompt_template="", + select_speaker_prompt_template=None, ) @@ -1498,6 +1516,8 @@ def test_speaker_selection_validate_speaker_name(): If multiple agent names are return and there are no attempts left, it will add a relevant message to the chat messages and return True, None If no agent names are returned and there are attempts left, it will return a message to be used to prompt the LLM to try again If no agent names are returned and there are no attempts left, it will add a relevant message to the chat messages and return True, None + + When returning a message, it will include the 'override_role' key and value to support the GroupChat role_for_select_speaker_messages attribute """ # Group Chat setup @@ -1516,6 +1536,7 @@ def test_speaker_selection_validate_speaker_name(): ) agent_list = [cmo, pm] + agent_list_string = f"{[agent.name for agent in agent_list]}" groupchat = autogen.GroupChat(agents=agent_list, messages=[], max_round=3) # Speaker Selection 2-agent chat setup @@ -1576,12 +1597,10 @@ def test_speaker_selection_validate_speaker_name(): assert result == ( True, - """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 - 3. Otherwise, choose the first provided speaker's name in the context - The names are case-sensitive and should not be abbreviated or changed. - Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + { + "content": groupchat.select_speaker_auto_multiple_template.format(agentlist=agent_list_string), + "override_role": groupchat.role_for_select_speaker_messages, + }, ) ### Multiple agent names returned with no attempts left @@ -1624,13 +1643,10 @@ def test_speaker_selection_validate_speaker_name(): assert result == ( True, - f"""You didn't choose a speaker. As a reminder, 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 - 3. Otherwise, choose the first provided speaker's name in the context - The names are case-sensitive and should not be abbreviated or changed. - The only names that are accepted are {[agent.name for agent in agent_list]}. - Respond with ONLY the name of the speaker and DO NOT provide a reason.""", + { + "content": groupchat.select_speaker_auto_none_template.format(agentlist=agent_list_string), + "override_role": groupchat.role_for_select_speaker_messages, + }, ) ### Multiple agents returned with no attempts left @@ -1655,6 +1671,101 @@ def test_speaker_selection_validate_speaker_name(): ) +def test_select_speaker_auto_messages(): + """ + In this test, two agents are part of a group chat which has customized select speaker "auto" multiple and no-name prompt messages. Both valid and empty string values will be used. + The expected behaviour is that the customized speaker selection "auto" messages will override the default values or throw exceptions if empty. + """ + + agent1 = autogen.ConversableAgent( + "Alice", + description="A wonderful employee named Alice.", + human_input_mode="NEVER", + llm_config=False, + ) + agent2 = autogen.ConversableAgent( + "Bob", + description="An amazing employee named Bob.", + human_input_mode="NEVER", + llm_config=False, + ) + + # Customised message for select speaker auto method where multiple agent names are returned + custom_multiple_names_msg = "You mentioned multiple names but we need just one. Select the best one. A reminder that the options are {agentlist}." + + # Customised message for select speaker auto method where no agent names are returned + custom_no_names_msg = "You forgot to select a single names and we need one, and only one. Select the best one. A reminder that the options are {agentlist}." + + # Test empty is_termination_msg function + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template=custom_multiple_names_msg, + select_speaker_auto_none_template=custom_no_names_msg, + ) + + # Test using the _validate_speaker_name function, checking for the correct string and agentlist to be included + agents = [agent1, agent2] + + messages = [{"content": "Alice and Bob should both speak.", "name": "speaker_selector", "role": "user"}] + assert groupchat._validate_speaker_name(None, messages, None, None, 1, 1, agents) == ( + True, + { + "content": custom_multiple_names_msg.replace("{agentlist}", "['Alice', 'Bob']"), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + messages = [{"content": "Fred should both speak.", "name": "speaker_selector", "role": "user"}] + assert groupchat._validate_speaker_name(None, messages, None, None, 1, 1, agents) == ( + True, + { + "content": custom_no_names_msg.replace("{agentlist}", "['Alice', 'Bob']"), + "override_role": groupchat.role_for_select_speaker_messages, + }, + ) + + # Test with empty strings + with pytest.raises(ValueError, match="select_speaker_auto_multiple_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template="", + ) + + with pytest.raises(ValueError, match="select_speaker_auto_none_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_none_template="", + ) + + # Test with None + with pytest.raises(ValueError, match="select_speaker_auto_multiple_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_multiple_template=None, + ) + + with pytest.raises(ValueError, match="select_speaker_auto_none_template cannot be empty or None."): + groupchat = autogen.GroupChat( + agents=[agent1, agent2], + messages=[], + speaker_selection_method="auto", + max_round=10, + select_speaker_auto_none_template=None, + ) + + if __name__ == "__main__": # test_func_call_groupchat() # test_broadcast() @@ -1675,4 +1786,5 @@ def test_speaker_selection_validate_speaker_name(): # test_speaker_selection_agent_name_match() test_speaker_selection_auto_process_result() test_speaker_selection_validate_speaker_name() + test_select_speaker_auto_messages() # pass From 298d36b6dc4bd95b12a441b8ff9a38d9e3e27a6a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Apr 2024 21:48:40 +0000 Subject: [PATCH 13/14] Removed block for manual selection in select_speaker function. --- autogen/agentchat/groupchat.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index a1b6e55aefb5..68dd86924937 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -521,13 +521,8 @@ def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Age if selected_agent: return selected_agent - if self.speaker_selection_method == "manual": - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = selector.generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) - else: - # auto speaker selection with 2-agent chat - return self._auto_select_speaker(last_speaker, selector, messages, agents) + # auto speaker selection with 2-agent chat + return self._auto_select_speaker(last_speaker, selector, messages, agents) async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent: """Select the next speaker (with requery), asynchronously.""" @@ -536,13 +531,8 @@ async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent if selected_agent: return selected_agent - if self.speaker_selection_method == "manual": - selector.update_system_message(self.select_speaker_msg(agents)) - final, name = await selector.a_generate_oai_reply(messages) - return self._finalize_speaker(last_speaker, final, name, agents) - else: - # auto speaker selection with 2-agent chat - return await self.a_auto_select_speaker(last_speaker, selector, messages, agents) + # auto speaker selection with 2-agent chat + return await self.a_auto_select_speaker(last_speaker, selector, messages, agents) def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: Optional[List[Agent]]) -> Agent: if not final: From 7e5a44a546d0d3d5f04186172f4d75be07435a41 Mon Sep 17 00:00:00 2001 From: Mark Sze <66362098+marklysze@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:24:53 +1000 Subject: [PATCH 14/14] Catered for no-selection during manual selection mode --- autogen/agentchat/groupchat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index 68dd86924937..864924550803 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -520,6 +520,9 @@ def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Age selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent + elif self.speaker_selection_method == "manual": + # An agent has not been selected while in manual mode, so move to the next agent + return self.next_agent(last_speaker) # auto speaker selection with 2-agent chat return self._auto_select_speaker(last_speaker, selector, messages, agents) @@ -530,6 +533,9 @@ async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker) if selected_agent: return selected_agent + elif self.speaker_selection_method == "manual": + # An agent has not been selected while in manual mode, so move to the next agent + return self.next_agent(last_speaker) # auto speaker selection with 2-agent chat return await self.a_auto_select_speaker(last_speaker, selector, messages, agents)