mirror of
https://github.com/microsoft/autogen.git
synced 2025-11-02 10:50:03 +00:00
Human agent (#1025)
* add human agent and chat agent * feedback msg * clean print * remove redundant import * make coding agent work * import check * terminate condition * rename * add docstr * exitcode to str * print * save and execute code * add max_turn_num * add max_turn_num in test_agent.py * reduce max_turn_num in the test * change max_turn_num to max_consecutive_auto_reply * update human proxy agent * remove execution agent and dated docstr * clean doc * add back work_dir * add is_termination_msg when mode is NEVER * revise stop condition * remove work_dir in coding agent * human_proxy_agent docstr * auto_reply * clean auto_reply --------- Co-authored-by: Chi Wang <wang.chi@microsoft.com>
This commit is contained in:
parent
f01acb67f6
commit
2e43509690
@ -3,11 +3,17 @@ from collections import defaultdict
|
||||
|
||||
class Agent:
|
||||
"""(Experimental) An abstract class for AI agent.
|
||||
An agent can communicate with other agents, human and perform actions.
|
||||
Different agents can differ in how and who they communicate with, and what actions they can perform. For example, an autonomous agent can communicate with human and other agents, and perform actions by creating agents and sending messages to other agents. A planning agent can communicate with other agents to make a plan and keep track of tasks. An execution agent can only communicate with other agents, and perform actions such as executing a command or code.
|
||||
An agent can communicate with other agents and perform actions.
|
||||
Different agents can differ in what actions they perform in the `receive` method.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, system_message=""):
|
||||
"""
|
||||
Args:
|
||||
name (str): name of the agent
|
||||
system_message (str): system message to be sent to the agent
|
||||
"""
|
||||
# empty memory
|
||||
self._memory = []
|
||||
# a dictionary of conversations, default value is list
|
||||
@ -31,7 +37,8 @@ class Agent:
|
||||
|
||||
def _receive(self, message, sender):
|
||||
"""Receive a message from another agent."""
|
||||
# print(self.name, "received message from", sender.name, ":", message)
|
||||
print("****", self.name, "received message from", sender.name, "****")
|
||||
print(message)
|
||||
self._conversations[sender.name].append({"content": message, "role": "user"})
|
||||
|
||||
def receive(self, message, sender):
|
||||
|
||||
35
flaml/autogen/agent/chat_agent.py
Normal file
35
flaml/autogen/agent/chat_agent.py
Normal file
@ -0,0 +1,35 @@
|
||||
from .agent import Agent
|
||||
from flaml.autogen.code_utils import DEFAULT_MODEL
|
||||
from flaml import oai
|
||||
|
||||
|
||||
class ChatAgent(Agent):
|
||||
"""(Experimental) Chat."""
|
||||
|
||||
DEFAULT_SYSTEM_MESSAGE = """You are a chat agent.
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"model": DEFAULT_MODEL,
|
||||
}
|
||||
|
||||
def __init__(self, name, system_message=DEFAULT_SYSTEM_MESSAGE, work_dir=None, **config):
|
||||
"""
|
||||
Args:
|
||||
name (str): agent name
|
||||
system_message (str): system message to be sent to the agent
|
||||
work_dir (str): working directory for the agent to execute code
|
||||
config (dict): other configurations.
|
||||
"""
|
||||
super().__init__(name, system_message)
|
||||
self._work_dir = work_dir
|
||||
self._config = self.DEFAULT_CONFIG.copy()
|
||||
self._config.update(config)
|
||||
self._sender_dict = {}
|
||||
|
||||
def receive(self, message, sender):
|
||||
super().receive(message, sender)
|
||||
responses = oai.ChatCompletion.create(messages=self._conversations[sender.name], **self._config)
|
||||
# cost = oai.ChatCompletion.cost(responses)
|
||||
response = oai.ChatCompletion.extract_text(responses)[0]
|
||||
self._send(response, sender)
|
||||
@ -1,24 +1,28 @@
|
||||
from .agent import Agent
|
||||
from .execution_agent import ExecutionAgent
|
||||
from flaml.autogen.code_utils import generate_code, DEFAULT_MODEL
|
||||
from flaml.autogen.code_utils import DEFAULT_MODEL
|
||||
from flaml import oai
|
||||
|
||||
|
||||
class PythonAgent(Agent):
|
||||
"""(Experimental) Suggest code blocks."""
|
||||
|
||||
DEFAULT_SYSTEM_MESSAGE = """You are a coding agent. You suggest python code for a user to execute for a given task. Don't suggest shell command. Output the code in a coding block. Check the execution result. If the result indicates there is an error, fix the error and output the code again.
|
||||
DEFAULT_SYSTEM_MESSAGE = """You suggest python code (in a python coding block) for a user to execute for a given task. If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Finish the task smartly. Don't suggest shell command. Don't include multiple code blocks in one response. Use 'print' function for the output when relevant. Check the execution result returned by the user.
|
||||
If the result indicates there is an error, fix the error and output the code again.
|
||||
Reply "TERMINATE" in the end when the task is done.
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"model": DEFAULT_MODEL,
|
||||
}
|
||||
EXECUTION_AGENT_PREFIX = "execution_agent4"
|
||||
SUCCESS_EXIT_CODE = "exitcode: 0\n"
|
||||
|
||||
def __init__(self, name, system_message=DEFAULT_SYSTEM_MESSAGE, work_dir=None, **config):
|
||||
def __init__(self, name, system_message=DEFAULT_SYSTEM_MESSAGE, **config):
|
||||
"""
|
||||
Args:
|
||||
name (str): agent name
|
||||
system_message (str): system message to be sent to the agent
|
||||
config (dict): other configurations.
|
||||
"""
|
||||
super().__init__(name, system_message)
|
||||
self._work_dir = work_dir
|
||||
self._config = self.DEFAULT_CONFIG.copy()
|
||||
self._config.update(config)
|
||||
self._sender_dict = {}
|
||||
@ -28,26 +32,10 @@ class PythonAgent(Agent):
|
||||
self._sender_dict[sender.name] = sender
|
||||
self._conversations[sender.name] = [{"content": self._system_message, "role": "system"}]
|
||||
super().receive(message, sender)
|
||||
if sender.name.startswith(self.EXECUTION_AGENT_PREFIX) and message.startswith(self.SUCCESS_EXIT_CODE):
|
||||
# the code is correct, respond to the original sender
|
||||
name = sender.name[len(self.EXECUTION_AGENT_PREFIX) :]
|
||||
original_sender = self._sender_dict[name]
|
||||
output = message[len(self.SUCCESS_EXIT_CODE) :]
|
||||
if output:
|
||||
self._send(f"{output}", original_sender)
|
||||
else:
|
||||
self._send("Done. No output.", original_sender)
|
||||
return
|
||||
responses = oai.ChatCompletion.create(messages=self._conversations[sender.name], **self._config)
|
||||
# cost = oai.ChatCompletion.cost(responses)
|
||||
response = oai.ChatCompletion.extract_text(responses)[0]
|
||||
if sender.name.startswith(self.EXECUTION_AGENT_PREFIX):
|
||||
execution_agent = sender
|
||||
else:
|
||||
# create an execution agent
|
||||
execution_agent = ExecutionAgent(f"{self.EXECUTION_AGENT_PREFIX}{sender.name}", work_dir=self._work_dir)
|
||||
# initialize the conversation
|
||||
self._conversations[execution_agent.name] = self._conversations[sender.name].copy()
|
||||
self._sender_dict[execution_agent.name] = execution_agent
|
||||
# send the response to the execution agent
|
||||
self._send(response, execution_agent)
|
||||
self._send(response, sender)
|
||||
|
||||
def reset(self):
|
||||
self._sender_dict.clear()
|
||||
self._conversations.clear()
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
from .agent import Agent
|
||||
from flaml.autogen.code_utils import execute_code, extract_code
|
||||
|
||||
|
||||
class ExecutionAgent(Agent):
|
||||
"""(Experimental) Perform actions based on instructions from other agents.
|
||||
An execution agent can only communicate with other agents, and perform actions such as executing a command or code.
|
||||
"""
|
||||
|
||||
def __init__(self, name, system_message="", work_dir=None):
|
||||
super().__init__(name, system_message)
|
||||
self._word_dir = work_dir
|
||||
|
||||
def receive(self, message, sender):
|
||||
super().receive(message, sender)
|
||||
# extract code
|
||||
code, lang = extract_code(message)
|
||||
if lang == "bash":
|
||||
assert code.startswith("python ")
|
||||
file_name = code[len("python ") :]
|
||||
exitcode, logs = execute_code(filename=file_name, work_dir=self._word_dir)
|
||||
else:
|
||||
exitcode, logs = execute_code(code, work_dir=self._word_dir)
|
||||
self._send(f"exitcode: {exitcode}\n{logs.decode('utf-8')}", sender)
|
||||
117
flaml/autogen/agent/human_proxy_agent.py
Normal file
117
flaml/autogen/agent/human_proxy_agent.py
Normal file
@ -0,0 +1,117 @@
|
||||
from .agent import Agent
|
||||
from flaml.autogen.code_utils import extract_code, execute_code
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class HumanProxyAgent(Agent):
|
||||
"""(Experimental) A proxy agent for human, that can execute code and provide feedback to the other agents."""
|
||||
|
||||
MAX_CONSECUTIVE_AUTO_REPLY = 100 # maximum number of consecutive auto replies (subject to future change)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
system_message="",
|
||||
work_dir=None,
|
||||
human_input_mode="ALWAYS",
|
||||
max_consecutive_auto_reply=None,
|
||||
is_termination_msg=None,
|
||||
**config,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
name (str): name of the agent
|
||||
system_message (str): system message to be sent to the agent
|
||||
work_dir (str): working directory for the agent
|
||||
human_input_mode (bool): whether to ask for human inputs every time a message is received.
|
||||
Possible values are "ALWAYS", "TERMINATE", "NEVER".
|
||||
(1) When "ALWAYS", the agent prompts for human input every time a message is received.
|
||||
Under this mode, the conversation stops when the human input is "exit",
|
||||
or when is_termination_msg is True and there is no human input.
|
||||
(2) When "TERMINATE", the agent only prompts for human input only when a termination message is received or
|
||||
the number of auto reply reaches the max_consecutive_auto_reply.
|
||||
(3) When "NEVER", the agent will never prompt for human input. Under this mode, the conversation stops
|
||||
when the number of auto reply reaches the max_consecutive_auto_reply or or when is_termination_msg is True.
|
||||
max_consecutive_auto_reply (int): the maximum number of consecutive auto replies.
|
||||
default: None (no limit provided, class attribute MAX_CONSECUTIVE_AUTO_REPLY will be used as the limit in this case).
|
||||
The limit only plays a role when human_input_mode is not "ALWAYS".
|
||||
is_termination_msg (function): a function that takes a message and returns a boolean value.
|
||||
This function is used to determine if a received message is a termination message.
|
||||
config (dict): other configurations.
|
||||
|
||||
"""
|
||||
super().__init__(name, system_message)
|
||||
self._work_dir = work_dir
|
||||
self._human_input_mode = human_input_mode
|
||||
self._is_termination_msg = (
|
||||
is_termination_msg if is_termination_msg is not None else (lambda x: x == "TERMINATE")
|
||||
)
|
||||
self._config = config
|
||||
self._max_consecutive_auto_reply = (
|
||||
max_consecutive_auto_reply if max_consecutive_auto_reply is not None else self.MAX_CONSECUTIVE_AUTO_REPLY
|
||||
)
|
||||
self._consecutive_auto_reply_counter = defaultdict(int)
|
||||
|
||||
def _execute_code(self, code, lang):
|
||||
"""Execute the code and return the result."""
|
||||
if lang == "bash":
|
||||
assert code.startswith("python "), code
|
||||
file_name = code[len("python ") :]
|
||||
exitcode, logs = execute_code(filename=file_name, work_dir=self._work_dir)
|
||||
elif lang == "python":
|
||||
if code.startswith("# filename: "):
|
||||
filename = code[11 : code.find("\n")].strip()
|
||||
else:
|
||||
filename = None
|
||||
exitcode, logs = execute_code(code, work_dir=self._work_dir, filename=filename)
|
||||
else:
|
||||
# TODO: could this happen?
|
||||
exitcode, logs = 1, "unknown language"
|
||||
# raise NotImplementedError
|
||||
return exitcode, logs
|
||||
|
||||
def auto_reply(self, message, sender, default_reply=""):
|
||||
"""Generate an auto reply."""
|
||||
code, lang = extract_code(message)
|
||||
if lang == "unknown":
|
||||
# no code block is found, lang should be "unknown"
|
||||
self._send(default_reply, sender)
|
||||
else:
|
||||
# try to execute the code
|
||||
exitcode, logs = self._execute_code(code, lang)
|
||||
exitcode2str = "execution succeeded" if exitcode == 0 else "execution failed"
|
||||
self._send(f"exitcode: {exitcode} ({exitcode2str})\nCode output: {logs.decode('utf-8')}", sender)
|
||||
|
||||
def receive(self, message, sender):
|
||||
"""Receive a message from the sender agent.
|
||||
Once a message is received, this function sends a reply to the sender or simply stop.
|
||||
The reply can be generated automatically or entered manually by a human.
|
||||
"""
|
||||
super().receive(message, sender)
|
||||
# default reply is empty (i.e., no reply, in this case we will try to generate auto reply)
|
||||
reply = ""
|
||||
if self._human_input_mode == "ALWAYS":
|
||||
reply = input(
|
||||
"Provide feedback to the sender. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: "
|
||||
)
|
||||
elif self._consecutive_auto_reply_counter[
|
||||
sender.name
|
||||
] >= self._max_consecutive_auto_reply or self._is_termination_msg(message):
|
||||
if self._human_input_mode == "TERMINATE":
|
||||
reply = input(
|
||||
"Please give feedback to the sender. (Press enter or type 'exit' to stop the conversation): "
|
||||
)
|
||||
reply = reply if reply else "exit"
|
||||
else:
|
||||
# this corresponds to the case when self._human_input_mode == "NEVER"
|
||||
reply = "exit"
|
||||
if reply == "exit" or (self._is_termination_msg(message) and not reply):
|
||||
return
|
||||
elif reply:
|
||||
# reset the consecutive_auto_reply_counter
|
||||
self._consecutive_auto_reply_counter[sender.name] = 0
|
||||
self._send(reply, sender)
|
||||
return
|
||||
|
||||
self._consecutive_auto_reply_counter[sender.name] += 1
|
||||
self.auto_reply(message, sender, default_reply=reply)
|
||||
@ -6,18 +6,28 @@ def test_extract_code():
|
||||
print(extract_code("```bash\npython temp.py\n```"))
|
||||
|
||||
|
||||
def test_coding_agent():
|
||||
def test_coding_agent(human_input_mode="NEVER", max_consecutive_auto_reply=10):
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
return
|
||||
from flaml.autogen.agent.coding_agent import PythonAgent
|
||||
from flaml.autogen.agent.agent import Agent
|
||||
from flaml.autogen.agent.human_proxy_agent import HumanProxyAgent
|
||||
|
||||
conversations = {}
|
||||
oai.ChatCompletion.start_logging(conversations)
|
||||
agent = PythonAgent("coding_agent")
|
||||
user = Agent("user")
|
||||
agent = PythonAgent("coding_agent", request_timeout=600, seed=42)
|
||||
user = HumanProxyAgent(
|
||||
"user",
|
||||
human_input_mode=human_input_mode,
|
||||
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
||||
is_termination_msg=lambda x: x.rstrip().endswith("TERMINATE"),
|
||||
)
|
||||
agent.receive(
|
||||
"""Create and execute a script to plot a rocket without using matplotlib""",
|
||||
user,
|
||||
)
|
||||
agent.reset()
|
||||
agent.receive(
|
||||
"""Create a temp.py file with the following content:
|
||||
```
|
||||
@ -32,13 +42,13 @@ print('Hello world!')
|
||||
oai.ChatCompletion.stop_logging()
|
||||
|
||||
|
||||
def test_tsp():
|
||||
def test_tsp(human_input_mode="NEVER", max_consecutive_auto_reply=10):
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
return
|
||||
from flaml.autogen.agent.coding_agent import PythonAgent
|
||||
from flaml.autogen.agent.agent import Agent
|
||||
from flaml.autogen.agent.human_proxy_agent import HumanProxyAgent
|
||||
|
||||
hard_questions = [
|
||||
"What if we must go from node 1 to node 2?",
|
||||
@ -47,8 +57,13 @@ def test_tsp():
|
||||
]
|
||||
|
||||
oai.ChatCompletion.start_logging()
|
||||
agent = PythonAgent("coding_agent", work_dir="test/autogen", temperature=0)
|
||||
user = Agent("user")
|
||||
agent = PythonAgent("coding_agent", temperature=0)
|
||||
user = HumanProxyAgent(
|
||||
"user",
|
||||
work_dir="test/autogen",
|
||||
human_input_mode=human_input_mode,
|
||||
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
||||
)
|
||||
with open("test/autogen/tsp_prompt.txt", "r") as f:
|
||||
prompt = f.read()
|
||||
# agent.receive(prompt.format(question=hard_questions[0]), user)
|
||||
@ -68,5 +83,8 @@ if __name__ == "__main__":
|
||||
# openai.api_version = "2023-03-15-preview" # change if necessary
|
||||
# openai.api_key = "<your_api_key>"
|
||||
# test_extract_code()
|
||||
test_coding_agent()
|
||||
test_tsp()
|
||||
test_coding_agent(human_input_mode="TERMINATE")
|
||||
# when GPT-4, i.e., the DEFAULT_MODEL, is used, conversation in the following test
|
||||
# should terminate in 2-3 rounds of interactions (because is_termination_msg should be true after 2-3 rounds)
|
||||
# although the max_consecutive_auto_reply is set to 10.
|
||||
test_tsp(human_input_mode="NEVER", max_consecutive_auto_reply=10)
|
||||
|
||||
39
test/autogen/test_human_proxy_agent.py
Normal file
39
test/autogen/test_human_proxy_agent.py
Normal file
@ -0,0 +1,39 @@
|
||||
from flaml import oai
|
||||
|
||||
|
||||
def test_human_agent():
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
return
|
||||
from flaml.autogen.agent.chat_agent import ChatAgent
|
||||
from flaml.autogen.agent.human_proxy_agent import HumanProxyAgent
|
||||
|
||||
conversations = {}
|
||||
oai.ChatCompletion.start_logging(conversations)
|
||||
agent = ChatAgent("chat_agent")
|
||||
user = HumanProxyAgent("human_user", human_input_mode="NEVER", max_consecutive_auto_reply=2)
|
||||
agent.receive(
|
||||
"""Write python code to solve the equation x^3=125. You must write code in the following format. You must always print the result.
|
||||
Wait for me to return the result.
|
||||
```python
|
||||
# your code
|
||||
print(your_result)
|
||||
```
|
||||
""",
|
||||
user,
|
||||
)
|
||||
print(conversations)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import openai
|
||||
|
||||
openai.api_key_path = "test/openai/key.txt"
|
||||
# if you use Azure OpenAI, comment the above line and uncomment the following lines
|
||||
# openai.api_type = "azure"
|
||||
# openai.api_base = "https://<your_endpoint>.openai.azure.com/"
|
||||
# openai.api_version = "2023-03-15-preview" # change if necessary
|
||||
# openai.api_key = "<your_api_key>"
|
||||
# test_extract_code()
|
||||
test_human_agent()
|
||||
Loading…
x
Reference in New Issue
Block a user