Improve auto reply registration (#1170)

* Improve auto reply registration

* object key

* fix test error

* bug fix in math user proxy agent

* allow send/receive without reply

* reset -> stop
This commit is contained in:
Chi Wang 2023-08-04 07:26:58 -07:00 committed by GitHub
parent 45b7d908e4
commit 2208dfb79e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 896 additions and 321 deletions

View File

@ -2,7 +2,7 @@ from .agent import Agent
from .responsive_agent import ResponsiveAgent from .responsive_agent import ResponsiveAgent
from .assistant_agent import AssistantAgent from .assistant_agent import AssistantAgent
from .user_proxy_agent import UserProxyAgent from .user_proxy_agent import UserProxyAgent
from .groupchat import GroupChatManager, GroupChatParticipant from .groupchat import GroupChatManager
__all__ = [ __all__ = [
"Agent", "Agent",
@ -10,5 +10,4 @@ __all__ = [
"AssistantAgent", "AssistantAgent",
"UserProxyAgent", "UserProxyAgent",
"GroupChatManager", "GroupChatManager",
"GroupChatParticipant",
] ]

View File

@ -24,10 +24,10 @@ class Agent:
"""Get the name of the agent.""" """Get the name of the agent."""
return self._name 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.""" """(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.""" """(Abstract method) Receive a message from another agent."""
def reset(self): def reset(self):

View File

@ -165,7 +165,7 @@ class MathUserProxyAgent(UserProxyAgent):
default_auto_reply=default_auto_reply, default_auto_reply=default_auto_reply,
**kwargs, **kwargs,
) )
self.register_auto_reply(Agent, self._generate_math_reply) self.register_auto_reply(Agent, self._generate_math_reply, 1)
# fixed var # fixed var
self._max_invalid_q_per_step = max_invalid_q_per_step self._max_invalid_q_per_step = max_invalid_q_per_step
@ -283,7 +283,7 @@ class MathUserProxyAgent(UserProxyAgent):
): ):
"""Generate an auto reply.""" """Generate an auto reply."""
if messages is None: if messages is None:
messages = self._oai_messages[sender.name] messages = self._oai_messages[sender]
message = messages[-1] message = messages[-1]
message = message.get("content", "") message = message.get("content", "")
code_blocks = extract_code(message) code_blocks = extract_code(message)
@ -313,7 +313,7 @@ class MathUserProxyAgent(UserProxyAgent):
reply = reply.strip() reply = reply.strip()
if self.last_reply == reply: 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 self.last_reply = reply
if not all_success: if not all_success:

View File

@ -1,5 +1,5 @@
import sys import sys
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Union
from .agent import Agent from .agent import Agent
from .responsive_agent import ResponsiveAgent from .responsive_agent import ResponsiveAgent
@ -7,19 +7,16 @@ from .responsive_agent import ResponsiveAgent
class GroupChatManager(ResponsiveAgent): class GroupChatManager(ResponsiveAgent):
"""(WIP) A chat manager agent that can manage a group chat of multiple agents.""" """(WIP) A chat manager agent that can manage a group chat of multiple agents."""
agents: List["GroupChatParticipant"] agents: List[Agent]
max_round: int max_round: int
def _participant_roles(self): def _participant_roles(self):
return "\n".join([f"{agent.name}: {agent.system_message}" for agent in self.agents]) return "\n".join([f"{agent.name}: {agent.system_message}" for agent in self.agents])
def _select_speaker_msg(self): def _select_speaker_msg(self):
return { return f"""You are in a role play game. The following roles are available:
"role": "system",
"content": f"""You are in a role play game. The following roles are available:
{self._participant_roles()}. Read the following conversation. {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__( def __init__(
self, self,
@ -28,6 +25,7 @@ Then select the next role from {self._agent_names} to play. Only return the role
# unlimited consecutive auto reply by default # unlimited consecutive auto reply by default
max_consecutive_auto_reply: Optional[int] = sys.maxsize, max_consecutive_auto_reply: Optional[int] = sys.maxsize,
human_input_mode: Optional[str] = "NEVER", human_input_mode: Optional[str] = "NEVER",
system_message: Optional[str] = "Group chat manager.",
# seed: Optional[int] = 4, # seed: Optional[int] = 4,
**kwargs, **kwargs,
): ):
@ -37,11 +35,9 @@ Then select the next role from {self._agent_names} to play. Only return the role
human_input_mode=human_input_mode, human_input_mode=human_input_mode,
**kwargs, **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.max_round = max_round
self._agent_names = [] self._agent_names = []
self._next_speaker = None
self._round = 0
self._messages = [] self._messages = []
# self._random = random.Random(seed) # self._random = random.Random(seed)
@ -50,94 +46,43 @@ Then select the next role from {self._agent_names} to play. Only return the role
messages: Optional[List[Dict]] = None, messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None, sender: Optional[Agent] = None,
) -> Union[str, Dict, None]: ) -> Union[str, Dict, None]:
if messages is None:
messages = self._oai_messages[sender.name]
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._agent_names = [agent.name for agent in self.agents]
self._round += 1 if messages is None:
if self._round >= self.max_round: messages = self._oai_messages[sender]
return True, None message = messages[-1]
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 selection msg from an agent
self._next_speaker = self._select_speaker(sender) speaker = self._select_speaker(speaker)
self._next_speaker.send(self._next_speaker.generate_reply(sender=self), self) speaker.send(speaker.generate_reply(sender=self), self, request_reply=False)
message = self.last_message(speaker)
return True, None return True, None
@property def _select_speaker(self, last_speaker: Agent):
def next_speaker(self):
"""Return the next speaker."""
return self._next_speaker
def _select_speaker(self, last_speaker: "GroupChatParticipant"):
"""Select the next speaker.""" """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: if not final:
# i = self._random.randint(0, len(self._agent_names) - 1) # randomly pick an id # 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.agents[(self._agent_names.index(last_speaker.name) + 1) % len(self._agent_names)]
try:
return self.agent_by_name(name) 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.""" """Find the next speaker based on the message."""
return self.agents[self._agent_names.index(name)] return self.agents[self._agent_names.index(name)]
def reset(self): def reset(self):
super().reset() super().reset()
self._round = 0
self._messages.clear() 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

View File

@ -109,22 +109,27 @@ class ResponsiveAgent(Agent):
self._function_map = {} if function_map is None else function_map self._function_map = {} if function_map is None else function_map
self._default_auto_reply = default_auto_reply self._default_auto_reply = default_auto_reply
self._class_specific_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_oai_reply)
self.register_auto_reply(Agent, self._generate_code_execution_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._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. """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 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: Args:
class_type (Class): the class type. class_type (Class): the class type.
reply_func (Callable): the reply function. 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): def system_message(self):
"""Return the system message.""" """Return the system message."""
return self._oai_system_message[0]["content"] return self._oai_system_message[0]["content"]
@ -149,13 +154,11 @@ class ResponsiveAgent(Agent):
for k in self._max_consecutive_auto_reply_dict: for k in self._max_consecutive_auto_reply_dict:
self._max_consecutive_auto_reply_dict[k] = value self._max_consecutive_auto_reply_dict[k] = value
else: 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: def max_consecutive_auto_reply(self, sender: Optional[Agent] = None) -> int:
"""The maximum number of consecutive auto replies.""" """The maximum number of consecutive auto replies."""
return ( return self._max_consecutive_auto_reply if sender is None else self._max_consecutive_auto_reply_dict[sender]
self._max_consecutive_auto_reply if sender is None else self._max_consecutive_auto_reply_dict[sender.name]
)
@property @property
def chat_messages(self) -> Dict[str, List[Dict]]: def chat_messages(self) -> Dict[str, List[Dict]]:
@ -181,7 +184,7 @@ class ResponsiveAgent(Agent):
for conversation in self._oai_messages.values(): for conversation in self._oai_messages.values():
return conversation[-1] return conversation[-1]
raise ValueError("More than one conversation is found. Please specify the sender to get the last message.") 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 @property
def use_docker(self) -> Union[bool, str, None]: def use_docker(self) -> Union[bool, str, None]:
@ -200,7 +203,7 @@ class ResponsiveAgent(Agent):
else: else:
return message 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. """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. If the message received is a string, it will be put in the "content" field of the new dictionary.
@ -210,7 +213,7 @@ class ResponsiveAgent(Agent):
Args: Args:
message (dict or str): message to be appended to the ChatCompletion conversation. message (dict or str): message to be appended to the ChatCompletion conversation.
role (str): role of the message, can be "assistant" or "function". 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: Returns:
bool: whether the message is appended to the ChatCompletion conversation. bool: whether the message is appended to the ChatCompletion conversation.
@ -225,7 +228,7 @@ class ResponsiveAgent(Agent):
self._oai_messages[conversation_id].append(oai_message) self._oai_messages[conversation_id].append(oai_message)
return True 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. """Send a message to another agent.
Args: Args:
@ -252,15 +255,16 @@ class ResponsiveAgent(Agent):
So effectively, this provides a way for an agent to send a "link" and modify So effectively, this provides a way for an agent to send a "link" and modify
the content of the "link" later. the content of the "link" later.
recipient (Agent): the recipient of the message. recipient (Agent): the recipient of the message.
request_reply (bool or None): whether to request a reply from the recipient.
Raises: Raises:
ValueError: if the message can't be converted into a valid ChatCompletion message. 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" # When the agent composes and sends the message, the role of the message is "assistant"
# unless it's "function". # unless it's "function".
valid = self._append_oai_message(message, "assistant", recipient.name) valid = self._append_oai_message(message, "assistant", recipient)
if valid: if valid:
recipient.receive(message, self) recipient.receive(message, self, request_reply)
else: else:
raise ValueError( raise ValueError(
"Message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." "Message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided."
@ -296,7 +300,7 @@ class ResponsiveAgent(Agent):
print(colored("*" * len(func_print), "green"), flush=True) print(colored("*" * len(func_print), "green"), flush=True)
print("\n", "-" * 80, flush=True, sep="") 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. """Receive a message from another agent.
Once a message is received, this function sends a reply to the sender or stop. Once a message is received, this function sends a reply to the sender or stop.
@ -312,18 +316,22 @@ class ResponsiveAgent(Agent):
5. "context" (dict): the context of the message, which will be passed to 5. "context" (dict): the context of the message, which will be passed to
[autogen.Completion.create](../oai/Completion#create). [autogen.Completion.create](../oai/Completion#create).
sender: sender of an Agent instance. 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: Raises:
ValueError: if the message can't be converted into a valid ChatCompletion message. ValueError: if the message can't be converted into a valid ChatCompletion message.
""" """
message = self._message_to_dict(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.) # 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: if not valid:
raise ValueError( raise ValueError(
"Received message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided." "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) 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) reply = self.generate_reply(sender=sender)
if reply is not None: if reply is not None:
self.send(reply, sender) self.send(reply, sender)
@ -343,6 +351,7 @@ class ResponsiveAgent(Agent):
""" """
self.reset_consecutive_auto_reply_counter(recipient) self.reset_consecutive_auto_reply_counter(recipient)
recipient.reset_consecutive_auto_reply_counter(self) recipient.reset_consecutive_auto_reply_counter(self)
self.reply_at_receive[recipient] = recipient.reply_at_receive[self] = True
if clear_history: if clear_history:
self.clear_history(recipient) self.clear_history(recipient)
recipient.clear_history(self) recipient.clear_history(self)
@ -352,13 +361,21 @@ class ResponsiveAgent(Agent):
"""Reset the agent.""" """Reset the agent."""
self.clear_history() self.clear_history()
self.reset_consecutive_auto_reply_counter() 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): def reset_consecutive_auto_reply_counter(self, sender: Optional[Agent] = None):
"""Reset the consecutive_auto_reply_counter of the sender.""" """Reset the consecutive_auto_reply_counter of the sender."""
if sender is None: if sender is None:
self._consecutive_auto_reply_counter.clear() self._consecutive_auto_reply_counter.clear()
else: else:
self._consecutive_auto_reply_counter[sender.name] = 0 self._consecutive_auto_reply_counter[sender] = 0
def clear_history(self, agent: Optional[Agent] = None): def clear_history(self, agent: Optional[Agent] = None):
"""Clear the chat history of the agent. """Clear the chat history of the agent.
@ -369,7 +386,7 @@ class ResponsiveAgent(Agent):
if agent is None: if agent is None:
self._oai_messages.clear() self._oai_messages.clear()
else: else:
self._oai_messages[agent.name].clear() self._oai_messages[agent].clear()
def _generate_oai_reply( def _generate_oai_reply(
self, self,
@ -379,7 +396,7 @@ class ResponsiveAgent(Agent):
if self.llm_config is False: if self.llm_config is False:
return False, None return False, None
if messages is None: if messages is None:
messages = self._oai_messages[sender.name] messages = self._oai_messages[sender]
# TODO: #1143 handle token limit exceeded error # TODO: #1143 handle token limit exceeded error
response = oai.ChatCompletion.create( response = oai.ChatCompletion.create(
@ -387,13 +404,48 @@ class ResponsiveAgent(Agent):
) )
return True, oai.ChatCompletion.extract_text_or_function_call(response)[0] 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( def _check_termination_and_human_reply(
self, self,
messages: Optional[List[Dict]] = None, messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None, sender: Optional[Agent] = None,
) -> Tuple[bool, Union[str, Dict, None]]: ) -> Tuple[bool, Union[str, Dict, None]]:
if messages is None: if messages is None:
messages = self._oai_messages[sender.name] messages = self._oai_messages[sender]
message = messages[-1] message = messages[-1]
reply = "" reply = ""
no_human_input_msg = "" no_human_input_msg = ""
@ -405,7 +457,7 @@ class ResponsiveAgent(Agent):
# if the human input is empty, and the message is a termination message, then we will terminate the conversation # 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" reply = reply if reply or not self._is_termination_msg(message) else "exit"
else: 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": if self.human_input_mode == "NEVER":
reply = "exit" reply = "exit"
else: else:
@ -438,84 +490,58 @@ class ResponsiveAgent(Agent):
# stop the conversation # stop the conversation
if reply == "exit": if reply == "exit":
# reset the consecutive_auto_reply_counter # reset the consecutive_auto_reply_counter
self._consecutive_auto_reply_counter[sender.name] = 0 self._consecutive_auto_reply_counter[sender] = 0
return True, None return True, None
# send the human reply # 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 # reset the consecutive_auto_reply_counter
self._consecutive_auto_reply_counter[sender.name] = 0 self._consecutive_auto_reply_counter[sender] = 0
return True, reply return True, reply
# increment the consecutive_auto_reply_counter # 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": if self.human_input_mode != "NEVER":
print(colored("\n>>>>>>>> USING AUTO REPLY...", "red"), flush=True) print(colored("\n>>>>>>>> USING AUTO REPLY...", "red"), flush=True)
return False, None 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( def generate_reply(
self, self,
messages: Optional[List[Dict]] = None, messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None, sender: Optional[Agent] = None,
exclude: Optional[List[Callable]] = None,
) -> Union[str, Dict, 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. 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: Args:
messages: a list of messages in the conversation history. messages: a list of messages in the conversation history.
default_reply (str or dict): default reply. default_reply (str or dict): default reply.
sender: sender of an Agent instance. sender: sender of an Agent instance.
exclude: a list of functions to exclude.
Returns: Returns:
str or dict or None: reply. None if no reply is generated. 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." 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: if sender is not None:
for class_specifc_reply in self._class_specific_reply[-1::-1]: for class_specifc_reply in self._class_specific_reply:
if isinstance(sender, class_specifc_reply[0]): 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) final, reply = class_specifc_reply[1](messages, sender)
if final: if final:
return reply return reply

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
{ {
"cells": [ "cells": [
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -8,6 +9,7 @@
] ]
}, },
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -34,6 +36,7 @@
] ]
}, },
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -71,6 +74,7 @@
] ]
}, },
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -106,6 +110,7 @@
] ]
}, },
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -114,44 +119,31 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 5, "execution_count": 3,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"import sys\n",
"\n",
"llm_config = {\"config_list\": config_list_gpt4}\n", "llm_config = {\"config_list\": config_list_gpt4}\n",
"group_chat_manager = autogen.GroupChatManager(max_round=4, llm_config=llm_config)\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", " name=\"Human\",\n",
" system_message=\"A human admin.\",\n", " system_message=\"A human admin.\",\n",
" human_input_mode=\"ALWAYS\",\n",
" llm_config=False,\n",
" group_chat_manager=group_chat_manager,\n",
")\n", ")\n",
"alice = autogen.GroupChatParticipant(\n", "alice = autogen.AssistantAgent(\n",
" name=\"Alice\",\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", " llm_config=llm_config,\n",
" code_execution_config=False,\n",
" group_chat_manager=group_chat_manager,\n",
")\n", ")\n",
"bob = autogen.GroupChatParticipant(\n", "bob = autogen.AssistantAgent(\n",
" name=\"Bob\",\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", " 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", " llm_config=llm_config,\n",
" code_execution_config=False,\n",
" group_chat_manager=group_chat_manager,\n",
")\n", ")\n",
"\n", "\n",
"group_chat_manager.agents = [human, alice, bob]" "group_chat_manager.agents = [human, alice, bob]"
] ]
}, },
{ {
"attachments": {},
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
@ -160,7 +152,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 4,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
@ -181,13 +173,7 @@
"\n", "\n",
"find a latest paper about generative agents\n", "find a latest paper about generative agents\n",
"\n", "\n",
"--------------------------------------------------------------------------------\n" "--------------------------------------------------------------------------------\n",
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33mAlice\u001b[0m (to chat_manager):\n", "\u001b[33mAlice\u001b[0m (to chat_manager):\n",
"\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", "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", "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",
"--------------------------------------------------------------------------------\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", "\u001b[33mchat_manager\u001b[0m (to Bob):\n",
"\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", "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", "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",
"--------------------------------------------------------------------------------\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", "\u001b[33mchat_manager\u001b[0m (to Alice):\n",
"\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", "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": [ "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\")"
] ]
} }
], ],

View File

@ -3,31 +3,29 @@ from flaml import autogen
def test_chat_manager(): def test_chat_manager():
group_chat_manager = autogen.GroupChatManager(max_round=2, llm_config=False) group_chat_manager = autogen.GroupChatManager(max_round=2, llm_config=False)
agent1 = autogen.GroupChatParticipant( agent1 = autogen.ResponsiveAgent(
"alice", "alice",
max_consecutive_auto_reply=2, max_consecutive_auto_reply=2,
human_input_mode="NEVER", human_input_mode="NEVER",
llm_config=False, llm_config=False,
default_auto_reply="This is alice sepaking.", default_auto_reply="This is alice sepaking.",
group_chat_manager=group_chat_manager,
) )
agent2 = autogen.GroupChatParticipant( agent2 = autogen.ResponsiveAgent(
"bob", "bob",
max_consecutive_auto_reply=2, max_consecutive_auto_reply=2,
human_input_mode="NEVER", human_input_mode="NEVER",
llm_config=False, llm_config=False,
default_auto_reply="This is bob speaking.", default_auto_reply="This is bob speaking.",
group_chat_manager=group_chat_manager,
) )
group_chat_manager.agents = [agent1, agent2] 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() group_chat_manager.reset()
agent1.reset() agent1.reset()
agent2.reset() agent2.reset()
agent2.send("start", group_chat_manager) agent2.initiate_chat(group_chat_manager, message="hello")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,5 +1,3 @@
import sys
from io import StringIO
import pytest import pytest
from flaml.autogen.agentchat import ResponsiveAgent 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 assert agent.max_consecutive_auto_reply() == agent.max_consecutive_auto_reply(agent1) == 1
agent1.initiate_chat(agent, message="hello") 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") agent1.initiate_chat(agent, message="hello again")
# with auto reply because the counter is reset # with auto reply because the counter is reset
assert agent1.last_message(agent)["role"] == "user" assert agent1.last_message(agent)["role"] == "user"
assert len(agent1.chat_messages[agent.name]) == 2 assert len(agent1.chat_messages[agent]) == 2
assert len(agent.chat_messages[agent1.name]) == 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) agent1.send(message="bye", recipient=agent)
# no auto reply # no auto reply
assert agent1.last_message(agent)["role"] == "assistant" assert agent1.last_message(agent)["role"] == "assistant"
agent1.initiate_chat(agent, clear_history=False, message="hi") agent1.initiate_chat(agent, clear_history=False, message="hi")
assert len(agent1.chat_messages[agent.name]) > 2 assert len(agent1.chat_messages[agent]) > 2
assert len(agent.chat_messages[agent1.name]) > 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_1 = ResponsiveAgent(name="dummy_agent_1", human_input_mode="ALWAYS")
dummy_agent_2 = ResponsiveAgent(name="dummy_agent_2", human_input_mode="TERMINATE") 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 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( dummy_agent_1.receive(
{ {
"content": "hello {name}", "content": "hello {name}",
@ -81,18 +83,18 @@ def test_responsive_agent(monkeypatch):
}, },
dummy_agent_2, dummy_agent_2,
) # receive a dict ) # 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. # 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): with pytest.raises(ValueError):
dummy_agent_1.receive({"message": "hello"}, dummy_agent_2) dummy_agent_1.receive({"message": "hello"}, dummy_agent_2)
assert pre_len == len( 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." ), "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 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( dummy_agent_1.send(
{ {
"content": "TERMINATE", "content": "TERMINATE",
@ -101,17 +103,17 @@ def test_responsive_agent(monkeypatch):
) # send a dict ) # send a dict
# send dict with no openai fields # 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): with pytest.raises(ValueError):
dummy_agent_1.send({"message": "hello"}, dummy_agent_2) dummy_agent_1.send({"message": "hello"}, dummy_agent_2)
assert pre_len == len( 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." ), "When the message is not a valid openai message, it should not be appended to the oai conversation."
# update system message # update system message
dummy_agent_1.update_system_message("new 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__": if __name__ == "__main__":

View File

@ -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 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 ## Enhanced Inference
One can use [`flaml.autogen.Completion.create`](/docs/reference/autogen/oai/completion#create) to perform inference. One can use [`flaml.autogen.Completion.create`](/docs/reference/autogen/oai/completion#create) to perform inference.