diff --git a/flaml/autogen/agentchat/__init__.py b/flaml/autogen/agentchat/__init__.py index 9058e4fd78..27cc2e495e 100644 --- a/flaml/autogen/agentchat/__init__.py +++ b/flaml/autogen/agentchat/__init__.py @@ -2,7 +2,7 @@ from .responsive_agent import ResponsiveAgent from .assistant_agent import AssistantAgent from .user_proxy_agent import UserProxyAgent -from .groupchat import GroupChatManager, GroupChatParticipant +from .groupchat import GroupChatManager __all__ = [ "Agent", @@ -10,5 +10,4 @@ "AssistantAgent", "UserProxyAgent", "GroupChatManager", - "GroupChatParticipant", ] diff --git a/flaml/autogen/agentchat/agent.py b/flaml/autogen/agentchat/agent.py index ba23c7b643..a25880ed4f 100644 --- a/flaml/autogen/agentchat/agent.py +++ b/flaml/autogen/agentchat/agent.py @@ -24,10 +24,10 @@ def name(self): """Get the name of the agent.""" return self._name - def send(self, message: Union[Dict, str], recipient: "Agent"): + def send(self, message: Union[Dict, str], recipient: "Agent", request_reply: Optional[bool] = None): """(Aabstract method) Send a message to another agent.""" - def receive(self, message: Union[Dict, str], sender: "Agent"): + def receive(self, message: Union[Dict, str], sender: "Agent", request_reply: Optional[bool] = None): """(Abstract method) Receive a message from another agent.""" def reset(self): diff --git a/flaml/autogen/agentchat/contrib/math_user_proxy_agent.py b/flaml/autogen/agentchat/contrib/math_user_proxy_agent.py index 2ab0a6401a..cc388f028c 100644 --- a/flaml/autogen/agentchat/contrib/math_user_proxy_agent.py +++ b/flaml/autogen/agentchat/contrib/math_user_proxy_agent.py @@ -165,7 +165,7 @@ def __init__( default_auto_reply=default_auto_reply, **kwargs, ) - self.register_auto_reply(Agent, self._generate_math_reply) + self.register_auto_reply(Agent, self._generate_math_reply, 1) # fixed var self._max_invalid_q_per_step = max_invalid_q_per_step @@ -283,7 +283,7 @@ def _generate_math_reply( ): """Generate an auto reply.""" if messages is None: - messages = self._oai_messages[sender.name] + messages = self._oai_messages[sender] message = messages[-1] message = message.get("content", "") code_blocks = extract_code(message) @@ -313,7 +313,7 @@ def _generate_math_reply( reply = reply.strip() if self.last_reply == reply: - return reply + "\nYour query or result is same from the last, please try a new approach." + return True, reply + "\nYour query or result is same from the last, please try a new approach." self.last_reply = reply if not all_success: diff --git a/flaml/autogen/agentchat/groupchat.py b/flaml/autogen/agentchat/groupchat.py index d61a617f49..51d99d012b 100644 --- a/flaml/autogen/agentchat/groupchat.py +++ b/flaml/autogen/agentchat/groupchat.py @@ -1,5 +1,5 @@ import sys -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Union from .agent import Agent from .responsive_agent import ResponsiveAgent @@ -7,19 +7,16 @@ class GroupChatManager(ResponsiveAgent): """(WIP) A chat manager agent that can manage a group chat of multiple agents.""" - agents: List["GroupChatParticipant"] + agents: List[Agent] max_round: int def _participant_roles(self): return "\n".join([f"{agent.name}: {agent.system_message}" for agent in self.agents]) def _select_speaker_msg(self): - return { - "role": "system", - "content": f"""You are in a role play game. The following roles are available: + return f"""You are in a role play game. The following roles are available: {self._participant_roles()}. Read the following conversation. -Then select the next role from {self._agent_names} to play. Only return the role.""", - } +Then select the next role from {self._agent_names} to play. Only return the role.""" def __init__( self, @@ -28,6 +25,7 @@ def __init__( # unlimited consecutive auto reply by default max_consecutive_auto_reply: Optional[int] = sys.maxsize, human_input_mode: Optional[str] = "NEVER", + system_message: Optional[str] = "Group chat manager.", # seed: Optional[int] = 4, **kwargs, ): @@ -37,11 +35,9 @@ def __init__( human_input_mode=human_input_mode, **kwargs, ) - self.register_auto_reply(GroupChatParticipant, self._generate_reply_for_participant) + self.register_auto_reply(Agent, self._generate_reply_for_participant) self.max_round = max_round self._agent_names = [] - self._next_speaker = None - self._round = 0 self._messages = [] # self._random = random.Random(seed) @@ -50,94 +46,43 @@ def _generate_reply_for_participant( messages: Optional[List[Dict]] = None, sender: Optional[Agent] = None, ) -> Union[str, Dict, None]: + self._agent_names = [agent.name for agent in self.agents] if messages is None: - messages = self._oai_messages[sender.name] + messages = self._oai_messages[sender] message = messages[-1] - # set the name to sender's name if the role is not function - if message["role"] != "function": - message["name"] = sender.name - self._messages.append(message) - self._next_speaker = None - # broadcast the message to all agents except the sender - for agent in self.agents: - if agent != sender: - self.send(message, agent) - if self._round == 0: - self._agent_names = [agent.name for agent in self.agents] - self._round += 1 - if self._round >= self.max_round: - return True, None - # speaker selection msg from an agent - self._next_speaker = self._select_speaker(sender) - self._next_speaker.send(self._next_speaker.generate_reply(sender=self), self) + speaker = sender + for i in range(self.max_round): + # set the name to speaker's name if the role is not function + if message["role"] != "function": + message["name"] = speaker.name + self._messages.append(message) + # broadcast the message to all agents except the speaker + for agent in self.agents: + if agent != speaker: + self.send(message, agent, request_reply=False) + if i != self.max_round - 1: + # speaker selection msg from an agent + speaker = self._select_speaker(speaker) + speaker.send(speaker.generate_reply(sender=self), self, request_reply=False) + message = self.last_message(speaker) return True, None - @property - def next_speaker(self): - """Return the next speaker.""" - return self._next_speaker - - def _select_speaker(self, last_speaker: "GroupChatParticipant"): + def _select_speaker(self, last_speaker: Agent): """Select the next speaker.""" - final, name = self._generate_oai_reply([self._select_speaker_msg()] + self._messages) + self.update_system_message(self._select_speaker_msg()) + final, name = self._generate_oai_reply(self._messages) if not final: # i = self._random.randint(0, len(self._agent_names) - 1) # randomly pick an id - name = self._agent_names[(self._agent_names.index(last_speaker.name) + 1) % len(self._agent_names)] - return self.agent_by_name(name) + return self.agents[(self._agent_names.index(last_speaker.name) + 1) % len(self._agent_names)] + try: + return self.agent_by_name(name) + except ValueError: + return self.agents[(self._agent_names.index(last_speaker.name) + 1) % len(self._agent_names)] - def agent_by_name(self, name: str) -> "GroupChatParticipant": + def agent_by_name(self, name: str) -> Agent: """Find the next speaker based on the message.""" return self.agents[self._agent_names.index(name)] def reset(self): super().reset() - self._round = 0 self._messages.clear() - self._next_speaker = None - - -class GroupChatParticipant(ResponsiveAgent): - """(WIP) A group chat participant agent that can participate in a group chat.""" - - group_chat_manager: GroupChatManager - - def __init__( - self, - name, - group_chat_manager=None, - **kwargs, - ): - super().__init__( - name=name, - **kwargs, - ) - self.register_auto_reply(GroupChatManager, self._generate_reply_for_chat_manager) - self.group_chat_manager = group_chat_manager - - def _generate_reply_for_chat_manager( - self, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - ) -> Tuple[bool, Union[str, Dict, None]]: - """Generate reply for the chat manager.""" - return self.group_chat_manager.next_speaker != self, None - - -# def _speaker_selection(self, instruction): -# """Select the next speaker.""" -# if self.llm_config is False: -# if self.human_input_mode == "NEVER": -# return self.name -# else: -# return self.get_human_input(instruction["content"]) -# sender = self.chat_manager.room -# roles_msg = { -# "content": f"""The following roles are available: -# {self._participant_roles()}""", -# "role": "system", -# } -# old_system_msg = self.system_message -# self.update_system_message(instruction["content"]) -# reply = self._generate_oai_reply([roles_msg] + self.chat_messages[sender.name]) -# self.update_system_message(old_system_msg) -# return reply diff --git a/flaml/autogen/agentchat/responsive_agent.py b/flaml/autogen/agentchat/responsive_agent.py index 203c790ce9..145a1a341f 100644 --- a/flaml/autogen/agentchat/responsive_agent.py +++ b/flaml/autogen/agentchat/responsive_agent.py @@ -109,22 +109,27 @@ def __init__( self._function_map = {} if function_map is None else function_map self._default_auto_reply = default_auto_reply self._class_specific_reply = [] + self.reply_at_receive = defaultdict(bool) self.register_auto_reply(Agent, self._generate_oai_reply) self.register_auto_reply(Agent, self._generate_code_execution_reply) self.register_auto_reply(Agent, self._generate_function_call_reply) + self.register_auto_reply(Agent, self._check_termination_and_human_reply) - def register_auto_reply(self, class_type, reply_func: Callable): + def register_auto_reply(self, class_type, reply_func: Callable, position: int = 0): """Register a class-specific reply function. The class-specific reply function will be called when the sender is an instance of the class_type. - The function registered later will be checked earlier. + The function registered later will be checked earlier by default. + To change the order, set the position to a positive integer. Args: class_type (Class): the class type. reply_func (Callable): the reply function. + position (int): the position of the reply function in the reply function list. """ - self._class_specific_reply.append((class_type, reply_func)) + self._class_specific_reply.insert(position, (class_type, reply_func)) + @property def system_message(self): """Return the system message.""" return self._oai_system_message[0]["content"] @@ -149,13 +154,11 @@ def update_max_consecutive_auto_reply(self, value: int, sender: Optional[Agent] for k in self._max_consecutive_auto_reply_dict: self._max_consecutive_auto_reply_dict[k] = value else: - self._max_consecutive_auto_reply_dict[sender.name] = value + self._max_consecutive_auto_reply_dict[sender] = value def max_consecutive_auto_reply(self, sender: Optional[Agent] = None) -> int: """The maximum number of consecutive auto replies.""" - return ( - self._max_consecutive_auto_reply if sender is None else self._max_consecutive_auto_reply_dict[sender.name] - ) + return self._max_consecutive_auto_reply if sender is None else self._max_consecutive_auto_reply_dict[sender] @property def chat_messages(self) -> Dict[str, List[Dict]]: @@ -181,7 +184,7 @@ def last_message(self, agent: Optional[Agent] = None) -> Dict: for conversation in self._oai_messages.values(): return conversation[-1] raise ValueError("More than one conversation is found. Please specify the sender to get the last message.") - return self._oai_messages[agent.name][-1] + return self._oai_messages[agent][-1] @property def use_docker(self) -> Union[bool, str, None]: @@ -200,7 +203,7 @@ def _message_to_dict(message: Union[Dict, str]): else: return message - def _append_oai_message(self, message: Union[Dict, str], role, conversation_id) -> bool: + def _append_oai_message(self, message: Union[Dict, str], role, conversation_id: Agent) -> bool: """Append a message to the ChatCompletion conversation. If the message received is a string, it will be put in the "content" field of the new dictionary. @@ -210,7 +213,7 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id) Args: message (dict or str): message to be appended to the ChatCompletion conversation. role (str): role of the message, can be "assistant" or "function". - conversation_id (str): id of the conversation, should be the name of the recipient or sender. + conversation_id (Agent): id of the conversation, should be the recipient or sender. Returns: bool: whether the message is appended to the ChatCompletion conversation. @@ -225,7 +228,7 @@ def _append_oai_message(self, message: Union[Dict, str], role, conversation_id) self._oai_messages[conversation_id].append(oai_message) return True - def send(self, message: Union[Dict, str], recipient: Agent): + def send(self, message: Union[Dict, str], recipient: Agent, request_reply: Optional[bool] = None) -> bool: """Send a message to another agent. Args: @@ -252,15 +255,16 @@ def send(self, message: Union[Dict, str], recipient: Agent): So effectively, this provides a way for an agent to send a "link" and modify the content of the "link" later. recipient (Agent): the recipient of the message. + request_reply (bool or None): whether to request a reply from the recipient. Raises: ValueError: if the message can't be converted into a valid ChatCompletion message. """ # When the agent composes and sends the message, the role of the message is "assistant" # unless it's "function". - valid = self._append_oai_message(message, "assistant", recipient.name) + valid = self._append_oai_message(message, "assistant", recipient) if valid: - recipient.receive(message, self) + recipient.receive(message, self, request_reply) else: raise ValueError( "Message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." @@ -296,7 +300,7 @@ def _print_received_message(self, message: Union[Dict, str], sender: Agent): print(colored("*" * len(func_print), "green"), flush=True) print("\n", "-" * 80, flush=True, sep="") - def receive(self, message: Union[Dict, str], sender: Agent): + def receive(self, message: Union[Dict, str], sender: Agent, request_reply: Optional[bool] = None): """Receive a message from another agent. Once a message is received, this function sends a reply to the sender or stop. @@ -312,18 +316,22 @@ def receive(self, message: Union[Dict, str], sender: Agent): 5. "context" (dict): the context of the message, which will be passed to [autogen.Completion.create](../oai/Completion#create). sender: sender of an Agent instance. + request_reply (bool or None): whether a reply is requested from the sender. + If None, the value is determined by `self.reply_at_receive[sender]`. Raises: ValueError: if the message can't be converted into a valid ChatCompletion message. """ message = self._message_to_dict(message) # When the agent receives a message, the role of the message is "user". (If 'role' exists and is 'function', it will remain unchanged.) - valid = self._append_oai_message(message, "user", sender.name) + valid = self._append_oai_message(message, "user", sender) if not valid: raise ValueError( "Received message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." ) self._print_received_message(message, sender) + if request_reply is False or request_reply is None and self.reply_at_receive[sender] is False: + return reply = self.generate_reply(sender=sender) if reply is not None: self.send(reply, sender) @@ -343,6 +351,7 @@ def initiate_chat(self, recipient: "ResponsiveAgent", clear_history: Optional[bo """ self.reset_consecutive_auto_reply_counter(recipient) recipient.reset_consecutive_auto_reply_counter(self) + self.reply_at_receive[recipient] = recipient.reply_at_receive[self] = True if clear_history: self.clear_history(recipient) recipient.clear_history(self) @@ -352,13 +361,21 @@ def reset(self): """Reset the agent.""" self.clear_history() self.reset_consecutive_auto_reply_counter() + self.stop_reply_at_receive() + + def stop_reply_at_receive(self, sender: Optional[Agent] = None): + """Reset the reply_at_receive of the sender.""" + if sender is None: + self.reply_at_receive.clear() + else: + self.reply_at_receive[sender] = False def reset_consecutive_auto_reply_counter(self, sender: Optional[Agent] = None): """Reset the consecutive_auto_reply_counter of the sender.""" if sender is None: self._consecutive_auto_reply_counter.clear() else: - self._consecutive_auto_reply_counter[sender.name] = 0 + self._consecutive_auto_reply_counter[sender] = 0 def clear_history(self, agent: Optional[Agent] = None): """Clear the chat history of the agent. @@ -369,7 +386,7 @@ def clear_history(self, agent: Optional[Agent] = None): if agent is None: self._oai_messages.clear() else: - self._oai_messages[agent.name].clear() + self._oai_messages[agent].clear() def _generate_oai_reply( self, @@ -379,7 +396,7 @@ def _generate_oai_reply( if self.llm_config is False: return False, None if messages is None: - messages = self._oai_messages[sender.name] + messages = self._oai_messages[sender] # TODO: #1143 handle token limit exceeded error response = oai.ChatCompletion.create( @@ -387,13 +404,48 @@ def _generate_oai_reply( ) return True, oai.ChatCompletion.extract_text_or_function_call(response)[0] + def _generate_code_execution_reply( + self, + messages: Optional[List[Dict]] = None, + sender: Optional[Agent] = None, + ): + if self._code_execution_config is False: + return False, None + if messages is None: + messages = self._oai_messages[sender] + message = messages[-1] + code_blocks = extract_code(message["content"]) + if len(code_blocks) == 1 and code_blocks[0][0] == UNKNOWN: + # no code block is found, lang should be `UNKNOWN` + return False, None + # code_blocks, _ = find_code(messages, sys_msg=self._oai_system_message, **self.llm_config) + # if len(code_blocks) == 1 and code_blocks[0][0] == UNKNOWN: + # return code_blocks[0][1] + # try to execute the code + exitcode, logs = self.execute_code_blocks(code_blocks) + exitcode2str = "execution succeeded" if exitcode == 0 else "execution failed" + return True, f"exitcode: {exitcode} ({exitcode2str})\nCode output: {logs}" + + def _generate_function_call_reply( + self, + messages: Optional[List[Dict]] = None, + sender: Optional[Agent] = None, + ): + if messages is None: + messages = self._oai_messages[sender] + message = messages[-1] + if "function_call" in message: + _, func_return = self.execute_function(message["function_call"]) + return True, func_return + return False, None + def _check_termination_and_human_reply( self, messages: Optional[List[Dict]] = None, sender: Optional[Agent] = None, ) -> Tuple[bool, Union[str, Dict, None]]: if messages is None: - messages = self._oai_messages[sender.name] + messages = self._oai_messages[sender] message = messages[-1] reply = "" no_human_input_msg = "" @@ -405,7 +457,7 @@ def _check_termination_and_human_reply( # if the human input is empty, and the message is a termination message, then we will terminate the conversation reply = reply if reply or not self._is_termination_msg(message) else "exit" else: - if self._consecutive_auto_reply_counter[sender.name] >= self._max_consecutive_auto_reply_dict[sender.name]: + if self._consecutive_auto_reply_counter[sender] >= self._max_consecutive_auto_reply_dict[sender]: if self.human_input_mode == "NEVER": reply = "exit" else: @@ -438,84 +490,58 @@ def _check_termination_and_human_reply( # stop the conversation if reply == "exit": # reset the consecutive_auto_reply_counter - self._consecutive_auto_reply_counter[sender.name] = 0 + self._consecutive_auto_reply_counter[sender] = 0 return True, None # send the human reply - if reply or self._max_consecutive_auto_reply_dict[sender.name] == 0: + if reply or self._max_consecutive_auto_reply_dict[sender] == 0: # reset the consecutive_auto_reply_counter - self._consecutive_auto_reply_counter[sender.name] = 0 + self._consecutive_auto_reply_counter[sender] = 0 return True, reply # increment the consecutive_auto_reply_counter - self._consecutive_auto_reply_counter[sender.name] += 1 + self._consecutive_auto_reply_counter[sender] += 1 if self.human_input_mode != "NEVER": print(colored("\n>>>>>>>> USING AUTO REPLY...", "red"), flush=True) return False, None - def _generate_function_call_reply( - self, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - ): - if messages is None: - messages = self._oai_messages[sender.name] - message = messages[-1] - if "function_call" in message: - _, func_return = self.execute_function(message["function_call"]) - return True, func_return - return False, None - - def _generate_code_execution_reply( - self, - messages: Optional[List[Dict]] = None, - sender: Optional[Agent] = None, - ): - if self._code_execution_config is False: - return False, None - if messages is None: - messages = self._oai_messages[sender.name] - message = messages[-1] - code_blocks = extract_code(message["content"]) - if len(code_blocks) == 1 and code_blocks[0][0] == UNKNOWN: - # no code block is found, lang should be `UNKNOWN` - return False, None - # code_blocks, _ = find_code(messages, sys_msg=self._oai_system_message, **self.llm_config) - # if len(code_blocks) == 1 and code_blocks[0][0] == UNKNOWN: - # return code_blocks[0][1] - # try to execute the code - exitcode, logs = self.execute_code_blocks(code_blocks) - exitcode2str = "execution succeeded" if exitcode == 0 else "execution failed" - return True, f"exitcode: {exitcode} ({exitcode2str})\nCode output: {logs}" - def generate_reply( self, messages: Optional[List[Dict]] = None, sender: Optional[Agent] = None, + exclude: Optional[List[Callable]] = None, ) -> Union[str, Dict, None]: - """Reply based on the conversation history. + """Reply based on the conversation history and the sender. - First, execute function or code and return the result. - AI replies are generated only when no code execution is performed. - Subclasses can override this method to customize the reply. Either messages or sender must be provided. + Use registered class-specific reply functions to generate replies. + By default, the following functions are checked in order: + 1. _check_termination_and_human_reply + 2. _generate_function_call_reply + 3. _generate_code_execution_reply + 4. _generate_oai_reply + Every function returns a tuple (final, reply). + When a function returns final=False, the next function will be checked. + So by default, termination and human reply will be checked first. + If not terminating and human reply is skipped, execute function or code and return the result. + AI replies are generated only when no code execution is performed. Args: messages: a list of messages in the conversation history. default_reply (str or dict): default reply. sender: sender of an Agent instance. + exclude: a list of functions to exclude. Returns: str or dict or None: reply. None if no reply is generated. """ assert messages is not None or sender is not None, "Either messages or sender must be provided." - final, reply = self._check_termination_and_human_reply(sender=sender) - if final: - return reply if sender is not None: - for class_specifc_reply in self._class_specific_reply[-1::-1]: - if isinstance(sender, class_specifc_reply[0]): + for class_specifc_reply in self._class_specific_reply: + if isinstance(sender, class_specifc_reply[0]) and ( + not exclude or class_specifc_reply[1] not in exclude + ): final, reply = class_specifc_reply[1](messages, sender) if final: return reply diff --git a/notebook/autogen_agentchat_chess.ipynb b/notebook/autogen_agentchat_chess.ipynb index acb091953b..65cf772d9b 100644 --- a/notebook/autogen_agentchat_chess.ipynb +++ b/notebook/autogen_agentchat_chess.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -8,6 +9,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -45,6 +47,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -82,6 +85,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -117,6 +121,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -131,6 +136,7 @@ "metadata": {}, "outputs": [], "source": [ + "from collections import defaultdict\n", "from typing import Dict, List, Optional, Union\n", "\n", "sys_msg = \"\"\"You are an AI-powered chess board agent.\n", @@ -158,39 +164,39 @@ " llm_config={\"temperature\": 0.0, \"config_list\": config_list_gpt4},\n", " max_consecutive_auto_reply=10,\n", " )\n", + " self.register_auto_reply(autogen.ResponsiveAgent, self._generate_board_reply)\n", " self._board = board\n", - " \n", - " def generate_reply(\n", + " self._correct_move_messages = defaultdict(list)\n", + "\n", + " def _generate_board_reply(\n", " self,\n", " messages: Optional[List[Dict]] = None,\n", " sender: Optional[autogen.Agent] = None,\n", " ) -> Union[str, Dict, None]:\n", " # Filter for messages that do not contain error.\n", " if messages is None:\n", - " messages = self._oai_messages[sender.name]\n", - " filtered_messages = []\n", - " buf = []\n", - " for message in messages:\n", - " buf.append(message)\n", - " if message.get(\"role\") != \"user\":\n", - " if not message.get(\"content\").startswith(\"Error\"):\n", - " filtered_messages.extend(buf)\n", - " buf = []\n", + " messages = self._oai_messages[sender]\n", " message = messages[-1]\n", " assert message.get(\"role\") == \"user\"\n", - " reply = super().generate_reply(filtered_messages + [message], sender)\n", + " # extract a UCI move from player's message\n", + " reply = self.generate_reply(self._correct_move_messages[sender] + [message], sender, exclude=[self._generate_board_reply])\n", " if isinstance(reply, str):\n", " uci_move = reply\n", " else:\n", " uci_move = str(reply[\"content\"])\n", " try:\n", " self._board.push_uci(uci_move)\n", - " m = chess.Move.from_uci(uci_move)\n", - " display(chess.svg.board(self._board, arrows=[(m.from_square, m.to_square)], fill={m.from_square: \"gray\"}, size=200))\n", - " return uci_move\n", " except ValueError as e:\n", + " # invalid move\n", " error = f\"Error: {e}\"\n", - " return error" + " return True, error\n", + " else:\n", + " # valid move\n", + " m = chess.Move.from_uci(uci_move)\n", + " display(chess.svg.board(self._board, arrows=[(m.from_square, m.to_square)], fill={m.from_square: \"gray\"}, size=200))\n", + " self._correct_move_messages[sender].extend([message, self._message_to_dict(uci_move)])\n", + " self._correct_move_messages[sender][-1][\"role\"] = \"assistant\"\n", + " return True, uci_move\n" ] }, { @@ -204,7 +210,8 @@ "You are playing as {color}. \n", "You communicate your move using universal chess interface language.\n", "You also chit-chat with your opponent when you communicate a move to light up the mood.\n", - "You should make sure both you and the opponent are making legal moves.\"\"\"\n", + "You should make sure both you and the opponent are making legal moves.\n", + "Do not apologize for making illegal moves.\"\"\"\n", "\n", "\n", "class ChessPlayerAgent(autogen.AssistantAgent):\n", @@ -235,39 +242,54 @@ " max_consecutive_auto_reply=max_turns,\n", " **kwargs,\n", " )\n", + " self.register_auto_reply(BoardAgent, self._generate_reply_for_board)\n", + " self.register_auto_reply(ChessPlayerAgent, self._generate_reply_for_player)\n", " self._board_agent = board_agent\n", " self.update_max_consecutive_auto_reply(self._board_agent.max_consecutive_auto_reply(), self._board_agent)\n", "\n", - " def generate_reply(\n", + " def _generate_reply_for_board(\n", + " self,\n", + " messages: Optional[List[Dict]] = None,\n", + " sender: Optional[autogen.Agent] = None,\n", + " ) -> Union[str, Dict, None]:\n", + " if messages is None:\n", + " messages = self._oai_messages[sender]\n", + " # add a system message about the current state of the board.\n", + " board_state_msg = [{\"role\": \"system\", \"content\": f\"Current board:\\n{self._board_agent._board}\"}]\n", + " last_message = messages[-1]\n", + " if last_message[\"content\"].startswith(\"Error\"):\n", + " # try again\n", + " last_message[\"role\"] = \"system\"\n", + " return True, self.generate_reply(messages + board_state_msg, sender, exclude=[self._generate_reply_for_board])\n", + " else:\n", + " return True, None\n", + "\n", + " def _generate_reply_for_player(\n", " self,\n", " messages: Optional[List[Dict]] = None,\n", " sender: Optional[autogen.Agent] = None,\n", " ) -> Union[str, Dict, None]:\n", " if messages is None:\n", - " messages = self._oai_messages[sender.name]\n", + " messages = self._oai_messages[sender]\n", " # add a system message about the current state of the board.\n", " board_state_msg = [{\"role\": \"system\", \"content\": f\"Current board:\\n{self._board_agent._board}\"}]\n", - " if sender == self._board_agent:\n", - " last_message = messages[-1]\n", - " if last_message[\"content\"].startswith(\"Error\"):\n", - " last_message[\"role\"] = \"system\"\n", - " return super().generate_reply(messages + board_state_msg, sender)\n", - " else:\n", - " return\n", - " message = super().generate_reply(messages + board_state_msg, sender)\n", + " # propose a reply which will be sent to the board agent for verification.\n", + " message = self.generate_reply(messages + board_state_msg, sender, exclude=[self._generate_reply_for_player])\n", " if message is None:\n", - " return\n", + " return True, None\n", + " # converse with the board until a legal move is made or max allowed retries.\n", " self.initiate_chat(self._board_agent, clear_history=False, message=message)\n", " # last message sent by the board agent\n", - " last_message = self._oai_messages[self._board_agent.name][-1]\n", + " last_message = self._oai_messages[self._board_agent][-1]\n", " if last_message[\"role\"] == \"assistant\":\n", " # didn't make a legal move after a limit times of retries.\n", " print(f\"{self.name}: I yield.\")\n", - " return\n", - " return self._oai_messages[self._board_agent.name][-2]\n" + " return True, None\n", + " return True, self._oai_messages[self._board_agent][-2]\n" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -280,23 +302,26 @@ "metadata": {}, "outputs": [], "source": [ + "max_turn = 10\n", + "\n", "board = chess.Board()\n", "board_agent = BoardAgent(board=board)\n", "player_black = ChessPlayerAgent(\n", " color=\"black\",\n", " board_agent=board_agent,\n", - " max_turns=3,\n", + " max_turns=max_turn,\n", " llm_config={\"temperature\": 0.5, \"seed\": 1, \"config_list\": config_list_gpt4},\n", ")\n", "player_white = ChessPlayerAgent(\n", " color=\"white\",\n", " board_agent=board_agent,\n", - " max_turns=3,\n", + " max_turns=max_turn,\n", " llm_config={\"temperature\": 0.5, \"seed\": 2, \"config_list\": config_list_gpt4},\n", ")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -319,9 +344,8 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", "\n", - "Alright, let's get this game started. I'll move my pawn from e2 to e4. How's your day going, by the way? \n", - "\n", - "e2e4\n", + "e2e4. \n", + "A classic opening move, my friend. Let's see how this game unfolds. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -356,18 +380,14 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer white\u001b[0m (to Player black):\n", "\n", - "Alright, let's get this game started. I'll move my pawn from e2 to e4. How's your day going, by the way? \n", - "\n", - "e2e4\n", + "e2e4. \n", + "A classic opening move, my friend. Let's see how this game unfolds. Your move.\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", "\n", - "Hey, there seems to be a misunderstanding. You're playing as white and I am black. As white, you made the first move which is correct. My day's going well, thanks for asking. How about yours?\n", - "\n", - "Now, let me make my move. I'll move my pawn from e7 to e5.\n", - "\n", - "e7e5\n", + "e7e5. \n", + "Matching your classic opening with one of my own. Let's keep the game interesting. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -402,20 +422,14 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to Player white):\n", "\n", - "Hey, there seems to be a misunderstanding. You're playing as white and I am black. As white, you made the first move which is correct. My day's going well, thanks for asking. How about yours?\n", - "\n", - "Now, let me make my move. I'll move my pawn from e7 to e5.\n", - "\n", - "e7e5\n", + "e7e5. \n", + "Matching your classic opening with one of my own. Let's keep the game interesting. Your move.\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", "\n", - "Oh, I see. My apologies for the confusion. My day's been quite interesting. Thank you for asking. \n", - "\n", - "Now, let's continue with the game. I'll move my knight from g1 to f3. \n", - "\n", - "g1f3\n", + "g1f3. \n", + "Aiming to control the center of the board. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -450,20 +464,14 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer white\u001b[0m (to Player black):\n", "\n", - "Oh, I see. My apologies for the confusion. My day's been quite interesting. Thank you for asking. \n", - "\n", - "Now, let's continue with the game. I'll move my knight from g1 to f3. \n", - "\n", - "g1f3\n", + "g1f3. \n", + "Aiming to control the center of the board. Your move.\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", "\n", - "No worries at all. Interesting days are often the best ones, aren't they?\n", - "\n", - "Alright, I see your knight move. Let me respond by moving my knight from g8 to f6.\n", - "\n", - "g8f6\n", + "g8f6. \n", + "Aiming to control the center as well. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -498,20 +506,56 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to Player white):\n", "\n", - "No worries at all. Interesting days are often the best ones, aren't they?\n", + "g8f6. \n", + "Aiming to control the center as well. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", "\n", - "Alright, I see your knight move. Let me respond by moving my knight from g8 to f6.\n", + "b1c3. \n", + "Developing my knight to a good square. Your move.\n", "\n", - "g8f6\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r n b q k b . r\n",
+       "p p p p . p p p\n",
+       ". . . . . n . .\n",
+       ". . . . p . . .\n",
+       ". . . . P . . .\n",
+       ". . N . . N . .\n",
+       "P P P P . P P P\n",
+       "R . B Q K B . R
" + ], + "text/plain": [ + "'
r n b q k b . r\\np p p p . p p p\\n. . . . . n . .\\n. . . . p . . .\\n. . . . P . . .\\n. . N . . N . .\\nP P P P . P P P\\nR . B Q K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "b1c3\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", "\n", - "Indeed, interesting days keep life exciting. \n", + "b1c3. \n", + "Developing my knight to a good square. Your move.\n", "\n", - "Alright, your knight's move is noted. I'll proceed by moving my pawn from d2 to d4. \n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", "\n", - "d2d4\n", + "b8c6. \n", + "Developing my knight to match yours. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -519,17 +563,59 @@ { "data": { "image/svg+xml": [ - "
r n b q k b . r\n",
+       "
r . b q k b . r\n",
        "p p p p . p p p\n",
