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:
Qingyun Wu 2023-05-15 20:37:38 -04:00 committed by GitHub
parent f01acb67f6
commit 2e43509690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 245 additions and 65 deletions

View File

@ -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):

View 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)

View File

@ -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()

View File

@ -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)

View 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)

View File

@ -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)

View 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()