diff --git a/autogen/agentchat/groupchat.py b/autogen/agentchat/groupchat.py index fae72f26ac49..0ab8753964d4 100644 --- a/autogen/agentchat/groupchat.py +++ b/autogen/agentchat/groupchat.py @@ -3,61 +3,109 @@ from typing import Dict, List, Optional, Union from .agent import Agent from .conversable_agent import ConversableAgent +from .. import oai +import random @dataclass class GroupChat: """A group chat class that contains a list of agents and the maximum number of rounds.""" - agents: List[Agent] + agents: List[ConversableAgent] messages: List[Dict] max_round: int = 10 - admin_name: str = "Admin" # the name of the admin agent + llm_config: Optional[Dict] = None + admin_name: Optional[str] = None + + @property + def admin(self): + """ + if admin_name is None, then return the first agent in the list + otherwise, return the agent with the name admin_name + """ + if self.admin_name is None: + return self.agents[0] + else: + return self.agent_by_name(self.admin_name) @property def agent_names(self) -> List[str]: """Return the names of the agents in the group chat.""" - return [agent.name for agent in self.agents] + return [agent.name.lower() for agent in self.agents] def reset(self): """Reset the group chat.""" self.messages.clear() + def process_role_play_msgs(self, messages: List[Dict]) -> List[Dict]: + return [ + { + "role": "user", + "content": f"""From {message["name"]}: +{message["content"]}""", + } + for message in messages + ] + def agent_by_name(self, name: str) -> Agent: """Find the next speaker based on the message.""" - return self.agents[self.agent_names.index(name)] + return self.agents[self.agent_names.index(name.lower())] def next_agent(self, agent: Agent) -> Agent: """Return the next agent in the list.""" - return self.agents[(self.agent_names.index(agent.name) + 1) % len(self.agents)] + return self.agents[(self.agent_names.index(agent.name.lower()) + 1) % len(self.agents)] - def select_speaker_msg(self): + def select_speaker_msgs(self) -> List[Dict]: """Return the message for selecting the next speaker.""" - return f"""You are in a role play game. The following roles are available: -{self._participant_roles()}. + msgs = [ + { + "role": "system", + "content": "You are in a role play game. Each conversation must start with 'From {name}:', e.g: From admin: //your message//.", + } + ] -Read the following conversation. -Then select the next role from {self.agent_names} to play. Only return the role.""" + # # process role information + # # each agent introduce the next agent + # for i in range(len(self.agents)): + # current_agent = self.agents[i] + # next_agent = self.next_agent(current_agent) + # msgs.append({ + # "role": "user", + # "content": f'''From {current_agent.name}: + # {next_agent.name}, {next_agent.system_message}''', + # }) - def select_speaker(self, last_speaker: Agent, selector: ConversableAgent): + return msgs + + def select_speaker(self, last_speaker: Agent): """Select the next speaker.""" - selector.update_system_message(self.select_speaker_msg()) - final, name = selector.generate_oai_reply( - self.messages - + [ - { - "role": "system", - "content": f"Read the above conversation. Then select the next role from {self.agent_names} to play. Only return the role.", - } - ] - ) - if not final: - # i = self._random.randint(0, len(self._agent_names) - 1) # randomly pick an id + llm_config = self.llm_config + + # if self.llm_config is None, randomly select + if llm_config is None: + # search through its agents and randomly select a llm_config from one of them if it exists + # shuffle the agents + llm_configs = [agent.llm_config.copy() for agent in self.agents if isinstance(agent.llm_config, dict)] + if len(llm_configs) > 0: + llm_config = random.choice(llm_configs) + else: + llm_config = None + + # if llm_config is still None, then return the next agent + if llm_config is None: return self.next_agent(last_speaker) + try: + system_messages = self.select_speaker_msgs() + chat_messages = self.process_role_play_msgs(self.messages) + llm_config["stop"] = [":"] + msgs = system_messages + chat_messages + reply = oai.ChatCompletion.create(messages=msgs, **llm_config) + msg = reply["choices"][0]["message"]["content"] + name = msg.split(":")[0].split("From ")[1] return self.agent_by_name(name) - except ValueError: - return self.next_agent(last_speaker) + except Exception: + return self.admin def _participant_roles(self): return "\n".join([f"{agent.name}: {agent.system_message}" for agent in self.agents]) @@ -84,9 +132,19 @@ def __init__( system_message=system_message, **kwargs, ) + self.groupchat = groupchat self.register_reply(Agent, GroupChatManager.run_chat, config=groupchat, reset_config=GroupChat.reset) # self._random = random.Random(seed) + def _process_received_message(self, message, sender, silent): + message_with_name = { + "content": message, + "role": "user", + "name": sender.name, + } + self.groupchat.messages.append(message_with_name) + return super()._process_received_message(message, sender, silent) + def run_chat( self, messages: Optional[List[Dict]] = None, @@ -94,6 +152,7 @@ def run_chat( config: Optional[GroupChat] = None, ) -> Union[str, Dict, None]: """Run a group chat.""" + if messages is None: messages = self._oai_messages[sender] message = messages[-1] @@ -103,19 +162,56 @@ def run_chat( # set the name to speaker's name if the role is not function if message["role"] != "function": message["name"] = speaker.name - groupchat.messages.append(message) - # broadcast the message to all agents except the speaker + + # sync the message + msg = { + "content": message["content"], + "name": speaker.name if speaker is not None else "Unknown", + } + + if not isinstance(self, GroupChatManager): + groupchat.messages.append(msg) + + # distribute the message to all agents + msg_with_name = { + "content": f"""From {msg["name"]}: +{msg["content"]}""", + "role": "user", + } for agent in groupchat.agents: if agent != speaker: - self.send(message, agent, request_reply=False, silent=True) + agent.receive(msg_with_name, self, request_reply=False, silent=True) + # self.send(msg_with_name["content"], agent, request_reply=False, silent=True) + if i == groupchat.max_round - 1: # the last round break try: # select the next speaker - speaker = groupchat.select_speaker(speaker, self) + speaker = groupchat.select_speaker(speaker) + + # add : as stop sequence if llm_config is not None + if isinstance(speaker, ConversableAgent) and isinstance(speaker.llm_config, dict): + if "stop" in speaker.llm_config: + speaker.llm_config["stop"].append(":") + else: + speaker.llm_config["stop"] = [":"] # let the speaker speak reply = speaker.generate_reply(sender=self) + # restore the stop sequence + if isinstance(speaker, ConversableAgent) and isinstance(speaker.llm_config, dict): + if "stop" in speaker.llm_config: + speaker.llm_config["stop"].remove(":") + + if reply is None: + break + # if reply is 'From xxx', then set reply to xxx, it's your turn to speak + if reply.startswith("From ") and reply.split("From ")[1].lower() in groupchat.agent_names: + name = reply.split("From ")[1] + if name.lower() == speaker.name.lower(): + agents_except_speaker = [agent for agent in groupchat.agents if agent != speaker] + speaker = random.choice(agents_except_speaker) + reply = f"{name}, it's your turn to speak." except KeyboardInterrupt: # let the admin agent speak if interrupted if groupchat.admin_name in groupchat.agent_names: @@ -127,6 +223,7 @@ def run_chat( raise if reply is None: break + # The speaker sends the message without requesting a reply speaker.send(reply, self, request_reply=False) message = self.last_message(speaker) diff --git a/test/agentchat/test_groupchat.py b/test/agentchat/test_groupchat.py index 5c5d3fb8257b..82b2af828759 100644 --- a/test/agentchat/test_groupchat.py +++ b/test/agentchat/test_groupchat.py @@ -1,36 +1,265 @@ +import pytest import autogen +import random +from test_assistant_agent import KEY_LOC, OAI_CONFIG_LIST -def test_chat_manager(): - agent1 = autogen.ConversableAgent( +def skip_if_openai_not_available(): + try: + import openai + except ImportError: + pytest.skip("OpenAI API key not found.") + + +def test_group_chat_when_group_chat_llm_config_is_none(): + """ + Test group chat when groupchat's llm config is None. + In this case, the group chat manager will simply select the next agent. + """ + alice = autogen.ConversableAgent( "alice", max_consecutive_auto_reply=2, human_input_mode="NEVER", llm_config=False, default_auto_reply="This is alice sepaking.", ) - agent2 = autogen.ConversableAgent( + bob = autogen.ConversableAgent( "bob", max_consecutive_auto_reply=2, human_input_mode="NEVER", llm_config=False, default_auto_reply="This is bob speaking.", ) - groupchat = autogen.GroupChat(agents=[agent1, agent2], messages=[], max_round=2) + groupchat = autogen.GroupChat(agents=[alice, bob], messages=[], max_round=2) group_chat_manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=False) - agent1.initiate_chat(group_chat_manager, message="hello") + alice.initiate_chat(group_chat_manager, message="hello") - assert len(agent1.chat_messages[group_chat_manager]) == 2 assert len(groupchat.messages) == 2 + assert len(alice.chat_messages[group_chat_manager]) == 2 + + assert alice.chat_messages[group_chat_manager][0]["content"] == "hello" + assert alice.chat_messages[group_chat_manager][0]["role"] == "assistant" + assert alice.chat_messages[group_chat_manager][1]["content"] == "From bob:\nThis is bob speaking." + assert alice.chat_messages[group_chat_manager][1]["role"] == "user" + assert bob.chat_messages[group_chat_manager][0]["content"] == "From alice:\nhello" + assert bob.chat_messages[group_chat_manager][0]["role"] == "user" + assert bob.chat_messages[group_chat_manager][1]["content"] == "This is bob speaking." + assert bob.chat_messages[group_chat_manager][1]["role"] == "assistant" group_chat_manager.reset() assert len(groupchat.messages) == 0 - agent1.reset() - agent2.reset() - agent2.initiate_chat(group_chat_manager, message="hello") + alice.reset() + bob.reset() + bob.initiate_chat(group_chat_manager, message="hello") assert len(groupchat.messages) == 2 +def test_group_chat_math_class(): + """ + This test case is to simulate a math class. + where teacher creates math questions and student resolves the questions. + teacher will create a question, student will resolve the question and tell teacher the answer. + If the answer is correct, teacher will create another question, otherwise, teacher will ask student to resolve the question again. + The class will end when teacher has created 10 questions. + + This test case is created to test the following features: + - speaker selection should work under a continuous q&a scenario among two agents and GPT 3.5 model. + - admin should end the class when teacher has created 10 questions. + """ + skip_if_openai_not_available() + config_list = autogen.config_list_from_json( + OAI_CONFIG_LIST, + file_location=KEY_LOC, + filter_dict={ + "model": ["gpt-3.5-turbo"], + }, + ) + gpt3_5_config = { + "model": "gpt-3.5-turbo", + "seed": random.randint(0, 100), # change the seed for different trials + "temperature": 0, + "config_list": config_list, + "request_timeout": 120, + } + user_proxy = autogen.UserProxyAgent( + name="Admin", + system_message="You say TERMINATE when teacher says [COMPLETE].", + code_execution_config=False, + llm_config=gpt3_5_config, + human_input_mode="NEVER", + ) + teacher = autogen.AssistantAgent( + "teacher", + system_message="""You are a pre-school math teacher, you create 10 math questions for student to resolve. + Create 1 question at a time, then ask student to resolve the question and check the answer. + If the answer is correct, you create another question, otherwise, you ask student to resolve the question again. + + Here are a few examples of questions: + ## question 1 + 1 + 1 = ? + student, please resolve the question and tell me the answer. + + ## question 2 + 1 + 2 = ? + student, please resolve the question and tell me the answer. + + Repeat the process until you have created 10 questions. Then say [COMPLETE] to let admin know the task is completed. + """, + llm_config=gpt3_5_config, + ) + + student = autogen.AssistantAgent( + "student", + system_message="""You are a pre-school student, you resolve the math questions from teacher. + Teacher will create a question, you resolve the question and tell teacher the answer. + If the answer is correct, teacher will create another question, otherwise, teacher will ask you to resolve the question again. + + Here are a few examples of answers: + ## answer 1 + hello teacher, the answer is 2. + + ## answer 2 + hello teacher, the answer is 3. + """, + llm_config=gpt3_5_config, + ) + groupchat = autogen.GroupChat(agents=[user_proxy, student, teacher], messages=[], max_round=50) + manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=gpt3_5_config) + user_proxy.send( + "welcome to the class. I'm admin here. Teacher, you create 10 math questions for student to answer. Let me know when student resolve all questions.", + manager, + ) + + teacher.send("I'm teacher, I will create 10 math questions for student to answer.", manager) + student.send("I'm student, I will answer teacher's questions.", manager) + + user_proxy.initiate_chat( + manager, + message="""teacher, please start""", + ) + + assert len(groupchat.messages) < 50 + # verify if teacher's last message is [COMPLETE] + teacher.chat_messages[manager][-1]["content"] == "[COMPLETE]" + + # verify if admin's last message is TERMINATE + user_proxy.chat_messages[manager][-1]["content"] == "TERMINATE" + + +def test_group_chat_coding_class(): + """ + This test case is to simulate a coding class. + where teacher creates algorithm questions and student resolves the questions. + teacher will create a question, student will resolve the question with python code and execute the code. Student will fix the code if there is any error. + If the code is correct, teacher will create another question, otherwise, teacher will help student fix the code. + The class will end when teacher has created 5 questions. + + This test case is created to test the following features: + - speaker selection should work under a continuous q&a scenario among multiple agents with back and forth conversation using GPT 3.5 model. + - admin should end the class when teacher has created 5 questions. + """ + skip_if_openai_not_available() + config_list_gpt_35 = autogen.config_list_from_json( + OAI_CONFIG_LIST, + file_location=KEY_LOC, + filter_dict={ + "model": ["gpt-3.5-turbo"], + }, + ) + gpt_3_5_config = { + "model": "gpt-3.5-turbo", + "seed": random.randint(0, 100), # change the seed for different trials + "temperature": 0, + "config_list": config_list_gpt_35, + "request_timeout": 120, + } + user_proxy = autogen.UserProxyAgent( + name="Admin", + system_message="You say TERMINATE when teacher says [COMPLETE].", + code_execution_config=False, + llm_config=gpt_3_5_config, + human_input_mode="NEVER", + ) + + teacher = autogen.AssistantAgent( + "teacher", + system_message="""You are a python teacher, you create 5 mid-level leetcode algorithm questions for student to resolve. + Create 1 question at a time, then ask student to resolve the question using python. + If the answer is correct, you create another question, otherwise, you provide hint to help student fix the bug and ask student to resolve the question again. + + Here are a few examples of questions: + ## question 1 + // a leetcode question + student, please resolve the question and tell me the answer. + + ## question 2 + // a leetcode question + student, please resolve the question and tell me the answer. + + Repeat the process until student successfully resolve 5 questions. Then say [COMPLETE] to let admin know the task is completed. + """, + llm_config=gpt_3_5_config, + ) + + student = autogen.AssistantAgent( + "student", + system_message="""You are a student who wants to learn python, you resolve the algorithm questions from teacher. + Teacher will create a question, you resolve the question by providing python code. Teacher will help you fix the code if there is any error. + If the code is bug-free and correct, teacher will create another question, otherwise, teacher will ask you to resolve the question again. + + Here are a few examples of answers: + ## answer 1 + ```python + #code + ``` + + executer, please run the code. teacher, please check the result and let me know if the code is correct. + + ## answer 2 + ```python + #code + ``` + executer, please run the code. teacher, please check the result and let me know if the code is correct. + """, + llm_config=gpt_3_5_config, + ) + + executor = autogen.UserProxyAgent( + name="executor", + system_message="You are the executor. You run student's code and report result.", + code_execution_config={"last_n_messages": 3, "work_dir": "leetcode"}, + llm_config=gpt_3_5_config, + human_input_mode="NEVER", + default_auto_reply="no code received, student please send code.", + ) + + groupchat = autogen.GroupChat(agents=[user_proxy, student, teacher, executor], messages=[], max_round=50) + manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=gpt_3_5_config) + user_proxy.send( + "welcome to the class. I'm admin here. Teacher, you create 5 easy-level leetcode questions for student to resolve . Let me know when student resolve all questions.", + manager, + ) + + teacher.send("I'm teacher, I will create 5 easy-level leetcode question for student to answer.", manager) + student.send( + "I'm student, I will provide python code to resolve leetcode question. I'll try to fix the bug if there's any.", + manager, + ) + executor.send("I'm executor, I will run student's code and report result.", manager) + + user_proxy.initiate_chat( + manager, + message="""teacher, please start""", + ) + + assert len(groupchat.messages) < 50 + # verify if teacher's last message is [COMPLETE] + teacher.chat_messages[manager][-1]["content"] == "[COMPLETE]" + + # verify if admin's last message is TERMINATE + user_proxy.chat_messages[manager][-1]["content"] == "TERMINATE" + + def test_plugin(): # Give another Agent class ability to manage group chat agent1 = autogen.ConversableAgent( @@ -64,4 +293,6 @@ def test_plugin(): if __name__ == "__main__": # test_broadcast() # test_chat_manager() - test_plugin() + test_group_chat_math_class() + # test_group_chat_coding_class() + # test_plugin()