-       ". . . . . n . .\n",
+       ". . n . . n . .\n",
+       ". . . . p . . .\n",
+       ". . . . P . . .\n",
+       ". . N . . N . .\n",
+       "P P P P . P P P\n",
+       "R . B Q K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p p . p p p\\n. . n . . n . .\\n. . . . p . . .\\n. . . . P . . .\\n. . N . . N . .\\nP P P P . P P P\\nR . B Q K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "b8c6\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "b8c6. \n", + "Developing my knight to match yours. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "d2d4.\n", + "Challenging your pawn in the center. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p p . p p p\n",
+       ". . n . . n . .\n",
        ". . . . p . . .\n",
        ". . . P P . . .\n",
-       ". . . . . N . .\n",
+       ". . N . . N . .\n",
        "P P P . . P P P\n",
-       "R N B Q K B . R
" + "R . B Q K B . R
" ], "text/plain": [ - "'
r n b q k b . r\\np p p p . p p p\\n. . . . . n . .\\n. . . . p . . .\\n. . . P P . . .\\n. . . . . N . .\\nP P P . . P P P\\nR N B Q K B . R
'" + "'
r . b q k b . r\\np p p p . p p p\\n. . n . . n . .\\n. . . . p . . .\\n. . . P P . . .\\n. . N . . N . .\\nP P P . . P P P\\nR . B Q K B . R
'" ] }, "metadata": {}, @@ -546,20 +632,140 @@ "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer white\u001b[0m (to Player black):\n", "\n", - "Indeed, interesting days keep life exciting. \n", + "d2d4.\n", + "Challenging your pawn in the center. Your move.\n", "\n", - "Alright, your knight's move is noted. I'll proceed by moving my pawn from d2 to d4. \n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", "\n", - "d2d4\n", + "e5d4. \n", + "Taking your pawn. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p p . p p p\n",
+       ". . n . . n . .\n",
+       ". . . . . . . .\n",
+       ". . . p P . . .\n",
+       ". . N . . N . .\n",
+       "P P P . . P P P\n",
+       "R . B Q K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p p . p p p\\n. . n . . n . .\\n. . . . . . . .\\n. . . p P . . .\\n. . N . . N . .\\nP P P . . P P P\\nR . B Q K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "e5d4\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "e5d4. \n", + "Taking your pawn. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "f3d4.\n", + "Taking back the pawn and keeping the center balanced. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p p . p p p\n",
+       ". . n . . n . .\n",
+       ". . . . . . . .\n",
+       ". . . N P . . .\n",
+       ". . N . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B Q K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p p . p p p\\n. . n . . n . .\\n. . . . . . . .\\n. . . N P . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B Q K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "f3d4\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "f3d4.\n", + "Taking back the pawn and keeping the center balanced. Your move.\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", "\n", - "I see your pawn move. I hope the excitement of the day is translating into our game too!\n", + "c6d4. \n", + "Taking your knight with my knight. Your move.\n", "\n", - "Now, let me capture your pawn by moving my pawn from e5 to d4.\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p p . p p p\n",
+       ". . . . . n . .\n",
+       ". . . . . . . .\n",
+       ". . . n P . . .\n",
+       ". . N . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B Q K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p p . p p p\\n. . . . . n . .\\n. . . . . . . .\\n. . . n P . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B Q K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", "\n", - "e5d4\n", + "c6d4\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "c6d4. \n", + "Taking your knight with my knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "q1d4.\n", + "Taking back the knight with my queen. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] @@ -567,17 +773,59 @@ { "data": { "image/svg+xml": [ - "
r n b q k b . r\n",
+       "
r . b q k b . r\n",
        "p p p p . p p p\n",
        ". . . . . n . .\n",
        ". . . . . . . .\n",
