From 5e8ada5cfffd470a7513630391077f0f291e8f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 31 Oct 2023 15:23:37 +0800 Subject: [PATCH 001/115] refactor: Message --- metagpt/schema.py | 106 +++++++++++++++++++++++++--------- tests/metagpt/test_message.py | 22 ++++--- 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index bdca093c2..1124fb28e 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -4,13 +4,15 @@ @Time : 2023/5/8 22:12 @Author : alexanderwu @File : schema.py +@Modified By: mashenquan, 2023-10-31, optimize class members. """ from __future__ import annotations -from dataclasses import dataclass, field -from typing import Type, TypedDict +import json +from json import JSONDecodeError +from typing import Dict, List, TypedDict -from pydantic import BaseModel +from pydantic import BaseModel, Field from metagpt.logs import logger @@ -20,16 +22,44 @@ class RawMessage(TypedDict): role: str -@dataclass -class Message: +class Message(BaseModel): """list[: ]""" + content: str - instruct_content: BaseModel = field(default=None) - role: str = field(default='user') # system / user / assistant - cause_by: Type["Action"] = field(default="") - sent_from: str = field(default="") - send_to: str = field(default="") - restricted_to: str = field(default="") + instruct_content: BaseModel = None + meta_info: Dict = Field(default_factory=dict) + route: List[Dict] = Field(default_factory=list) + + def __init__(self, content, **kwargs): + super(Message, self).__init__( + content=content or kwargs.get("content"), + instruct_content=kwargs.get("instruct_content"), + meta_info=kwargs.get("meta_info", {}), + route=kwargs.get("route", []), + ) + + attribute_names = Message.__annotations__.keys() + for k, v in kwargs.items(): + if k in attribute_names: + continue + self.meta_info[k] = v + + def get_meta(self, key): + return self.meta_info.get(key) + + def set_meta(self, key, value): + self.meta_info[key] = value + + @property + def role(self): + return self.get_meta("role") + + @property + def cause_by(self): + return self.get_meta("cause_by") + + def set_role(self, v): + self.set_meta("role", v) def __str__(self): # prefix = '-'.join([self.role, str(self.cause_by)]) @@ -39,45 +69,67 @@ class Message: return self.__str__() def to_dict(self) -> dict: - return { - "role": self.role, - "content": self.content - } + return {"role": self.role, "content": self.content} + + def save(self) -> str: + return self.json(exclude_none=True) + + @staticmethod + def load(v): + try: + d = json.loads(v) + return Message(**d) + except JSONDecodeError as err: + logger.error(f"parse json failed: {v}, error:{err}") + return None -@dataclass class UserMessage(Message): """便于支持OpenAI的消息 - Facilitate support for OpenAI messages + Facilitate support for OpenAI messages """ + def __init__(self, content: str): - super().__init__(content, 'user') + super(Message, self).__init__(content=content, meta_info={"role": "user"}) -@dataclass class SystemMessage(Message): """便于支持OpenAI的消息 - Facilitate support for OpenAI messages + Facilitate support for OpenAI messages """ + def __init__(self, content: str): - super().__init__(content, 'system') + super().__init__(content=content, meta_info={"role": "system"}) -@dataclass class AIMessage(Message): """便于支持OpenAI的消息 - Facilitate support for OpenAI messages + Facilitate support for OpenAI messages """ + def __init__(self, content: str): - super().__init__(content, 'assistant') + super().__init__(content=content, meta_info={"role": "assistant"}) -if __name__ == '__main__': - test_content = 'test_message' +if __name__ == "__main__": + m = Message("a", role="v1") + m.set_role("v2") + v = m.save() + m = Message.load(v) + + test_content = "test_message" msgs = [ UserMessage(test_content), SystemMessage(test_content), AIMessage(test_content), - Message(test_content, role='QA') + Message(test_content, role="QA"), ] logger.info(msgs) + + jsons = [ + UserMessage(test_content).save(), + SystemMessage(test_content).save(), + AIMessage(test_content).save(), + Message(test_content, role="QA").save(), + ] + logger.info(jsons) diff --git a/tests/metagpt/test_message.py b/tests/metagpt/test_message.py index e26f38381..4f46311ce 100644 --- a/tests/metagpt/test_message.py +++ b/tests/metagpt/test_message.py @@ -11,26 +11,30 @@ from metagpt.schema import AIMessage, Message, RawMessage, SystemMessage, UserMe def test_message(): - msg = Message(role='User', content='WTF') - assert msg.to_dict()['role'] == 'User' - assert 'User' in str(msg) + msg = Message(role="User", content="WTF") + assert msg.to_dict()["role"] == "User" + assert "User" in str(msg) def test_all_messages(): - test_content = 'test_message' + test_content = "test_message" msgs = [ UserMessage(test_content), SystemMessage(test_content), AIMessage(test_content), - Message(test_content, role='QA') + Message(test_content, role="QA"), ] for msg in msgs: assert msg.content == test_content def test_raw_message(): - msg = RawMessage(role='user', content='raw') - assert msg['role'] == 'user' - assert msg['content'] == 'raw' + msg = RawMessage(role="user", content="raw") + assert msg["role"] == "user" + assert msg["content"] == "raw" with pytest.raises(KeyError): - assert msg['1'] == 1, "KeyError: '1'" + assert msg["1"] == 1, "KeyError: '1'" + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From 545d77ce0deac125c14ff8c902ca49ff5ded8cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 1 Nov 2023 20:08:58 +0800 Subject: [PATCH 002/115] refactor: Refactor Message transmission & filtering --- examples/agent_creator.py | 20 +- examples/build_customized_agent.py | 28 +-- examples/debate.py | 47 +++-- examples/sk_agent.py | 9 +- metagpt/actions/action.py | 3 +- metagpt/actions/write_code.py | 9 +- metagpt/const.py | 5 + metagpt/environment.py | 48 +++-- metagpt/memory/longterm_memory.py | 16 +- metagpt/memory/memory.py | 11 +- metagpt/roles/engineer.py | 68 +++++-- metagpt/roles/qa_engineer.py | 46 +++-- metagpt/roles/researcher.py | 15 +- metagpt/roles/role.py | 163 ++++++++++------ metagpt/roles/seacher.py | 36 ++-- metagpt/roles/sk_agent.py | 6 +- metagpt/schema.py | 193 ++++++++++++++++++- metagpt/software_company.py | 22 ++- metagpt/utils/common.py | 16 +- metagpt/utils/named.py | 21 ++ tests/metagpt/actions/test_write_prd.py | 3 +- tests/metagpt/memory/test_longterm_memory.py | 31 +-- tests/metagpt/memory/test_memory_storage.py | 70 +++---- tests/metagpt/planner/test_action_planner.py | 7 +- tests/metagpt/planner/test_basic_planner.py | 6 +- tests/metagpt/roles/mock.py | 27 +-- tests/metagpt/roles/test_architect.py | 5 +- tests/metagpt/roles/test_engineer.py | 13 +- tests/metagpt/test_environment.py | 3 +- tests/metagpt/utils/test_serialize.py | 7 +- 30 files changed, 658 insertions(+), 296 deletions(-) create mode 100644 metagpt/utils/named.py diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 325e7c260..d13cbcff2 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -1,22 +1,24 @@ -''' +""" Filename: MetaGPT/examples/agent_creator.py Created Date: Tuesday, September 12th 2023, 3:28:37 pm Author: garylin2099 -''' +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" import re -from metagpt.const import PROJECT_ROOT, WORKSPACE_ROOT from metagpt.actions import Action +from metagpt.const import PROJECT_ROOT, WORKSPACE_ROOT +from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.logs import logger +from metagpt.utils.common import get_object_name with open(PROJECT_ROOT / "examples/build_customized_agent.py", "r") as f: # use official example script to guide AgentCreator MULTI_ACTION_AGENT_CODE_EXAMPLE = f.read() -class CreateAgent(Action): +class CreateAgent(Action): PROMPT_TEMPLATE = """ ### BACKGROUND You are using an agent framework called metagpt to write agents capable of different actions, @@ -34,7 +36,6 @@ class CreateAgent(Action): """ async def run(self, example: str, instruction: str): - prompt = self.PROMPT_TEMPLATE.format(example=example, instruction=instruction) # logger.info(prompt) @@ -46,13 +47,14 @@ class CreateAgent(Action): @staticmethod def parse_code(rsp): - pattern = r'```python(.*)```' + pattern = r"```python(.*)```" match = re.search(pattern, rsp, re.DOTALL) code_text = match.group(1) if match else "" with open(WORKSPACE_ROOT / "agent_created_agent.py", "w") as f: f.write(code_text) return code_text + class AgentCreator(Role): def __init__( self, @@ -72,15 +74,15 @@ class AgentCreator(Role): instruction = msg.content code_text = await CreateAgent().run(example=self.agent_template, instruction=instruction) - msg = Message(content=code_text, role=self.profile, cause_by=todo) + msg = Message(content=code_text, role=self.profile, cause_by=get_object_name(todo)) return msg + if __name__ == "__main__": import asyncio async def main(): - agent_template = MULTI_ACTION_AGENT_CODE_EXAMPLE creator = AgentCreator(agent_template=agent_template) diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index 87d7a9c76..a953dee15 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -1,21 +1,23 @@ -''' +""" Filename: MetaGPT/examples/build_customized_agent.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -''' +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" +import asyncio import re import subprocess -import asyncio import fire from metagpt.actions import Action +from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.logs import logger +from metagpt.utils.common import get_object_name + 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, @@ -35,7 +37,6 @@ class SimpleWriteCode(Action): super().__init__(name, context, llm) async def run(self, instruction: str): - prompt = self.PROMPT_TEMPLATE.format(instruction=instruction) rsp = await self._aask(prompt) @@ -46,11 +47,12 @@ class SimpleWriteCode(Action): @staticmethod def parse_code(rsp): - pattern = r'```python(.*)```' + pattern = r"```python(.*)```" match = re.search(pattern, rsp, re.DOTALL) code_text = match.group(1) if match else rsp return code_text + class SimpleRunCode(Action): def __init__(self, name="SimpleRunCode", context=None, llm=None): super().__init__(name, context, llm) @@ -61,6 +63,7 @@ class SimpleRunCode(Action): logger.info(f"{code_result=}") return code_result + class SimpleCoder(Role): def __init__( self, @@ -75,14 +78,15 @@ class SimpleCoder(Role): logger.info(f"{self._setting}: ready to {self._rc.todo}") todo = self._rc.todo - msg = self._rc.memory.get()[-1] # retrieve the latest memory + msg = self._rc.memory.get()[-1] # retrieve the latest memory instruction = msg.content code_text = await SimpleWriteCode().run(instruction) - msg = Message(content=code_text, role=self.profile, cause_by=todo) + msg = Message(content=code_text, role=self.profile, cause_by=get_object_name(todo)) return msg + class RunnableCoder(Role): def __init__( self, @@ -116,7 +120,7 @@ class RunnableCoder(Role): code_text = msg.content result = await SimpleRunCode().run(code_text) - msg = Message(content=result, role=self.profile, cause_by=todo) + msg = Message(content=result, role=self.profile, cause_by=get_object_name(todo)) self._rc.memory.add(msg) return msg @@ -128,6 +132,7 @@ class RunnableCoder(Role): await self._act() return Message(content="All job done", role=self.profile) + def main(msg="write a function that calculates the sum of a list"): # role = SimpleCoder() role = RunnableCoder() @@ -135,5 +140,6 @@ def main(msg="write a function that calculates the sum of a list"): result = asyncio.run(role.run(msg)) logger.info(result) -if __name__ == '__main__': + +if __name__ == "__main__": fire.Fire(main) diff --git a/examples/debate.py b/examples/debate.py index 05db28070..ade1a6fc4 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -1,17 +1,20 @@ -''' +""" Filename: MetaGPT/examples/debate.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -''' +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" import asyncio import platform + import fire -from metagpt.software_company import SoftwareCompany from metagpt.actions import Action, BossRequirement +from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.logs import logger +from metagpt.software_company import SoftwareCompany + class ShoutOut(Action): """Action: Shout out loudly in a debate (quarrel)""" @@ -31,7 +34,6 @@ class ShoutOut(Action): super().__init__(name, context, llm) async def run(self, context: str, name: str, opponent_name: str): - prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name) # logger.info(prompt) @@ -39,6 +41,7 @@ class ShoutOut(Action): return rsp + class Trump(Role): def __init__( self, @@ -55,13 +58,13 @@ class Trump(Role): async def _observe(self) -> int: await super()._observe() # accept messages sent (from opponent) to self, disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.send_to == self.name] + self._rc.news = [msg for msg in self._rc.news if msg.is_recipient({self.name})] return len(self._rc.news) async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([ShoutOut]) + msg_history = self._rc.memory.get_by_actions([ShoutOut.get_class_name()]) context = [] for m in msg_history: context.append(str(m)) @@ -72,13 +75,14 @@ class Trump(Role): msg = Message( content=rsp, role=self.profile, - cause_by=ShoutOut, - sent_from=self.name, - send_to=self.opponent_name, + cause_by=ShoutOut.get_class_name(), + tx_from=self.name, + tx_to=self.opponent_name, ) return msg + class Biden(Role): def __init__( self, @@ -96,13 +100,14 @@ class Biden(Role): await super()._observe() # accept the very first human instruction (the debate topic) or messages sent (from opponent) to self, # disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.cause_by == BossRequirement or msg.send_to == self.name] + message_filter = {BossRequirement.get_class_name(), self.name} + self._rc.news = [msg for msg in self._rc.news if msg.is_recipient(message_filter)] return len(self._rc.news) async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut]) + msg_history = self._rc.memory.get_by_actions([BossRequirement.get_class_name(), ShoutOut.get_class_name()]) context = [] for m in msg_history: context.append(str(m)) @@ -113,17 +118,19 @@ class Biden(Role): msg = Message( content=rsp, role=self.profile, - cause_by=ShoutOut, - sent_from=self.name, - send_to=self.opponent_name, + cause_by=ShoutOut.get_class_name(), + tx_from=self.name, + tx_to=self.opponent_name, ) return msg -async def startup(idea: str, investment: float = 3.0, n_round: int = 5, - code_review: bool = False, run_tests: bool = False): + +async def startup( + idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False, run_tests: bool = False +): """We reuse the startup paradigm for roles to interact with each other. - Now we run a startup of presidents and watch they quarrel. :) """ + Now we run a startup of presidents and watch they quarrel. :)""" company = SoftwareCompany() company.hire([Biden(), Trump()]) company.invest(investment) @@ -133,7 +140,7 @@ async def startup(idea: str, investment: float = 3.0, n_round: int = 5, def main(idea: str, investment: float = 3.0, n_round: int = 10): """ - :param idea: Debate topic, such as "Topic: The U.S. should commit more in climate change fighting" + :param idea: Debate topic, such as "Topic: The U.S. should commit more in climate change fighting" or "Trump: Climate change is a hoax" :param investment: contribute a certain dollar amount to watch the debate :param n_round: maximum rounds of the debate @@ -144,5 +151,5 @@ def main(idea: str, investment: float = 3.0, n_round: int = 10): asyncio.run(startup(idea, investment, n_round)) -if __name__ == '__main__': +if __name__ == "__main__": fire.Fire(main) diff --git a/examples/sk_agent.py b/examples/sk_agent.py index a7513e838..19ee53669 100644 --- a/examples/sk_agent.py +++ b/examples/sk_agent.py @@ -4,6 +4,7 @@ @Time : 2023/9/13 12:36 @Author : femto Zheng @File : sk_agent.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ import asyncio @@ -39,7 +40,7 @@ async def basic_planner_example(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - await role.run(Message(content=task, cause_by=BossRequirement)) + await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) async def sequential_planner_example(): @@ -53,7 +54,7 @@ async def sequential_planner_example(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - await role.run(Message(content=task, cause_by=BossRequirement)) + await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) async def basic_planner_web_search_example(): @@ -64,7 +65,7 @@ async def basic_planner_web_search_example(): role.import_skill(SkSearchEngine(), "WebSearchSkill") # role.import_semantic_skill_from_directory(skills_directory, "QASkill") - await role.run(Message(content=task, cause_by=BossRequirement)) + await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) async def action_planner_example(): @@ -75,7 +76,7 @@ async def action_planner_example(): role.import_skill(TimeSkill(), "time") role.import_skill(TextSkill(), "text") task = "What is the sum of 110 and 990?" - await role.run(Message(content=task, cause_by=BossRequirement)) # it will choose mathskill.Add + await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) # it will choose mathskill.Add if __name__ == "__main__": diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 790295d55..1954e750a 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -16,9 +16,10 @@ from metagpt.llm import LLM from metagpt.logs import logger from metagpt.utils.common import OutputParser from metagpt.utils.custom_decoder import CustomDecoder +from metagpt.utils.named import Named -class Action(ABC): +class Action(ABC, Named): def __init__(self, name: str = "", context=None, llm: LLM = None): self.name: str = name if llm is None: diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index c000805c5..421211d60 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -5,13 +5,14 @@ @Author : alexanderwu @File : write_code.py """ +from tenacity import retry, stop_after_attempt, wait_fixed + from metagpt.actions import WriteDesign from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_fixed PROMPT_TEMPLATE = """ NOTICE @@ -55,7 +56,8 @@ class WriteCode(Action): if self._is_invalid(filename): return - design = [i for i in context if i.cause_by == WriteDesign][0] + message_filter = {WriteDesign.get_class_name()} + design = [i for i in context if i.is_recipient(message_filter)][0] ws_name = CodeParser.parse_str(block="Python package name", text=design.content) ws_path = WORKSPACE_ROOT / ws_name @@ -74,9 +76,8 @@ class WriteCode(Action): async def run(self, context, filename): prompt = PROMPT_TEMPLATE.format(context=context, filename=filename) - logger.info(f'Writing {filename}..') + logger.info(f"Writing {filename}..") code = await self.write_code(prompt) # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) # self._save(context, filename, code) return code - \ No newline at end of file diff --git a/metagpt/const.py b/metagpt/const.py index 7f3f87dfa..3fbc26784 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -41,3 +41,8 @@ INVOICE_OCR_TABLE_PATH = DATA_PATH / "invoice_table" SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills" MEM_TTL = 24 * 30 * 3600 + +MESSAGE_ROUTE_FROM = "tx_from" +MESSAGE_ROUTE_TO = "tx_to" +MESSAGE_ROUTE_CAUSE_BY = "cause_by" +MESSAGE_META_ROLE = "role" diff --git a/metagpt/environment.py b/metagpt/environment.py index 24e6ada2f..ba0645a36 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -4,60 +4,61 @@ @Time : 2023/5/11 22:12 @Author : alexanderwu @File : environment.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Remove the functionality of `Environment` class as a public message buffer. + 2. Standardize the message forwarding behavior of the `Environment` class. + 3. Add the `is_idle` property. """ import asyncio from typing import Iterable from pydantic import BaseModel, Field -from metagpt.memory import Memory +from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message class Environment(BaseModel): """环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到 - Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles - + Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles + """ roles: dict[str, Role] = Field(default_factory=dict) - memory: Memory = Field(default_factory=Memory) - history: str = Field(default='') class Config: arbitrary_types_allowed = True def add_role(self, role: Role): """增加一个在当前环境的角色 - Add a role in the current environment + Add a role in the current environment """ role.set_env(self) self.roles[role.profile] = role def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 - Add a batch of characters in the current environment + Add a batch of characters in the current environment """ for role in roles: self.add_role(role) def publish_message(self, message: Message): - """向当前环境发布信息 - Post information to the current environment - """ - # self.message_queue.put(message) - self.memory.add(message) - self.history += f"\n{message}" + """Distribute the message to the recipients.""" + logger.info(f"publish_message: {message.save()}") + found = False + for r in self.roles.values(): + if message.is_recipient(r.subscribed_tags): + r.async_put_message(message) + found = True + if not found: + logger.warning(f"Message no recipients: {message.save()}") async def run(self, k=1): """处理一次所有信息的运行 Process all Role runs at once """ - # while not self.message_queue.empty(): - # message = self.message_queue.get() - # rsp = await self.manager.handle(message, self) - # self.message_queue.put(rsp) for _ in range(k): futures = [] for role in self.roles.values(): @@ -65,15 +66,24 @@ class Environment(BaseModel): futures.append(future) await asyncio.gather(*futures) + logger.info(f"is idle: {self.is_idle}") def get_roles(self) -> dict[str, Role]: """获得环境内的所有角色 - Process all Role runs at once + Process all Role runs at once """ return self.roles def get_role(self, name: str) -> Role: """获得环境内的指定角色 - get all the environment roles + get all the environment roles """ return self.roles.get(name, None) + + @property + def is_idle(self): + """If true, all actions have been executed.""" + for r in self.roles.values(): + if not r.is_idle: + return False + return True diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index f8abea5f3..b5bb73b6b 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -1,6 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : the implement of Long-term memory +""" +@Desc : the implement of Long-term memory +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Replace code related to message filtering with the `Message.is_recipient` function. +""" from metagpt.logs import logger from metagpt.memory import Memory @@ -36,11 +40,10 @@ class LongTermMemory(Memory): def add(self, message: Message): super(LongTermMemory, self).add(message) - for action in self.rc.watch: - if message.cause_by == action and not self.msg_from_recover: - # currently, only add role's watching messages to its memory_storage - # and ignore adding messages from recover repeatedly - self.memory_storage.add(message) + if message.is_recipient(self.rc.watch) and not self.msg_from_recover: + # currently, only add role's watching messages to its memory_storage + # and ignore adding messages from recover repeatedly + self.memory_storage.add(message) def find_news(self, observed: list[Message], k=0) -> list[Message]: """ @@ -68,4 +71,3 @@ class LongTermMemory(Memory): def clear(self): super(LongTermMemory, self).clear() self.memory_storage.clean() - \ No newline at end of file diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index c818fa707..8e01544f1 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -4,11 +4,11 @@ @Time : 2023/5/20 12:15 @Author : alexanderwu @File : memory.py +@Modified By: mashenquan, 2023-11-1. Standardize the design of message filtering-related features. """ from collections import defaultdict -from typing import Iterable, Type +from typing import Iterable, Set -from metagpt.actions import Action from metagpt.schema import Message @@ -18,7 +18,7 @@ class Memory: def __init__(self): """Initialize an empty storage list and an empty index dictionary""" self.storage: list[Message] = [] - self.index: dict[Type[Action], list[Message]] = defaultdict(list) + self.index: dict[str, list[Message]] = defaultdict(list) def add(self, message: Message): """Add a new message to storage, while updating the index""" @@ -73,11 +73,11 @@ class Memory: news.append(i) return news - def get_by_action(self, action: Type[Action]) -> list[Message]: + def get_by_action(self, action: str) -> list[Message]: """Return all messages triggered by a specified Action""" return self.index[action] - def get_by_actions(self, actions: Iterable[Type[Action]]) -> list[Message]: + def get_by_actions(self, actions: Set[str]) -> list[Message]: """Return all messages triggered by specified Actions""" rsp = [] for action in actions: @@ -85,4 +85,3 @@ class Memory: continue rsp += self.index[action] return rsp - \ No newline at end of file diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 6d65575a8..9826ea0b7 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -4,6 +4,10 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : engineer.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Consolidate message reception and processing logic within `_observe`. + 2. Fix bug: Add logic for handling asynchronous message processing when messages are not ready. + 3. Supplemented the external transmission of internal messages. """ import asyncio import shutil @@ -15,7 +19,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser +from metagpt.utils.common import CodeParser, get_object_name from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -75,7 +79,7 @@ class Engineer(Role): self.use_code_review = use_code_review if self.use_code_review: self._init_actions([WriteCode, WriteCodeReview]) - self._watch([WriteTasks]) + self._watch([WriteTasks, WriteDesign]) self.todos = [] self.n_borg = n_borg @@ -96,7 +100,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -119,17 +123,13 @@ class Engineer(Role): file.write_text(code) return file - def recv(self, message: Message) -> None: - self._rc.memory.add(message) - if message in self._rc.important_memory: - self.todos = self.parse_tasks(message) - async def _act_mp(self) -> Message: # self.recreate_workspace() todo_coros = [] for todo in self.todos: todo_coro = WriteCode().run( - context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo + context=self._rc.memory.get_by_actions([WriteTasks.get_class_name(), WriteDesign.get_class_name()]), + filename=todo, ) todo_coros.append(todo_coro) @@ -139,12 +139,13 @@ class Engineer(Role): logger.info(todo) logger.info(code_rsp) # self.write_file(todo, code) - msg = Message(content=code_rsp, role=self.profile, cause_by=type(self._rc.todo)) + msg = Message(content=code_rsp, role=self.profile, cause_by=get_object_name(self._rc.todo)) self._rc.memory.add(msg) + self.publish_message(msg) del self.todos[0] logger.info(f"Done {self.get_workspace()} generating.") - msg = Message(content="all done.", role=self.profile, cause_by=type(self._rc.todo)) + msg = Message(content="all done.", role=self.profile, cause_by=get_object_name(self._rc.todo)) return msg async def _act_sp(self) -> Message: @@ -155,15 +156,19 @@ class Engineer(Role): # logger.info(code_rsp) # code = self.parse_code(code_rsp) file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo)) + msg = Message(content=code, role=self.profile, cause_by=get_object_name(self._rc.todo)) self._rc.memory.add(msg) + self.publish_message(msg) code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) logger.info(f"Done {self.get_workspace()} generating.") msg = Message( - content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" + content=MSG_SEP.join(code_msg_all), + role=self.profile, + cause_by=get_object_name(self._rc.todo), + tx_to="QaEngineer", ) return msg @@ -178,7 +183,8 @@ class Engineer(Role): TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ context = [] - msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) + msg_filters = [WriteDesign.get_class_name(), WriteTasks.get_class_name(), WriteCode.get_class_name()] + msg = self._rc.memory.get_by_actions(msg_filters) for m in msg: context.append(m.content) context_str = "\n".join(context) @@ -193,20 +199,50 @@ class Engineer(Role): logger.error("code review failed!", e) pass file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=WriteCode) + msg = Message(content=code, role=self.profile, cause_by=WriteCode.get_class_name()) self._rc.memory.add(msg) + self.publish_message(msg) code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) logger.info(f"Done {self.get_workspace()} generating.") msg = Message( - content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" + content=MSG_SEP.join(code_msg_all), + role=self.profile, + cause_by=get_object_name(self._rc.todo), + tx_to="QaEngineer", ) return msg async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" + if not self._rc.todo: + return None if self.use_code_review: return await self._act_sp_precision() return await self._act_sp() + + async def _observe(self) -> int: + ret = await super(Engineer, self)._observe() + if ret == 0: + return ret + + # Parse task lists + message_filter = {WriteTasks.get_class_name()} + for message in self._rc.news: + if not message.is_recipient(message_filter): + continue + self.todos = self.parse_tasks(message) + + return ret + + async def _think(self) -> None: + # In asynchronous scenarios, first check if the required messages are ready. + filters = {WriteTasks.get_class_name()} + msgs = self._rc.memory.get_by_actions(filters) + if not msgs: + self._rc.todo = None + return + + await super(Engineer, self)._think() diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index a763c2ce8..b83ab6e21 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -4,6 +4,7 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : qa_engineer.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ import os from pathlib import Path @@ -48,7 +49,7 @@ class QaEngineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -97,11 +98,11 @@ class QaEngineer(Role): msg = Message( content=str(file_info), role=self.profile, - cause_by=WriteTest, - sent_from=self.profile, - send_to=self.profile, + cause_by=WriteTest.get_class_name(), + tx_from=self.profile, + tx_to=self.profile, ) - self._publish_message(msg) + self.publish_message(msg) logger.info(f"Done {self.get_workspace()}/tests generating.") @@ -131,8 +132,10 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient) - self._publish_message(msg) + msg = Message( + content=content, role=self.profile, cause_by=RunCode.get_class_name(), tx_from=self.profile, tx_to=recipient + ) + self.publish_message(msg) async def _debug_error(self, msg): file_info, context = msg.content.split(FILENAME_CODE_SEP) @@ -141,14 +144,18 @@ class QaEngineer(Role): self.write_file(file_name, code) recipient = msg.sent_from # send back to the one who ran the code for another run, might be one's self msg = Message( - content=file_info, role=self.profile, cause_by=DebugError, sent_from=self.profile, send_to=recipient + content=file_info, + role=self.profile, + cause_by=DebugError.get_class_name(), + tx_from=self.profile, + tx_to=recipient, ) - self._publish_message(msg) + self.publish_message(msg) async def _observe(self) -> int: await super()._observe() self._rc.news = [ - msg for msg in self._rc.news if msg.send_to == self.profile + msg for msg in self._rc.news if msg.is_recipient({self.profile}) ] # only relevant msgs count as observed news return len(self._rc.news) @@ -157,30 +164,31 @@ class QaEngineer(Role): result_msg = Message( content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, - cause_by=WriteTest, - sent_from=self.profile, - send_to="", + cause_by=WriteTest.get_class_name(), + tx_from=self.profile, ) return result_msg + code_filters = {WriteCode.get_class_name(), WriteCodeReview.get_class_name()} + test_filters = {WriteTest.get_class_name(), DebugError.get_class_name()} + run_filters = {RunCode.get_class_name()} for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself - if msg.cause_by in [WriteCode, WriteCodeReview]: + if msg.is_recipient(code_filters): # engineer wrote a code, time to write a test for it await self._write_test(msg) - elif msg.cause_by in [WriteTest, DebugError]: + elif msg.is_recipient(test_filters): # I wrote or debugged my test code, time to run it await self._run_code(msg) - elif msg.cause_by == RunCode: + elif msg.is_recipient(run_filters): # I ran my test code, time to fix bugs, if any await self._debug_error(msg) self.test_round += 1 result_msg = Message( content=f"Round {self.test_round} of tests done", role=self.profile, - cause_by=WriteTest, - sent_from=self.profile, - send_to="", + cause_by=WriteTest.get_class_name(), + tx_from=self.profile, ) return result_msg diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index acb46c718..43ee7971d 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +""" +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" + import asyncio @@ -10,6 +14,7 @@ from metagpt.const import RESEARCH_PATH from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message +from metagpt.utils.common import get_object_name class Report(BaseModel): @@ -58,18 +63,22 @@ class Researcher(Role): research_system_text = get_research_system_text(topic, self.language) if isinstance(todo, CollectLinks): links = await todo.run(topic, 4, 4) - ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=type(todo)) + ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=get_object_name(todo)) elif isinstance(todo, WebBrowseAndSummarize): links = instruct_content.links todos = (todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items()) summaries = await asyncio.gather(*todos) summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary) - ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=type(todo)) + ret = Message( + "", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=get_object_name(todo) + ) else: summaries = instruct_content.summaries summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries) content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text) - ret = Message("", Report(topic=topic, content=content), role=self.profile, cause_by=type(self._rc.todo)) + ret = Message( + "", Report(topic=topic, content=content), role=self.profile, get_object_name=type(self._rc.todo) + ) self._rc.memory.add(ret) return ret diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 44bb3e976..0a6716428 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -4,20 +4,32 @@ @Time : 2023/5/11 14:42 @Author : alexanderwu @File : role.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Merge the `recv` functionality into the `_observe` function. Future message reading operations will be + consolidated within the `_observe` function. + 2. Standardize the message filtering for string label matching. Role objects can access the message labels + they've subscribed to through the `subscribed_tags` property. + 3. Move the message receive buffer from the global variable `self._rc.env.memory` to the role's private variable + `self._rc.msg_buffer` for easier message identification and asynchronous appending of messages. + 4. Standardize the way messages are passed: `publish_message` sends messages out, while `async_put_message` places + messages into the Role object's private message receive buffer. There are no other message transmit methods. + 5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes + only. In the normal workflow, you should use `publish_message` or `async_put_message` to transmit messages. """ from __future__ import annotations -from typing import Iterable, Type +from typing import Iterable, Set, Type from pydantic import BaseModel, Field -# from metagpt.environment import Environment -from metagpt.config import CONFIG from metagpt.actions import Action, ActionOutput +from metagpt.config import CONFIG from metagpt.llm import LLM from metagpt.logs import logger -from metagpt.memory import Memory, LongTermMemory -from metagpt.schema import Message +from metagpt.memory import LongTermMemory, Memory +from metagpt.schema import Message, MessageQueue +from metagpt.utils.common import get_class_name, get_object_name +from metagpt.utils.named import Named PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -49,6 +61,7 @@ ROLE_TEMPLATE = """Your response should be based on the previous conversation hi class RoleSetting(BaseModel): """Role Settings""" + name: str profile: str goal: str @@ -64,12 +77,14 @@ class RoleSetting(BaseModel): class RoleContext(BaseModel): """Role Runtime Context""" - env: 'Environment' = Field(default=None) + + env: "Environment" = Field(default=None) + msg_buffer: MessageQueue = Field(default_factory=MessageQueue) # Message Buffer with Asynchronous Updates memory: Memory = Field(default_factory=Memory) long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory) state: int = Field(default=0) todo: Action = Field(default=None) - watch: set[Type[Action]] = Field(default_factory=set) + watch: set[str] = Field(default_factory=set) news: list[Type[Message]] = Field(default=[]) class Config: @@ -90,7 +105,7 @@ class RoleContext(BaseModel): return self.memory.get() -class Role: +class Role(Named): """Role/Agent""" def __init__(self, name="", profile="", goal="", constraints="", desc=""): @@ -118,7 +133,8 @@ class Role: def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" - self._rc.watch.update(actions) + tags = [get_class_name(t) for t in actions] + self._rc.watch.update(tags) # check RoleContext after adding watch actions self._rc.check(self._role_id) @@ -128,7 +144,7 @@ class Role: logger.debug(self._actions) self._rc.todo = self._actions[self._rc.state] - def set_env(self, env: 'Environment'): + def set_env(self, env: "Environment"): """Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing.""" self._rc.env = env @@ -137,6 +153,24 @@ class Role: """Get the role description (position)""" return self._setting.profile + @property + def name(self): + """Get virtual user name""" + return self._setting.name + + @property + def subscribed_tags(self) -> Set: + """The labels for messages to be consumed by the Role object.""" + if self._rc.watch: + return self._rc.watch + return { + self.name, + self.get_object_name(), + self.profile, + f"{self.name}({self.profile})", + f"{self.name}({self.get_object_name()})", + } + def _get_prefix(self): """Get the role prefix""" if self._setting.desc: @@ -150,94 +184,99 @@ class Role: self._set_state(0) return prompt = self._get_prefix() - prompt += STATE_TEMPLATE.format(history=self._rc.history, states="\n".join(self._states), - n_states=len(self._states) - 1) + prompt += STATE_TEMPLATE.format( + history=self._rc.history, states="\n".join(self._states), n_states=len(self._states) - 1 + ) next_state = await self._llm.aask(prompt) logger.debug(f"{prompt=}") if not next_state.isdigit() or int(next_state) not in range(len(self._states)): - logger.warning(f'Invalid answer of state, {next_state=}') + logger.warning(f"Invalid answer of state, {next_state=}") next_state = "0" self._set_state(int(next_state)) async def _act(self) -> Message: - # prompt = self.get_prefix() - # prompt += ROLE_TEMPLATE.format(name=self.profile, state=self.states[self.state], result=response, - # history=self.history) - logger.info(f"{self._setting}: ready to {self._rc.todo}") response = await self._rc.todo.run(self._rc.important_memory) - # logger.info(response) if isinstance(response, ActionOutput): - msg = Message(content=response.content, instruct_content=response.instruct_content, - role=self.profile, cause_by=type(self._rc.todo)) + msg = Message( + content=response.content, + instruct_content=response.instruct_content, + role=self.profile, + cause_by=get_object_name(self._rc.todo), + tx_from=get_object_name(self), + ) else: - msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) - self._rc.memory.add(msg) - # logger.debug(f"{response}") + msg = Message( + content=response, + role=self.profile, + cause_by=get_object_name(self._rc.todo), + tx_from=get_object_name(self), + ) return msg async def _observe(self) -> int: - """Observe from the environment, obtain important information, and add it to memory""" - if not self._rc.env: - return 0 - env_msgs = self._rc.env.memory.get() - - observed = self._rc.env.memory.get_by_actions(self._rc.watch) - - self._rc.news = self._rc.memory.find_news(observed) # find news (previously unseen messages) from observed messages - - for i in env_msgs: - self.recv(i) + """Prepare new messages for processing from the message buffer and other sources.""" + # Read unprocessed messages from the msg buffer. + self._rc.news = self._rc.msg_buffer.pop_all() + # Store the read messages in your own memory to prevent duplicate processing. + self._rc.memory.add_batch(self._rc.news) + # Design Rules: + # If you need to further categorize Message objects, you can do so using the Message.set_meta function. + # msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer. news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news] if news_text: - logger.debug(f'{self._setting} observed: {news_text}') + logger.debug(f"{self._setting} observed: {news_text}") return len(self._rc.news) - def _publish_message(self, msg): + def publish_message(self, msg): """If the role belongs to env, then the role's messages will be broadcast to env""" + if not msg: + return if not self._rc.env: # If env does not exist, do not publish the message return self._rc.env.publish_message(msg) + def async_put_message(self, message): + """Place the message into the Role object's private message buffer.""" + if not message: + return + self._rc.msg_buffer.push(message) + async def _react(self) -> Message: """Think first, then act""" await self._think() logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}") return await self._act() - def recv(self, message: Message) -> None: - """add message to history.""" - # self._history += f"\n{message}" - # self._context = self._history - if message in self._rc.memory.get(): - return - self._rc.memory.add(message) - - async def handle(self, message: Message) -> Message: - """Receive information and reply with actions""" - # logger.debug(f"{self.name=}, {self.profile=}, {message.role=}") - self.recv(message) - - return await self._react() - - async def run(self, message=None): + async def run(self, test_message=None): """Observe, and think and act based on the results of the observation""" - if message: - if isinstance(message, str): - message = Message(message) - if isinstance(message, Message): - self.recv(message) - if isinstance(message, list): - self.recv(Message("\n".join(message))) - elif not await self._observe(): + if test_message: # For test + seed = None + if isinstance(test_message, str): + seed = Message(test_message) + elif isinstance(test_message, Message): + seed = test_message + elif isinstance(test_message, list): + seed = Message("\n".join(test_message)) + self.async_put_message(seed) + + if not await self._observe(): # If there is no new information, suspend and wait logger.debug(f"{self._setting}: no news. waiting.") return rsp = await self._react() - # Publish the reply to the environment, waiting for the next subscriber to process - self._publish_message(rsp) + + # Reset the next action to be taken. + self._rc.todo = None + # Send the response message to the Environment object to have it relay the message to the subscribers. + self.publish_message(rsp) return rsp + + @property + def is_idle(self) -> bool: + """If true, all actions have been executed.""" + return not self._rc.news and not self._rc.todo and self._rc.msg_buffer.empty() diff --git a/metagpt/roles/seacher.py b/metagpt/roles/seacher.py index 0b6e089da..95be89277 100644 --- a/metagpt/roles/seacher.py +++ b/metagpt/roles/seacher.py @@ -4,18 +4,20 @@ @Time : 2023/5/23 17:25 @Author : alexanderwu @File : seacher.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ from metagpt.actions import ActionOutput, SearchAndSummarize from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.tools import SearchEngineType +from metagpt.utils.common import get_object_name class Searcher(Role): """ Represents a Searcher role responsible for providing search services to users. - + Attributes: name (str): Name of the searcher. profile (str): Role profile. @@ -23,17 +25,19 @@ class Searcher(Role): constraints (str): Constraints or limitations for the searcher. engine (SearchEngineType): The type of search engine to use. """ - - def __init__(self, - name: str = 'Alice', - profile: str = 'Smart Assistant', - goal: str = 'Provide search services for users', - constraints: str = 'Answer is rich and complete', - engine=SearchEngineType.SERPAPI_GOOGLE, - **kwargs) -> None: + + def __init__( + self, + name: str = "Alice", + profile: str = "Smart Assistant", + goal: str = "Provide search services for users", + constraints: str = "Answer is rich and complete", + engine=SearchEngineType.SERPAPI_GOOGLE, + **kwargs, + ) -> None: """ Initializes the Searcher role with given attributes. - + Args: name (str): Name of the searcher. profile (str): Role profile. @@ -53,12 +57,16 @@ class Searcher(Role): """Performs the search action in a single process.""" logger.info(f"{self._setting}: ready to {self._rc.todo}") response = await self._rc.todo.run(self._rc.memory.get(k=0)) - + if isinstance(response, ActionOutput): - msg = Message(content=response.content, instruct_content=response.instruct_content, - role=self.profile, cause_by=type(self._rc.todo)) + msg = Message( + content=response.content, + instruct_content=response.instruct_content, + role=self.profile, + cause_by=get_object_name(self._rc.todo), + ) else: - msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) + msg = Message(content=response, role=self.profile, cause_by=get_object_name(self._rc.todo)) self._rc.memory.add(msg) return msg diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index b27841d74..abebb9605 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -4,6 +4,7 @@ @Time : 2023/9/13 12:23 @Author : femto Zheng @File : sk_agent.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner @@ -14,6 +15,7 @@ from metagpt.actions.execute_task import ExecuteTask from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message +from metagpt.utils.common import get_object_name from metagpt.utils.make_sk_kernel import make_sk_kernel @@ -70,7 +72,7 @@ class SkAgent(Role): result = (await self.plan.invoke_async()).result logger.info(result) - msg = Message(content=result, role=self.profile, cause_by=type(self._rc.todo)) + msg = Message(content=result, role=self.profile, cause_by=get_object_name(self._rc.todo)) self._rc.memory.add(msg) - # logger.debug(f"{response}") + self.publish_message(msg) return msg diff --git a/metagpt/schema.py b/metagpt/schema.py index 1124fb28e..e0d17e0ed 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -8,12 +8,20 @@ """ from __future__ import annotations +import asyncio import json +from asyncio import Queue, QueueEmpty, wait_for from json import JSONDecodeError -from typing import Dict, List, TypedDict +from typing import Dict, List, Set, TypedDict from pydantic import BaseModel, Field +from metagpt.const import ( + MESSAGE_META_ROLE, + MESSAGE_ROUTE_CAUSE_BY, + MESSAGE_ROUTE_FROM, + MESSAGE_ROUTE_TO, +) from metagpt.logs import logger @@ -22,44 +30,150 @@ class RawMessage(TypedDict): role: str +class Routes(BaseModel): + """Responsible for managing routing information for the Message class.""" + + routes: List[Dict] = Field(default_factory=list) + + def set_from(self, value): + """Set the label of the message sender.""" + route = self._get_route() + route[MESSAGE_ROUTE_FROM] = value + + def set_to(self, tags: Set): + """Set the labels of the message recipient.""" + route = self._get_route() + if tags: + route[MESSAGE_ROUTE_TO] = tags + return + + if MESSAGE_ROUTE_TO in route: + del route[MESSAGE_ROUTE_TO] + + def add_to(self, tag: str): + """Add a label of the message recipient.""" + route = self._get_route() + tags = route.get(MESSAGE_ROUTE_TO, set()) + tags.add(tag) + route[MESSAGE_ROUTE_TO] = tags + + def _get_route(self) -> Dict: + if not self.routes: + self.routes.append({}) + return self.routes[0] + + def is_recipient(self, tags: Set) -> bool: + """Check if it is the message recipient.""" + route = self._get_route() + to_tags = route.get(MESSAGE_ROUTE_TO) + if not to_tags: + return True + + for k in tags: + if k in to_tags: + return True + return False + + @property + def tx_from(self): + """Message route info tells who sent this message.""" + route = self._get_route() + return route.get(MESSAGE_ROUTE_FROM) + + @property + def tx_to(self): + """Labels for the consumer to filter its subscribed messages.""" + route = self._get_route() + return route.get(MESSAGE_ROUTE_TO) + + class Message(BaseModel): """list[: ]""" content: str instruct_content: BaseModel = None meta_info: Dict = Field(default_factory=dict) - route: List[Dict] = Field(default_factory=list) + route: Routes = Field(default_factory=Routes) def __init__(self, content, **kwargs): + """ + :param content: Message content. + :param instruct_content: Message content struct. + :param meta_info: Message meta info. + :param route: Message route configuration. + :param tx_from: Message route info tells who sent this message. + :param tx_to: Labels for the consumer to filter its subscribed messages. + :param cause_by: Labels for the consumer to filter its subscribed messages, also serving as meta info. + :param role: Message meta info tells who sent this message. + """ super(Message, self).__init__( content=content or kwargs.get("content"), instruct_content=kwargs.get("instruct_content"), meta_info=kwargs.get("meta_info", {}), - route=kwargs.get("route", []), + route=kwargs.get("route", Routes()), ) attribute_names = Message.__annotations__.keys() for k, v in kwargs.items(): if k in attribute_names: continue + if k == MESSAGE_ROUTE_FROM: + self.set_from(v) + continue + if k == MESSAGE_ROUTE_CAUSE_BY: + self.meta_info[k] = v + if k == MESSAGE_ROUTE_TO or k == MESSAGE_ROUTE_CAUSE_BY: + self.add_to(v) + continue self.meta_info[k] = v def get_meta(self, key): + """Get meta info""" return self.meta_info.get(key) def set_meta(self, key, value): + """Set meta info""" self.meta_info[key] = value @property def role(self): - return self.get_meta("role") + """Message meta info tells who sent this message.""" + return self.get_meta(MESSAGE_META_ROLE) @property def cause_by(self): - return self.get_meta("cause_by") + """Labels for the consumer to filter its subscribed messages, also serving as meta info.""" + return self.get_meta(MESSAGE_ROUTE_CAUSE_BY) + + @property + def tx_from(self): + """Message route info tells who sent this message.""" + return self.route.tx_from + + @property + def tx_to(self): + """Labels for the consumer to filter its subscribed messages.""" + return self.route.tx_to def set_role(self, v): - self.set_meta("role", v) + """Set the message's meta info indicating the sender.""" + self.set_meta(MESSAGE_META_ROLE, v) + + def set_from(self, v): + """Set the message's meta info indicating the sender.""" + self.route.set_from(v) + + def set_to(self, tags: Set): + """Set the message's meta info indicating the sender.""" + self.route.set_to(tags) + + def add_to(self, tag: str): + """Add a subscription label for the recipients.""" + self.route.add_to(tag) + + def is_recipient(self, tags: Set): + """Return true if any input label exists in the message's subscription labels.""" + return self.route.is_recipient(tags) def __str__(self): # prefix = '-'.join([self.role, str(self.cause_by)]) @@ -69,13 +183,16 @@ class Message(BaseModel): return self.__str__() def to_dict(self) -> dict: + """Return a dict containing `role` and `content` for the LLM call.l""" return {"role": self.role, "content": self.content} def save(self) -> str: + """Convert the object to json string""" return self.json(exclude_none=True) @staticmethod def load(v): + """Convert the json string to object.""" try: d = json.loads(v) return Message(**d) @@ -90,7 +207,7 @@ class UserMessage(Message): """ def __init__(self, content: str): - super(Message, self).__init__(content=content, meta_info={"role": "user"}) + super().__init__(content=content, role="user") class SystemMessage(Message): @@ -99,7 +216,7 @@ class SystemMessage(Message): """ def __init__(self, content: str): - super().__init__(content=content, meta_info={"role": "system"}) + super().__init__(content=content, role="system") class AIMessage(Message): @@ -108,7 +225,65 @@ class AIMessage(Message): """ def __init__(self, content: str): - super().__init__(content=content, meta_info={"role": "assistant"}) + super().__init__(content=content, role="assistant") + + +class MessageQueue: + def __init__(self): + self._queue = Queue() + + def pop(self) -> Message | None: + try: + item = self._queue.get_nowait() + if item: + self._queue.task_done() + return item + except QueueEmpty: + return None + + def pop_all(self) -> List[Message]: + ret = [] + while True: + msg = self.pop() + if not msg: + break + ret.append(msg) + return ret + + def push(self, msg: Message): + self._queue.put_nowait(msg) + + def empty(self): + return self._queue.empty() + + async def save(self) -> str: + if self.empty(): + return "[]" + + lst = [] + try: + while True: + item = await wait_for(self._queue.get(), timeout=1.0) + if item is None: + break + lst.append(item.dict(exclude_none=True)) + self._queue.task_done() + except asyncio.TimeoutError: + logger.debug("Queue is empty, exiting...") + return json.dumps(lst) + + @staticmethod + def load(self, v) -> "MessageQueue": + q = MessageQueue() + try: + lst = json.loads(v) + for i in lst: + msg = Message(**i) + q.push(msg) + except JSONDecodeError as e: + logger.warning(f"JSON load failed: {v}, error:{e}") + + return q if __name__ == "__main__": diff --git a/metagpt/software_company.py b/metagpt/software_company.py index b2bd18c58..4bedec0e1 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -4,6 +4,9 @@ @Time : 2023/5/12 00:30 @Author : alexanderwu @File : software_company.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Standardize the design of message filtering-related features. + 2. Abandon the design of having `Environment` store all messages. """ from pydantic import BaseModel, Field @@ -14,13 +17,15 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.utils.common import NoMoneyException +from metagpt.utils.named import Named -class SoftwareCompany(BaseModel): +class SoftwareCompany(BaseModel, Named): """ Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging, dedicated to writing executable code. """ + environment: Environment = Field(default_factory=Environment) investment: float = Field(default=10.0) idea: str = Field(default="") @@ -36,16 +41,23 @@ class SoftwareCompany(BaseModel): """Invest company. raise NoMoneyException when exceed max_budget.""" self.investment = investment CONFIG.max_budget = investment - logger.info(f'Investment: ${investment}.') + logger.info(f"Investment: ${investment}.") def _check_balance(self): if CONFIG.total_cost > CONFIG.max_budget: - raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}') + raise NoMoneyException(CONFIG.total_cost, f"Insufficient funds: {CONFIG.max_budget}") def start_project(self, idea): """Start a project from publishing boss requirement.""" self.idea = idea - self.environment.publish_message(Message(role="BOSS", content=idea, cause_by=BossRequirement)) + self.environment.publish_message( + Message( + role="BOSS", + content=idea, + cause_by=BossRequirement.get_class_name(), + tx_from=SoftwareCompany.get_class_name(), + ) + ) def _save(self): logger.info(self.json()) @@ -58,5 +70,3 @@ class SoftwareCompany(BaseModel): logger.debug(f"{n_round=}") self._check_balance() await self.environment.run() - return self.environment.history - \ No newline at end of file diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index f09666beb..df4688378 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -85,10 +85,7 @@ class OutputParser: @staticmethod def parse_python_code(text: str) -> str: - for pattern in ( - r"(.*?```python.*?\s+)?(?P.*)(```.*?)", - r"(.*?```python.*?\s+)?(?P.*)", - ): + for pattern in (r"(.*?```python.*?\s+)?(?P.*)(```.*?)", r"(.*?```python.*?\s+)?(?P.*)"): match = re.search(pattern, text, re.DOTALL) if not match: continue @@ -305,3 +302,14 @@ def parse_recipient(text): pattern = r"## Send To:\s*([A-Za-z]+)\s*?" # hard code for now recipient = re.search(pattern, text) return recipient.group(1) if recipient else "" + + +def get_class_name(cls) -> str: + """Return class name""" + return f"{cls.__module__}.{cls.__name__}" + + +def get_object_name(obj) -> str: + """Return class name of the object""" + cls = type(obj) + return f"{cls.__module__}.{cls.__name__}" diff --git a/metagpt/utils/named.py b/metagpt/utils/named.py new file mode 100644 index 000000000..e4da574e8 --- /dev/null +++ b/metagpt/utils/named.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/11/1 +@Author : mashenquan +@File : named.py +""" + + +class Named: + """A base class with functions for converting classes to names and objects to class names.""" + + @classmethod + def get_class_name(cls): + """Return class name""" + return f"{cls.__module__}.{cls.__name__}" + + def get_object_name(self): + """Return class name of the object""" + cls = type(self) + return f"{cls.__module__}.{cls.__name__}" diff --git a/tests/metagpt/actions/test_write_prd.py b/tests/metagpt/actions/test_write_prd.py index 38e4e5221..40ab20dad 100644 --- a/tests/metagpt/actions/test_write_prd.py +++ b/tests/metagpt/actions/test_write_prd.py @@ -4,6 +4,7 @@ @Time : 2023/5/11 17:45 @Author : alexanderwu @File : test_write_prd.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ import pytest @@ -17,7 +18,7 @@ from metagpt.schema import Message async def test_write_prd(): product_manager = ProductManager() requirements = "开发一个基于大语言模型与私有知识库的搜索引擎,希望可以基于大语言模型进行搜索总结" - prd = await product_manager.handle(Message(content=requirements, cause_by=BossRequirement)) + prd = await product_manager.handle(Message(content=requirements, cause_by=BossRequirement.get_class_name())) logger.info(requirements) logger.info(prd) diff --git a/tests/metagpt/memory/test_longterm_memory.py b/tests/metagpt/memory/test_longterm_memory.py index dc5540520..c40d7ab9d 100644 --- a/tests/metagpt/memory/test_longterm_memory.py +++ b/tests/metagpt/memory/test_longterm_memory.py @@ -1,12 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : unittest of `metagpt/memory/longterm_memory.py` +""" +@Desc : unittest of `metagpt/memory/longterm_memory.py` +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" -from metagpt.config import CONFIG -from metagpt.schema import Message from metagpt.actions import BossRequirement -from metagpt.roles.role import RoleContext +from metagpt.config import CONFIG from metagpt.memory import LongTermMemory +from metagpt.roles.role import RoleContext +from metagpt.schema import Message def test_ltm_search(): @@ -14,25 +17,25 @@ def test_ltm_search(): openai_api_key = CONFIG.openai_api_key assert len(openai_api_key) > 20 - role_id = 'UTUserLtm(Product Manager)' - rc = RoleContext(watch=[BossRequirement]) + role_id = "UTUserLtm(Product Manager)" + rc = RoleContext(watch=[BossRequirement.get_class_name()]) ltm = LongTermMemory() ltm.recover_memory(role_id, rc) - idea = 'Write a cli snake game' - message = Message(role='BOSS', content=idea, cause_by=BossRequirement) + idea = "Write a cli snake game" + message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name()) news = ltm.find_news([message]) assert len(news) == 1 ltm.add(message) - sim_idea = 'Write a game of cli snake' - sim_message = Message(role='BOSS', content=sim_idea, cause_by=BossRequirement) + sim_idea = "Write a game of cli snake" + sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name()) news = ltm.find_news([sim_message]) assert len(news) == 0 ltm.add(sim_message) - new_idea = 'Write a 2048 web game' - new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement) + new_idea = "Write a 2048 web game" + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) news = ltm.find_news([new_message]) assert len(news) == 1 ltm.add(new_message) @@ -47,8 +50,8 @@ def test_ltm_search(): news = ltm_new.find_news([sim_message]) assert len(news) == 0 - new_idea = 'Write a Battle City' - new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement) + new_idea = "Write a Battle City" + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) news = ltm_new.find_news([new_message]) assert len(news) == 1 diff --git a/tests/metagpt/memory/test_memory_storage.py b/tests/metagpt/memory/test_memory_storage.py index 6bb3e8f1d..881b47d6f 100644 --- a/tests/metagpt/memory/test_memory_storage.py +++ b/tests/metagpt/memory/test_memory_storage.py @@ -1,20 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : the unittests of metagpt/memory/memory_storage.py +""" +@Desc : the unittests of metagpt/memory/memory_storage.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" + from typing import List +from metagpt.actions import BossRequirement, WritePRD +from metagpt.actions.action_output import ActionOutput from metagpt.memory.memory_storage import MemoryStorage from metagpt.schema import Message -from metagpt.actions import BossRequirement -from metagpt.actions import WritePRD -from metagpt.actions.action_output import ActionOutput def test_idea_message(): - idea = 'Write a cli snake game' - role_id = 'UTUser1(Product Manager)' - message = Message(role='BOSS', content=idea, cause_by=BossRequirement) + idea = "Write a cli snake game" + role_id = "UTUser1(Product Manager)" + message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name()) memory_storage: MemoryStorage = MemoryStorage() messages = memory_storage.recover_memory(role_id) @@ -23,13 +26,13 @@ def test_idea_message(): memory_storage.add(message) assert memory_storage.is_initialized is True - sim_idea = 'Write a game of cli snake' - sim_message = Message(role='BOSS', content=sim_idea, cause_by=BossRequirement) + sim_idea = "Write a game of cli snake" + sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name()) new_messages = memory_storage.search(sim_message) - assert len(new_messages) == 0 # similar, return [] + assert len(new_messages) == 0 # similar, return [] - new_idea = 'Write a 2048 web game' - new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement) + new_idea = "Write a 2048 web game" + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) new_messages = memory_storage.search(new_message) assert new_messages[0].content == message.content @@ -38,22 +41,15 @@ def test_idea_message(): def test_actionout_message(): - out_mapping = { - 'field1': (str, ...), - 'field2': (List[str], ...) - } - out_data = { - 'field1': 'field1 value', - 'field2': ['field2 value1', 'field2 value2'] - } - ic_obj = ActionOutput.create_model_class('prd', out_mapping) + out_mapping = {"field1": (str, ...), "field2": (List[str], ...)} + out_data = {"field1": "field1 value", "field2": ["field2 value1", "field2 value2"]} + ic_obj = ActionOutput.create_model_class("prd", out_mapping) - role_id = 'UTUser2(Architect)' - content = 'The boss has requested the creation of a command-line interface (CLI) snake game' - message = Message(content=content, - instruct_content=ic_obj(**out_data), - role='user', - cause_by=WritePRD) # WritePRD as test action + role_id = "UTUser2(Architect)" + content = "The boss has requested the creation of a command-line interface (CLI) snake game" + message = Message( + content=content, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() + ) # WritePRD as test action memory_storage: MemoryStorage = MemoryStorage() messages = memory_storage.recover_memory(role_id) @@ -62,19 +58,17 @@ def test_actionout_message(): memory_storage.add(message) assert memory_storage.is_initialized is True - sim_conent = 'The request is command-line interface (CLI) snake game' - sim_message = Message(content=sim_conent, - instruct_content=ic_obj(**out_data), - role='user', - cause_by=WritePRD) + sim_conent = "The request is command-line interface (CLI) snake game" + sim_message = Message( + content=sim_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() + ) new_messages = memory_storage.search(sim_message) - assert len(new_messages) == 0 # similar, return [] + assert len(new_messages) == 0 # similar, return [] - new_conent = 'Incorporate basic features of a snake game such as scoring and increasing difficulty' - new_message = Message(content=new_conent, - instruct_content=ic_obj(**out_data), - role='user', - cause_by=WritePRD) + new_conent = "Incorporate basic features of a snake game such as scoring and increasing difficulty" + new_message = Message( + content=new_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() + ) new_messages = memory_storage.search(new_message) assert new_messages[0].content == message.content diff --git a/tests/metagpt/planner/test_action_planner.py b/tests/metagpt/planner/test_action_planner.py index 5ab9a493f..a3831c08d 100644 --- a/tests/metagpt/planner/test_action_planner.py +++ b/tests/metagpt/planner/test_action_planner.py @@ -4,6 +4,9 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Standardize the usage of message filtering-related features. + 2. Standardize the usage of message transmission. """ import pytest from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill @@ -23,7 +26,7 @@ async def test_action_planner(): role.import_skill(TimeSkill(), "time") role.import_skill(TextSkill(), "text") task = "What is the sum of 110 and 990?" - role.recv(Message(content=task, cause_by=BossRequirement)) - + role.async_put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + await role._observe() await role._think() # it will choose mathskill.Add assert "1100" == (await role._act()).content diff --git a/tests/metagpt/planner/test_basic_planner.py b/tests/metagpt/planner/test_basic_planner.py index 03a82ec5e..9efcb9367 100644 --- a/tests/metagpt/planner/test_basic_planner.py +++ b/tests/metagpt/planner/test_basic_planner.py @@ -4,6 +4,9 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py +@Modified By: mashenquan, 2023-11-1. Optimization: + 1. Standardize the usage of message filtering-related features. + 2. Standardize the usage of message transmission. """ import pytest from semantic_kernel.core_skills import TextSkill @@ -26,7 +29,8 @@ async def test_basic_planner(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - role.recv(Message(content=task, cause_by=BossRequirement)) + role.async_put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + await role._observe() await role._think() # assuming sk_agent will think he needs WriterSkill.Brainstorm and WriterSkill.Translate assert "WriterSkill.Brainstorm" in role.plan.generated_plan.result diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index 52fc4a3c1..b9891cd81 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -4,6 +4,7 @@ @Time : 2023/5/12 13:05 @Author : alexanderwu @File : mock.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. """ from metagpt.actions import BossRequirement, WriteDesign, WritePRD, WriteTasks from metagpt.schema import Message @@ -71,7 +72,7 @@ PRD = '''## 原始需求 ``` ''' -SYSTEM_DESIGN = '''## Python package name +SYSTEM_DESIGN = """## Python package name ```python "smart_search_engine" ``` @@ -149,10 +150,10 @@ sequenceDiagram S-->>SE: return summary SE-->>M: return summary ``` -''' +""" -TASKS = '''## Logic Analysis +TASKS = """## Logic Analysis 在这个项目中,所有的模块都依赖于“SearchEngine”类,这是主入口,其他的模块(Index、Ranking和Summary)都通过它交互。另外,"Index"类又依赖于"KnowledgeBase"类,因为它需要从知识库中获取数据。 @@ -181,7 +182,7 @@ task_list = [ ] ``` 这个任务列表首先定义了最基础的模块,然后是依赖这些模块的模块,最后是辅助模块。可以根据团队的能力和资源,同时开发多个任务,只要满足依赖关系。例如,在开发"search.py"之前,可以同时开发"knowledge_base.py"、"index.py"、"ranking.py"和"summary.py"。 -''' +""" TASKS_TOMATO_CLOCK = '''## Required Python third-party packages: Provided in requirements.txt format @@ -224,35 +225,35 @@ task_list = [ TASK = """smart_search_engine/knowledge_base.py""" STRS_FOR_PARSING = [ -""" + """ ## 1 ```python a ``` """, -""" + """ ##2 ```python "a" ``` """, -""" + """ ## 3 ```python a = "a" ``` """, -""" + """ ## 4 ```python a = 'a' ``` -""" +""", ] class MockMessages: - req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement) - prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD) - system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign) - tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks) + req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement.get_class_name()) + prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD.get_class_name()) + system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign.get_class_name()) + tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks.get_class_name()) diff --git a/tests/metagpt/roles/test_architect.py b/tests/metagpt/roles/test_architect.py index d44e0d923..910c589ca 100644 --- a/tests/metagpt/roles/test_architect.py +++ b/tests/metagpt/roles/test_architect.py @@ -4,6 +4,7 @@ @Time : 2023/5/20 14:37 @Author : alexanderwu @File : test_architect.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. """ import pytest @@ -15,7 +16,7 @@ from tests.metagpt.roles.mock import MockMessages @pytest.mark.asyncio async def test_architect(): role = Architect() - role.recv(MockMessages.req) - rsp = await role.handle(MockMessages.prd) + role.async_put_message(MockMessages.req) + rsp = await role.run(MockMessages.prd) logger.info(rsp) assert len(rsp.content) > 0 diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index c0c48d0b1..e80234b3b 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -4,6 +4,7 @@ @Time : 2023/5/12 10:14 @Author : alexanderwu @File : test_engineer.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. """ import pytest @@ -22,10 +23,10 @@ from tests.metagpt.roles.mock import ( async def test_engineer(): engineer = Engineer() - engineer.recv(MockMessages.req) - engineer.recv(MockMessages.prd) - engineer.recv(MockMessages.system_design) - rsp = await engineer.handle(MockMessages.tasks) + engineer.async_put_message(MockMessages.req) + engineer.async_put_message(MockMessages.prd) + engineer.async_put_message(MockMessages.system_design) + rsp = await engineer.run(MockMessages.tasks) logger.info(rsp) assert "all done." == rsp.content @@ -35,13 +36,13 @@ def test_parse_str(): for idx, i in enumerate(STRS_FOR_PARSING): text = CodeParser.parse_str(f"{idx+1}", i) # logger.info(text) - assert text == 'a' + assert text == "a" def test_parse_blocks(): tasks = CodeParser.parse_blocks(TASKS) logger.info(tasks.keys()) - assert 'Task list' in tasks.keys() + assert "Task list" in tasks.keys() target_list = [ diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index a0f1f6257..755798b17 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -4,6 +4,7 @@ @Time : 2023/5/12 00:47 @Author : alexanderwu @File : test_environment.py +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. """ import pytest @@ -49,7 +50,7 @@ async def test_publish_and_process_message(env: Environment): env.add_roles([product_manager, architect]) env.set_manager(Manager()) - env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement)) + env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement.get_class_name())) await env.run(k=2) logger.info(f"{env.history=}") diff --git a/tests/metagpt/utils/test_serialize.py b/tests/metagpt/utils/test_serialize.py index 69f317f79..5a0840c87 100644 --- a/tests/metagpt/utils/test_serialize.py +++ b/tests/metagpt/utils/test_serialize.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : the unittest of serialize +""" +@Desc : the unittest of serialize +@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +""" from typing import List, Tuple @@ -55,7 +58,7 @@ def test_serialize_and_deserialize_message(): ic_obj = ActionOutput.create_model_class("prd", out_mapping) message = Message( - content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD + content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() ) # WritePRD as test action message_ser = serialize_message(message) From bd813d2b90d16d2c439c4693ab27dca595786b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 1 Nov 2023 20:17:23 +0800 Subject: [PATCH 003/115] refactor: Refactor Message transmission & filtering --- metagpt/roles/researcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 43ee7971d..6e89b9fe7 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -77,7 +77,7 @@ class Researcher(Role): summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries) content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text) ret = Message( - "", Report(topic=topic, content=content), role=self.profile, get_object_name=type(self._rc.todo) + "", Report(topic=topic, content=content), role=self.profile, cause_by=get_object_name(self._rc.todo) ) self._rc.memory.add(ret) return ret From 8582f219624d75440d33e32648ebf1b44c389011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 1 Nov 2023 20:33:34 +0800 Subject: [PATCH 004/115] refactor: Refactor Message transmission & filtering --- metagpt/schema.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/metagpt/schema.py b/metagpt/schema.py index e0d17e0ed..806b0e94e 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -229,10 +229,13 @@ class AIMessage(Message): class MessageQueue: + """Message queue which supports asynchronous updates.""" + def __init__(self): self._queue = Queue() def pop(self) -> Message | None: + """Pop one message from queue.""" try: item = self._queue.get_nowait() if item: @@ -242,6 +245,7 @@ class MessageQueue: return None def pop_all(self) -> List[Message]: + """Pop all messages from queue.""" ret = [] while True: msg = self.pop() @@ -251,12 +255,15 @@ class MessageQueue: return ret def push(self, msg: Message): + """Push a message into the queue.""" self._queue.put_nowait(msg) def empty(self): + """Return true if the queue is empty.""" return self._queue.empty() async def save(self) -> str: + """Convert the `MessageQueue` object to a json string.""" if self.empty(): return "[]" @@ -274,6 +281,7 @@ class MessageQueue: @staticmethod def load(self, v) -> "MessageQueue": + """Convert the json string to the `MessageQueue` object.""" q = MessageQueue() try: lst = json.loads(v) From d127586320d7fc1ecc81fae198dc2908cad34815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 1 Nov 2023 20:35:37 +0800 Subject: [PATCH 005/115] refactor: Refactor Message transmission & filtering --- metagpt/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 806b0e94e..1adfd525c 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -235,7 +235,7 @@ class MessageQueue: self._queue = Queue() def pop(self) -> Message | None: - """Pop one message from queue.""" + """Pop one message from the queue.""" try: item = self._queue.get_nowait() if item: @@ -245,7 +245,7 @@ class MessageQueue: return None def pop_all(self) -> List[Message]: - """Pop all messages from queue.""" + """Pop all messages from the queue.""" ret = [] while True: msg = self.pop() From d685252aa0f8f1f393697b76c9e236ecc9c5117a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 10:21:15 +0800 Subject: [PATCH 006/115] feat: +unit test --- tests/metagpt/utils/test_named.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/metagpt/utils/test_named.py diff --git a/tests/metagpt/utils/test_named.py b/tests/metagpt/utils/test_named.py new file mode 100644 index 000000000..89a68b5e7 --- /dev/null +++ b/tests/metagpt/utils/test_named.py @@ -0,0 +1,21 @@ +import pytest + +from metagpt.utils.named import Named + + +@pytest.mark.asyncio +async def test_suite(): + class A(Named): + pass + + class B(A): + pass + + assert A.get_class_name() == "tests.metagpt.utils.test_named.A" + assert A().get_object_name() == "tests.metagpt.utils.test_named.A" + assert B.get_class_name() == "tests.metagpt.utils.test_named.B" + assert B().get_object_name() == "tests.metagpt.utils.test_named.B" + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From bfaeda0a90c65487b758c4c1011802e32e8f848f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 10:28:54 +0800 Subject: [PATCH 007/115] feat: +unit tests --- tests/metagpt/test_schema.py | 32 ++++++++++++++++++++++++++++--- tests/metagpt/utils/test_named.py | 7 +++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 12666e0d3..71bb39c77 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -4,18 +4,44 @@ @Time : 2023/5/20 10:40 @Author : alexanderwu @File : test_schema.py +@Modified By: mashenquan, 2023-11-1. Add `test_message`. """ +import json + +import pytest + from metagpt.schema import AIMessage, Message, SystemMessage, UserMessage +@pytest.mark.asyncio def test_messages(): - test_content = 'test_message' + test_content = "test_message" msgs = [ UserMessage(test_content), SystemMessage(test_content), AIMessage(test_content), - Message(test_content, role='QA') + Message(test_content, role="QA"), ] text = str(msgs) - roles = ['user', 'system', 'assistant', 'QA'] + roles = ["user", "system", "assistant", "QA"] assert all([i in text for i in roles]) + + +@pytest.mark.asyncio +def test_message(): + m = Message("a", role="v1") + v = m.save() + d = json.loads(v) + assert d + assert d.get("content") == "a" + assert d.get("meta_info") == {"role": "v1"} + m.set_role("v2") + v = m.save() + assert v + m = Message.load(v) + assert m.content == "a" + assert m.role == "v2" + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_named.py b/tests/metagpt/utils/test_named.py index 89a68b5e7..ff1f07205 100644 --- a/tests/metagpt/utils/test_named.py +++ b/tests/metagpt/utils/test_named.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023-11-1 +@Author : mashenquan +@File : test_named.py +""" import pytest from metagpt.utils.named import Named From bc1a757293b0967c3d98ad01edcf531d5c88aef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 10:40:26 +0800 Subject: [PATCH 008/115] refactor: rename async_put_message to put_message --- metagpt/environment.py | 2 +- metagpt/roles/role.py | 8 ++++---- tests/metagpt/planner/test_action_planner.py | 2 +- tests/metagpt/planner/test_basic_planner.py | 2 +- tests/metagpt/roles/test_architect.py | 2 +- tests/metagpt/roles/test_engineer.py | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index ba0645a36..7ba077080 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -50,7 +50,7 @@ class Environment(BaseModel): found = False for r in self.roles.values(): if message.is_recipient(r.subscribed_tags): - r.async_put_message(message) + r.put_message(message) found = True if not found: logger.warning(f"Message no recipients: {message.save()}") diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 0a6716428..6fba40bd8 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -11,10 +11,10 @@ they've subscribed to through the `subscribed_tags` property. 3. Move the message receive buffer from the global variable `self._rc.env.memory` to the role's private variable `self._rc.msg_buffer` for easier message identification and asynchronous appending of messages. - 4. Standardize the way messages are passed: `publish_message` sends messages out, while `async_put_message` places + 4. Standardize the way messages are passed: `publish_message` sends messages out, while `put_message` places messages into the Role object's private message receive buffer. There are no other message transmit methods. 5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes - only. In the normal workflow, you should use `publish_message` or `async_put_message` to transmit messages. + only. In the normal workflow, you should use `publish_message` or `put_message` to transmit messages. """ from __future__ import annotations @@ -239,7 +239,7 @@ class Role(Named): return self._rc.env.publish_message(msg) - def async_put_message(self, message): + def put_message(self, message): """Place the message into the Role object's private message buffer.""" if not message: return @@ -261,7 +261,7 @@ class Role(Named): seed = test_message elif isinstance(test_message, list): seed = Message("\n".join(test_message)) - self.async_put_message(seed) + self.put_message(seed) if not await self._observe(): # If there is no new information, suspend and wait diff --git a/tests/metagpt/planner/test_action_planner.py b/tests/metagpt/planner/test_action_planner.py index a3831c08d..99cc25b72 100644 --- a/tests/metagpt/planner/test_action_planner.py +++ b/tests/metagpt/planner/test_action_planner.py @@ -26,7 +26,7 @@ async def test_action_planner(): role.import_skill(TimeSkill(), "time") role.import_skill(TextSkill(), "text") task = "What is the sum of 110 and 990?" - role.async_put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) await role._observe() await role._think() # it will choose mathskill.Add assert "1100" == (await role._act()).content diff --git a/tests/metagpt/planner/test_basic_planner.py b/tests/metagpt/planner/test_basic_planner.py index 9efcb9367..fa7ed7074 100644 --- a/tests/metagpt/planner/test_basic_planner.py +++ b/tests/metagpt/planner/test_basic_planner.py @@ -29,7 +29,7 @@ async def test_basic_planner(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - role.async_put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) await role._observe() await role._think() # assuming sk_agent will think he needs WriterSkill.Brainstorm and WriterSkill.Translate diff --git a/tests/metagpt/roles/test_architect.py b/tests/metagpt/roles/test_architect.py index 910c589ca..665242379 100644 --- a/tests/metagpt/roles/test_architect.py +++ b/tests/metagpt/roles/test_architect.py @@ -16,7 +16,7 @@ from tests.metagpt.roles.mock import MockMessages @pytest.mark.asyncio async def test_architect(): role = Architect() - role.async_put_message(MockMessages.req) + role.put_message(MockMessages.req) rsp = await role.run(MockMessages.prd) logger.info(rsp) assert len(rsp.content) > 0 diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index e80234b3b..93c3132ac 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -23,9 +23,9 @@ from tests.metagpt.roles.mock import ( async def test_engineer(): engineer = Engineer() - engineer.async_put_message(MockMessages.req) - engineer.async_put_message(MockMessages.prd) - engineer.async_put_message(MockMessages.system_design) + engineer.put_message(MockMessages.req) + engineer.put_message(MockMessages.prd) + engineer.put_message(MockMessages.system_design) rsp = await engineer.run(MockMessages.tasks) logger.info(rsp) From 8572fa8ecd6f409f339b07db053749d2f6361c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 10:48:45 +0800 Subject: [PATCH 009/115] feat: +unit tests --- tests/metagpt/test_schema.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 71bb39c77..06bb57a70 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -10,7 +10,7 @@ import json import pytest -from metagpt.schema import AIMessage, Message, SystemMessage, UserMessage +from metagpt.schema import AIMessage, Message, Routes, SystemMessage, UserMessage @pytest.mark.asyncio @@ -42,6 +42,29 @@ def test_message(): assert m.content == "a" assert m.role == "v2" + m = Message("a", role="b", cause_by="c", x="d") + assert m.content == "a" + assert m.role == "b" + assert m.is_recipient({"c"}) + assert m.cause_by == "c" + assert m.get_meta("x") == "d" + + +@pytest.mark.asyncio +def test_routes(): + route = Routes() + route.set_from("a") + assert route.tx_from == "a" + route.add_to("b") + assert route.tx_to == {"b"} + route.add_to("c") + assert route.tx_to == {"b", "c"} + route.set_to({"e", "f"}) + assert route.tx_to == {"e", "f"} + assert route.is_recipient({"e"}) + assert route.is_recipient({"f"}) + assert not route.is_recipient({"a"}) + if __name__ == "__main__": pytest.main([__file__, "-s"]) From 660f788683b2ace2ef8ac020f044be949fd19ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 11:51:10 +0800 Subject: [PATCH 010/115] feat: + subscribe --- metagpt/roles/role.py | 4 ++ tests/metagpt/roles/test_role.py | 64 ++++++++++++++++++++++++++++++++ tests/metagpt/test_role.py | 14 ------- 3 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 tests/metagpt/roles/test_role.py delete mode 100644 tests/metagpt/test_role.py diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 6fba40bd8..318b7d7a8 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -134,6 +134,10 @@ class Role(Named): def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" tags = [get_class_name(t) for t in actions] + self.subscribe(tags) + + def subscribe(self, tags: Set[str]): + """Listen to the corresponding behaviors""" self._rc.watch.update(tags) # check RoleContext after adding watch actions self._rc.check(self._role_id) diff --git a/tests/metagpt/roles/test_role.py b/tests/metagpt/roles/test_role.py new file mode 100644 index 000000000..cefd71ada --- /dev/null +++ b/tests/metagpt/roles/test_role.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023-11-1 +@Author : mashenquan +@File : test_role.py +""" +import pytest +from pydantic import BaseModel + +from metagpt.actions import Action, ActionOutput +from metagpt.environment import Environment +from metagpt.roles import Role +from metagpt.schema import Message + + +class MockAction(Action): + async def run(self, messages, *args, **kwargs): + assert messages + return ActionOutput(content=messages[-1].content, instruct_content=messages[-1]) + + +class MockRole(Role): + def __init__(self, name="", profile="", goal="", constraints="", desc=""): + super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc) + self._init_actions([MockAction()]) + + +@pytest.mark.asyncio +async def test_react(): + class Input(BaseModel): + name: str + profile: str + goal: str + constraints: str + desc: str + subscription: str + + inputs = [ + { + "name": "A", + "profile": "Tester", + "goal": "Test", + "constraints": "constraints", + "desc": "desc", + "subscription": "start", + } + ] + + for i in inputs: + seed = Input(**i) + role = MockRole( + name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints, desc=seed.desc + ) + role.subscribe({seed.subscription}) + env = Environment() + env.add_role(role) + env.publish_message(Message(content="test", cause_by=seed.subscription)) + while not env.is_idle: + await env.run() + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py deleted file mode 100644 index 11fd804ec..000000000 --- a/tests/metagpt/test_role.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/11 14:44 -@Author : alexanderwu -@File : test_role.py -""" -from metagpt.roles import Role - - -def test_role_desc(): - i = Role(profile='Sales', desc='Best Seller') - assert i.profile == 'Sales' - assert i._setting.desc == 'Best Seller' From d5d520f6a1fac0fd512911042156215502a5d2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 11:54:14 +0800 Subject: [PATCH 011/115] feat: + subscribe --- tests/metagpt/roles/test_role.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/metagpt/roles/test_role.py b/tests/metagpt/roles/test_role.py index cefd71ada..a11e69a23 100644 --- a/tests/metagpt/roles/test_role.py +++ b/tests/metagpt/roles/test_role.py @@ -53,11 +53,17 @@ async def test_react(): name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints, desc=seed.desc ) role.subscribe({seed.subscription}) + assert role._rc.watch == {seed.subscription} + assert role.name == seed.name + assert role.profile == seed.profile + assert role.is_idle env = Environment() env.add_role(role) env.publish_message(Message(content="test", cause_by=seed.subscription)) + assert not role.is_idle while not env.is_idle: await env.run() + assert role.is_idle if __name__ == "__main__": From 526751073b2a48659a9959a1e365213005d2856b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 11:58:49 +0800 Subject: [PATCH 012/115] feat: + subscribe --- tests/metagpt/{roles => }/test_role.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/metagpt/{roles => }/test_role.py (100%) diff --git a/tests/metagpt/roles/test_role.py b/tests/metagpt/test_role.py similarity index 100% rename from tests/metagpt/roles/test_role.py rename to tests/metagpt/test_role.py From 834c59df19bc4aa31065268a309d913f809f0ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 12:00:45 +0800 Subject: [PATCH 013/115] feat: + subscribe --- tests/metagpt/test_role.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index a11e69a23..1b92c88cd 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -56,6 +56,9 @@ async def test_react(): assert role._rc.watch == {seed.subscription} assert role.name == seed.name assert role.profile == seed.profile + assert role._setting.goal == seed.goal + assert role._setting.constraints == seed.constraints + assert role._setting.desc == seed.desc assert role.is_idle env = Environment() env.add_role(role) From bc67109fae1195debaf747beaa52b7ba452d02b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 12:02:05 +0800 Subject: [PATCH 014/115] feat: + subscribe --- tests/metagpt/test_role.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 1b92c88cd..98646041d 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -1,9 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -@Time : 2023-11-1 -@Author : mashenquan +@Time : 2023/5/11 14:44 +@Author : alexanderwu @File : test_role.py +@Modified By: mashenquan, 2023/11/1. Add unit tests. """ import pytest from pydantic import BaseModel From 2e9a265b916fe4d1bae390195e0ccca1067c16fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 2 Nov 2023 16:27:41 +0800 Subject: [PATCH 015/115] feat: + subscribe --- metagpt/schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/schema.py b/metagpt/schema.py index 1adfd525c..7c84dd4bb 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -97,6 +97,7 @@ class Message(BaseModel): def __init__(self, content, **kwargs): """ + Parameters not listed below will be stored as meta info. :param content: Message content. :param instruct_content: Message content struct. :param meta_info: Message meta info. From a7632e85481550bfab4531248fa530524d9b5263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:04:25 +0800 Subject: [PATCH 016/115] refactor: update notations --- examples/agent_creator.py | 3 ++- examples/build_customized_agent.py | 3 ++- examples/debate.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index d13cbcff2..5a1398456 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -2,7 +2,8 @@ Filename: MetaGPT/examples/agent_creator.py Created Date: Tuesday, September 12th 2023, 3:28:37 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import re diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index a953dee15..af15c90ca 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -2,7 +2,8 @@ Filename: MetaGPT/examples/build_customized_agent.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import asyncio import re diff --git a/examples/debate.py b/examples/debate.py index ade1a6fc4..475d2da55 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -2,7 +2,9 @@ Filename: MetaGPT/examples/debate.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the 'cause_by' value in the 'Message' to a string, and utilize the new message distribution + feature for message filtering. """ import asyncio import platform From 93eda7f4a364fe838f1eb0839209e4aa5a49c671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:05:57 +0800 Subject: [PATCH 017/115] refactor: update notations --- examples/debate.py | 2 +- examples/sk_agent.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 475d2da55..1f5e58839 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -3,7 +3,7 @@ Filename: MetaGPT/examples/debate.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the 'cause_by' value in the 'Message' to a string, and utilize the new message distribution + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution feature for message filtering. """ import asyncio diff --git a/examples/sk_agent.py b/examples/sk_agent.py index 19ee53669..900696762 100644 --- a/examples/sk_agent.py +++ b/examples/sk_agent.py @@ -4,7 +4,8 @@ @Time : 2023/9/13 12:36 @Author : femto Zheng @File : sk_agent.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import asyncio From 96f29dadb875ba4fd5e1be06557eb3161cbb6821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:12:59 +0800 Subject: [PATCH 018/115] refactor: update notations --- metagpt/actions/action.py | 1 + metagpt/actions/write_code.py | 2 ++ metagpt/const.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 1954e750a..c6f1f1534 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -4,6 +4,7 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : action.py +@Modified By: mashenquan, 2023-11-1. Add generic class-to-string and object-to-string conversion functionality. """ import re from abc import ABC diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 421211d60..f0ef2b6d6 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -4,6 +4,8 @@ @Time : 2023/5/11 17:45 @Author : alexanderwu @File : write_code.py +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from tenacity import retry, stop_after_attempt, wait_fixed diff --git a/metagpt/const.py b/metagpt/const.py index 3fbc26784..e783ec8d0 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -4,6 +4,8 @@ @Time : 2023/5/1 11:59 @Author : alexanderwu @File : const.py +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, added key definitions for + common properties in the Message. """ from pathlib import Path From e49f8a010e7ea9797ae25c6d1b61c33f26373a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:15:01 +0800 Subject: [PATCH 019/115] refactor: update notations --- metagpt/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index 7ba077080..028e98e8e 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -4,7 +4,7 @@ @Time : 2023/5/11 22:12 @Author : alexanderwu @File : environment.py -@Modified By: mashenquan, 2023-11-1. Optimization: +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.2 of RFC 116: 1. Remove the functionality of `Environment` class as a public message buffer. 2. Standardize the message forwarding behavior of the `Environment` class. 3. Add the `is_idle` property. From 67f07b66cda2598d4e0887e95cd8d6099a6d6336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:26:33 +0800 Subject: [PATCH 020/115] refactor: update notations --- metagpt/memory/longterm_memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index b5bb73b6b..e73ae334e 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ @Desc : the implement of Long-term memory -@Modified By: mashenquan, 2023-11-1. Optimization: +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: 1. Replace code related to message filtering with the `Message.is_recipient` function. """ From ddd2d40ff3c5bed217918e64d346ce0ce7fa5f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:29:37 +0800 Subject: [PATCH 021/115] refactor: update notations --- metagpt/memory/memory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 8e01544f1..282e89b17 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -4,7 +4,8 @@ @Time : 2023/5/20 12:15 @Author : alexanderwu @File : memory.py -@Modified By: mashenquan, 2023-11-1. Standardize the design of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: + Modify the new message distribution feature for message filtering. """ from collections import defaultdict from typing import Iterable, Set From a996440d5e7bbfd6af24ed026a3bc332f6856e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:36:27 +0800 Subject: [PATCH 022/115] refactor: update notations --- metagpt/memory/memory.py | 2 +- metagpt/roles/engineer.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 282e89b17..7f04be63d 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : memory.py @Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - Modify the new message distribution feature for message filtering. + Updated the message filtering logic. """ from collections import defaultdict from typing import Iterable, Set diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 9826ea0b7..ff71a61d8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -4,10 +4,12 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : engineer.py -@Modified By: mashenquan, 2023-11-1. Optimization: - 1. Consolidate message reception and processing logic within `_observe`. - 2. Fix bug: Add logic for handling asynchronous message processing when messages are not ready. - 3. Supplemented the external transmission of internal messages. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116: + 1. Modify the data type of the `cause_by` value in the `Message` to a string, and utilize the new message + distribution feature for message filtering. + 2. Consolidate message reception and processing logic within `_observe`. + 3. Fix bug: Add logic for handling asynchronous message processing when messages are not ready. + 4. Supplemented the external transmission of internal messages. """ import asyncio import shutil From 6bd9a76997b9be323c539d0a6c34ac4658df49b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:43:40 +0800 Subject: [PATCH 023/115] refactor: update notations --- metagpt/roles/qa_engineer.py | 3 ++- metagpt/roles/researcher.py | 3 ++- metagpt/roles/role.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index b83ab6e21..5cc35a878 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -4,7 +4,8 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : qa_engineer.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message filtering feature. """ import os from pathlib import Path diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 6e89b9fe7..4ec6f31e1 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """ -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 318b7d7a8..79a9fb2de 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -4,7 +4,7 @@ @Time : 2023/5/11 14:42 @Author : alexanderwu @File : role.py -@Modified By: mashenquan, 2023-11-1. Optimization: +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: 1. Merge the `recv` functionality into the `_observe` function. Future message reading operations will be consolidated within the `_observe` function. 2. Standardize the message filtering for string label matching. Role objects can access the message labels From 17c5f80d809085cd324261b9661fc06089940780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:48:47 +0800 Subject: [PATCH 024/115] refactor: update notations --- metagpt/roles/seacher.py | 3 ++- metagpt/roles/sk_agent.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/seacher.py b/metagpt/roles/seacher.py index 95be89277..d0b841f39 100644 --- a/metagpt/roles/seacher.py +++ b/metagpt/roles/seacher.py @@ -4,7 +4,8 @@ @Time : 2023/5/23 17:25 @Author : alexanderwu @File : seacher.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from metagpt.actions import ActionOutput, SearchAndSummarize from metagpt.logs import logger diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index abebb9605..5b8d333bd 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -4,7 +4,9 @@ @Time : 2023/9/13 12:23 @Author : femto Zheng @File : sk_agent.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution + feature for message filtering. """ from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner From 953a003e1e57b2dbf741b53e0a7cdee344bae593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:53:47 +0800 Subject: [PATCH 025/115] refactor: update notations --- metagpt/schema.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 7c84dd4bb..34e6fa07b 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -4,7 +4,8 @@ @Time : 2023/5/8 22:12 @Author : alexanderwu @File : schema.py -@Modified By: mashenquan, 2023-10-31, optimize class members. +@Modified By: mashenquan, 2023-10-31. According to Chapter 2.2.1 of RFC 116: + Replanned the distribution of responsibilities and functional positioning of `Message` class attributes. """ from __future__ import annotations @@ -97,7 +98,7 @@ class Message(BaseModel): def __init__(self, content, **kwargs): """ - Parameters not listed below will be stored as meta info. + Parameters not listed below will be stored as meta info, including custom parameters. :param content: Message content. :param instruct_content: Message content struct. :param meta_info: Message meta info. From b1386a01f5ce016268902f4fc82845079f8d089b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 11:57:10 +0800 Subject: [PATCH 026/115] refactor: update notations --- metagpt/software_company.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index 4bedec0e1..d29d8926d 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -4,9 +4,10 @@ @Time : 2023/5/12 00:30 @Author : alexanderwu @File : software_company.py -@Modified By: mashenquan, 2023-11-1. Optimization: - 1. Standardize the design of message filtering-related features. - 2. Abandon the design of having `Environment` store all messages. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: + 1. Change the data type of the `cause_by` value in the `Message` to a string to support the new message + distribution feature. + 2. Abandon the design of having `Environment` store all messages. """ from pydantic import BaseModel, Field From 290479969b0c0386b8004cd46d78b22f603aa805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 12:00:18 +0800 Subject: [PATCH 027/115] refactor: update notations --- metagpt/utils/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index df4688378..219ed9f04 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -4,6 +4,8 @@ @Time : 2023/4/29 16:07 @Author : alexanderwu @File : common.py +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.2 of RFC 116: + Add generic class-to-string and object-to-string conversion functionality. """ import ast import contextlib From bdf59b67bd4bfe5b381b9c61cf59086af01127c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 12:02:52 +0800 Subject: [PATCH 028/115] refactor: update notations --- tests/metagpt/actions/test_write_prd.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/metagpt/actions/test_write_prd.py b/tests/metagpt/actions/test_write_prd.py index 40ab20dad..0da7831c6 100644 --- a/tests/metagpt/actions/test_write_prd.py +++ b/tests/metagpt/actions/test_write_prd.py @@ -4,7 +4,8 @@ @Time : 2023/5/11 17:45 @Author : alexanderwu @File : test_write_prd.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import pytest @@ -18,7 +19,7 @@ from metagpt.schema import Message async def test_write_prd(): product_manager = ProductManager() requirements = "开发一个基于大语言模型与私有知识库的搜索引擎,希望可以基于大语言模型进行搜索总结" - prd = await product_manager.handle(Message(content=requirements, cause_by=BossRequirement.get_class_name())) + prd = await product_manager.run(Message(content=requirements, cause_by=BossRequirement.get_class_name())) logger.info(requirements) logger.info(prd) From 4b6745baaa26b78c2d0c7fcff70079f87674f35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 12:04:00 +0800 Subject: [PATCH 029/115] refactor: update notations --- tests/metagpt/memory/test_longterm_memory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/metagpt/memory/test_longterm_memory.py b/tests/metagpt/memory/test_longterm_memory.py index c40d7ab9d..712402db1 100644 --- a/tests/metagpt/memory/test_longterm_memory.py +++ b/tests/metagpt/memory/test_longterm_memory.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- """ @Desc : unittest of `metagpt/memory/longterm_memory.py` -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from metagpt.actions import BossRequirement From 2c551c1fd38ad0bf318591bc92f5075f8aa6eead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 12:05:11 +0800 Subject: [PATCH 030/115] refactor: update notations --- tests/metagpt/memory/test_memory_storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/metagpt/memory/test_memory_storage.py b/tests/metagpt/memory/test_memory_storage.py index 881b47d6f..c9585054a 100644 --- a/tests/metagpt/memory/test_memory_storage.py +++ b/tests/metagpt/memory/test_memory_storage.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- """ @Desc : the unittests of metagpt/memory/memory_storage.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ From 7e71ad85ca2dd638a044f9130737bb3685e7089d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 13:29:05 +0800 Subject: [PATCH 031/115] refactor: update notations --- tests/metagpt/planner/test_action_planner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/planner/test_action_planner.py b/tests/metagpt/planner/test_action_planner.py index 99cc25b72..f0a18da46 100644 --- a/tests/metagpt/planner/test_action_planner.py +++ b/tests/metagpt/planner/test_action_planner.py @@ -4,9 +4,9 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py -@Modified By: mashenquan, 2023-11-1. Optimization: - 1. Standardize the usage of message filtering-related features. - 2. Standardize the usage of message transmission. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution + feature for message handling. """ import pytest from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill From 55fe826b06f40298fc46b46bd56d43fe5a580536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 13:29:35 +0800 Subject: [PATCH 032/115] refactor: update notations --- tests/metagpt/planner/test_basic_planner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/planner/test_basic_planner.py b/tests/metagpt/planner/test_basic_planner.py index fa7ed7074..7623aee95 100644 --- a/tests/metagpt/planner/test_basic_planner.py +++ b/tests/metagpt/planner/test_basic_planner.py @@ -4,9 +4,9 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py -@Modified By: mashenquan, 2023-11-1. Optimization: - 1. Standardize the usage of message filtering-related features. - 2. Standardize the usage of message transmission. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution + feature for message handling. """ import pytest from semantic_kernel.core_skills import TextSkill From 78f3f128c046c93c29a906761fcbc4de03e88a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 13:40:48 +0800 Subject: [PATCH 033/115] refactor: update notations --- tests/metagpt/roles/mock.py | 3 ++- tests/metagpt/roles/test_architect.py | 4 +++- tests/metagpt/roles/test_engineer.py | 4 +++- tests/metagpt/test_environment.py | 3 ++- tests/metagpt/test_message.py | 1 + tests/metagpt/test_role.py | 3 ++- tests/metagpt/test_schema.py | 3 ++- tests/metagpt/utils/test_serialize.py | 3 ++- 8 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index b9891cd81..e67d64abc 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -4,7 +4,8 @@ @Time : 2023/5/12 13:05 @Author : alexanderwu @File : mock.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from metagpt.actions import BossRequirement, WriteDesign, WritePRD, WriteTasks from metagpt.schema import Message diff --git a/tests/metagpt/roles/test_architect.py b/tests/metagpt/roles/test_architect.py index 665242379..4effadaaa 100644 --- a/tests/metagpt/roles/test_architect.py +++ b/tests/metagpt/roles/test_architect.py @@ -4,7 +4,9 @@ @Time : 2023/5/20 14:37 @Author : alexanderwu @File : test_architect.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution + feature for message handling. """ import pytest diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index 93c3132ac..93f2efb77 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -4,7 +4,9 @@ @Time : 2023/5/12 10:14 @Author : alexanderwu @File : test_engineer.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data + type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution + feature for message handling. """ import pytest diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index 755798b17..714618852 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -4,7 +4,8 @@ @Time : 2023/5/12 00:47 @Author : alexanderwu @File : test_environment.py -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message transmission. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import pytest diff --git a/tests/metagpt/test_message.py b/tests/metagpt/test_message.py index 4f46311ce..04d85d9e4 100644 --- a/tests/metagpt/test_message.py +++ b/tests/metagpt/test_message.py @@ -4,6 +4,7 @@ @Time : 2023/5/16 10:57 @Author : alexanderwu @File : test_message.py +@Modified By: mashenquan, 2023-11-1. Modify coding style. """ import pytest diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 98646041d..f0ef4b3d9 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -4,7 +4,8 @@ @Time : 2023/5/11 14:44 @Author : alexanderwu @File : test_role.py -@Modified By: mashenquan, 2023/11/1. Add unit tests. +@Modified By: mashenquan, 2023-11-1. In line with Chapter 2.2.1 and 2.2.2 of RFC 116, introduce unit tests for + the utilization of the new message distribution feature in message handling. """ import pytest from pydantic import BaseModel diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 06bb57a70..2fa76fcad 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -4,7 +4,8 @@ @Time : 2023/5/20 10:40 @Author : alexanderwu @File : test_schema.py -@Modified By: mashenquan, 2023-11-1. Add `test_message`. +@Modified By: mashenquan, 2023-11-1. In line with Chapter 2.2.1 and 2.2.2 of RFC 116, introduce unit tests for + the utilization of the new feature of `Message` class. """ import json diff --git a/tests/metagpt/utils/test_serialize.py b/tests/metagpt/utils/test_serialize.py index 5a0840c87..7889f96fe 100644 --- a/tests/metagpt/utils/test_serialize.py +++ b/tests/metagpt/utils/test_serialize.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- """ @Desc : the unittest of serialize -@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of + the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from typing import List, Tuple From e667fb4f00e19c3c8e36c51793c20dbcedf662dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 13:53:12 +0800 Subject: [PATCH 034/115] refactor: update notations --- metagpt/roles/role.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 79a9fb2de..753c22134 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -258,14 +258,14 @@ class Role(Named): async def run(self, test_message=None): """Observe, and think and act based on the results of the observation""" if test_message: # For test - seed = None + msg = None if isinstance(test_message, str): - seed = Message(test_message) + msg = Message(test_message) elif isinstance(test_message, Message): - seed = test_message + msg = test_message elif isinstance(test_message, list): - seed = Message("\n".join(test_message)) - self.put_message(seed) + msg = Message("\n".join(test_message)) + self.put_message(msg) if not await self._observe(): # If there is no new information, suspend and wait From 532099a7c6c7ebe5e20a657067e3a8540e7a068f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 15:14:05 +0800 Subject: [PATCH 035/115] refactor: update notations --- metagpt/environment.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index 028e98e8e..b93eeb6b2 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -44,8 +44,15 @@ class Environment(BaseModel): for role in roles: self.add_role(role) - def publish_message(self, message: Message): - """Distribute the message to the recipients.""" + def publish_message(self, message: Message) -> bool: + """ + Distribute the message to the recipients. + In accordance with the Message routing structure design in Chapter 2.2.1 of RFC 116, as already planned + in RFC 113 for the entire system, the routing information in the Message is only responsible for + specifying the message recipient, without concern for where the message recipient is located. How to + route the message to the message recipient is a problem addressed by the transport framework designed + in RFC 113. + """ logger.info(f"publish_message: {message.save()}") found = False for r in self.roles.values(): @@ -55,6 +62,12 @@ class Environment(BaseModel): if not found: logger.warning(f"Message no recipients: {message.save()}") + # Implemented the functionality related to remote message forwarding as described in RFC 113. Awaiting release. + # if self._parent: + # return self._parent.publish_message(message) + + return True + async def run(self, k=1): """处理一次所有信息的运行 Process all Role runs at once From 8137e1af5018169542055f064c1a8ef9b4333dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 3 Nov 2023 18:08:57 +0800 Subject: [PATCH 036/115] fixbug: creation of separate indices for each label --- metagpt/memory/memory.py | 6 ++++-- tests/metagpt/test_role.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 7f04be63d..cf3140bdb 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -26,8 +26,10 @@ class Memory: if message in self.storage: return self.storage.append(message) - if message.cause_by: - self.index[message.cause_by].append(message) + # According to the design of RFC 116, it allows message filtering based on different labels, thus + # necessitating the creation of separate indices for each label. + for k in message.tx_to: + self.index[k].append(message) def add_batch(self, messages: Iterable[Message]): for message in messages: diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index f0ef4b3d9..829f75bc5 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -64,6 +64,11 @@ async def test_react(): assert role.is_idle env = Environment() env.add_role(role) + env.publish_message(Message(content="test", tx_to=seed.subscription)) + assert not role.is_idle + while not env.is_idle: + await env.run() + assert role.is_idle env.publish_message(Message(content="test", cause_by=seed.subscription)) assert not role.is_idle while not env.is_idle: From 2688fe680adb60e355f7176d439df31b28237db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 14:07:33 +0800 Subject: [PATCH 037/115] feat: According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing functionality is to be consolidated into the Environment class. --- metagpt/environment.py | 26 ++++++++++++++++++-------- metagpt/roles/role.py | 9 +++++++-- tests/metagpt/test_role.py | 8 ++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index b93eeb6b2..0fa330a83 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -8,9 +8,11 @@ 1. Remove the functionality of `Environment` class as a public message buffer. 2. Standardize the message forwarding behavior of the `Environment` class. 3. Add the `is_idle` property. +@Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing + functionality is to be consolidated into the `Environment` class. """ import asyncio -from typing import Iterable +from typing import Iterable, Set from pydantic import BaseModel, Field @@ -26,6 +28,7 @@ class Environment(BaseModel): """ roles: dict[str, Role] = Field(default_factory=dict) + consumers: dict[Role, Set] = Field(default_factory=dict) class Config: arbitrary_types_allowed = True @@ -36,6 +39,8 @@ class Environment(BaseModel): """ role.set_env(self) self.roles[role.profile] = role + # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 + self.set_subscribed_tags(role, role.subscribed_tags) def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 @@ -55,17 +60,14 @@ class Environment(BaseModel): """ logger.info(f"publish_message: {message.save()}") found = False - for r in self.roles.values(): - if message.is_recipient(r.subscribed_tags): - r.put_message(message) + # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 + for obj, subscribed_tags in self.consumers.items(): + if message.is_recipient(subscribed_tags): + obj.put_message(message) found = True if not found: logger.warning(f"Message no recipients: {message.save()}") - # Implemented the functionality related to remote message forwarding as described in RFC 113. Awaiting release. - # if self._parent: - # return self._parent.publish_message(message) - return True async def run(self, k=1): @@ -100,3 +102,11 @@ class Environment(BaseModel): if not r.is_idle: return False return True + + def get_subscribed_tags(self, obj): + """Get the labels for messages to be consumed by the object.""" + return self.consumers.get(obj, {}) + + def set_subscribed_tags(self, obj, tags): + """Set the labels for message to be consumed by the object""" + self.consumers[obj] = tags diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 753c22134..eacaa0034 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -15,6 +15,8 @@ messages into the Role object's private message receive buffer. There are no other message transmit methods. 5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes only. In the normal workflow, you should use `publish_message` or `put_message` to transmit messages. +@Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing + functionality is to be consolidated into the `Environment` class. """ from __future__ import annotations @@ -133,7 +135,7 @@ class Role(Named): def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" - tags = [get_class_name(t) for t in actions] + tags = {get_class_name(t) for t in actions} self.subscribe(tags) def subscribe(self, tags: Set[str]): @@ -141,6 +143,8 @@ class Role(Named): self._rc.watch.update(tags) # check RoleContext after adding watch actions self._rc.check(self._role_id) + if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 + self._rc.env.set_subscribed_tags(self, self.subscribed_tags) def _set_state(self, state): """Update the current state.""" @@ -149,7 +153,8 @@ class Role(Named): self._rc.todo = self._actions[self._rc.state] def set_env(self, env: "Environment"): - """Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing.""" + """Set the environment in which the role works. The role can talk to the environment and can also receive + messages by observing.""" self._rc.env = env @property diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 829f75bc5..7794c9b57 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -6,7 +6,11 @@ @File : test_role.py @Modified By: mashenquan, 2023-11-1. In line with Chapter 2.2.1 and 2.2.2 of RFC 116, introduce unit tests for the utilization of the new message distribution feature in message handling. +@Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing + functionality is to be consolidated into the `Environment` class. """ +import uuid + import pytest from pydantic import BaseModel @@ -64,6 +68,7 @@ async def test_react(): assert role.is_idle env = Environment() env.add_role(role) + assert env.get_subscribed_tags(role) == {seed.subscription} env.publish_message(Message(content="test", tx_to=seed.subscription)) assert not role.is_idle while not env.is_idle: @@ -74,6 +79,9 @@ async def test_react(): while not env.is_idle: await env.run() assert role.is_idle + tag = uuid.uuid4().hex + role.subscribe({tag}) + assert env.get_subscribed_tags(role) == {seed.subscription, tag} if __name__ == "__main__": From c4eb028a8303a2dfb9fbb8018d751e5343c01d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 14:26:48 +0800 Subject: [PATCH 038/115] refactor: save -> dump --- metagpt/environment.py | 4 ++-- metagpt/schema.py | 14 +++++++------- tests/metagpt/test_schema.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index 0fa330a83..a7e6322ff 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -58,7 +58,7 @@ class Environment(BaseModel): route the message to the message recipient is a problem addressed by the transport framework designed in RFC 113. """ - logger.info(f"publish_message: {message.save()}") + logger.info(f"publish_message: {message.dump()}") found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 for obj, subscribed_tags in self.consumers.items(): @@ -66,7 +66,7 @@ class Environment(BaseModel): obj.put_message(message) found = True if not found: - logger.warning(f"Message no recipients: {message.save()}") + logger.warning(f"Message no recipients: {message.dump()}") return True diff --git a/metagpt/schema.py b/metagpt/schema.py index 34e6fa07b..bb8d8b42c 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -188,7 +188,7 @@ class Message(BaseModel): """Return a dict containing `role` and `content` for the LLM call.l""" return {"role": self.role, "content": self.content} - def save(self) -> str: + def dump(self) -> str: """Convert the object to json string""" return self.json(exclude_none=True) @@ -264,7 +264,7 @@ class MessageQueue: """Return true if the queue is empty.""" return self._queue.empty() - async def save(self) -> str: + async def dump(self) -> str: """Convert the `MessageQueue` object to a json string.""" if self.empty(): return "[]" @@ -299,7 +299,7 @@ class MessageQueue: if __name__ == "__main__": m = Message("a", role="v1") m.set_role("v2") - v = m.save() + v = m.dump() m = Message.load(v) test_content = "test_message" @@ -312,9 +312,9 @@ if __name__ == "__main__": logger.info(msgs) jsons = [ - UserMessage(test_content).save(), - SystemMessage(test_content).save(), - AIMessage(test_content).save(), - Message(test_content, role="QA").save(), + UserMessage(test_content).dump(), + SystemMessage(test_content).dump(), + AIMessage(test_content).dump(), + Message(test_content, role="QA").dump(), ] logger.info(jsons) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 2fa76fcad..21ba3fd14 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -31,13 +31,13 @@ def test_messages(): @pytest.mark.asyncio def test_message(): m = Message("a", role="v1") - v = m.save() + v = m.dump() d = json.loads(v) assert d assert d.get("content") == "a" assert d.get("meta_info") == {"role": "v1"} m.set_role("v2") - v = m.save() + v = m.dump() assert v m = Message.load(v) assert m.content == "a" From 1febf168e7bd7e2e10becbdad14ed42d03f2b443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:20:47 +0800 Subject: [PATCH 039/115] refactor: Override cause_by --- metagpt/schema.py | 33 +++++++++++++++++++++++++++++++++ tests/metagpt/test_schema.py | 10 ++++++++++ 2 files changed, 43 insertions(+) diff --git a/metagpt/schema.py b/metagpt/schema.py index bb8d8b42c..52020c468 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -24,6 +24,7 @@ from metagpt.const import ( MESSAGE_ROUTE_TO, ) from metagpt.logs import logger +from metagpt.utils.common import get_class_name, get_object_name class RawMessage(TypedDict): @@ -87,6 +88,14 @@ class Routes(BaseModel): route = self._get_route() return route.get(MESSAGE_ROUTE_TO) + def replace(self, old_val, new_val): + """Replace old value with new value""" + route = self._get_route() + tags = route.get(MESSAGE_ROUTE_TO, set()) + tags.discard(old_val) + tags.add(new_val) + route[MESSAGE_ROUTE_TO] = tags + class Message(BaseModel): """list[: ]""" @@ -147,6 +156,26 @@ class Message(BaseModel): """Labels for the consumer to filter its subscribed messages, also serving as meta info.""" return self.get_meta(MESSAGE_ROUTE_CAUSE_BY) + def __setattr__(self, key, val): + """Override `@property.setter`""" + if key == MESSAGE_ROUTE_CAUSE_BY: + self.set_cause_by(val) + return + super().__setattr__(key, val) + + def set_cause_by(self, val): + """Update the value of `cause_by` in the `meta_info` and `routes` attributes.""" + old_value = self.get_meta(MESSAGE_ROUTE_CAUSE_BY) + new_value = None + if isinstance(val, str): + new_value = val + elif not callable(val): + new_value = get_object_name(val) + else: + new_value = get_class_name(val) + self.set_meta(MESSAGE_ROUTE_CAUSE_BY, new_value) + self.route.replace(old_value, new_value) + @property def tx_from(self): """Message route info tells who sent this message.""" @@ -301,6 +330,10 @@ if __name__ == "__main__": m.set_role("v2") v = m.dump() m = Message.load(v) + m.cause_by = "Message" + m.cause_by = Routes + m.cause_by = Routes() + m.content = "b" test_content = "test_message" msgs = [ diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 21ba3fd14..e4aa0c0dd 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -11,6 +11,7 @@ import json import pytest +from metagpt.actions import Action from metagpt.schema import AIMessage, Message, Routes, SystemMessage, UserMessage @@ -50,6 +51,15 @@ def test_message(): assert m.cause_by == "c" assert m.get_meta("x") == "d" + m.cause_by = "Message" + assert m.cause_by == "Message" + m.cause_by = Action + assert m.cause_by == Action.get_class_name() + m.cause_by = Action() + assert m.cause_by == Action.get_class_name() + m.content = "b" + assert m.content == "b" + @pytest.mark.asyncio def test_routes(): From d9775037b68eee015f372e27e664e6f5952e9f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:46:32 +0800 Subject: [PATCH 040/115] refactor: @cause_by.setter --- examples/debate.py | 10 +++---- examples/sk_agent.py | 8 +++--- metagpt/actions/action.py | 3 +-- metagpt/actions/write_code.py | 2 +- metagpt/roles/engineer.py | 12 ++++----- metagpt/roles/qa_engineer.py | 20 +++++++------- metagpt/roles/role.py | 7 +++-- metagpt/schema.py | 19 ++++++------- metagpt/software_company.py | 7 +++-- metagpt/utils/common.py | 10 +++++++ metagpt/utils/named.py | 21 --------------- tests/metagpt/actions/test_write_prd.py | 2 +- tests/metagpt/memory/test_longterm_memory.py | 10 +++---- tests/metagpt/memory/test_memory_storage.py | 16 +++++------ tests/metagpt/planner/test_action_planner.py | 2 +- tests/metagpt/planner/test_basic_planner.py | 2 +- tests/metagpt/roles/mock.py | 8 +++--- tests/metagpt/test_environment.py | 2 +- tests/metagpt/test_schema.py | 5 ++-- tests/metagpt/utils/test_named.py | 28 -------------------- tests/metagpt/utils/test_serialize.py | 2 +- 21 files changed, 73 insertions(+), 123 deletions(-) delete mode 100644 metagpt/utils/named.py delete mode 100644 tests/metagpt/utils/test_named.py diff --git a/examples/debate.py b/examples/debate.py index 1f5e58839..c1d997678 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -66,7 +66,7 @@ class Trump(Role): async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([ShoutOut.get_class_name()]) + msg_history = self._rc.memory.get_by_actions([ShoutOut]) context = [] for m in msg_history: context.append(str(m)) @@ -77,7 +77,7 @@ class Trump(Role): msg = Message( content=rsp, role=self.profile, - cause_by=ShoutOut.get_class_name(), + cause_by=ShoutOut, tx_from=self.name, tx_to=self.opponent_name, ) @@ -102,14 +102,14 @@ class Biden(Role): await super()._observe() # accept the very first human instruction (the debate topic) or messages sent (from opponent) to self, # disregard own messages from the last round - message_filter = {BossRequirement.get_class_name(), self.name} + message_filter = {BossRequirement, self.name} self._rc.news = [msg for msg in self._rc.news if msg.is_recipient(message_filter)] return len(self._rc.news) async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([BossRequirement.get_class_name(), ShoutOut.get_class_name()]) + msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut]) context = [] for m in msg_history: context.append(str(m)) @@ -120,7 +120,7 @@ class Biden(Role): msg = Message( content=rsp, role=self.profile, - cause_by=ShoutOut.get_class_name(), + cause_by=ShoutOut, tx_from=self.name, tx_to=self.opponent_name, ) diff --git a/examples/sk_agent.py b/examples/sk_agent.py index 900696762..21714cca1 100644 --- a/examples/sk_agent.py +++ b/examples/sk_agent.py @@ -41,7 +41,7 @@ async def basic_planner_example(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) + await role.run(Message(content=task, cause_by=BossRequirement)) async def sequential_planner_example(): @@ -55,7 +55,7 @@ async def sequential_planner_example(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) + await role.run(Message(content=task, cause_by=BossRequirement)) async def basic_planner_web_search_example(): @@ -66,7 +66,7 @@ async def basic_planner_web_search_example(): role.import_skill(SkSearchEngine(), "WebSearchSkill") # role.import_semantic_skill_from_directory(skills_directory, "QASkill") - await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) + await role.run(Message(content=task, cause_by=BossRequirement)) async def action_planner_example(): @@ -77,7 +77,7 @@ async def action_planner_example(): role.import_skill(TimeSkill(), "time") role.import_skill(TextSkill(), "text") task = "What is the sum of 110 and 990?" - await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) # it will choose mathskill.Add + await role.run(Message(content=task, cause_by=BossRequirement)) # it will choose mathskill.Add if __name__ == "__main__": diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index c6f1f1534..fd114b332 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -17,10 +17,9 @@ from metagpt.llm import LLM from metagpt.logs import logger from metagpt.utils.common import OutputParser from metagpt.utils.custom_decoder import CustomDecoder -from metagpt.utils.named import Named -class Action(ABC, Named): +class Action(ABC): def __init__(self, name: str = "", context=None, llm: LLM = None): self.name: str = name if llm is None: diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index f0ef2b6d6..8b6451134 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -58,7 +58,7 @@ class WriteCode(Action): if self._is_invalid(filename): return - message_filter = {WriteDesign.get_class_name()} + message_filter = {WriteDesign} design = [i for i in context if i.is_recipient(message_filter)][0] ws_name = CodeParser.parse_str(block="Python package name", text=design.content) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index ff71a61d8..7f05c52c5 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -102,7 +102,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1] + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -130,7 +130,7 @@ class Engineer(Role): todo_coros = [] for todo in self.todos: todo_coro = WriteCode().run( - context=self._rc.memory.get_by_actions([WriteTasks.get_class_name(), WriteDesign.get_class_name()]), + context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo, ) todo_coros.append(todo_coro) @@ -185,7 +185,7 @@ class Engineer(Role): TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ context = [] - msg_filters = [WriteDesign.get_class_name(), WriteTasks.get_class_name(), WriteCode.get_class_name()] + msg_filters = [WriteDesign, WriteTasks, WriteCode] msg = self._rc.memory.get_by_actions(msg_filters) for m in msg: context.append(m.content) @@ -201,7 +201,7 @@ class Engineer(Role): logger.error("code review failed!", e) pass file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=WriteCode.get_class_name()) + msg = Message(content=code, role=self.profile, cause_by=WriteCode) self._rc.memory.add(msg) self.publish_message(msg) @@ -231,7 +231,7 @@ class Engineer(Role): return ret # Parse task lists - message_filter = {WriteTasks.get_class_name()} + message_filter = {WriteTasks} for message in self._rc.news: if not message.is_recipient(message_filter): continue @@ -241,7 +241,7 @@ class Engineer(Role): async def _think(self) -> None: # In asynchronous scenarios, first check if the required messages are ready. - filters = {WriteTasks.get_class_name()} + filters = {WriteTasks} msgs = self._rc.memory.get_by_actions(filters) if not msgs: self._rc.todo = None diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 5cc35a878..64d7f9702 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -50,7 +50,7 @@ class QaEngineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1] + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -99,7 +99,7 @@ class QaEngineer(Role): msg = Message( content=str(file_info), role=self.profile, - cause_by=WriteTest.get_class_name(), + cause_by=WriteTest, tx_from=self.profile, tx_to=self.profile, ) @@ -133,9 +133,7 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message( - content=content, role=self.profile, cause_by=RunCode.get_class_name(), tx_from=self.profile, tx_to=recipient - ) + msg = Message(content=content, role=self.profile, cause_by=RunCode, tx_from=self.profile, tx_to=recipient) self.publish_message(msg) async def _debug_error(self, msg): @@ -147,7 +145,7 @@ class QaEngineer(Role): msg = Message( content=file_info, role=self.profile, - cause_by=DebugError.get_class_name(), + cause_by=DebugError, tx_from=self.profile, tx_to=recipient, ) @@ -165,14 +163,14 @@ class QaEngineer(Role): result_msg = Message( content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, - cause_by=WriteTest.get_class_name(), + cause_by=WriteTest, tx_from=self.profile, ) return result_msg - code_filters = {WriteCode.get_class_name(), WriteCodeReview.get_class_name()} - test_filters = {WriteTest.get_class_name(), DebugError.get_class_name()} - run_filters = {RunCode.get_class_name()} + code_filters = {WriteCode, WriteCodeReview} + test_filters = {WriteTest, DebugError} + run_filters = {RunCode} for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself @@ -189,7 +187,7 @@ class QaEngineer(Role): result_msg = Message( content=f"Round {self.test_round} of tests done", role=self.profile, - cause_by=WriteTest.get_class_name(), + cause_by=WriteTest, tx_from=self.profile, ) return result_msg diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index eacaa0034..87a03b391 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -31,7 +31,6 @@ from metagpt.logs import logger from metagpt.memory import LongTermMemory, Memory from metagpt.schema import Message, MessageQueue from metagpt.utils.common import get_class_name, get_object_name -from metagpt.utils.named import Named PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -107,7 +106,7 @@ class RoleContext(BaseModel): return self.memory.get() -class Role(Named): +class Role: """Role/Agent""" def __init__(self, name="", profile="", goal="", constraints="", desc=""): @@ -174,10 +173,10 @@ class Role(Named): return self._rc.watch return { self.name, - self.get_object_name(), + get_object_name(self), self.profile, f"{self.name}({self.profile})", - f"{self.name}({self.get_object_name()})", + f"{self.name}({get_object_name(self)})", } def _get_prefix(self): diff --git a/metagpt/schema.py b/metagpt/schema.py index 52020c468..1082c5ddb 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -24,7 +24,7 @@ from metagpt.const import ( MESSAGE_ROUTE_TO, ) from metagpt.logs import logger -from metagpt.utils.common import get_class_name, get_object_name +from metagpt.utils.common import any_to_str class RawMessage(TypedDict): @@ -129,11 +129,12 @@ class Message(BaseModel): if k in attribute_names: continue if k == MESSAGE_ROUTE_FROM: - self.set_from(v) + self.set_from(any_to_str(v)) continue if k == MESSAGE_ROUTE_CAUSE_BY: - self.meta_info[k] = v - if k == MESSAGE_ROUTE_TO or k == MESSAGE_ROUTE_CAUSE_BY: + self.set_cause_by(v) + continue + if k == MESSAGE_ROUTE_TO: self.add_to(v) continue self.meta_info[k] = v @@ -161,18 +162,14 @@ class Message(BaseModel): if key == MESSAGE_ROUTE_CAUSE_BY: self.set_cause_by(val) return + if key == MESSAGE_ROUTE_FROM: + self.set_from(any_to_str(val)) super().__setattr__(key, val) def set_cause_by(self, val): """Update the value of `cause_by` in the `meta_info` and `routes` attributes.""" old_value = self.get_meta(MESSAGE_ROUTE_CAUSE_BY) - new_value = None - if isinstance(val, str): - new_value = val - elif not callable(val): - new_value = get_object_name(val) - else: - new_value = get_class_name(val) + new_value = any_to_str(val) self.set_meta(MESSAGE_ROUTE_CAUSE_BY, new_value) self.route.replace(old_value, new_value) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index d29d8926d..57bd5db19 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -18,10 +18,9 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.utils.common import NoMoneyException -from metagpt.utils.named import Named -class SoftwareCompany(BaseModel, Named): +class SoftwareCompany(BaseModel): """ Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging, dedicated to writing executable code. @@ -55,8 +54,8 @@ class SoftwareCompany(BaseModel, Named): Message( role="BOSS", content=idea, - cause_by=BossRequirement.get_class_name(), - tx_from=SoftwareCompany.get_class_name(), + cause_by=BossRequirement, + tx_from=SoftwareCompany, ) ) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 219ed9f04..b372f0d8d 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -315,3 +315,13 @@ def get_object_name(obj) -> str: """Return class name of the object""" cls = type(obj) return f"{cls.__module__}.{cls.__name__}" + + +def any_to_str(val) -> str: + """Return the class name or the class name of the object, or 'val' if it's a string type.""" + if isinstance(val, str): + return val + if not callable(val): + return get_object_name(val) + + return get_class_name(val) diff --git a/metagpt/utils/named.py b/metagpt/utils/named.py deleted file mode 100644 index e4da574e8..000000000 --- a/metagpt/utils/named.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/11/1 -@Author : mashenquan -@File : named.py -""" - - -class Named: - """A base class with functions for converting classes to names and objects to class names.""" - - @classmethod - def get_class_name(cls): - """Return class name""" - return f"{cls.__module__}.{cls.__name__}" - - def get_object_name(self): - """Return class name of the object""" - cls = type(self) - return f"{cls.__module__}.{cls.__name__}" diff --git a/tests/metagpt/actions/test_write_prd.py b/tests/metagpt/actions/test_write_prd.py index 0da7831c6..5a121adce 100644 --- a/tests/metagpt/actions/test_write_prd.py +++ b/tests/metagpt/actions/test_write_prd.py @@ -19,7 +19,7 @@ from metagpt.schema import Message async def test_write_prd(): product_manager = ProductManager() requirements = "开发一个基于大语言模型与私有知识库的搜索引擎,希望可以基于大语言模型进行搜索总结" - prd = await product_manager.run(Message(content=requirements, cause_by=BossRequirement.get_class_name())) + prd = await product_manager.run(Message(content=requirements, cause_by=BossRequirement)) logger.info(requirements) logger.info(prd) diff --git a/tests/metagpt/memory/test_longterm_memory.py b/tests/metagpt/memory/test_longterm_memory.py index 712402db1..b33dd312d 100644 --- a/tests/metagpt/memory/test_longterm_memory.py +++ b/tests/metagpt/memory/test_longterm_memory.py @@ -19,24 +19,24 @@ def test_ltm_search(): assert len(openai_api_key) > 20 role_id = "UTUserLtm(Product Manager)" - rc = RoleContext(watch=[BossRequirement.get_class_name()]) + rc = RoleContext(watch=[BossRequirement]) ltm = LongTermMemory() ltm.recover_memory(role_id, rc) idea = "Write a cli snake game" - message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name()) + message = Message(role="BOSS", content=idea, cause_by=BossRequirement) news = ltm.find_news([message]) assert len(news) == 1 ltm.add(message) sim_idea = "Write a game of cli snake" - sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name()) + sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement) news = ltm.find_news([sim_message]) assert len(news) == 0 ltm.add(sim_message) new_idea = "Write a 2048 web game" - new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement) news = ltm.find_news([new_message]) assert len(news) == 1 ltm.add(new_message) @@ -52,7 +52,7 @@ def test_ltm_search(): assert len(news) == 0 new_idea = "Write a Battle City" - new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement) news = ltm_new.find_news([new_message]) assert len(news) == 1 diff --git a/tests/metagpt/memory/test_memory_storage.py b/tests/metagpt/memory/test_memory_storage.py index c9585054a..c40bbbba5 100644 --- a/tests/metagpt/memory/test_memory_storage.py +++ b/tests/metagpt/memory/test_memory_storage.py @@ -18,7 +18,7 @@ from metagpt.schema import Message def test_idea_message(): idea = "Write a cli snake game" role_id = "UTUser1(Product Manager)" - message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name()) + message = Message(role="BOSS", content=idea, cause_by=BossRequirement) memory_storage: MemoryStorage = MemoryStorage() messages = memory_storage.recover_memory(role_id) @@ -28,12 +28,12 @@ def test_idea_message(): assert memory_storage.is_initialized is True sim_idea = "Write a game of cli snake" - sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name()) + sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement) new_messages = memory_storage.search(sim_message) assert len(new_messages) == 0 # similar, return [] new_idea = "Write a 2048 web game" - new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name()) + new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement) new_messages = memory_storage.search(new_message) assert new_messages[0].content == message.content @@ -49,7 +49,7 @@ def test_actionout_message(): role_id = "UTUser2(Architect)" content = "The boss has requested the creation of a command-line interface (CLI) snake game" message = Message( - content=content, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() + content=content, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD ) # WritePRD as test action memory_storage: MemoryStorage = MemoryStorage() @@ -60,16 +60,12 @@ def test_actionout_message(): assert memory_storage.is_initialized is True sim_conent = "The request is command-line interface (CLI) snake game" - sim_message = Message( - content=sim_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() - ) + sim_message = Message(content=sim_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD) new_messages = memory_storage.search(sim_message) assert len(new_messages) == 0 # similar, return [] new_conent = "Incorporate basic features of a snake game such as scoring and increasing difficulty" - new_message = Message( - content=new_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() - ) + new_message = Message(content=new_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD) new_messages = memory_storage.search(new_message) assert new_messages[0].content == message.content diff --git a/tests/metagpt/planner/test_action_planner.py b/tests/metagpt/planner/test_action_planner.py index f0a18da46..e8350b6e6 100644 --- a/tests/metagpt/planner/test_action_planner.py +++ b/tests/metagpt/planner/test_action_planner.py @@ -26,7 +26,7 @@ async def test_action_planner(): role.import_skill(TimeSkill(), "time") role.import_skill(TextSkill(), "text") task = "What is the sum of 110 and 990?" - role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + role.put_message(Message(content=task, cause_by=BossRequirement)) await role._observe() await role._think() # it will choose mathskill.Add assert "1100" == (await role._act()).content diff --git a/tests/metagpt/planner/test_basic_planner.py b/tests/metagpt/planner/test_basic_planner.py index 7623aee95..0935dd98c 100644 --- a/tests/metagpt/planner/test_basic_planner.py +++ b/tests/metagpt/planner/test_basic_planner.py @@ -29,7 +29,7 @@ async def test_basic_planner(): role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill") role.import_skill(TextSkill(), "TextSkill") # using BasicPlanner - role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name())) + role.put_message(Message(content=task, cause_by=BossRequirement)) await role._observe() await role._think() # assuming sk_agent will think he needs WriterSkill.Brainstorm and WriterSkill.Translate diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index e67d64abc..1bf20e9b7 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -254,7 +254,7 @@ a = 'a' class MockMessages: - req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement.get_class_name()) - prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD.get_class_name()) - system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign.get_class_name()) - tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks.get_class_name()) + req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement) + prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD) + system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign) + tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks) diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index 714618852..472d4cd9d 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -51,7 +51,7 @@ async def test_publish_and_process_message(env: Environment): env.add_roles([product_manager, architect]) env.set_manager(Manager()) - env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement.get_class_name())) + env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement)) await env.run(k=2) logger.info(f"{env.history=}") diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index e4aa0c0dd..e18ebbe79 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -13,6 +13,7 @@ import pytest from metagpt.actions import Action from metagpt.schema import AIMessage, Message, Routes, SystemMessage, UserMessage +from metagpt.utils.common import get_class_name @pytest.mark.asyncio @@ -54,9 +55,9 @@ def test_message(): m.cause_by = "Message" assert m.cause_by == "Message" m.cause_by = Action - assert m.cause_by == Action.get_class_name() + assert m.cause_by == get_class_name(Action) m.cause_by = Action() - assert m.cause_by == Action.get_class_name() + assert m.cause_by == get_class_name(Action) m.content = "b" assert m.content == "b" diff --git a/tests/metagpt/utils/test_named.py b/tests/metagpt/utils/test_named.py deleted file mode 100644 index ff1f07205..000000000 --- a/tests/metagpt/utils/test_named.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023-11-1 -@Author : mashenquan -@File : test_named.py -""" -import pytest - -from metagpt.utils.named import Named - - -@pytest.mark.asyncio -async def test_suite(): - class A(Named): - pass - - class B(A): - pass - - assert A.get_class_name() == "tests.metagpt.utils.test_named.A" - assert A().get_object_name() == "tests.metagpt.utils.test_named.A" - assert B.get_class_name() == "tests.metagpt.utils.test_named.B" - assert B().get_object_name() == "tests.metagpt.utils.test_named.B" - - -if __name__ == "__main__": - pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_serialize.py b/tests/metagpt/utils/test_serialize.py index 7889f96fe..3f566d64d 100644 --- a/tests/metagpt/utils/test_serialize.py +++ b/tests/metagpt/utils/test_serialize.py @@ -59,7 +59,7 @@ def test_serialize_and_deserialize_message(): ic_obj = ActionOutput.create_model_class("prd", out_mapping) message = Message( - content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name() + content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD ) # WritePRD as test action message_ser = serialize_message(message) From 56f544a675ef7417b46e2f683609b497af31feef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:49:13 +0800 Subject: [PATCH 041/115] refactor: @cause_by.setter --- examples/agent_creator.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 5a1398456..3618c0608 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -2,8 +2,6 @@ Filename: MetaGPT/examples/agent_creator.py Created Date: Tuesday, September 12th 2023, 3:28:37 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import re @@ -12,7 +10,6 @@ from metagpt.const import PROJECT_ROOT, WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import get_object_name with open(PROJECT_ROOT / "examples/build_customized_agent.py", "r") as f: # use official example script to guide AgentCreator @@ -75,7 +72,7 @@ class AgentCreator(Role): instruction = msg.content code_text = await CreateAgent().run(example=self.agent_template, instruction=instruction) - msg = Message(content=code_text, role=self.profile, cause_by=get_object_name(todo)) + msg = Message(content=code_text, role=self.profile, cause_by=todo) return msg From 8ea52d8a83ce9615b56831fdd9c27c82e0c885f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:52:21 +0800 Subject: [PATCH 042/115] refactor: @cause_by.setter --- examples/build_customized_agent.py | 4 +--- metagpt/schema.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index af15c90ca..f7f554e53 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -2,8 +2,6 @@ Filename: MetaGPT/examples/build_customized_agent.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import asyncio import re @@ -83,7 +81,7 @@ class SimpleCoder(Role): instruction = msg.content code_text = await SimpleWriteCode().run(instruction) - msg = Message(content=code_text, role=self.profile, cause_by=get_object_name(todo)) + msg = Message(content=code_text, role=self.profile, cause_by=todo) return msg diff --git a/metagpt/schema.py b/metagpt/schema.py index 1082c5ddb..0be067cfe 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -135,7 +135,7 @@ class Message(BaseModel): self.set_cause_by(v) continue if k == MESSAGE_ROUTE_TO: - self.add_to(v) + self.add_to(any_to_str(v)) continue self.meta_info[k] = v From 87882bf7ab56c3bcff54b5bb8fd41c09afd5bcb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:54:36 +0800 Subject: [PATCH 043/115] refactor: @cause_by.setter --- examples/build_customized_agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index f7f554e53..ef274be8b 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -13,7 +13,6 @@ from metagpt.actions import Action from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import get_object_name class SimpleWriteCode(Action): @@ -119,7 +118,7 @@ class RunnableCoder(Role): code_text = msg.content result = await SimpleRunCode().run(code_text) - msg = Message(content=result, role=self.profile, cause_by=get_object_name(todo)) + msg = Message(content=result, role=self.profile, cause_by=todo) self._rc.memory.add(msg) return msg From b0d451d4d60246d36ff77f28830d6ce16f846c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 16:55:51 +0800 Subject: [PATCH 044/115] refactor: @cause_by.setter --- examples/sk_agent.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/sk_agent.py b/examples/sk_agent.py index 21714cca1..a7513e838 100644 --- a/examples/sk_agent.py +++ b/examples/sk_agent.py @@ -4,8 +4,6 @@ @Time : 2023/9/13 12:36 @Author : femto Zheng @File : sk_agent.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import asyncio From 2129c904ea498ef54b233b94235ae5faacb6eb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:02:40 +0800 Subject: [PATCH 045/115] refactor: @cause_by.setter --- metagpt/actions/action.py | 1 - metagpt/software_company.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index fd114b332..790295d55 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -4,7 +4,6 @@ @Time : 2023/5/11 14:43 @Author : alexanderwu @File : action.py -@Modified By: mashenquan, 2023-11-1. Add generic class-to-string and object-to-string conversion functionality. """ import re from abc import ABC diff --git a/metagpt/software_company.py b/metagpt/software_company.py index 57bd5db19..354773444 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -5,9 +5,7 @@ @Author : alexanderwu @File : software_company.py @Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - 1. Change the data type of the `cause_by` value in the `Message` to a string to support the new message - distribution feature. - 2. Abandon the design of having `Environment` store all messages. + 1. Abandon the design of having `Environment` store all messages. """ from pydantic import BaseModel, Field From 8f85d80b181825dd2d43e4c6fe24ab0c306a3e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:05:15 +0800 Subject: [PATCH 046/115] refactor: @cause_by.setter --- tests/metagpt/actions/test_write_prd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/metagpt/actions/test_write_prd.py b/tests/metagpt/actions/test_write_prd.py index 5a121adce..07d701cb9 100644 --- a/tests/metagpt/actions/test_write_prd.py +++ b/tests/metagpt/actions/test_write_prd.py @@ -4,8 +4,7 @@ @Time : 2023/5/11 17:45 @Author : alexanderwu @File : test_write_prd.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. +@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, replace `handle` with `run`. """ import pytest From 2b2f29dcd579675ae3f0cb30217625787918474c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:06:21 +0800 Subject: [PATCH 047/115] refactor: @cause_by.setter --- tests/metagpt/memory/test_longterm_memory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/metagpt/memory/test_longterm_memory.py b/tests/metagpt/memory/test_longterm_memory.py index b33dd312d..c5b5c6eb1 100644 --- a/tests/metagpt/memory/test_longterm_memory.py +++ b/tests/metagpt/memory/test_longterm_memory.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ @Desc : unittest of `metagpt/memory/longterm_memory.py` -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from metagpt.actions import BossRequirement From 9de646c01d5b4ffb977d44e335232c840a6bce7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:07:24 +0800 Subject: [PATCH 048/115] refactor: @cause_by.setter --- tests/metagpt/memory/test_memory_storage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/metagpt/memory/test_memory_storage.py b/tests/metagpt/memory/test_memory_storage.py index c40bbbba5..251c70b02 100644 --- a/tests/metagpt/memory/test_memory_storage.py +++ b/tests/metagpt/memory/test_memory_storage.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ @Desc : the unittests of metagpt/memory/memory_storage.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ From e696442db935609c842f9d855a4926b048551414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:08:58 +0800 Subject: [PATCH 049/115] refactor: @cause_by.setter --- tests/metagpt/planner/test_action_planner.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/planner/test_action_planner.py b/tests/metagpt/planner/test_action_planner.py index e8350b6e6..b8d4c1ad9 100644 --- a/tests/metagpt/planner/test_action_planner.py +++ b/tests/metagpt/planner/test_action_planner.py @@ -4,9 +4,8 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message handling. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message + distribution feature for message handling. """ import pytest from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill From e86d8a3952ec3dd28a46ff4c8a118d08ecb7249c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:09:48 +0800 Subject: [PATCH 050/115] refactor: @cause_by.setter --- tests/metagpt/planner/test_basic_planner.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/planner/test_basic_planner.py b/tests/metagpt/planner/test_basic_planner.py index 0935dd98c..24250a0b0 100644 --- a/tests/metagpt/planner/test_basic_planner.py +++ b/tests/metagpt/planner/test_basic_planner.py @@ -4,9 +4,8 @@ @Time : 2023/9/16 20:03 @Author : femto Zheng @File : test_basic_planner.py -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message handling. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message + distribution feature for message handling. """ import pytest from semantic_kernel.core_skills import TextSkill From be77a9c30866cefe99105d2975d5236c67284875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:10:44 +0800 Subject: [PATCH 051/115] refactor: @cause_by.setter --- tests/metagpt/roles/mock.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index 1bf20e9b7..1b02fbaa5 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -4,8 +4,6 @@ @Time : 2023/5/12 13:05 @Author : alexanderwu @File : mock.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from metagpt.actions import BossRequirement, WriteDesign, WritePRD, WriteTasks from metagpt.schema import Message From 3a5bfcafc52613b5691aaa7121634d60a834f402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:12:02 +0800 Subject: [PATCH 052/115] refactor: @cause_by.setter --- tests/metagpt/roles/test_architect.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/roles/test_architect.py b/tests/metagpt/roles/test_architect.py index 4effadaaa..111438b0b 100644 --- a/tests/metagpt/roles/test_architect.py +++ b/tests/metagpt/roles/test_architect.py @@ -4,9 +4,8 @@ @Time : 2023/5/20 14:37 @Author : alexanderwu @File : test_architect.py -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message handling. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message + distribution feature for message handling. """ import pytest From 327c047fa51226f36a8dd414ca77c7fcde319493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:13:10 +0800 Subject: [PATCH 053/115] refactor: @cause_by.setter --- tests/metagpt/roles/test_engineer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index 93f2efb77..3dc599770 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -4,9 +4,8 @@ @Time : 2023/5/12 10:14 @Author : alexanderwu @File : test_engineer.py -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message handling. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message + distribution feature for message handling. """ import pytest From eba7f868e71678338125221c85f0aa2d527c16b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:17:02 +0800 Subject: [PATCH 054/115] refactor: @cause_by.setter --- tests/metagpt/test_environment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index 472d4cd9d..a0f1f6257 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -4,8 +4,6 @@ @Time : 2023/5/12 00:47 @Author : alexanderwu @File : test_environment.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ import pytest From ed7eb4d08a07a0c4dc2530e7e1c55d6c5bae0bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 4 Nov 2023 17:18:26 +0800 Subject: [PATCH 055/115] refactor: @cause_by.setter --- tests/metagpt/utils/test_serialize.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/metagpt/utils/test_serialize.py b/tests/metagpt/utils/test_serialize.py index 3f566d64d..ffa34866c 100644 --- a/tests/metagpt/utils/test_serialize.py +++ b/tests/metagpt/utils/test_serialize.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ @Desc : the unittest of serialize -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ from typing import List, Tuple From c6f97f748717c030f752ebe492342aace4a4ab13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 6 Nov 2023 11:47:29 +0800 Subject: [PATCH 056/115] refactor: tx_from/tx_to --- examples/debate.py | 8 ++++---- metagpt/const.py | 4 ++-- metagpt/memory/memory.py | 2 +- metagpt/roles/engineer.py | 4 ++-- metagpt/roles/qa_engineer.py | 14 +++++++------- metagpt/roles/role.py | 4 ++-- metagpt/schema.py | 16 ++++++++-------- metagpt/software_company.py | 2 +- tests/metagpt/test_role.py | 2 +- tests/metagpt/test_schema.py | 8 ++++---- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index c1d997678..77a2ce129 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -78,8 +78,8 @@ class Trump(Role): content=rsp, role=self.profile, cause_by=ShoutOut, - tx_from=self.name, - tx_to=self.opponent_name, + msg_from=self.name, + msg_to=self.opponent_name, ) return msg @@ -121,8 +121,8 @@ class Biden(Role): content=rsp, role=self.profile, cause_by=ShoutOut, - tx_from=self.name, - tx_to=self.opponent_name, + msg_from=self.name, + msg_to=self.opponent_name, ) return msg diff --git a/metagpt/const.py b/metagpt/const.py index e783ec8d0..7b8203bce 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -44,7 +44,7 @@ SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills" MEM_TTL = 24 * 30 * 3600 -MESSAGE_ROUTE_FROM = "tx_from" -MESSAGE_ROUTE_TO = "tx_to" +MESSAGE_ROUTE_FROM = "msg_from" +MESSAGE_ROUTE_TO = "msg_to" MESSAGE_ROUTE_CAUSE_BY = "cause_by" MESSAGE_META_ROLE = "role" diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index cf3140bdb..c6b732076 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -28,7 +28,7 @@ class Memory: self.storage.append(message) # According to the design of RFC 116, it allows message filtering based on different labels, thus # necessitating the creation of separate indices for each label. - for k in message.tx_to: + for k in message.msg_to: self.index[k].append(message) def add_batch(self, messages: Iterable[Message]): diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 7f05c52c5..8778471cc 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -170,7 +170,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - tx_to="QaEngineer", + msg_to="QaEngineer", ) return msg @@ -213,7 +213,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - tx_to="QaEngineer", + msg_to="QaEngineer", ) return msg diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 64d7f9702..05fc5b217 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -100,8 +100,8 @@ class QaEngineer(Role): content=str(file_info), role=self.profile, cause_by=WriteTest, - tx_from=self.profile, - tx_to=self.profile, + msg_from=self.profile, + msg_to=self.profile, ) self.publish_message(msg) @@ -133,7 +133,7 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message(content=content, role=self.profile, cause_by=RunCode, tx_from=self.profile, tx_to=recipient) + msg = Message(content=content, role=self.profile, cause_by=RunCode, msg_from=self.profile, msg_to=recipient) self.publish_message(msg) async def _debug_error(self, msg): @@ -146,8 +146,8 @@ class QaEngineer(Role): content=file_info, role=self.profile, cause_by=DebugError, - tx_from=self.profile, - tx_to=recipient, + msg_from=self.profile, + msg_to=recipient, ) self.publish_message(msg) @@ -164,7 +164,7 @@ class QaEngineer(Role): content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, cause_by=WriteTest, - tx_from=self.profile, + msg_from=self.profile, ) return result_msg @@ -188,6 +188,6 @@ class QaEngineer(Role): content=f"Round {self.test_round} of tests done", role=self.profile, cause_by=WriteTest, - tx_from=self.profile, + msg_from=self.profile, ) return result_msg diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 87a03b391..9bbba2070 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -211,14 +211,14 @@ class Role: instruct_content=response.instruct_content, role=self.profile, cause_by=get_object_name(self._rc.todo), - tx_from=get_object_name(self), + msg_from=get_object_name(self), ) else: msg = Message( content=response, role=self.profile, cause_by=get_object_name(self._rc.todo), - tx_from=get_object_name(self), + msg_from=get_object_name(self), ) return msg diff --git a/metagpt/schema.py b/metagpt/schema.py index 0be067cfe..39a62e706 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -77,13 +77,13 @@ class Routes(BaseModel): return False @property - def tx_from(self): + def msg_from(self): """Message route info tells who sent this message.""" route = self._get_route() return route.get(MESSAGE_ROUTE_FROM) @property - def tx_to(self): + def msg_to(self): """Labels for the consumer to filter its subscribed messages.""" route = self._get_route() return route.get(MESSAGE_ROUTE_TO) @@ -112,8 +112,8 @@ class Message(BaseModel): :param instruct_content: Message content struct. :param meta_info: Message meta info. :param route: Message route configuration. - :param tx_from: Message route info tells who sent this message. - :param tx_to: Labels for the consumer to filter its subscribed messages. + :param msg_from: Message route info tells who sent this message. + :param msg_to: Labels for the consumer to filter its subscribed messages. :param cause_by: Labels for the consumer to filter its subscribed messages, also serving as meta info. :param role: Message meta info tells who sent this message. """ @@ -174,14 +174,14 @@ class Message(BaseModel): self.route.replace(old_value, new_value) @property - def tx_from(self): + def msg_from(self): """Message route info tells who sent this message.""" - return self.route.tx_from + return self.route.msg_from @property - def tx_to(self): + def msg_to(self): """Labels for the consumer to filter its subscribed messages.""" - return self.route.tx_to + return self.route.msg_to def set_role(self, v): """Set the message's meta info indicating the sender.""" diff --git a/metagpt/software_company.py b/metagpt/software_company.py index 354773444..1b6936870 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -53,7 +53,7 @@ class SoftwareCompany(BaseModel): role="BOSS", content=idea, cause_by=BossRequirement, - tx_from=SoftwareCompany, + msg_from=SoftwareCompany, ) ) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 7794c9b57..69386e28c 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -69,7 +69,7 @@ async def test_react(): env = Environment() env.add_role(role) assert env.get_subscribed_tags(role) == {seed.subscription} - env.publish_message(Message(content="test", tx_to=seed.subscription)) + env.publish_message(Message(content="test", msg_to=seed.subscription)) assert not role.is_idle while not env.is_idle: await env.run() diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index e18ebbe79..5ebc7ce1d 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -66,13 +66,13 @@ def test_message(): def test_routes(): route = Routes() route.set_from("a") - assert route.tx_from == "a" + assert route.msg_from == "a" route.add_to("b") - assert route.tx_to == {"b"} + assert route.msg_to == {"b"} route.add_to("c") - assert route.tx_to == {"b", "c"} + assert route.msg_to == {"b", "c"} route.set_to({"e", "f"}) - assert route.tx_to == {"e", "f"} + assert route.msg_to == {"e", "f"} assert route.is_recipient({"e"}) assert route.is_recipient({"f"}) assert not route.is_recipient({"a"}) From c496b6b5f604cfa46e239b360bf6a2a743114536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 6 Nov 2023 22:38:43 +0800 Subject: [PATCH 057/115] feat: add default subscriptions to all Role --- metagpt/const.py | 1 + metagpt/roles/role.py | 4 ++++ metagpt/schema.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/metagpt/const.py b/metagpt/const.py index 7b8203bce..2ba875543 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -48,3 +48,4 @@ MESSAGE_ROUTE_FROM = "msg_from" MESSAGE_ROUTE_TO = "msg_to" MESSAGE_ROUTE_CAUSE_BY = "cause_by" MESSAGE_META_ROLE = "role" +MESSAGE_ROUTE_TO_ALL = "" diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 9bbba2070..6e8c5e421 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -135,6 +135,10 @@ class Role: def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" tags = {get_class_name(t) for t in actions} + # Add default subscription tags for developers' direct use. + if self.name: + tags.add(self.name) + tags.add(get_object_name(self)) self.subscribe(tags) def subscribe(self, tags: Set[str]): diff --git a/metagpt/schema.py b/metagpt/schema.py index 39a62e706..fb8885614 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -22,6 +22,7 @@ from metagpt.const import ( MESSAGE_ROUTE_CAUSE_BY, MESSAGE_ROUTE_FROM, MESSAGE_ROUTE_TO, + MESSAGE_ROUTE_TO_ALL, ) from metagpt.logs import logger from metagpt.utils.common import any_to_str @@ -71,6 +72,8 @@ class Routes(BaseModel): if not to_tags: return True + if MESSAGE_ROUTE_TO_ALL in to_tags: + return True for k in tags: if k in to_tags: return True From a045f73fec38536441a53f27f948ec5b9f1a5594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 6 Nov 2023 23:13:58 +0800 Subject: [PATCH 058/115] feat: Support more versatile parameter formats. --- metagpt/schema.py | 6 +++++- tests/metagpt/test_role.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index fb8885614..e89ac00ea 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -138,7 +138,11 @@ class Message(BaseModel): self.set_cause_by(v) continue if k == MESSAGE_ROUTE_TO: - self.add_to(any_to_str(v)) + if isinstance(v, tuple) or isinstance(v, list) or isinstance(v, set): + for i in v: + self.add_to(any_to_str(i)) + else: + self.add_to(any_to_str(v)) continue self.meta_info[k] = v diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 69386e28c..447de7ee5 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -18,6 +18,7 @@ from metagpt.actions import Action, ActionOutput from metagpt.environment import Environment from metagpt.roles import Role from metagpt.schema import Message +from metagpt.utils.common import get_class_name class MockAction(Action): @@ -84,5 +85,17 @@ async def test_react(): assert env.get_subscribed_tags(role) == {seed.subscription, tag} +@pytest.mark.asyncio +async def test_msg_to(): + m = Message(content="a", msg_to=["a", MockRole, Message]) + assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message)} + + m = Message(content="a", cause_by=MockAction, msg_to={"a", MockRole, Message}) + assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message), get_class_name(MockAction)} + + m = Message(content="a", msg_to=("a", MockRole, Message)) + assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message)} + + if __name__ == "__main__": pytest.main([__file__, "-s"]) From 93ebe8c103388ffbe48119b0600ea0bd4c55b64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 7 Nov 2023 14:12:20 +0800 Subject: [PATCH 059/115] feat: recover `history` --- metagpt/environment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metagpt/environment.py b/metagpt/environment.py index a7e6322ff..75a790714 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -29,6 +29,7 @@ class Environment(BaseModel): roles: dict[str, Role] = Field(default_factory=dict) consumers: dict[Role, Set] = Field(default_factory=dict) + history: str = Field(default="") # For debug class Config: arbitrary_types_allowed = True @@ -67,6 +68,7 @@ class Environment(BaseModel): found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") + self.history += f"\n{message}" # For debug return True From af4c87e1234db2828e3f76a7db17b1ceb7ba81ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 13:42:08 +0800 Subject: [PATCH 060/115] refactor: rename is_recipient --- examples/debate.py | 4 ++-- metagpt/actions/write_code.py | 2 +- metagpt/environment.py | 2 +- metagpt/memory/longterm_memory.py | 4 ++-- metagpt/roles/engineer.py | 2 +- metagpt/roles/qa_engineer.py | 8 ++++---- metagpt/schema.py | 8 ++++---- tests/metagpt/test_schema.py | 8 ++++---- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 77a2ce129..cf0c0124c 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -60,7 +60,7 @@ class Trump(Role): async def _observe(self) -> int: await super()._observe() # accept messages sent (from opponent) to self, disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.is_recipient({self.name})] + self._rc.news = [msg for msg in self._rc.news if msg.contain_any({self.name})] return len(self._rc.news) async def _act(self) -> Message: @@ -103,7 +103,7 @@ class Biden(Role): # accept the very first human instruction (the debate topic) or messages sent (from opponent) to self, # disregard own messages from the last round message_filter = {BossRequirement, self.name} - self._rc.news = [msg for msg in self._rc.news if msg.is_recipient(message_filter)] + self._rc.news = [msg for msg in self._rc.news if msg.contain_any(message_filter)] return len(self._rc.news) async def _act(self) -> Message: diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 8b6451134..f2a4744d9 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -59,7 +59,7 @@ class WriteCode(Action): return message_filter = {WriteDesign} - design = [i for i in context if i.is_recipient(message_filter)][0] + design = [i for i in context if i.contain_any(message_filter)][0] ws_name = CodeParser.parse_str(block="Python package name", text=design.content) ws_path = WORKSPACE_ROOT / ws_name diff --git a/metagpt/environment.py b/metagpt/environment.py index 75a790714..fb564e1ab 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -63,7 +63,7 @@ class Environment(BaseModel): found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 for obj, subscribed_tags in self.consumers.items(): - if message.is_recipient(subscribed_tags): + if message.contain_any(subscribed_tags): obj.put_message(message) found = True if not found: diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index e73ae334e..2a4b604e0 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -3,7 +3,7 @@ """ @Desc : the implement of Long-term memory @Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - 1. Replace code related to message filtering with the `Message.is_recipient` function. + 1. Replace code related to message filtering with the `Message.contain_any` function. """ from metagpt.logs import logger @@ -40,7 +40,7 @@ class LongTermMemory(Memory): def add(self, message: Message): super(LongTermMemory, self).add(message) - if message.is_recipient(self.rc.watch) and not self.msg_from_recover: + if message.contain_any(self.rc.watch) and not self.msg_from_recover: # currently, only add role's watching messages to its memory_storage # and ignore adding messages from recover repeatedly self.memory_storage.add(message) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 8778471cc..882cf89dd 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -233,7 +233,7 @@ class Engineer(Role): # Parse task lists message_filter = {WriteTasks} for message in self._rc.news: - if not message.is_recipient(message_filter): + if not message.contain_any(message_filter): continue self.todos = self.parse_tasks(message) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 05fc5b217..104aa3dfb 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -154,7 +154,7 @@ class QaEngineer(Role): async def _observe(self) -> int: await super()._observe() self._rc.news = [ - msg for msg in self._rc.news if msg.is_recipient({self.profile}) + msg for msg in self._rc.news if msg.contain_any({self.profile}) ] # only relevant msgs count as observed news return len(self._rc.news) @@ -174,13 +174,13 @@ class QaEngineer(Role): for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself - if msg.is_recipient(code_filters): + if msg.contain_any(code_filters): # engineer wrote a code, time to write a test for it await self._write_test(msg) - elif msg.is_recipient(test_filters): + elif msg.contain_any(test_filters): # I wrote or debugged my test code, time to run it await self._run_code(msg) - elif msg.is_recipient(run_filters): + elif msg.contain_any(run_filters): # I ran my test code, time to fix bugs, if any await self._debug_error(msg) self.test_round += 1 diff --git a/metagpt/schema.py b/metagpt/schema.py index e89ac00ea..1b00843a6 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -65,8 +65,8 @@ class Routes(BaseModel): self.routes.append({}) return self.routes[0] - def is_recipient(self, tags: Set) -> bool: - """Check if it is the message recipient.""" + def contain_any(self, tags: Set) -> bool: + """Check if this object contains these tags.""" route = self._get_route() to_tags = route.get(MESSAGE_ROUTE_TO) if not to_tags: @@ -206,9 +206,9 @@ class Message(BaseModel): """Add a subscription label for the recipients.""" self.route.add_to(tag) - def is_recipient(self, tags: Set): + def contain_any(self, tags: Set): """Return true if any input label exists in the message's subscription labels.""" - return self.route.is_recipient(tags) + return self.route.contain_any(tags) def __str__(self): # prefix = '-'.join([self.role, str(self.cause_by)]) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 5ebc7ce1d..05127362b 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -48,7 +48,7 @@ def test_message(): m = Message("a", role="b", cause_by="c", x="d") assert m.content == "a" assert m.role == "b" - assert m.is_recipient({"c"}) + assert m.contain_any({"c"}) assert m.cause_by == "c" assert m.get_meta("x") == "d" @@ -73,9 +73,9 @@ def test_routes(): assert route.msg_to == {"b", "c"} route.set_to({"e", "f"}) assert route.msg_to == {"e", "f"} - assert route.is_recipient({"e"}) - assert route.is_recipient({"f"}) - assert not route.is_recipient({"a"}) + assert route.contain_any({"e"}) + assert route.contain_any({"f"}) + assert not route.contain_any({"a"}) if __name__ == "__main__": From c18bc7c876f062c3427159146d8274d7012979d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 20:27:18 +0800 Subject: [PATCH 061/115] refactor: Simplify the Message class. --- metagpt/const.py | 4 +- metagpt/schema.py | 235 ++++++----------------------------- metagpt/utils/common.py | 11 ++ tests/metagpt/test_schema.py | 28 ++--- 4 files changed, 63 insertions(+), 215 deletions(-) diff --git a/metagpt/const.py b/metagpt/const.py index 2ba875543..fa0ccc536 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -44,8 +44,8 @@ SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills" MEM_TTL = 24 * 30 * 3600 -MESSAGE_ROUTE_FROM = "msg_from" -MESSAGE_ROUTE_TO = "msg_to" +MESSAGE_ROUTE_FROM = "sent_from" +MESSAGE_ROUTE_TO = "send_to" MESSAGE_ROUTE_CAUSE_BY = "cause_by" MESSAGE_META_ROLE = "role" MESSAGE_ROUTE_TO_ALL = "" diff --git a/metagpt/schema.py b/metagpt/schema.py index 1b00843a6..7fdcef2ed 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -13,19 +13,18 @@ import asyncio import json from asyncio import Queue, QueueEmpty, wait_for from json import JSONDecodeError -from typing import Dict, List, Set, TypedDict +from typing import List, Set, TypedDict from pydantic import BaseModel, Field from metagpt.const import ( - MESSAGE_META_ROLE, MESSAGE_ROUTE_CAUSE_BY, MESSAGE_ROUTE_FROM, MESSAGE_ROUTE_TO, MESSAGE_ROUTE_TO_ALL, ) from metagpt.logs import logger -from metagpt.utils.common import any_to_str +from metagpt.utils.common import any_to_str, any_to_str_set class RawMessage(TypedDict): @@ -33,182 +32,56 @@ class RawMessage(TypedDict): role: str -class Routes(BaseModel): - """Responsible for managing routing information for the Message class.""" - - routes: List[Dict] = Field(default_factory=list) - - def set_from(self, value): - """Set the label of the message sender.""" - route = self._get_route() - route[MESSAGE_ROUTE_FROM] = value - - def set_to(self, tags: Set): - """Set the labels of the message recipient.""" - route = self._get_route() - if tags: - route[MESSAGE_ROUTE_TO] = tags - return - - if MESSAGE_ROUTE_TO in route: - del route[MESSAGE_ROUTE_TO] - - def add_to(self, tag: str): - """Add a label of the message recipient.""" - route = self._get_route() - tags = route.get(MESSAGE_ROUTE_TO, set()) - tags.add(tag) - route[MESSAGE_ROUTE_TO] = tags - - def _get_route(self) -> Dict: - if not self.routes: - self.routes.append({}) - return self.routes[0] - - def contain_any(self, tags: Set) -> bool: - """Check if this object contains these tags.""" - route = self._get_route() - to_tags = route.get(MESSAGE_ROUTE_TO) - if not to_tags: - return True - - if MESSAGE_ROUTE_TO_ALL in to_tags: - return True - for k in tags: - if k in to_tags: - return True - return False - - @property - def msg_from(self): - """Message route info tells who sent this message.""" - route = self._get_route() - return route.get(MESSAGE_ROUTE_FROM) - - @property - def msg_to(self): - """Labels for the consumer to filter its subscribed messages.""" - route = self._get_route() - return route.get(MESSAGE_ROUTE_TO) - - def replace(self, old_val, new_val): - """Replace old value with new value""" - route = self._get_route() - tags = route.get(MESSAGE_ROUTE_TO, set()) - tags.discard(old_val) - tags.add(new_val) - route[MESSAGE_ROUTE_TO] = tags - - class Message(BaseModel): """list[: ]""" content: str - instruct_content: BaseModel = None - meta_info: Dict = Field(default_factory=dict) - route: Routes = Field(default_factory=Routes) + instruct_content: BaseModel = Field(default=None) + role: str = "user" # system / user / assistant + cause_by: str = "" + sent_from: str = "" + send_to: Set = Field(default_factory=set) - def __init__(self, content, **kwargs): + def __init__( + self, + content, + instruct_content=None, + role="user", + cause_by="", + sent_from="", + send_to=MESSAGE_ROUTE_TO_ALL, + **kwargs, + ): """ Parameters not listed below will be stored as meta info, including custom parameters. :param content: Message content. :param instruct_content: Message content struct. - :param meta_info: Message meta info. - :param route: Message route configuration. - :param msg_from: Message route info tells who sent this message. - :param msg_to: Labels for the consumer to filter its subscribed messages. - :param cause_by: Labels for the consumer to filter its subscribed messages, also serving as meta info. + :param cause_by: Message producer + :param sent_from: Message route info tells who sent this message. + :param send_to: Labels for the consumer to filter its subscribed messages. :param role: Message meta info tells who sent this message. """ - super(Message, self).__init__( - content=content or kwargs.get("content"), - instruct_content=kwargs.get("instruct_content"), - meta_info=kwargs.get("meta_info", {}), - route=kwargs.get("route", Routes()), + super().__init__( + content=content, + instruct_content=instruct_content, + role=role, + cause_by=any_to_str(cause_by), + sent_from=any_to_str(sent_from), + send_to=any_to_str_set(send_to), + **kwargs, ) - attribute_names = Message.__annotations__.keys() - for k, v in kwargs.items(): - if k in attribute_names: - continue - if k == MESSAGE_ROUTE_FROM: - self.set_from(any_to_str(v)) - continue - if k == MESSAGE_ROUTE_CAUSE_BY: - self.set_cause_by(v) - continue - if k == MESSAGE_ROUTE_TO: - if isinstance(v, tuple) or isinstance(v, list) or isinstance(v, set): - for i in v: - self.add_to(any_to_str(i)) - else: - self.add_to(any_to_str(v)) - continue - self.meta_info[k] = v - - def get_meta(self, key): - """Get meta info""" - return self.meta_info.get(key) - - def set_meta(self, key, value): - """Set meta info""" - self.meta_info[key] = value - - @property - def role(self): - """Message meta info tells who sent this message.""" - return self.get_meta(MESSAGE_META_ROLE) - - @property - def cause_by(self): - """Labels for the consumer to filter its subscribed messages, also serving as meta info.""" - return self.get_meta(MESSAGE_ROUTE_CAUSE_BY) - def __setattr__(self, key, val): - """Override `@property.setter`""" + """Override `@property.setter`, convert non-string parameters into string parameters.""" if key == MESSAGE_ROUTE_CAUSE_BY: - self.set_cause_by(val) - return - if key == MESSAGE_ROUTE_FROM: - self.set_from(any_to_str(val)) - super().__setattr__(key, val) - - def set_cause_by(self, val): - """Update the value of `cause_by` in the `meta_info` and `routes` attributes.""" - old_value = self.get_meta(MESSAGE_ROUTE_CAUSE_BY) - new_value = any_to_str(val) - self.set_meta(MESSAGE_ROUTE_CAUSE_BY, new_value) - self.route.replace(old_value, new_value) - - @property - def msg_from(self): - """Message route info tells who sent this message.""" - return self.route.msg_from - - @property - def msg_to(self): - """Labels for the consumer to filter its subscribed messages.""" - return self.route.msg_to - - def set_role(self, v): - """Set the message's meta info indicating the sender.""" - self.set_meta(MESSAGE_META_ROLE, v) - - def set_from(self, v): - """Set the message's meta info indicating the sender.""" - self.route.set_from(v) - - def set_to(self, tags: Set): - """Set the message's meta info indicating the sender.""" - self.route.set_to(tags) - - def add_to(self, tag: str): - """Add a subscription label for the recipients.""" - self.route.add_to(tag) - - def contain_any(self, tags: Set): - """Return true if any input label exists in the message's subscription labels.""" - return self.route.contain_any(tags) + new_val = any_to_str(val) + elif key == MESSAGE_ROUTE_FROM: + new_val = any_to_str(val) + elif key == MESSAGE_ROUTE_TO: + new_val = any_to_str_set(val) + else: + new_val = val + super().__setattr__(key, new_val) def __str__(self): # prefix = '-'.join([self.role, str(self.cause_by)]) @@ -226,13 +99,13 @@ class Message(BaseModel): return self.json(exclude_none=True) @staticmethod - def load(v): + def load(val): """Convert the json string to object.""" try: - d = json.loads(v) + d = json.loads(val) return Message(**d) except JSONDecodeError as err: - logger.error(f"parse json failed: {v}, error:{err}") + logger.error(f"parse json failed: {val}, error:{err}") return None @@ -327,31 +200,3 @@ class MessageQueue: logger.warning(f"JSON load failed: {v}, error:{e}") return q - - -if __name__ == "__main__": - m = Message("a", role="v1") - m.set_role("v2") - v = m.dump() - m = Message.load(v) - m.cause_by = "Message" - m.cause_by = Routes - m.cause_by = Routes() - m.content = "b" - - test_content = "test_message" - msgs = [ - UserMessage(test_content), - SystemMessage(test_content), - AIMessage(test_content), - Message(test_content, role="QA"), - ] - logger.info(msgs) - - jsons = [ - UserMessage(test_content).dump(), - SystemMessage(test_content).dump(), - AIMessage(test_content).dump(), - Message(test_content, role="QA").dump(), - ] - logger.info(jsons) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index b372f0d8d..cd42b1412 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -325,3 +325,14 @@ def any_to_str(val) -> str: return get_object_name(val) return get_class_name(val) + + +def any_to_str_set(val) -> set: + """Convert any type to string set.""" + res = set() + if isinstance(val, dict) or isinstance(val, list) or isinstance(val, set) or isinstance(val, tuple): + for i in val: + res.add(any_to_str(i)) + else: + res.add(any_to_str(val)) + return res diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 05127362b..51ebd5baa 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -12,7 +12,7 @@ import json import pytest from metagpt.actions import Action -from metagpt.schema import AIMessage, Message, Routes, SystemMessage, UserMessage +from metagpt.schema import AIMessage, Message, SystemMessage, UserMessage from metagpt.utils.common import get_class_name @@ -37,20 +37,19 @@ def test_message(): d = json.loads(v) assert d assert d.get("content") == "a" - assert d.get("meta_info") == {"role": "v1"} - m.set_role("v2") + assert d.get("role") == "v1" + m.role = "v2" v = m.dump() assert v m = Message.load(v) assert m.content == "a" assert m.role == "v2" - m = Message("a", role="b", cause_by="c", x="d") + m = Message("a", role="b", cause_by="c", x="d", send_to="c") assert m.content == "a" assert m.role == "b" - assert m.contain_any({"c"}) + assert m.send_to == {"c"} assert m.cause_by == "c" - assert m.get_meta("x") == "d" m.cause_by = "Message" assert m.cause_by == "Message" @@ -64,18 +63,11 @@ def test_message(): @pytest.mark.asyncio def test_routes(): - route = Routes() - route.set_from("a") - assert route.msg_from == "a" - route.add_to("b") - assert route.msg_to == {"b"} - route.add_to("c") - assert route.msg_to == {"b", "c"} - route.set_to({"e", "f"}) - assert route.msg_to == {"e", "f"} - assert route.contain_any({"e"}) - assert route.contain_any({"f"}) - assert not route.contain_any({"a"}) + m = Message("a", role="b", cause_by="c", x="d", send_to="c") + m.send_to = "b" + assert m.send_to == {"b"} + m.send_to = {"e", Action} + assert m.send_to == {"e", get_class_name(Action)} if __name__ == "__main__": From 09fe4593f6621aa4689f9d4697711a1bc9851de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 20:36:42 +0800 Subject: [PATCH 062/115] refactor: According to RFC 116: Updated the type of index key. --- metagpt/memory/memory.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index c6b732076..7f04be63d 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -26,10 +26,8 @@ class Memory: if message in self.storage: return self.storage.append(message) - # According to the design of RFC 116, it allows message filtering based on different labels, thus - # necessitating the creation of separate indices for each label. - for k in message.msg_to: - self.index[k].append(message) + if message.cause_by: + self.index[message.cause_by].append(message) def add_batch(self, messages: Iterable[Message]): for message in messages: From e5c792e51277677705d46716ad71bbe074e284cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 20:36:50 +0800 Subject: [PATCH 063/115] refactor: According to RFC 116: Updated the type of index key. --- metagpt/memory/memory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 7f04be63d..84289091f 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -4,8 +4,7 @@ @Time : 2023/5/20 12:15 @Author : alexanderwu @File : memory.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - Updated the message filtering logic. +@Modified By: mashenquan, 2023-11-1. According to RFC 116: Updated the type of index key. """ from collections import defaultdict from typing import Iterable, Set From 47d47d274e5d0d7e806d387db169019df7d961ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 20:44:39 +0800 Subject: [PATCH 064/115] refactor: According to RFC 113, add message dispatching functionality. --- metagpt/environment.py | 3 ++- metagpt/utils/common.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index fb564e1ab..81b5c2ac7 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, Field from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message +from metagpt.utils.common import is_subscribed class Environment(BaseModel): @@ -63,7 +64,7 @@ class Environment(BaseModel): found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 for obj, subscribed_tags in self.consumers.items(): - if message.contain_any(subscribed_tags): + if is_subscribed(message, subscribed_tags): obj.put_message(message) found = True if not found: diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index cd42b1412..798acf214 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -15,6 +15,7 @@ import platform import re from typing import List, Tuple, Union +from metagpt.const import MESSAGE_ROUTE_TO_ALL from metagpt.logs import logger @@ -336,3 +337,14 @@ def any_to_str_set(val) -> set: else: res.add(any_to_str(val)) return res + + +def is_subscribed(message, tags): + """Return whether it's consumer""" + if MESSAGE_ROUTE_TO_ALL in message.send_to: + return True + + for t in tags: + if t in message.send_to: + return True + return False From 7e83a4bb3159279855a5eca98f97efdbf468acfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 20:56:05 +0800 Subject: [PATCH 065/115] refactor: According to RFC 116: Updated the type of send_to. --- examples/debate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index cf0c0124c..7b03f785b 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -60,7 +60,7 @@ class Trump(Role): async def _observe(self) -> int: await super()._observe() # accept messages sent (from opponent) to self, disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.contain_any({self.name})] + self._rc.news = [msg for msg in self._rc.news if msg.send_to == {self.name}] return len(self._rc.news) async def _act(self) -> Message: @@ -78,8 +78,8 @@ class Trump(Role): content=rsp, role=self.profile, cause_by=ShoutOut, - msg_from=self.name, - msg_to=self.opponent_name, + sent_from=self.name, + send_to=self.opponent_name, ) return msg @@ -102,8 +102,7 @@ class Biden(Role): await super()._observe() # accept the very first human instruction (the debate topic) or messages sent (from opponent) to self, # disregard own messages from the last round - message_filter = {BossRequirement, self.name} - self._rc.news = [msg for msg in self._rc.news if msg.contain_any(message_filter)] + self._rc.news = [msg for msg in self._rc.news if msg.cause_by == BossRequirement or msg.send_to == {self.name}] return len(self._rc.news) async def _act(self) -> Message: @@ -121,8 +120,8 @@ class Biden(Role): content=rsp, role=self.profile, cause_by=ShoutOut, - msg_from=self.name, - msg_to=self.opponent_name, + sent_from=self.name, + send_to=self.opponent_name, ) return msg From 01f23d633ee2e688e7d423b448af810e6f9f1161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 21:38:52 +0800 Subject: [PATCH 066/115] refactor: In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `send_to` value of the `Message` object. --- examples/debate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 7b03f785b..87ac7050f 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -2,9 +2,8 @@ Filename: MetaGPT/examples/debate.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message filtering. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `send_to` + value of the `Message` object. """ import asyncio import platform From c502b1403a6c2fb2e15f25040528873abb1eb869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 21:43:17 +0800 Subject: [PATCH 067/115] refactor: In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `cause_by` value of the `Message` object. --- metagpt/actions/write_code.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index f2a4744d9..be8690314 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -4,8 +4,8 @@ @Time : 2023/5/11 17:45 @Author : alexanderwu @File : write_code.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of - the `cause_by` value in the `Message` to a string to support the new message distribution feature. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `cause_by` + value of the `Message` object. """ from tenacity import retry, stop_after_attempt, wait_fixed @@ -14,7 +14,7 @@ from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message -from metagpt.utils.common import CodeParser +from metagpt.utils.common import CodeParser, get_class_name PROMPT_TEMPLATE = """ NOTICE @@ -58,8 +58,7 @@ class WriteCode(Action): if self._is_invalid(filename): return - message_filter = {WriteDesign} - design = [i for i in context if i.contain_any(message_filter)][0] + design = [i for i in context if i.cause_by == get_class_name(WriteDesign)][0] ws_name = CodeParser.parse_str(block="Python package name", text=design.content) ws_path = WORKSPACE_ROOT / ws_name From d9939f437ad10f3b87959fa615ad4168f0f4e1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 21:57:58 +0800 Subject: [PATCH 068/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 6e8c5e421..ac8a2d702 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -116,6 +116,9 @@ class Role: self._actions = [] self._role_id = str(self._setting) self._rc = RoleContext() + self._subscription = {get_object_name(self)} + if name: + self._subscription.add(name) def _reset(self): self._states = [] @@ -133,21 +136,15 @@ class Role: self._states.append(f"{idx}. {action}") def _watch(self, actions: Iterable[Type[Action]]): - """Listen to the corresponding behaviors""" + """Listen to the corresponding behaviors in private message buffer""" tags = {get_class_name(t) for t in actions} - # Add default subscription tags for developers' direct use. - if self.name: - tags.add(self.name) - tags.add(get_object_name(self)) - self.subscribe(tags) + self._rc.watch.update(tags) def subscribe(self, tags: Set[str]): """Listen to the corresponding behaviors""" - self._rc.watch.update(tags) - # check RoleContext after adding watch actions - self._rc.check(self._role_id) + self._subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - self._rc.env.set_subscribed_tags(self, self.subscribed_tags) + self._rc.env.set_subscribed_tags(self, self._subscription) def _set_state(self, state): """Update the current state.""" @@ -159,6 +156,8 @@ class Role: """Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing.""" self._rc.env = env + if env: + env.set_subscribed_tags(self, self._subscription) @property def profile(self): From d232725a2991a2e43da9e07094d0cbc23b7a6a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:01:55 +0800 Subject: [PATCH 069/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index ac8a2d702..5bc241352 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -170,17 +170,9 @@ class Role: return self._setting.name @property - def subscribed_tags(self) -> Set: + def subscription(self) -> Set: """The labels for messages to be consumed by the Role object.""" - if self._rc.watch: - return self._rc.watch - return { - self.name, - get_object_name(self), - self.profile, - f"{self.name}({self.profile})", - f"{self.name}({get_object_name(self)})", - } + return self._subscription def _get_prefix(self): """Get the role prefix""" From f47977daa81357b552cd0e791dc798826b45bf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:03:06 +0800 Subject: [PATCH 070/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 5bc241352..32fa16e6a 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -139,6 +139,8 @@ class Role: """Listen to the corresponding behaviors in private message buffer""" tags = {get_class_name(t) for t in actions} self._rc.watch.update(tags) + # check RoleContext after adding watch actions + self._rc.check(self._role_id) def subscribe(self, tags: Set[str]): """Listen to the corresponding behaviors""" From 9cdbc0a0ae93c8295756b191faa05972075e9015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:06:46 +0800 Subject: [PATCH 071/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 32fa16e6a..5f54e57e0 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -208,15 +208,9 @@ class Role: instruct_content=response.instruct_content, role=self.profile, cause_by=get_object_name(self._rc.todo), - msg_from=get_object_name(self), ) else: - msg = Message( - content=response, - role=self.profile, - cause_by=get_object_name(self._rc.todo), - msg_from=get_object_name(self), - ) + msg = Message(content=response, role=self.profile, cause_by=get_object_name(self._rc.todo)) return msg From d1977f15864bb2f8386766f184ddf14daf856325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:22:09 +0800 Subject: [PATCH 072/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 5f54e57e0..59342fa99 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -146,7 +146,7 @@ class Role: """Listen to the corresponding behaviors""" self._subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - self._rc.env.set_subscribed_tags(self, self._subscription) + self._rc.env.set_subscription(self, self._subscription) def _set_state(self, state): """Update the current state.""" From 7a2193c3d26aee1cc4c9aa9ed2c7702305770bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:22:44 +0800 Subject: [PATCH 073/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 59342fa99..f3e11b294 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -159,7 +159,7 @@ class Role: messages by observing.""" self._rc.env = env if env: - env.set_subscribed_tags(self, self._subscription) + env.set_subscription(self, self._subscription) @property def profile(self): From 1ff99b95acaecc95c35dda8f5cbad5d0e421dc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:51:12 +0800 Subject: [PATCH 074/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/environment.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index 81b5c2ac7..e9a5c6467 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -41,8 +41,6 @@ class Environment(BaseModel): """ role.set_env(self) self.roles[role.profile] = role - # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - self.set_subscribed_tags(role, role.subscribed_tags) def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 @@ -63,8 +61,8 @@ class Environment(BaseModel): logger.info(f"publish_message: {message.dump()}") found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - for obj, subscribed_tags in self.consumers.items(): - if is_subscribed(message, subscribed_tags): + for obj, subscription in self.consumers.items(): + if is_subscribed(message, subscription): obj.put_message(message) found = True if not found: @@ -106,10 +104,10 @@ class Environment(BaseModel): return False return True - def get_subscribed_tags(self, obj): + def get_subscription(self, obj): """Get the labels for messages to be consumed by the object.""" return self.consumers.get(obj, {}) - def set_subscribed_tags(self, obj, tags): + def set_subscription(self, obj, tags): """Set the labels for message to be consumed by the object""" self.consumers[obj] = tags From c9f9c5c73e4386f4ea26dd49c65276c7e2d6eaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 22:57:53 +0800 Subject: [PATCH 075/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/memory/longterm_memory.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index 2a4b604e0..6e23a79ae 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -40,10 +40,11 @@ class LongTermMemory(Memory): def add(self, message: Message): super(LongTermMemory, self).add(message) - if message.contain_any(self.rc.watch) and not self.msg_from_recover: - # currently, only add role's watching messages to its memory_storage - # and ignore adding messages from recover repeatedly - self.memory_storage.add(message) + for action in self.rc.watch: + if message.cause_by == action and not self.msg_from_recover: + # currently, only add role's watching messages to its memory_storage + # and ignore adding messages from recover repeatedly + self.memory_storage.add(message) def find_news(self, observed: list[Message], k=0) -> list[Message]: """ From 894a2fd593734c7e8611b0033304884dac6d9397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 23:36:56 +0800 Subject: [PATCH 076/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/memory/longterm_memory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index 6e23a79ae..6fc8050ef 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ @Desc : the implement of Long-term memory -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - 1. Replace code related to message filtering with the `Message.contain_any` function. """ from metagpt.logs import logger From be19d9edcb88e6aa0ca2b8b7980f20191a903359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 23:49:47 +0800 Subject: [PATCH 077/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/engineer.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 882cf89dd..03519e0ef 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -21,7 +21,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, get_object_name +from metagpt.utils.common import CodeParser, get_class_name, get_object_name from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -170,7 +170,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - msg_to="QaEngineer", + send_to="QaEngineer", ) return msg @@ -185,8 +185,7 @@ class Engineer(Role): TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ context = [] - msg_filters = [WriteDesign, WriteTasks, WriteCode] - msg = self._rc.memory.get_by_actions(msg_filters) + msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) for m in msg: context.append(m.content) context_str = "\n".join(context) @@ -213,7 +212,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - msg_to="QaEngineer", + send_to="QaEngineer", ) return msg @@ -231,9 +230,8 @@ class Engineer(Role): return ret # Parse task lists - message_filter = {WriteTasks} for message in self._rc.news: - if not message.contain_any(message_filter): + if not message.cause_by == get_class_name(WriteTasks): continue self.todos = self.parse_tasks(message) From fba70452f3c8aba12699eb2cbf7e5a62f7655ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 23:51:28 +0800 Subject: [PATCH 078/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/engineer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 03519e0ef..000e81873 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -239,7 +239,7 @@ class Engineer(Role): async def _think(self) -> None: # In asynchronous scenarios, first check if the required messages are ready. - filters = {WriteTasks} + filters = {get_class_name(WriteTasks)} msgs = self._rc.memory.get_by_actions(filters) if not msgs: self._rc.todo = None From ac32cb5a67934c117108ebdde6d718b2206c51f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 8 Nov 2023 23:54:03 +0800 Subject: [PATCH 079/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/engineer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 000e81873..423fff68e 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -19,7 +19,7 @@ from pathlib import Path from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger -from metagpt.roles import Role +from metagpt.roles import QaEngineer, Role from metagpt.schema import Message from metagpt.utils.common import CodeParser, get_class_name, get_object_name from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -170,7 +170,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - send_to="QaEngineer", + send_to=QaEngineer, ) return msg @@ -212,7 +212,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - send_to="QaEngineer", + send_to=QaEngineer, ) return msg From df4ff5f701daf29b802381efe3a00e7080a3e447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 00:01:51 +0800 Subject: [PATCH 080/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/roles/qa_engineer.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 104aa3dfb..760b65736 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -22,7 +22,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, parse_recipient +from metagpt.utils.common import CodeParser, any_to_str_set, parse_recipient from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -100,8 +100,8 @@ class QaEngineer(Role): content=str(file_info), role=self.profile, cause_by=WriteTest, - msg_from=self.profile, - msg_to=self.profile, + sent_from=self.profile, + send_to=self.profile, ) self.publish_message(msg) @@ -133,7 +133,7 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message(content=content, role=self.profile, cause_by=RunCode, msg_from=self.profile, msg_to=recipient) + msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient) self.publish_message(msg) async def _debug_error(self, msg): @@ -146,15 +146,15 @@ class QaEngineer(Role): content=file_info, role=self.profile, cause_by=DebugError, - msg_from=self.profile, - msg_to=recipient, + sent_from=self.profile, + send_to=recipient, ) self.publish_message(msg) async def _observe(self) -> int: await super()._observe() self._rc.news = [ - msg for msg in self._rc.news if msg.contain_any({self.profile}) + msg for msg in self._rc.news if self.profile in msg.send_to ] # only relevant msgs count as observed news return len(self._rc.news) @@ -164,23 +164,23 @@ class QaEngineer(Role): content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, cause_by=WriteTest, - msg_from=self.profile, + sent_from=self.profile, ) return result_msg - code_filters = {WriteCode, WriteCodeReview} - test_filters = {WriteTest, DebugError} - run_filters = {RunCode} + code_filters = any_to_str_set({WriteCode, WriteCodeReview}) + test_filters = any_to_str_set({WriteTest, DebugError}) + run_filters = any_to_str_set({RunCode}) for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself - if msg.contain_any(code_filters): + if msg.cause_by in code_filters: # engineer wrote a code, time to write a test for it await self._write_test(msg) - elif msg.contain_any(test_filters): + elif msg.cause_by in test_filters: # I wrote or debugged my test code, time to run it await self._run_code(msg) - elif msg.contain_any(run_filters): + elif msg.cause_by in run_filters: # I ran my test code, time to fix bugs, if any await self._debug_error(msg) self.test_round += 1 @@ -188,6 +188,6 @@ class QaEngineer(Role): content=f"Round {self.test_round} of tests done", role=self.profile, cause_by=WriteTest, - msg_from=self.profile, + sent_from=self.profile, ) return result_msg From c4ac0c72d7053a67e1c0679fa79582862328507c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 00:41:29 +0800 Subject: [PATCH 081/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/software_company.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index 1b6936870..d12998242 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -48,14 +48,7 @@ class SoftwareCompany(BaseModel): def start_project(self, idea): """Start a project from publishing boss requirement.""" self.idea = idea - self.environment.publish_message( - Message( - role="BOSS", - content=idea, - cause_by=BossRequirement, - msg_from=SoftwareCompany, - ) - ) + self.environment.publish_message(Message(role="BOSS", content=idea, cause_by=BossRequirement)) def _save(self): logger.info(self.json()) @@ -68,3 +61,4 @@ class SoftwareCompany(BaseModel): logger.debug(f"{n_round=}") self._check_balance() await self.environment.run() + return self.environment.history From bb050142f708557785e80ea16eff5ca3b17aed56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 00:42:52 +0800 Subject: [PATCH 082/115] refactor: Update according to Chapter 2.1.3.2 of RFC 116 --- metagpt/software_company.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index d12998242..d3c2c463b 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -4,8 +4,6 @@ @Time : 2023/5/12 00:30 @Author : alexanderwu @File : software_company.py -@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116: - 1. Abandon the design of having `Environment` store all messages. """ from pydantic import BaseModel, Field From 7504ed57570152aaa23f196ef9945fa5f552ee98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 10:02:26 +0800 Subject: [PATCH 083/115] fixbug: recursive import --- metagpt/roles/engineer.py | 6 +++--- metagpt/schema.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 423fff68e..ba622429b 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -19,7 +19,7 @@ from pathlib import Path from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger -from metagpt.roles import QaEngineer, Role +from metagpt.roles import Role from metagpt.schema import Message from metagpt.utils.common import CodeParser, get_class_name, get_object_name from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -170,7 +170,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - send_to=QaEngineer, + send_to="Edward", ) return msg @@ -212,7 +212,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=get_object_name(self._rc.todo), - send_to=QaEngineer, + send_to="Edward", ) return msg diff --git a/metagpt/schema.py b/metagpt/schema.py index 7fdcef2ed..63fe41232 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -40,7 +40,7 @@ class Message(BaseModel): role: str = "user" # system / user / assistant cause_by: str = "" sent_from: str = "" - send_to: Set = Field(default_factory=set) + send_to: Set = Field(default_factory={MESSAGE_ROUTE_TO_ALL}) def __init__( self, From ea9875a7fc63b79181983bc8821cd1ab28be20dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 11:14:05 +0800 Subject: [PATCH 084/115] fixbug: recursive import --- examples/debate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 87ac7050f..8f5012d66 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -3,7 +3,7 @@ Filename: MetaGPT/examples/debate.py Created Date: Tuesday, September 19th 2023, 6:52:25 pm Author: garylin2099 @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `send_to` - value of the `Message` object. + value of the `Message` object; modify the argument type of `get_by_actions`. """ import asyncio import platform @@ -15,6 +15,7 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.software_company import SoftwareCompany +from metagpt.utils.common import any_to_str_set class ShoutOut(Action): @@ -65,7 +66,7 @@ class Trump(Role): async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([ShoutOut]) + msg_history = self._rc.memory.get_by_actions(any_to_str_set([ShoutOut])) context = [] for m in msg_history: context.append(str(m)) @@ -107,7 +108,7 @@ class Biden(Role): async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut]) + msg_history = self._rc.memory.get_by_actions(any_to_str_set([BossRequirement, ShoutOut])) context = [] for m in msg_history: context.append(str(m)) From ff11cf69afa2872673a42d0910a591a3400a200a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 11:21:20 +0800 Subject: [PATCH 085/115] fixbug: utilize the new message filtering feature --- metagpt/roles/engineer.py | 13 +++++++++---- metagpt/roles/qa_engineer.py | 9 +++++++-- metagpt/roles/role.py | 6 ++++-- requirements.txt | 2 +- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index ba622429b..62e1e92d2 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -21,7 +21,12 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, get_class_name, get_object_name +from metagpt.utils.common import ( + CodeParser, + any_to_str_set, + get_class_name, + get_object_name, +) from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -102,7 +107,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + msg = self._rc.memory.get_by_action(get_class_name(WriteDesign))[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -130,7 +135,7 @@ class Engineer(Role): todo_coros = [] for todo in self.todos: todo_coro = WriteCode().run( - context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), + context=self._rc.memory.get_by_actions(any_to_str_set([WriteTasks, WriteDesign])), filename=todo, ) todo_coros.append(todo_coro) @@ -185,7 +190,7 @@ class Engineer(Role): TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ context = [] - msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) + msg = self._rc.memory.get_by_actions(any_to_str_set([WriteDesign, WriteTasks, WriteCode])) for m in msg: context.append(m.content) context_str = "\n".join(context) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 760b65736..38fb5a24b 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -22,7 +22,12 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, any_to_str_set, parse_recipient +from metagpt.utils.common import ( + CodeParser, + any_to_str_set, + get_class_name, + parse_recipient, +) from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -50,7 +55,7 @@ class QaEngineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + msg = self._rc.memory.get_by_action(get_class_name(WriteDesign))[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index f3e11b294..b8be309bb 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -217,9 +217,11 @@ class Role: async def _observe(self) -> int: """Prepare new messages for processing from the message buffer and other sources.""" # Read unprocessed messages from the msg buffer. - self._rc.news = self._rc.msg_buffer.pop_all() + news = self._rc.msg_buffer.pop_all() # Store the read messages in your own memory to prevent duplicate processing. - self._rc.memory.add_batch(self._rc.news) + self._rc.memory.add_batch(news) + # Filter out messages of interest. + self._rc.news = [n for n in news if n.cause_by in self._rc.watch] # Design Rules: # If you need to further categorize Message objects, you can do so using the Message.set_meta function. diff --git a/requirements.txt b/requirements.txt index 24a2d94c3..c3b909e77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ langchain==0.0.231 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 -openai +openai==0.28.1 openpyxl beautifulsoup4==4.12.2 pandas==2.0.3 From 1be1bb56e3558a257e331e759cd71aa0a7b755eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 9 Nov 2023 11:52:34 +0800 Subject: [PATCH 086/115] fixbug: utilize the new message filtering feature --- metagpt/roles/engineer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 62e1e92d2..a108fa4f1 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -239,8 +239,9 @@ class Engineer(Role): if not message.cause_by == get_class_name(WriteTasks): continue self.todos = self.parse_tasks(message) + return 1 - return ret + return 0 async def _think(self) -> None: # In asynchronous scenarios, first check if the required messages are ready. From a3cb2b4fdcaa47f48704cbfead054b89f79cd3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 15:27:27 +0800 Subject: [PATCH 087/115] feat: replace get_class_name and get_object_name --- metagpt/roles/engineer.py | 23 +++++++++-------------- metagpt/roles/researcher.py | 10 ++++------ metagpt/roles/role.py | 10 +++++----- metagpt/roles/seacher.py | 6 +++--- metagpt/roles/sk_agent.py | 4 ++-- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index a108fa4f1..70dce41b1 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -21,12 +21,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import ( - CodeParser, - any_to_str_set, - get_class_name, - get_object_name, -) +from metagpt.utils.common import CodeParser, any_to_str, any_to_str_set from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -107,7 +102,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(get_class_name(WriteDesign))[-1] + msg = self._rc.memory.get_by_action(any_to_str(WriteDesign))[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -146,13 +141,13 @@ class Engineer(Role): logger.info(todo) logger.info(code_rsp) # self.write_file(todo, code) - msg = Message(content=code_rsp, role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content=code_rsp, role=self.profile, cause_by=any_to_str(self._rc.todo)) self._rc.memory.add(msg) self.publish_message(msg) del self.todos[0] logger.info(f"Done {self.get_workspace()} generating.") - msg = Message(content="all done.", role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content="all done.", role=self.profile, cause_by=any_to_str(self._rc.todo)) return msg async def _act_sp(self) -> Message: @@ -163,7 +158,7 @@ class Engineer(Role): # logger.info(code_rsp) # code = self.parse_code(code_rsp) file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content=code, role=self.profile, cause_by=any_to_str(self._rc.todo)) self._rc.memory.add(msg) self.publish_message(msg) @@ -174,7 +169,7 @@ class Engineer(Role): msg = Message( content=MSG_SEP.join(code_msg_all), role=self.profile, - cause_by=get_object_name(self._rc.todo), + cause_by=any_to_str(self._rc.todo), send_to="Edward", ) return msg @@ -216,7 +211,7 @@ class Engineer(Role): msg = Message( content=MSG_SEP.join(code_msg_all), role=self.profile, - cause_by=get_object_name(self._rc.todo), + cause_by=any_to_str(self._rc.todo), send_to="Edward", ) return msg @@ -236,7 +231,7 @@ class Engineer(Role): # Parse task lists for message in self._rc.news: - if not message.cause_by == get_class_name(WriteTasks): + if not message.cause_by == any_to_str(WriteTasks): continue self.todos = self.parse_tasks(message) return 1 @@ -245,7 +240,7 @@ class Engineer(Role): async def _think(self) -> None: # In asynchronous scenarios, first check if the required messages are ready. - filters = {get_class_name(WriteTasks)} + filters = {any_to_str(WriteTasks)} msgs = self._rc.memory.get_by_actions(filters) if not msgs: self._rc.todo = None diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 4ec6f31e1..8d5e43fab 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -15,7 +15,7 @@ from metagpt.const import RESEARCH_PATH from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import get_object_name +from metagpt.utils.common import any_to_str class Report(BaseModel): @@ -64,21 +64,19 @@ class Researcher(Role): research_system_text = get_research_system_text(topic, self.language) if isinstance(todo, CollectLinks): links = await todo.run(topic, 4, 4) - ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=get_object_name(todo)) + ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=any_to_str(todo)) elif isinstance(todo, WebBrowseAndSummarize): links = instruct_content.links todos = (todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items()) summaries = await asyncio.gather(*todos) summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary) - ret = Message( - "", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=get_object_name(todo) - ) + ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=any_to_str(todo)) else: summaries = instruct_content.summaries summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries) content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text) ret = Message( - "", Report(topic=topic, content=content), role=self.profile, cause_by=get_object_name(self._rc.todo) + "", Report(topic=topic, content=content), role=self.profile, cause_by=any_to_str(self._rc.todo) ) self._rc.memory.add(ret) return ret diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b8be309bb..90e85186b 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -30,7 +30,7 @@ from metagpt.llm import LLM from metagpt.logs import logger from metagpt.memory import LongTermMemory, Memory from metagpt.schema import Message, MessageQueue -from metagpt.utils.common import get_class_name, get_object_name +from metagpt.utils.common import any_to_str PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -116,7 +116,7 @@ class Role: self._actions = [] self._role_id = str(self._setting) self._rc = RoleContext() - self._subscription = {get_object_name(self)} + self._subscription = {any_to_str(self)} if name: self._subscription.add(name) @@ -137,7 +137,7 @@ class Role: def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors in private message buffer""" - tags = {get_class_name(t) for t in actions} + tags = {any_to_str(t) for t in actions} self._rc.watch.update(tags) # check RoleContext after adding watch actions self._rc.check(self._role_id) @@ -207,10 +207,10 @@ class Role: content=response.content, instruct_content=response.instruct_content, role=self.profile, - cause_by=get_object_name(self._rc.todo), + cause_by=any_to_str(self._rc.todo), ) else: - msg = Message(content=response, role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content=response, role=self.profile, cause_by=any_to_str(self._rc.todo)) return msg diff --git a/metagpt/roles/seacher.py b/metagpt/roles/seacher.py index d0b841f39..a37143196 100644 --- a/metagpt/roles/seacher.py +++ b/metagpt/roles/seacher.py @@ -12,7 +12,7 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.tools import SearchEngineType -from metagpt.utils.common import get_object_name +from metagpt.utils.common import any_to_str class Searcher(Role): @@ -64,10 +64,10 @@ class Searcher(Role): content=response.content, instruct_content=response.instruct_content, role=self.profile, - cause_by=get_object_name(self._rc.todo), + cause_by=any_to_str(self._rc.todo), ) else: - msg = Message(content=response, role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content=response, role=self.profile, cause_by=any_to_str(self._rc.todo)) self._rc.memory.add(msg) return msg diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index 5b8d333bd..bb923caf2 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -17,7 +17,7 @@ from metagpt.actions.execute_task import ExecuteTask from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import get_object_name +from metagpt.utils.common import any_to_str from metagpt.utils.make_sk_kernel import make_sk_kernel @@ -74,7 +74,7 @@ class SkAgent(Role): result = (await self.plan.invoke_async()).result logger.info(result) - msg = Message(content=result, role=self.profile, cause_by=get_object_name(self._rc.todo)) + msg = Message(content=result, role=self.profile, cause_by=any_to_str(self._rc.todo)) self._rc.memory.add(msg) self.publish_message(msg) return msg From d36b4e2088c4a2e48c4f1ab63fc9c41c163bbaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 15:41:27 +0800 Subject: [PATCH 088/115] refactor: replace obj with role --- metagpt/environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index e9a5c6467..b3c296dac 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -61,9 +61,9 @@ class Environment(BaseModel): logger.info(f"publish_message: {message.dump()}") found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - for obj, subscription in self.consumers.items(): + for role, subscription in self.consumers.items(): if is_subscribed(message, subscription): - obj.put_message(message) + role.put_message(message) found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") From 3c38c5c41678f64a43f430ab215618567b98471b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 15:55:33 +0800 Subject: [PATCH 089/115] refactor: get_class_name --- metagpt/actions/write_code.py | 4 ++-- metagpt/memory/memory.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index be8690314..aeaa10aec 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -14,7 +14,7 @@ from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message -from metagpt.utils.common import CodeParser, get_class_name +from metagpt.utils.common import CodeParser, any_to_str PROMPT_TEMPLATE = """ NOTICE @@ -58,7 +58,7 @@ class WriteCode(Action): if self._is_invalid(filename): return - design = [i for i in context if i.cause_by == get_class_name(WriteDesign)][0] + design = [i for i in context if i.cause_by == any_to_str(WriteDesign)][0] ws_name = CodeParser.parse_str(block="Python package name", text=design.content) ws_path = WORKSPACE_ROOT / ws_name diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 84289091f..9d526420f 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -10,6 +10,7 @@ from collections import defaultdict from typing import Iterable, Set from metagpt.schema import Message +from metagpt.utils.common import any_to_str, any_to_str_set class Memory: @@ -73,14 +74,16 @@ class Memory: news.append(i) return news - def get_by_action(self, action: str) -> list[Message]: + def get_by_action(self, action) -> list[Message]: """Return all messages triggered by a specified Action""" - return self.index[action] + idx = any_to_str(action) + return self.index[idx] - def get_by_actions(self, actions: Set[str]) -> list[Message]: + def get_by_actions(self, actions: Set) -> list[Message]: """Return all messages triggered by specified Actions""" + idxs = any_to_str_set(actions) rsp = [] - for action in actions: + for action in idxs: if action not in self.index: continue rsp += self.index[action] From 710bc40b0ab49e1e5c9331a7175487aab68b9db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 15:58:47 +0800 Subject: [PATCH 090/115] refactor: get_class_name --- metagpt/memory/memory.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 9d526420f..2f4c9d20b 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -10,7 +10,6 @@ from collections import defaultdict from typing import Iterable, Set from metagpt.schema import Message -from metagpt.utils.common import any_to_str, any_to_str_set class Memory: @@ -76,14 +75,12 @@ class Memory: def get_by_action(self, action) -> list[Message]: """Return all messages triggered by a specified Action""" - idx = any_to_str(action) - return self.index[idx] + return self.index[action] def get_by_actions(self, actions: Set) -> list[Message]: """Return all messages triggered by specified Actions""" - idxs = any_to_str_set(actions) rsp = [] - for action in idxs: + for action in actions: if action not in self.index: continue rsp += self.index[action] From 60bad1830482b69dfb4ac92b128f172d19242e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 15:59:50 +0800 Subject: [PATCH 091/115] refactor: get_class_name --- metagpt/memory/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 2f4c9d20b..71d999049 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -73,7 +73,7 @@ class Memory: news.append(i) return news - def get_by_action(self, action) -> list[Message]: + def get_by_action(self, action: str) -> list[Message]: """Return all messages triggered by a specified Action""" return self.index[action] From 83a5e03b72168714c277633e53e2f16dc0f57345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:05:12 +0800 Subject: [PATCH 092/115] refactor: get_class_name --- metagpt/roles/engineer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 70dce41b1..742e00cc8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -81,7 +81,7 @@ class Engineer(Role): self.use_code_review = use_code_review if self.use_code_review: self._init_actions([WriteCode, WriteCodeReview]) - self._watch([WriteTasks, WriteDesign]) + self._watch([WriteTasks]) self.todos = [] self.n_borg = n_borg From a61f3f80e97a2265120d15195036fbb8ccf4b370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:15:07 +0800 Subject: [PATCH 093/115] refactor: get_by_action(s) --- examples/debate.py | 5 ++--- metagpt/memory/memory.py | 9 ++++++--- metagpt/roles/engineer.py | 11 +++++------ metagpt/roles/qa_engineer.py | 11 +++-------- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 8f5012d66..630f78cd8 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -15,7 +15,6 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.software_company import SoftwareCompany -from metagpt.utils.common import any_to_str_set class ShoutOut(Action): @@ -66,7 +65,7 @@ class Trump(Role): async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions(any_to_str_set([ShoutOut])) + msg_history = self._rc.memory.get_by_actions([ShoutOut]) context = [] for m in msg_history: context.append(str(m)) @@ -108,7 +107,7 @@ class Biden(Role): async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") - msg_history = self._rc.memory.get_by_actions(any_to_str_set([BossRequirement, ShoutOut])) + msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut]) context = [] for m in msg_history: context.append(str(m)) diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index 71d999049..53b65fcf7 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -10,6 +10,7 @@ from collections import defaultdict from typing import Iterable, Set from metagpt.schema import Message +from metagpt.utils.common import any_to_str, any_to_str_set class Memory: @@ -73,14 +74,16 @@ class Memory: news.append(i) return news - def get_by_action(self, action: str) -> list[Message]: + def get_by_action(self, action) -> list[Message]: """Return all messages triggered by a specified Action""" - return self.index[action] + index = any_to_str(action) + return self.index[index] def get_by_actions(self, actions: Set) -> list[Message]: """Return all messages triggered by specified Actions""" rsp = [] - for action in actions: + indices = any_to_str_set(actions) + for action in indices: if action not in self.index: continue rsp += self.index[action] diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 742e00cc8..960f9c0f3 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -21,7 +21,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, any_to_str, any_to_str_set +from metagpt.utils.common import CodeParser, any_to_str from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -102,7 +102,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(any_to_str(WriteDesign))[-1] + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -130,7 +130,7 @@ class Engineer(Role): todo_coros = [] for todo in self.todos: todo_coro = WriteCode().run( - context=self._rc.memory.get_by_actions(any_to_str_set([WriteTasks, WriteDesign])), + context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo, ) todo_coros.append(todo_coro) @@ -185,7 +185,7 @@ class Engineer(Role): TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ context = [] - msg = self._rc.memory.get_by_actions(any_to_str_set([WriteDesign, WriteTasks, WriteCode])) + msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) for m in msg: context.append(m.content) context_str = "\n".join(context) @@ -240,8 +240,7 @@ class Engineer(Role): async def _think(self) -> None: # In asynchronous scenarios, first check if the required messages are ready. - filters = {any_to_str(WriteTasks)} - msgs = self._rc.memory.get_by_actions(filters) + msgs = self._rc.memory.get_by_actions({WriteTasks}) if not msgs: self._rc.todo = None return diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 38fb5a24b..9495e1a12 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -22,12 +22,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import ( - CodeParser, - any_to_str_set, - get_class_name, - parse_recipient, -) +from metagpt.utils.common import CodeParser, any_to_str, any_to_str_set, parse_recipient from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -55,7 +50,7 @@ class QaEngineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: - msg = self._rc.memory.get_by_action(get_class_name(WriteDesign))[-1] + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -104,7 +99,7 @@ class QaEngineer(Role): msg = Message( content=str(file_info), role=self.profile, - cause_by=WriteTest, + cause_by=any_to_str(WriteTest), sent_from=self.profile, send_to=self.profile, ) From bb8e2467ea6d0405cf42da1850b85962b3570915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:22:02 +0800 Subject: [PATCH 094/115] refactor: cause_by --- metagpt/roles/qa_engineer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 9495e1a12..0f932ebfb 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -133,7 +133,9 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient) + msg = Message( + content=content, role=self.profile, cause_by=any_to_str(RunCode), sent_from=self.profile, send_to=recipient + ) self.publish_message(msg) async def _debug_error(self, msg): @@ -145,7 +147,7 @@ class QaEngineer(Role): msg = Message( content=file_info, role=self.profile, - cause_by=DebugError, + cause_by=any_to_str(DebugError), sent_from=self.profile, send_to=recipient, ) @@ -163,7 +165,7 @@ class QaEngineer(Role): result_msg = Message( content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, - cause_by=WriteTest, + cause_by=any_to_str(WriteTest), sent_from=self.profile, ) return result_msg @@ -187,7 +189,7 @@ class QaEngineer(Role): result_msg = Message( content=f"Round {self.test_round} of tests done", role=self.profile, - cause_by=WriteTest, + cause_by=any_to_str(WriteTest), sent_from=self.profile, ) return result_msg From fc63cdf4df1fdf6e4bf1ed3ecff8a58a9ad0a098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:24:09 +0800 Subject: [PATCH 095/115] refactor: cause_by --- metagpt/roles/role.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 90e85186b..a0a35bdc2 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -116,9 +116,7 @@ class Role: self._actions = [] self._role_id = str(self._setting) self._rc = RoleContext() - self._subscription = {any_to_str(self)} - if name: - self._subscription.add(name) + self._subscription = {any_to_str(self), name} if name else {any_to_str(self)} def _reset(self): self._states = [] From 9ebd1d1bbb3ed55564cde68396624120f6d9dec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:28:29 +0800 Subject: [PATCH 096/115] refactor: notation --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index a0a35bdc2..75e41d4ae 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -134,7 +134,7 @@ class Role: self._states.append(f"{idx}. {action}") def _watch(self, actions: Iterable[Type[Action]]): - """Listen to the corresponding behaviors in private message buffer""" + """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message buffer during _observe.""" tags = {any_to_str(t) for t in actions} self._rc.watch.update(tags) # check RoleContext after adding watch actions From b1a14d057a6c5af98c624881db0590928bd04b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:29:26 +0800 Subject: [PATCH 097/115] refactor: notation --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 75e41d4ae..ec6d71684 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -141,7 +141,7 @@ class Role: self._rc.check(self._role_id) def subscribe(self, tags: Set[str]): - """Listen to the corresponding behaviors""" + """Used to receive Messages with certain tags from the environment. Message will be put into personal message buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name or profile.""" self._subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 self._rc.env.set_subscription(self, self._subscription) From e8eeb6cda97c26ac0b55e854c5d67910a6f218da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:29:41 +0800 Subject: [PATCH 098/115] refactor: notation --- metagpt/roles/role.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index ec6d71684..4201b0f92 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -134,14 +134,17 @@ class Role: self._states.append(f"{idx}. {action}") def _watch(self, actions: Iterable[Type[Action]]): - """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message buffer during _observe.""" + """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message + buffer during _observe.""" tags = {any_to_str(t) for t in actions} self._rc.watch.update(tags) # check RoleContext after adding watch actions self._rc.check(self._role_id) def subscribe(self, tags: Set[str]): - """Used to receive Messages with certain tags from the environment. Message will be put into personal message buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name or profile.""" + """Used to receive Messages with certain tags from the environment. Message will be put into personal message + buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name + or profile.""" self._subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 self._rc.env.set_subscription(self, self._subscription) From efe6ead27c263afc2a39b2ba7b0a65c6376458bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:30:23 +0800 Subject: [PATCH 099/115] refactor: notation --- metagpt/roles/role.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 4201b0f92..ccad0b018 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -135,7 +135,8 @@ class Role: def _watch(self, actions: Iterable[Type[Action]]): """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message - buffer during _observe.""" + buffer during _observe. + """ tags = {any_to_str(t) for t in actions} self._rc.watch.update(tags) # check RoleContext after adding watch actions @@ -144,7 +145,8 @@ class Role: def subscribe(self, tags: Set[str]): """Used to receive Messages with certain tags from the environment. Message will be put into personal message buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name - or profile.""" + or profile. + """ self._subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 self._rc.env.set_subscription(self, self._subscription) From 44aa1dd563d04957f084b5b4a91c7105533eb002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:48:34 +0800 Subject: [PATCH 100/115] refactor: cause_by --- examples/debate.py | 5 ++++- metagpt/roles/engineer.py | 10 +++++----- metagpt/roles/qa_engineer.py | 14 ++++++-------- metagpt/roles/researcher.py | 9 +++------ metagpt/roles/role.py | 4 ++-- metagpt/roles/seacher.py | 5 ++--- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 630f78cd8..597b44e8d 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -15,6 +15,7 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.software_company import SoftwareCompany +from metagpt.utils.common import any_to_str class ShoutOut(Action): @@ -101,7 +102,9 @@ class Biden(Role): await super()._observe() # accept the very first human instruction (the debate topic) or messages sent (from opponent) to self, # disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.cause_by == BossRequirement or msg.send_to == {self.name}] + self._rc.news = [ + msg for msg in self._rc.news if msg.cause_by == any_to_str(BossRequirement) or msg.send_to == {self.name} + ] return len(self._rc.news) async def _act(self) -> Message: diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 960f9c0f3..535a1e27f 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -141,13 +141,13 @@ class Engineer(Role): logger.info(todo) logger.info(code_rsp) # self.write_file(todo, code) - msg = Message(content=code_rsp, role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content=code_rsp, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) self.publish_message(msg) del self.todos[0] logger.info(f"Done {self.get_workspace()} generating.") - msg = Message(content="all done.", role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content="all done.", role=self.profile, cause_by=self._rc.todo) return msg async def _act_sp(self) -> Message: @@ -158,7 +158,7 @@ class Engineer(Role): # logger.info(code_rsp) # code = self.parse_code(code_rsp) file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content=code, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) self.publish_message(msg) @@ -169,7 +169,7 @@ class Engineer(Role): msg = Message( content=MSG_SEP.join(code_msg_all), role=self.profile, - cause_by=any_to_str(self._rc.todo), + cause_by=self._rc.todo, send_to="Edward", ) return msg @@ -211,7 +211,7 @@ class Engineer(Role): msg = Message( content=MSG_SEP.join(code_msg_all), role=self.profile, - cause_by=any_to_str(self._rc.todo), + cause_by=self._rc.todo, send_to="Edward", ) return msg diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 0f932ebfb..760b65736 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -22,7 +22,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import CodeParser, any_to_str, any_to_str_set, parse_recipient +from metagpt.utils.common import CodeParser, any_to_str_set, parse_recipient from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -99,7 +99,7 @@ class QaEngineer(Role): msg = Message( content=str(file_info), role=self.profile, - cause_by=any_to_str(WriteTest), + cause_by=WriteTest, sent_from=self.profile, send_to=self.profile, ) @@ -133,9 +133,7 @@ class QaEngineer(Role): recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself content = str(file_info) + FILENAME_CODE_SEP + result_msg - msg = Message( - content=content, role=self.profile, cause_by=any_to_str(RunCode), sent_from=self.profile, send_to=recipient - ) + msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient) self.publish_message(msg) async def _debug_error(self, msg): @@ -147,7 +145,7 @@ class QaEngineer(Role): msg = Message( content=file_info, role=self.profile, - cause_by=any_to_str(DebugError), + cause_by=DebugError, sent_from=self.profile, send_to=recipient, ) @@ -165,7 +163,7 @@ class QaEngineer(Role): result_msg = Message( content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)", role=self.profile, - cause_by=any_to_str(WriteTest), + cause_by=WriteTest, sent_from=self.profile, ) return result_msg @@ -189,7 +187,7 @@ class QaEngineer(Role): result_msg = Message( content=f"Round {self.test_round} of tests done", role=self.profile, - cause_by=any_to_str(WriteTest), + cause_by=WriteTest, sent_from=self.profile, ) return result_msg diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 8d5e43fab..29889b8ec 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -15,7 +15,6 @@ from metagpt.const import RESEARCH_PATH from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import any_to_str class Report(BaseModel): @@ -64,20 +63,18 @@ class Researcher(Role): research_system_text = get_research_system_text(topic, self.language) if isinstance(todo, CollectLinks): links = await todo.run(topic, 4, 4) - ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=any_to_str(todo)) + ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=todo) elif isinstance(todo, WebBrowseAndSummarize): links = instruct_content.links todos = (todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items()) summaries = await asyncio.gather(*todos) summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary) - ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=any_to_str(todo)) + ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=todo) else: summaries = instruct_content.summaries summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries) content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text) - ret = Message( - "", Report(topic=topic, content=content), role=self.profile, cause_by=any_to_str(self._rc.todo) - ) + ret = Message("", Report(topic=topic, content=content), role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(ret) return ret diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index ccad0b018..5c512b0f0 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -210,10 +210,10 @@ class Role: content=response.content, instruct_content=response.instruct_content, role=self.profile, - cause_by=any_to_str(self._rc.todo), + cause_by=self._rc.todo, ) else: - msg = Message(content=response, role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content=response, role=self.profile, cause_by=self._rc.todo) return msg diff --git a/metagpt/roles/seacher.py b/metagpt/roles/seacher.py index a37143196..587698d1d 100644 --- a/metagpt/roles/seacher.py +++ b/metagpt/roles/seacher.py @@ -12,7 +12,6 @@ from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message from metagpt.tools import SearchEngineType -from metagpt.utils.common import any_to_str class Searcher(Role): @@ -64,10 +63,10 @@ class Searcher(Role): content=response.content, instruct_content=response.instruct_content, role=self.profile, - cause_by=any_to_str(self._rc.todo), + cause_by=self._rc.todo, ) else: - msg = Message(content=response, role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content=response, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) return msg From 7fb33fd890a3a19ab46f60d2ac9df152e75278b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 16:56:50 +0800 Subject: [PATCH 101/115] refactor: cause_by --- metagpt/roles/sk_agent.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index bb923caf2..15b18dd3e 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -4,9 +4,8 @@ @Time : 2023/9/13 12:23 @Author : femto Zheng @File : sk_agent.py -@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data - type of the `cause_by` value in the `Message` to a string, and utilize the new message distribution - feature for message filtering. +@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message + distribution feature for message filtering. """ from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner @@ -17,7 +16,6 @@ from metagpt.actions.execute_task import ExecuteTask from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import any_to_str from metagpt.utils.make_sk_kernel import make_sk_kernel @@ -74,7 +72,7 @@ class SkAgent(Role): result = (await self.plan.invoke_async()).result logger.info(result) - msg = Message(content=result, role=self.profile, cause_by=any_to_str(self._rc.todo)) + msg = Message(content=result, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) self.publish_message(msg) return msg From d9a7443e5a1b67dc5286fcd1d56a8ba8b540ec90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 17:03:24 +0800 Subject: [PATCH 102/115] refactor: notation --- metagpt/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 63fe41232..82a0117ef 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -58,7 +58,7 @@ class Message(BaseModel): :param instruct_content: Message content struct. :param cause_by: Message producer :param sent_from: Message route info tells who sent this message. - :param send_to: Labels for the consumer to filter its subscribed messages. + :param send_to: Specifies the target recipient or consumer for message delivery in the environment. :param role: Message meta info tells who sent this message. """ super().__init__( From 282a86bfa7b28e44c27b425432e87b1bf1c0c37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 10 Nov 2023 17:14:58 +0800 Subject: [PATCH 103/115] refactor: unit tests --- tests/metagpt/test_role.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 447de7ee5..8fac2503c 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -60,7 +60,7 @@ async def test_react(): name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints, desc=seed.desc ) role.subscribe({seed.subscription}) - assert role._rc.watch == {seed.subscription} + assert role._rc.watch == set({}) assert role.name == seed.name assert role.profile == seed.profile assert role._setting.goal == seed.goal @@ -69,7 +69,7 @@ async def test_react(): assert role.is_idle env = Environment() env.add_role(role) - assert env.get_subscribed_tags(role) == {seed.subscription} + assert env.get_subscription(role) == {seed.subscription} env.publish_message(Message(content="test", msg_to=seed.subscription)) assert not role.is_idle while not env.is_idle: @@ -82,19 +82,19 @@ async def test_react(): assert role.is_idle tag = uuid.uuid4().hex role.subscribe({tag}) - assert env.get_subscribed_tags(role) == {seed.subscription, tag} + assert env.get_subscription(role) == {tag} @pytest.mark.asyncio async def test_msg_to(): - m = Message(content="a", msg_to=["a", MockRole, Message]) - assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message)} + m = Message(content="a", send_to=["a", MockRole, Message]) + assert m.send_to == set({"a", get_class_name(MockRole), get_class_name(Message)}) - m = Message(content="a", cause_by=MockAction, msg_to={"a", MockRole, Message}) - assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message), get_class_name(MockAction)} + m = Message(content="a", cause_by=MockAction, send_to={"a", MockRole, Message}) + assert m.send_to == set({"a", get_class_name(MockRole), get_class_name(Message)}) - m = Message(content="a", msg_to=("a", MockRole, Message)) - assert m.msg_to == {"a", get_class_name(MockRole), get_class_name(Message)} + m = Message(content="a", send_to=("a", MockRole, Message)) + assert m.send_to == set({"a", get_class_name(MockRole), get_class_name(Message)}) if __name__ == "__main__": From 23749212bfe3b705f0f47758c4dca42efe337eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sun, 12 Nov 2023 17:41:51 +0800 Subject: [PATCH 104/115] refactor: rename --- metagpt/roles/role.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 5c512b0f0..a8280cecf 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -255,16 +255,16 @@ class Role: logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}") return await self._act() - async def run(self, test_message=None): + async def run(self, with_message=None): """Observe, and think and act based on the results of the observation""" - if test_message: # For test + if with_message: # For test msg = None - if isinstance(test_message, str): - msg = Message(test_message) - elif isinstance(test_message, Message): - msg = test_message - elif isinstance(test_message, list): - msg = Message("\n".join(test_message)) + if isinstance(with_message, str): + msg = Message(with_message) + elif isinstance(with_message, Message): + msg = with_message + elif isinstance(with_message, list): + msg = Message("\n".join(with_message)) self.put_message(msg) if not await self._observe(): From 962109bd119ba7cdde0f4ef7c33b3830c8ff9bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 13 Nov 2023 16:26:24 +0800 Subject: [PATCH 105/115] refactor: notation --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index a8280cecf..2e3bcbbd5 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -257,7 +257,7 @@ class Role: async def run(self, with_message=None): """Observe, and think and act based on the results of the observation""" - if with_message: # For test + if with_message: msg = None if isinstance(with_message, str): msg = Message(with_message) From c2ffee61e6730d7dcf31168dd6f0cd713d92a98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 13 Nov 2023 20:45:05 +0800 Subject: [PATCH 106/115] refactor: remove useless code --- metagpt/roles/engineer.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 535a1e27f..d23d23d55 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -237,12 +237,3 @@ class Engineer(Role): return 1 return 0 - - async def _think(self) -> None: - # In asynchronous scenarios, first check if the required messages are ready. - msgs = self._rc.memory.get_by_actions({WriteTasks}) - if not msgs: - self._rc.todo = None - return - - await super(Engineer, self)._think() From b73cbe73798647b7e1e69ff4c2c7f41ad9ec7c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 21 Nov 2023 14:00:09 +0800 Subject: [PATCH 107/115] feat: +unit test --- tests/metagpt/utils/test_common.py | 56 ++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/metagpt/utils/test_common.py b/tests/metagpt/utils/test_common.py index ec4443175..6474b1233 100644 --- a/tests/metagpt/utils/test_common.py +++ b/tests/metagpt/utils/test_common.py @@ -4,27 +4,79 @@ @Time : 2023/4/29 16:19 @Author : alexanderwu @File : test_common.py +@Modified by: mashenquan, 2023/11/21. Add unit tests. """ import os +from typing import Any, Set import pytest +from pydantic import BaseModel +from metagpt.actions import RunCode from metagpt.const import get_project_root +from metagpt.roles.tutorial_assistant import TutorialAssistant +from metagpt.schema import Message +from metagpt.utils.common import any_to_str, any_to_str_set class TestGetProjectRoot: def change_etc_dir(self): # current_directory = Path.cwd() - abs_root = '/etc' + abs_root = "/etc" os.chdir(abs_root) def test_get_project_root(self): project_root = get_project_root() - assert project_root.name == 'metagpt' + assert project_root.name == "MetaGPT" def test_get_root_exception(self): with pytest.raises(Exception) as exc_info: self.change_etc_dir() get_project_root() assert str(exc_info.value) == "Project root not found." + + def test_any_to_str(self): + class Input(BaseModel): + x: Any + want: str + + inputs = [ + Input(x=TutorialAssistant, want="metagpt.roles.tutorial_assistant.TutorialAssistant"), + Input(x=TutorialAssistant(), want="metagpt.roles.tutorial_assistant.TutorialAssistant"), + Input(x=RunCode, want="metagpt.actions.run_code.RunCode"), + Input(x=RunCode(), want="metagpt.actions.run_code.RunCode"), + Input(x=Message, want="metagpt.schema.Message"), + Input(x=Message(""), want="metagpt.schema.Message"), + Input(x="A", want="A"), + ] + for i in inputs: + v = any_to_str(i.x) + assert v == i.want + + def test_any_to_str_set(self): + class Input(BaseModel): + x: Any + want: Set + + inputs = [ + Input( + x=[TutorialAssistant, RunCode(), "a"], + want={"metagpt.roles.tutorial_assistant.TutorialAssistant", "metagpt.actions.run_code.RunCode", "a"}, + ), + Input( + x={TutorialAssistant, RunCode(), "a"}, + want={"metagpt.roles.tutorial_assistant.TutorialAssistant", "metagpt.actions.run_code.RunCode", "a"}, + ), + Input( + x=(TutorialAssistant, RunCode(), "a"), + want={"metagpt.roles.tutorial_assistant.TutorialAssistant", "metagpt.actions.run_code.RunCode", "a"}, + ), + ] + for i in inputs: + v = any_to_str_set(i.x) + assert v == i.want + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From 5142cb59f7120e564d014e8e3ab2e69698e0972e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 22 Nov 2023 15:59:35 +0800 Subject: [PATCH 108/115] refactor: consumers -> members --- metagpt/environment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metagpt/environment.py b/metagpt/environment.py index b3c296dac..02eb3d340 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -29,7 +29,7 @@ class Environment(BaseModel): """ roles: dict[str, Role] = Field(default_factory=dict) - consumers: dict[Role, Set] = Field(default_factory=dict) + members: dict[Role, Set] = Field(default_factory=dict) history: str = Field(default="") # For debug class Config: @@ -61,7 +61,7 @@ class Environment(BaseModel): logger.info(f"publish_message: {message.dump()}") found = False # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - for role, subscription in self.consumers.items(): + for role, subscription in self.members.items(): if is_subscribed(message, subscription): role.put_message(message) found = True @@ -106,8 +106,8 @@ class Environment(BaseModel): def get_subscription(self, obj): """Get the labels for messages to be consumed by the object.""" - return self.consumers.get(obj, {}) + return self.members.get(obj, {}) def set_subscription(self, obj, tags): """Set the labels for message to be consumed by the object""" - self.consumers[obj] = tags + self.members[obj] = tags From 0fdeab3f200f4a5cdb0b6c8427373f4f037d6ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 23 Nov 2023 17:36:01 +0800 Subject: [PATCH 109/115] fixbug: Message was incorrectly filtered by the profile. --- metagpt/roles/qa_engineer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 760b65736..b57b64a7e 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -151,13 +151,6 @@ class QaEngineer(Role): ) self.publish_message(msg) - async def _observe(self) -> int: - await super()._observe() - self._rc.news = [ - msg for msg in self._rc.news if self.profile in msg.send_to - ] # only relevant msgs count as observed news - return len(self._rc.news) - async def _act(self) -> Message: if self.test_round > self.test_round_allowed: result_msg = Message( From 66cff9023fe0bd27843b0754110e7d8bda902a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 23 Nov 2023 18:10:44 +0800 Subject: [PATCH 110/115] fixbug: Delete the incorrect message. --- metagpt/roles/engineer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d23d23d55..c0e1b8a10 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -143,7 +143,6 @@ class Engineer(Role): # self.write_file(todo, code) msg = Message(content=code_rsp, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) - self.publish_message(msg) del self.todos[0] logger.info(f"Done {self.get_workspace()} generating.") @@ -160,7 +159,6 @@ class Engineer(Role): file_path = self.write_file(todo, code) msg = Message(content=code, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) - self.publish_message(msg) code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) @@ -202,7 +200,6 @@ class Engineer(Role): file_path = self.write_file(todo, code) msg = Message(content=code, role=self.profile, cause_by=WriteCode) self._rc.memory.add(msg) - self.publish_message(msg) code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) From 9a3c92ed1192387f28eee11fcca3e08b737f7fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 23 Nov 2023 21:38:39 +0800 Subject: [PATCH 111/115] fixbug: send useless message to nobody from QaEngineer --- metagpt/roles/qa_engineer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index b57b64a7e..23c8d1fdd 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -100,8 +100,8 @@ class QaEngineer(Role): content=str(file_info), role=self.profile, cause_by=WriteTest, - sent_from=self.profile, - send_to=self.profile, + sent_from=self, + send_to=self, ) self.publish_message(msg) @@ -182,5 +182,6 @@ class QaEngineer(Role): role=self.profile, cause_by=WriteTest, sent_from=self.profile, + send_to="" ) return result_msg From 3e8bba70bcd20fda71151f3b526171d96818cdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 23 Nov 2023 21:38:51 +0800 Subject: [PATCH 112/115] fixbug: send useless message to nobody from QaEngineer --- metagpt/roles/qa_engineer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 23c8d1fdd..59a4135b8 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -146,8 +146,8 @@ class QaEngineer(Role): content=file_info, role=self.profile, cause_by=DebugError, - sent_from=self.profile, - send_to=recipient, + sent_from=self, + send_to=self, ) self.publish_message(msg) @@ -158,6 +158,7 @@ class QaEngineer(Role): role=self.profile, cause_by=WriteTest, sent_from=self.profile, + send_to="" ) return result_msg From b794e5d73dc47722996508af4824b1e5496869a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 28 Nov 2023 16:22:40 +0800 Subject: [PATCH 113/115] feat: fix memory.add --- metagpt/roles/engineer.py | 4 ++-- metagpt/roles/role.py | 4 +++- metagpt/roles/sk_agent.py | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index c0e1b8a10..ffd96849b 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -168,7 +168,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=self._rc.todo, - send_to="Edward", + send_to="Edward", # name of QaEngineer ) return msg @@ -209,7 +209,7 @@ class Engineer(Role): content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=self._rc.todo, - send_to="Edward", + send_to="Edward", # name of QaEngineer ) return msg diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index e1f43ef3a..dbf800c03 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -217,6 +217,7 @@ class Role: ) else: msg = Message(content=response, role=self.profile, cause_by=self._rc.todo) + self._rc.memory.add(msg) return msg @@ -227,7 +228,8 @@ class Role: # Store the read messages in your own memory to prevent duplicate processing. self._rc.memory.add_batch(news) # Filter out messages of interest. - self._rc.news = [n for n in news if n.cause_by in self._rc.watch] + old_messages = self._rc.memory.get() + self._rc.news = [n for n in news if n.cause_by in self._rc.watch and n not in old_messages] # Design Rules: # If you need to further categorize Message objects, you can do so using the Message.set_meta function. diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index 15b18dd3e..2443b8b58 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -74,5 +74,4 @@ class SkAgent(Role): msg = Message(content=result, role=self.profile, cause_by=self._rc.todo) self._rc.memory.add(msg) - self.publish_message(msg) return msg From 9745dd12f6bdb27bc5bbc16401605c3f9bbe688c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 28 Nov 2023 16:28:53 +0800 Subject: [PATCH 114/115] feat: fix memory.add --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index dbf800c03..62c8b7708 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -226,9 +226,9 @@ class Role: # Read unprocessed messages from the msg buffer. news = self._rc.msg_buffer.pop_all() # Store the read messages in your own memory to prevent duplicate processing. + old_messages = self._rc.memory.get() self._rc.memory.add_batch(news) # Filter out messages of interest. - old_messages = self._rc.memory.get() self._rc.news = [n for n in news if n.cause_by in self._rc.watch and n not in old_messages] # Design Rules: From f2de34fdad26a31def47a136f7ed7f73fa58ddf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 28 Nov 2023 16:42:15 +0800 Subject: [PATCH 115/115] feat: fix memory.add --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 62c8b7708..424a28c6f 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -217,7 +217,7 @@ class Role: ) else: msg = Message(content=response, role=self.profile, cause_by=self._rc.todo) - self._rc.memory.add(msg) + self._rc.memory.add(msg) return msg