From 0bf2ce707e6fdf65a883da9dbf82d9bb2682f337 Mon Sep 17 00:00:00 2001
From: garylin2099
Date: Thu, 9 Nov 2023 01:41:24 +0800
Subject: [PATCH 1/6] allow human to play any roles
---
metagpt/llm.py | 1 +
metagpt/provider/human_gpt.py | 35 +++++++++++++++++++++++++++++++++++
metagpt/roles/role.py | 15 ++++++++++-----
3 files changed, 46 insertions(+), 5 deletions(-)
create mode 100644 metagpt/provider/human_gpt.py
diff --git a/metagpt/llm.py b/metagpt/llm.py
index e6f815950..8b4a13838 100644
--- a/metagpt/llm.py
+++ b/metagpt/llm.py
@@ -8,6 +8,7 @@
from metagpt.provider.anthropic_api import Claude2 as Claude
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM
+from metagpt.provider.human_gpt import HumanGPT
DEFAULT_LLM = LLM()
CLAUDE_LLM = Claude()
diff --git a/metagpt/provider/human_gpt.py b/metagpt/provider/human_gpt.py
new file mode 100644
index 000000000..bc4d6dc6e
--- /dev/null
+++ b/metagpt/provider/human_gpt.py
@@ -0,0 +1,35 @@
+'''
+Filename: MetaGPT/metagpt/provider/human_speaker.py
+Created Date: Wednesday, November 8th 2023, 11:55:46 pm
+Author: garylin2099
+'''
+from typing import Optional
+from metagpt.provider.base_gpt_api import BaseGPTAPI
+from metagpt.logs import logger
+
+class HumanGPT(BaseGPTAPI):
+ """A dummy GPT that actually takes in human input as its response.
+ This enables replacing LLM anywhere in the framework with a human, thus introducing human interaction
+ """
+
+ def ask(self, msg: str) -> str:
+ logger.info("It's your turn, please type in your response. You may also refer to the context below")
+ rsp = input(msg)
+ if rsp in ["exit", "quit"]:
+ exit()
+ return rsp
+
+ async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str:
+ return self.ask(msg)
+
+ def completion(self, messages: list[dict]):
+ """dummy implementation of abstract method in base"""
+ return []
+
+ async def acompletion(self, messages: list[dict]):
+ """dummy implementation of abstract method in base"""
+ return []
+
+ async def acompletion_text(self, messages: list[dict], stream=False) -> str:
+ """dummy implementation of abstract method in base"""
+ return []
diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py
index 0251176f7..fecca0beb 100644
--- a/metagpt/roles/role.py
+++ b/metagpt/roles/role.py
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.actions import Action, ActionOutput
-from metagpt.llm import LLM
+from metagpt.llm import LLM, HumanGPT
from metagpt.logs import logger
from metagpt.memory import Memory, LongTermMemory
from metagpt.schema import Message
@@ -65,6 +65,7 @@ class RoleSetting(BaseModel):
goal: str
constraints: str
desc: str
+ is_human: bool
def __str__(self):
return f"{self.name}({self.profile})"
@@ -106,9 +107,10 @@ class RoleContext(BaseModel):
class Role:
"""Role/Agent"""
- def __init__(self, name="", profile="", goal="", constraints="", desc=""):
- self._llm = LLM()
- self._setting = RoleSetting(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc)
+ def __init__(self, name="", profile="", goal="", constraints="", desc="", is_human=False):
+ self._llm = LLM() if not is_human else HumanGPT()
+ self._setting = RoleSetting(name=name, profile=profile, goal=goal,
+ constraints=constraints, desc=desc, is_human=is_human)
self._states = []
self._actions = []
self._role_id = str(self._setting)
@@ -122,8 +124,11 @@ class Role:
self._reset()
for idx, action in enumerate(actions):
if not isinstance(action, Action):
- i = action("")
+ i = action("", llm=self._llm)
else:
+ if self._setting.is_human and not isinstance(action.llm, HumanGPT):
+ logger.warning(f"is_human attribute does not take effect,"
+ f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances")
i = action
i.set_prefix(self._get_prefix(), self.profile)
self._actions.append(i)
From 7bf808ee258fe15ab08ad234a081ef1660548833 Mon Sep 17 00:00:00 2001
From: garylin2099
Date: Fri, 17 Nov 2023 00:20:46 +0800
Subject: [PATCH 2/6] renaming & provide example & small bug fix
---
examples/build_customized_agent.py | 17 +-
examples/build_customized_multi_agents.py | 150 ++++++++++++++++++
examples/debate.py | 2 +
metagpt/llm.py | 2 +-
.../{human_gpt.py => human_provider.py} | 6 +-
metagpt/roles/role.py | 6 +-
6 files changed, 164 insertions(+), 19 deletions(-)
create mode 100644 examples/build_customized_multi_agents.py
rename metagpt/provider/{human_gpt.py => human_provider.py} (85%)
diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py
index c7069b768..ed00e8320 100644
--- a/examples/build_customized_agent.py
+++ b/examples/build_customized_agent.py
@@ -19,15 +19,6 @@ class SimpleWriteCode(Action):
PROMPT_TEMPLATE = """
Write a python function that can {instruction} and provide two runnnable test cases.
Return ```python your_code_here ``` with NO other texts,
- example:
- ```python
- # function
- def add(a, b):
- return a + b
- # test cases
- print(add(1, 2))
- print(add(3, 4))
- ```
your code:
"""
@@ -73,12 +64,12 @@ class SimpleCoder(Role):
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
- todo = self._rc.todo
+ todo = self._rc.todo # todo will be SimpleWriteCode()
msg = self.get_memories(k=1)[0] # find the most recent messages
- code_text = await SimpleWriteCode().run(msg.content)
- msg = Message(content=code_text, role=self.profile, cause_by=todo)
+ code_text = await todo.run(msg.content)
+ msg = Message(content=code_text, role=self.profile, cause_by=type(todo))
return msg
@@ -95,6 +86,8 @@ class RunnableCoder(Role):
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
+ # By choosing the Action by order under the hood
+ # todo will be first SimpleWriteCode() then SimpleRunCode()
todo = self._rc.todo
msg = self.get_memories(k=1)[0] # find the most k recent messages
diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py
new file mode 100644
index 000000000..02221e8e9
--- /dev/null
+++ b/examples/build_customized_multi_agents.py
@@ -0,0 +1,150 @@
+'''
+Filename: MetaGPT/examples/build_customized_multi_agents.py
+Created Date: Wednesday, November 15th 2023, 7:12:39 pm
+Author: garylin2099
+'''
+import re
+import asyncio
+import fire
+from metagpt.actions import Action, BossRequirement
+from metagpt.roles import Role
+from metagpt.team import Team
+from metagpt.schema import Message
+from metagpt.logs import logger
+
+def parse_code(rsp):
+ pattern = r'```python(.*)```'
+ match = re.search(pattern, rsp, re.DOTALL)
+ code_text = match.group(1) if match else rsp
+ return code_text
+
+class SimpleWriteCode(Action):
+
+ PROMPT_TEMPLATE = """
+ Write a python function that can {instruction}.
+ Return ```python your_code_here ``` with NO other texts,
+ your code:
+ """
+
+ def __init__(self, name="SimpleWriteCode", context=None, llm=None):
+ super().__init__(name, context, llm)
+
+ async def run(self, instruction: str):
+
+ prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
+
+ rsp = await self._aask(prompt)
+
+ code_text = parse_code(rsp)
+
+ return code_text
+
+class SimpleCoder(Role):
+ def __init__(
+ self,
+ name: str = "Alice",
+ profile: str = "SimpleCoder",
+ **kwargs,
+ ):
+ super().__init__(name, profile, **kwargs)
+ self._watch([BossRequirement])
+ self._init_actions([SimpleWriteCode])
+
+class SimpleWriteTest(Action):
+
+ PROMPT_TEMPLATE = """
+ Context: {context}
+ Write {k} unit tests using pytest for the given function, assuming you have imported it.
+ Return ```python your_code_here ``` with NO other texts,
+ your code:
+ """
+
+ def __init__(self, name="SimpleWriteTest", context=None, llm=None):
+ super().__init__(name, context, llm)
+
+ async def run(self, context: str, k: int = 3):
+
+ prompt = self.PROMPT_TEMPLATE.format(context=context, k=k)
+
+ rsp = await self._aask(prompt)
+
+ code_text = parse_code(rsp)
+
+ return code_text
+
+class SimpleTester(Role):
+ def __init__(
+ self,
+ name: str = "Bob",
+ profile: str = "SimpleTester",
+ **kwargs,
+ ):
+ super().__init__(name, profile, **kwargs)
+ self._init_actions([SimpleWriteTest])
+ # self._watch([SimpleWriteCode])
+ self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too
+
+ async def _act(self) -> Message:
+ logger.info(f"{self._setting}: ready to {self._rc.todo}")
+ todo = self._rc.todo
+
+ # context = self.get_memories(k=1)[0].content # use the most recent memory as context
+ context = self.get_memories() # use all memories as context
+
+ code_text = await todo.run(context, k=5) # specify arguments
+ msg = Message(content=code_text, role=self.profile, cause_by=type(todo))
+
+ return msg
+
+class SimpleWriteReview(Action):
+
+ PROMPT_TEMPLATE = """
+ Context: {context}
+ Review the test cases and provide one critical comments:
+ """
+
+ def __init__(self, name="SimpleWriteReview", context=None, llm=None):
+ super().__init__(name, context, llm)
+
+ async def run(self, context: str):
+
+ prompt = self.PROMPT_TEMPLATE.format(context=context)
+
+ rsp = await self._aask(prompt)
+
+ return rsp
+
+class SimpleReviewer(Role):
+ def __init__(
+ self,
+ name: str = "Charlie",
+ profile: str = "SimpleReviewer",
+ **kwargs,
+ ):
+ super().__init__(name, profile, **kwargs)
+ self._init_actions([SimpleWriteReview])
+ self._watch([SimpleWriteTest])
+
+async def main(
+ idea: str = "write a function that calculates the product of a list",
+ investment: float = 3.0,
+ n_round: int = 5,
+ add_human: bool = False,
+):
+ logger.info(idea)
+
+ team = Team()
+ team.hire(
+ [
+ SimpleCoder(),
+ SimpleTester(),
+ SimpleReviewer(is_human=add_human),
+ ]
+ )
+
+ team.invest(investment=investment)
+ team.start_project(idea)
+ await team.run(n_round=n_round)
+
+if __name__ == '__main__':
+ fire.Fire(main)
diff --git a/examples/debate.py b/examples/debate.py
index 4db7567f0..a37e60848 100644
--- a/examples/debate.py
+++ b/examples/debate.py
@@ -77,6 +77,8 @@ class Debator(Role):
send_to=self.opponent_name,
)
+ self._rc.memory.add(msg)
+
return msg
async def debate(idea: str, investment: float = 3.0, n_round: int = 5):
diff --git a/metagpt/llm.py b/metagpt/llm.py
index 8b4a13838..9324da126 100644
--- a/metagpt/llm.py
+++ b/metagpt/llm.py
@@ -8,7 +8,7 @@
from metagpt.provider.anthropic_api import Claude2 as Claude
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM
-from metagpt.provider.human_gpt import HumanGPT
+from metagpt.provider.human_provider import HumanProvider
DEFAULT_LLM = LLM()
CLAUDE_LLM = Claude()
diff --git a/metagpt/provider/human_gpt.py b/metagpt/provider/human_provider.py
similarity index 85%
rename from metagpt/provider/human_gpt.py
rename to metagpt/provider/human_provider.py
index bc4d6dc6e..1d12f972f 100644
--- a/metagpt/provider/human_gpt.py
+++ b/metagpt/provider/human_provider.py
@@ -1,5 +1,5 @@
'''
-Filename: MetaGPT/metagpt/provider/human_speaker.py
+Filename: MetaGPT/metagpt/provider/human_provider.py
Created Date: Wednesday, November 8th 2023, 11:55:46 pm
Author: garylin2099
'''
@@ -7,8 +7,8 @@ from typing import Optional
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.logs import logger
-class HumanGPT(BaseGPTAPI):
- """A dummy GPT that actually takes in human input as its response.
+class HumanProvider(BaseGPTAPI):
+ """Humans provide themselves as a 'model', which actually takes in human input as its response.
This enables replacing LLM anywhere in the framework with a human, thus introducing human interaction
"""
diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py
index fecca0beb..b96c361c0 100644
--- a/metagpt/roles/role.py
+++ b/metagpt/roles/role.py
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.actions import Action, ActionOutput
-from metagpt.llm import LLM, HumanGPT
+from metagpt.llm import LLM, HumanProvider
from metagpt.logs import logger
from metagpt.memory import Memory, LongTermMemory
from metagpt.schema import Message
@@ -108,7 +108,7 @@ class Role:
"""Role/Agent"""
def __init__(self, name="", profile="", goal="", constraints="", desc="", is_human=False):
- self._llm = LLM() if not is_human else HumanGPT()
+ self._llm = LLM() if not is_human else HumanProvider()
self._setting = RoleSetting(name=name, profile=profile, goal=goal,
constraints=constraints, desc=desc, is_human=is_human)
self._states = []
@@ -126,7 +126,7 @@ class Role:
if not isinstance(action, Action):
i = action("", llm=self._llm)
else:
- if self._setting.is_human and not isinstance(action.llm, HumanGPT):
+ if self._setting.is_human and not isinstance(action.llm, HumanProvider):
logger.warning(f"is_human attribute does not take effect,"
f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances")
i = action
From 5c66b29a80e538cb5db0e11b5844c335c55c3864 Mon Sep 17 00:00:00 2001
From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com>
Date: Fri, 17 Nov 2023 13:56:29 +0800
Subject: [PATCH 3/6] Update README.md
rm as waitlist
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index d2ca26cdd..1307fbb53 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,6 @@ # MetaGPT: The Multi-Agent Framework
-
From aa14c6664bddc91742de7a30dc44657eb4a517f8 Mon Sep 17 00:00:00 2001
From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com>
Date: Fri, 17 Nov 2023 13:56:59 +0800
Subject: [PATCH 4/6] Update README_CN.md
rm as waitlist
---
docs/README_CN.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index 3aa0cb05d..a49ca9b77 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -19,7 +19,6 @@ # MetaGPT: 多智能体框架
-
From f8ebfa9a742abf3f0b187428a6c14d328c8c1460 Mon Sep 17 00:00:00 2001
From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com>
Date: Fri, 17 Nov 2023 13:57:28 +0800
Subject: [PATCH 5/6] Update README_JA.md
rm as waitlist
---
docs/README_JA.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/README_JA.md b/docs/README_JA.md
index 1ced60b83..cba84df80 100644
--- a/docs/README_JA.md
+++ b/docs/README_JA.md
@@ -19,7 +19,6 @@ # MetaGPT: マルチエージェントフレームワーク
-
From 705a3e962b2d5a7d8bb643901b84f0d9b34870e2 Mon Sep 17 00:00:00 2001
From: garylin2099
Date: Fri, 17 Nov 2023 14:40:07 +0800
Subject: [PATCH 6/6] formatting
---
examples/build_customized_agent.py | 9 +++++++--
examples/build_customized_multi_agents.py | 14 +++++++++++---
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py
index ed00e8320..be34e5e5e 100644
--- a/examples/build_customized_agent.py
+++ b/examples/build_customized_agent.py
@@ -9,6 +9,7 @@ import asyncio
import fire
+from metagpt.llm import LLM
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
@@ -22,7 +23,7 @@ class SimpleWriteCode(Action):
your code:
"""
- def __init__(self, name="SimpleWriteCode", context=None, llm=None):
+ def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)
async def run(self, instruction: str):
@@ -42,8 +43,9 @@ class SimpleWriteCode(Action):
code_text = match.group(1) if match else rsp
return code_text
+
class SimpleRunCode(Action):
- def __init__(self, name="SimpleRunCode", context=None, llm=None):
+ def __init__(self, name: str = "SimpleRunCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)
async def run(self, code_text: str):
@@ -52,6 +54,7 @@ class SimpleRunCode(Action):
logger.info(f"{code_result=}")
return code_result
+
class SimpleCoder(Role):
def __init__(
self,
@@ -73,6 +76,7 @@ class SimpleCoder(Role):
return msg
+
class RunnableCoder(Role):
def __init__(
self,
@@ -97,6 +101,7 @@ class RunnableCoder(Role):
self._rc.memory.add(msg)
return msg
+
def main(msg="write a function that calculates the product of a list and run it"):
# role = SimpleCoder()
role = RunnableCoder()
diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py
index 02221e8e9..0df927e32 100644
--- a/examples/build_customized_multi_agents.py
+++ b/examples/build_customized_multi_agents.py
@@ -6,6 +6,8 @@ Author: garylin2099
import re
import asyncio
import fire
+
+from metagpt.llm import LLM
from metagpt.actions import Action, BossRequirement
from metagpt.roles import Role
from metagpt.team import Team
@@ -26,7 +28,7 @@ class SimpleWriteCode(Action):
your code:
"""
- def __init__(self, name="SimpleWriteCode", context=None, llm=None):
+ def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)
async def run(self, instruction: str):
@@ -39,6 +41,7 @@ class SimpleWriteCode(Action):
return code_text
+
class SimpleCoder(Role):
def __init__(
self,
@@ -50,6 +53,7 @@ class SimpleCoder(Role):
self._watch([BossRequirement])
self._init_actions([SimpleWriteCode])
+
class SimpleWriteTest(Action):
PROMPT_TEMPLATE = """
@@ -59,7 +63,7 @@ class SimpleWriteTest(Action):
your code:
"""
- def __init__(self, name="SimpleWriteTest", context=None, llm=None):
+ def __init__(self, name: str = "SimpleWriteTest", context=None, llm: LLM = None):
super().__init__(name, context, llm)
async def run(self, context: str, k: int = 3):
@@ -72,6 +76,7 @@ class SimpleWriteTest(Action):
return code_text
+
class SimpleTester(Role):
def __init__(
self,
@@ -96,6 +101,7 @@ class SimpleTester(Role):
return msg
+
class SimpleWriteReview(Action):
PROMPT_TEMPLATE = """
@@ -103,7 +109,7 @@ class SimpleWriteReview(Action):
Review the test cases and provide one critical comments:
"""
- def __init__(self, name="SimpleWriteReview", context=None, llm=None):
+ def __init__(self, name: str = "SimpleWriteReview", context=None, llm: LLM = None):
super().__init__(name, context, llm)
async def run(self, context: str):
@@ -114,6 +120,7 @@ class SimpleWriteReview(Action):
return rsp
+
class SimpleReviewer(Role):
def __init__(
self,
@@ -125,6 +132,7 @@ class SimpleReviewer(Role):
self._init_actions([SimpleWriteReview])
self._watch([SimpleWriteTest])
+
async def main(
idea: str = "write a function that calculates the product of a list",
investment: float = 3.0,