-       ". . . p P . . .\n",
-       ". . . . . N . .\n",
+       ". . . Q P . . .\n",
+       ". . N . . . . .\n",
        "P P P . . P P P\n",
-       "R N B Q K B . R
" + "R . B . K B . R
" ], "text/plain": [ - "'
r n b q k b . r\\np p p p . p p p\\n. . . . . n . .\\n. . . . . . . .\\n. . . p P . . .\\n. . . . . N . .\\nP P P . . P P P\\nR N B Q K B . R
'" + "'
r . b q k b . r\\np p p p . p p p\\n. . . . . n . .\\n. . . . . . . .\\n. . . Q P . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "d1d4\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "q1d4.\n", + "Taking back the knight with my queen. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "d7d5. \n", + "Pushing my pawn to challenge your Queen. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p . . p p p\n",
+       ". . . . . n . .\n",
+       ". . . p . . . .\n",
+       ". . . Q P . . .\n",
+       ". . N . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p . . p p p\\n. . . . . n . .\\n. . . p . . . .\\n. . . Q P . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" ] }, "metadata": {}, @@ -589,23 +837,400 @@ "text": [ "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", "\n", - "e5d4\n", + "d7d5\n", "\n", "--------------------------------------------------------------------------------\n", "\u001b[33mPlayer black\u001b[0m (to Player white):\n", "\n", - "I see your pawn move. I hope the excitement of the day is translating into our game too!\n", + "d7d5. \n", + "Pushing my pawn to challenge your Queen. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", "\n", - "Now, let me capture your pawn by moving my pawn from e5 to d4.\n", + "e4d5.\n", + "Taking your pawn with my pawn. Your move.\n", "\n", - "e5d4\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p . . p p p\n",
+       ". . . . . n . .\n",
+       ". . . P . . . .\n",
+       ". . . Q . . . .\n",
+       ". . N . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p . . p p p\\n. . . . . n . .\\n. . . P . . . .\\n. . . Q . . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "e4d5\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "e4d5.\n", + "Taking your pawn with my pawn. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "f6d5. \n", + "Taking your pawn with my knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p . . p p p\n",
+       ". . . . . . . .\n",
+       ". . . n . . . .\n",
+       ". . . Q . . . .\n",
+       ". . N . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p . . p p p\\n. . . . . . . .\\n. . . n . . . .\\n. . . Q . . . .\\n. . N . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "f6d5\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "f6d5. \n", + "Taking your pawn with my knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "c3d5.\n", + "Taking your knight with my knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p p . . p p p\n",
+       ". . . . . . . .\n",
+       ". . . N . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p p . . p p p\\n. . . . . . . .\\n. . . N . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "c3d5\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "c3d5.\n", + "Taking your knight with my knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "c7c6. \n", + "Challenging your knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p . . . p p p\n",
+       ". . p . . . . .\n",
+       ". . . N . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p . . . p p p\\n. . p . . . . .\\n. . . N . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "c7c6\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "c7c6. \n", + "Challenging your knight. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "d5f6.\n", + "Moving my knight to capture your pawn, putting your king in check. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b q k b . r\n",
+       "p p . . . p p p\n",
+       ". . p . . N . .\n",
+       ". . . . . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b q k b . r\\np p . . . p p p\\n. . p . . N . .\\n. . . . . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "d5f6\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "d5f6.\n", + "Moving my knight to capture your pawn, putting your king in check. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "g8f6. \n", + "Moving my king to capture your knight and getting out of check. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "Error: illegal uci: 'g8f6' in r1bqkb1r/pp3ppp/2p2N2/8/3Q4/8/PPP2PPP/R1B1KB1R b KQkq - 1 9\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "d8f6. \n", + "My mistake, capturing your knight with my queen. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b . k b . r\n",
+       "p p . . . p p p\n",
+       ". . p . . q . .\n",
+       ". . . . . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . . P P P\n",
+       "R . B . K B . R
" + ], + "text/plain": [ + "'
r . b . k b . r\\np p . . . p p p\\n. . p . . q . .\\n. . . . . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . . P P P\\nR . B . K B . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "d8f6\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "d8f6. \n", + "My mistake, capturing your knight with my queen. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "d4d1.\n", + "Moving my queen away from your queen's attack. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "Error: illegal uci: 'd1d4' in r1b1kb1r/pp3ppp/2p2q2/8/3Q4/8/PPP2PPP/R1B1KB1R w KQkq - 0 10\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "e1g1.\n", + "Moving my king to safety with a castle. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "Error: illegal uci: 'e1g1' in r1b1kb1r/pp3ppp/2p2q2/8/3Q4/8/PPP2PPP/R1B1KB1R w KQkq - 0 10\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "g1e2.\n", + "Moving my king to safety. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "Error: illegal uci: 'g1e2' in r1b1kb1r/pp3ppp/2p2q2/8/3Q4/8/PPP2PPP/R1B1KB1R w KQkq - 0 10\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to BoardAgent):\n", + "\n", + "f1e2.\n", + "Moving my bishop to protect my king. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b . k b . r\n",
+       "p p . . . p p p\n",
+       ". . p . . q . .\n",
+       ". . . . . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . B P P P\n",
+       "R . B . K . . R
" + ], + "text/plain": [ + "'
r . b . k b . r\\np p . . . p p p\\n. . p . . q . .\\n. . . . . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . B P P P\\nR . B . K . . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player white):\n", + "\n", + "f1e2\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer white\u001b[0m (to Player black):\n", + "\n", + "f1e2.\n", + "Moving my bishop to protect my king. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to BoardAgent):\n", + "\n", + "f8e7. \n", + "Moving my bishop to protect my king. Your move.\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "
r . b . k . . r\n",
+       "p p . . b p p p\n",
+       ". . p . . q . .\n",
+       ". . . . . . . .\n",
+       ". . . Q . . . .\n",
+       ". . . . . . . .\n",
+       "P P P . B P P P\n",
+       "R . B . K . . R
" + ], + "text/plain": [ + "'
r . b . k . . r\\np p . . b p p p\\n. . p . . q . .\\n. . . . . . . .\\n. . . Q . . . .\\n. . . . . . . .\\nP P P . B P P P\\nR . B . K . . R
'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mBoardAgent\u001b[0m (to Player black):\n", + "\n", + "f8e7\n", + "\n", + "--------------------------------------------------------------------------------\n", + "\u001b[33mPlayer black\u001b[0m (to Player white):\n", + "\n", + "f8e7. \n", + "Moving my bishop to protect my king. Your move.\n", "\n", "--------------------------------------------------------------------------------\n" ] } ], "source": [ - "player_black.send(\"Your turn.\", player_white)" + "player_black.initiate_chat(player_white, message=\"Your turn.\")" ] } ], diff --git a/notebook/autogen_agentchat_groupchat.ipynb b/notebook/autogen_agentchat_groupchat.ipynb index fdfe77f972..3d46611c09 100644 --- a/notebook/autogen_agentchat_groupchat.ipynb +++ b/notebook/autogen_agentchat_groupchat.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -8,6 +9,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -34,6 +36,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -71,6 +74,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -106,6 +110,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -114,44 +119,31 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "import sys\n", - "\n", "llm_config = {\"config_list\": config_list_gpt4}\n", "group_chat_manager = autogen.GroupChatManager(max_round=4, llm_config=llm_config)\n", - "human = autogen.GroupChatParticipant(\n", + "human = autogen.UserProxyAgent(\n", " name=\"Human\",\n", " system_message=\"A human admin.\",\n", - " human_input_mode=\"ALWAYS\",\n", - " llm_config=False,\n", - " group_chat_manager=group_chat_manager,\n", ")\n", - "alice = autogen.GroupChatParticipant(\n", + "alice = autogen.AssistantAgent(\n", " name=\"Alice\",\n", - " system_message=autogen.AssistantAgent.DEFAULT_SYSTEM_MESSAGE,\n", - " max_consecutive_auto_reply=sys.maxsize,\n", - " human_input_mode=\"NEVER\",\n", " llm_config=llm_config,\n", - " code_execution_config=False,\n", - " group_chat_manager=group_chat_manager,\n", ")\n", - "bob = autogen.GroupChatParticipant(\n", + "bob = autogen.AssistantAgent(\n", " name=\"Bob\",\n", " system_message=\"Code reviewer. Prevent code execution if unsafe or not well documented. Suggest changes. Otherwise, approve and return the final code to execute.\",\n", - " max_consecutive_auto_reply=sys.maxsize,\n", - " human_input_mode=\"NEVER\",\n", " llm_config=llm_config,\n", - " code_execution_config=False,\n", - " group_chat_manager=group_chat_manager,\n", ")\n", "\n", "group_chat_manager.agents = [human, alice, bob]" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -160,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -181,13 +173,7 @@ "\n", "find a latest paper about generative agents\n", "\n", - "--------------------------------------------------------------------------------\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ + "--------------------------------------------------------------------------------\n", "\u001b[33mAlice\u001b[0m (to chat_manager):\n", "\n", "As an AI, I am unable to browse or search the web, download or read a file directly. But I can provide you with a Python script to scrape Google Scholar for the latest papers on generative agents.\n", @@ -256,10 +242,6 @@ "Alternatively, databases like PubMed or arXiv.org provide free access to a large number of scientific papers - you might want to check them out for latest research papers on your topic of interest.\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> NO HUMAN INPUT RECEIVED.\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", "\u001b[33mchat_manager\u001b[0m (to Bob):\n", "\n", "As an AI, I am unable to browse or search the web, download or read a file directly. But I can provide you with a Python script to scrape Google Scholar for the latest papers on generative agents.\n", @@ -388,10 +370,6 @@ "Always use this script carefully because web-scraping isn't always reliable or legal on all web pages. Always ensure you have express permission or that the website's terms and conditions don't forbid this kind of usage.\n", "\n", "--------------------------------------------------------------------------------\n", - "\u001b[31m\n", - ">>>>>>>> NO HUMAN INPUT RECEIVED.\u001b[0m\n", - "\u001b[31m\n", - ">>>>>>>> USING AUTO REPLY...\u001b[0m\n", "\u001b[33mchat_manager\u001b[0m (to Alice):\n", "\n", "Your code as it stands can throw an exception and result in an error if the HTTP request fails or if no search results are found. Also, the use of 'beautifulsoup4' and 'requests' should be well-documented.\n", @@ -476,7 +454,7 @@ } ], "source": [ - "human.send(\"find a latest paper about generative agents\", group_chat_manager)" + "human.initiate_chat(group_chat_manager, message=\"find a latest paper about generative agents\")" ] } ], diff --git a/test/autogen/agentchat/test_groupchat.py b/test/autogen/agentchat/test_groupchat.py index dd52fa3922..33c684b93b 100644 --- a/test/autogen/agentchat/test_groupchat.py +++ b/test/autogen/agentchat/test_groupchat.py @@ -3,31 +3,29 @@ def test_chat_manager(): group_chat_manager = autogen.GroupChatManager(max_round=2, llm_config=False) - agent1 = autogen.GroupChatParticipant( + agent1 = autogen.ResponsiveAgent( "alice", max_consecutive_auto_reply=2, human_input_mode="NEVER", llm_config=False, default_auto_reply="This is alice sepaking.", - group_chat_manager=group_chat_manager, ) - agent2 = autogen.GroupChatParticipant( + agent2 = autogen.ResponsiveAgent( "bob", max_consecutive_auto_reply=2, human_input_mode="NEVER", llm_config=False, default_auto_reply="This is bob speaking.", - group_chat_manager=group_chat_manager, ) group_chat_manager.agents = [agent1, agent2] - agent1.send("start", group_chat_manager) + agent1.initiate_chat(group_chat_manager, message="hello") - assert len(agent1.chat_messages[group_chat_manager.name]) == 2 + assert len(agent1.chat_messages[group_chat_manager]) == 2 group_chat_manager.reset() agent1.reset() agent2.reset() - agent2.send("start", group_chat_manager) + agent2.initiate_chat(group_chat_manager, message="hello") if __name__ == "__main__": diff --git a/test/autogen/agentchat/test_responsive_agent.py b/test/autogen/agentchat/test_responsive_agent.py index 33cdade054..9c9e39dad4 100644 --- a/test/autogen/agentchat/test_responsive_agent.py +++ b/test/autogen/agentchat/test_responsive_agent.py @@ -1,5 +1,3 @@ -import sys -from io import StringIO import pytest from flaml.autogen.agentchat import ResponsiveAgent @@ -48,30 +46,34 @@ def test_max_consecutive_auto_reply(): assert agent.max_consecutive_auto_reply() == agent.max_consecutive_auto_reply(agent1) == 1 agent1.initiate_chat(agent, message="hello") - assert agent._consecutive_auto_reply_counter[agent1.name] == 1 + assert agent._consecutive_auto_reply_counter[agent1] == 1 agent1.initiate_chat(agent, message="hello again") # with auto reply because the counter is reset assert agent1.last_message(agent)["role"] == "user" - assert len(agent1.chat_messages[agent.name]) == 2 - assert len(agent.chat_messages[agent1.name]) == 2 + assert len(agent1.chat_messages[agent]) == 2 + assert len(agent.chat_messages[agent1]) == 2 - assert agent._consecutive_auto_reply_counter[agent1.name] == 1 + assert agent._consecutive_auto_reply_counter[agent1] == 1 agent1.send(message="bye", recipient=agent) # no auto reply assert agent1.last_message(agent)["role"] == "assistant" agent1.initiate_chat(agent, clear_history=False, message="hi") - assert len(agent1.chat_messages[agent.name]) > 2 - assert len(agent.chat_messages[agent1.name]) > 2 + assert len(agent1.chat_messages[agent]) > 2 + assert len(agent.chat_messages[agent1]) > 2 + assert agent1.reply_at_receive[agent] == agent.reply_at_receive[agent1] is True + agent1.stop_reply_at_receive(agent) + assert agent1.reply_at_receive[agent] is False and agent.reply_at_receive[agent1] is True -def test_responsive_agent(monkeypatch): + +def test_responsive_agent(): dummy_agent_1 = ResponsiveAgent(name="dummy_agent_1", human_input_mode="ALWAYS") dummy_agent_2 = ResponsiveAgent(name="dummy_agent_2", human_input_mode="TERMINATE") - monkeypatch.setattr(sys, "stdin", StringIO("exit")) + # monkeypatch.setattr(sys, "stdin", StringIO("exit")) dummy_agent_1.receive("hello", dummy_agent_2) # receive a str - monkeypatch.setattr(sys, "stdin", StringIO("TERMINATE\n\n")) + # monkeypatch.setattr(sys, "stdin", StringIO("TERMINATE\n\n")) dummy_agent_1.receive( { "content": "hello {name}", @@ -81,18 +83,18 @@ def test_responsive_agent(monkeypatch): }, dummy_agent_2, ) # receive a dict - assert "context" in dummy_agent_1.chat_messages["dummy_agent_2"][-2] + assert "context" in dummy_agent_1.chat_messages[dummy_agent_2][-1] # receive dict without openai fields to be printed, such as "content", 'function_call'. There should be no error raised. - pre_len = len(dummy_agent_1.chat_messages["dummy_agent_2"]) + pre_len = len(dummy_agent_1.chat_messages[dummy_agent_2]) with pytest.raises(ValueError): dummy_agent_1.receive({"message": "hello"}, dummy_agent_2) assert pre_len == len( - dummy_agent_1.chat_messages["dummy_agent_2"] + dummy_agent_1.chat_messages[dummy_agent_2] ), "When the message is not an valid openai message, it should not be appended to the oai conversation." - monkeypatch.setattr(sys, "stdin", StringIO("exit")) + # monkeypatch.setattr(sys, "stdin", StringIO("exit")) dummy_agent_1.send("TERMINATE", dummy_agent_2) # send a str - monkeypatch.setattr(sys, "stdin", StringIO("exit")) + # monkeypatch.setattr(sys, "stdin", StringIO("exit")) dummy_agent_1.send( { "content": "TERMINATE", @@ -101,17 +103,17 @@ def test_responsive_agent(monkeypatch): ) # send a dict # send dict with no openai fields - pre_len = len(dummy_agent_1.chat_messages["dummy_agent_2"]) + pre_len = len(dummy_agent_1.chat_messages[dummy_agent_2]) with pytest.raises(ValueError): dummy_agent_1.send({"message": "hello"}, dummy_agent_2) assert pre_len == len( - dummy_agent_1.chat_messages["dummy_agent_2"] + dummy_agent_1.chat_messages[dummy_agent_2] ), "When the message is not a valid openai message, it should not be appended to the oai conversation." # update system message dummy_agent_1.update_system_message("new system message") - assert dummy_agent_1._oai_system_message[0]["content"] == "new system message" + assert dummy_agent_1.system_message == "new system message" if __name__ == "__main__": diff --git a/website/docs/Use-Cases/Auto-Generation.md b/website/docs/Use-Cases/Auto-Generation.md index 7823548886..372af3a6e5 100644 --- a/website/docs/Use-Cases/Auto-Generation.md +++ b/website/docs/Use-Cases/Auto-Generation.md @@ -156,6 +156,8 @@ user_proxy.initiate_chat( * [Automated Chess Game Playing & Chitchatting by GPT-4 Agents](https://github.com/microsoft/FLAML/blob/main/notebook/autogen_agentchat_chess.ipynb) +* [Automated Task Solving by Group Chat](https://github.com/microsoft/FLAML/blob/main/notebook/autogen_agentchat_groupchat.ipynb) + ## Enhanced Inference One can use [`flaml.autogen.Completion.create`](/docs/reference/autogen/oai/completion#create) to perform inference.