From 0bf2ce707e6fdf65a883da9dbf82d9bb2682f337 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Thu, 9 Nov 2023 01:41:24 +0800 Subject: [PATCH 1/3] 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/3] 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 705a3e962b2d5a7d8bb643901b84f0d9b34870e2 Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Fri, 17 Nov 2023 14:40:07 +0800 Subject: [PATCH 3/3] 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,