From 7f1642720509cae1d2ce28d3f9a49c81e889a4ea Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 22 Nov 2023 11:52:47 +0800 Subject: [PATCH 1/3] update readme --- README.md | 1 + docs/README_CN.md | 1 + docs/README_JA.md | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index ead43c9e7..0ad622576 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ # If executing, ensure that NPM is installed on your system. Then install mermai detail installation please refer to [cli_install](https://docs.deepwisdom.ai/guide/get_started/installation.html#install-stable-version) ### Docker installation +> Note: In the Windows, you need to replace "/opt/metagpt" with a directory that Docker has permission to create, such as "D:\Users\x\metagpt" ```bash # Step 1: Download metagpt official image and prepare config.yaml diff --git a/docs/README_CN.md b/docs/README_CN.md index 409bdc7af..ebdc63022 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -60,6 +60,7 @@ # 如果执行,确保您的系统上安装了 NPM。并使用npm安装mermaid- 详细的安装请安装 [cli_install](https://docs.deepwisdom.ai/guide/get_started/installation.html#install-stable-version) ### Docker安装 +> 注意:在Windows中,你需要将 "/opt/metagpt" 替换为Docker具有创建权限的目录,比如"D:\Users\x\metagpt" ```bash # 步骤1: 下载metagpt官方镜像并准备好config.yaml diff --git a/docs/README_JA.md b/docs/README_JA.md index 10cb7ee82..988dcde7a 100644 --- a/docs/README_JA.md +++ b/docs/README_JA.md @@ -163,6 +163,7 @@ # NPM がシステムにインストールされていることを確認して 注: この方法は pdf エクスポートに対応していません。 ### Docker によるインストール +> Windowsでは、"/opt/metagpt"をDockerが作成する権限を持つディレクトリに置き換える必要があります。例えば、"D:\Users\x\metagpt"などです。 ```bash # ステップ 1: metagpt 公式イメージをダウンロードし、config.yaml を準備する From 4702059caf3c76b05d2a6c7c119a56fbd03a8db9 Mon Sep 17 00:00:00 2001 From: stellahsr Date: Mon, 27 Nov 2023 21:12:50 +0800 Subject: [PATCH 2/3] update basic code for serialize --- metagpt/actions/action.py | 61 +++--- metagpt/actions/design_api.py | 30 ++- metagpt/actions/project_management.py | 27 ++- metagpt/actions/search_and_summarize.py | 52 +++-- metagpt/actions/write_code.py | 15 +- metagpt/actions/write_code_review.py | 13 +- metagpt/actions/write_prd.py | 32 ++- metagpt/environment.py | 5 +- metagpt/roles/architect.py | 18 +- metagpt/roles/engineer.py | 76 +++---- metagpt/roles/product_manager.py | 36 ++-- metagpt/roles/project_manager.py | 27 +-- metagpt/roles/role.py | 271 +++++++++++------------- 13 files changed, 342 insertions(+), 321 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 790295d55..7bb5a151b 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -7,8 +7,9 @@ """ import re from abc import ABC -from typing import Optional +from typing import Optional, Any +from pydantic import BaseModel, Field from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions.action_output import ActionOutput @@ -18,45 +19,45 @@ from metagpt.utils.common import OutputParser from metagpt.utils.custom_decoder import CustomDecoder -class Action(ABC): - def __init__(self, name: str = "", context=None, llm: LLM = None): - self.name: str = name - if llm is None: - llm = LLM() - self.llm = llm - self.context = context - self.prefix = "" - self.profile = "" - self.desc = "" - self.content = "" - self.instruct_content = None - +class Action(BaseModel): + name: str = "" + llm: LLM = Field(default_factory=LLM) + context = "" + prefix = "" + profile = "" + desc = "" + content: Optional[str] = None + instruct_content: Optional[str] = None + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + def set_prefix(self, prefix, profile): """Set prefix for later usage""" self.prefix = prefix self.profile = profile - + def __str__(self): return self.__class__.__name__ - + def __repr__(self): return self.__str__() - + async def _aask(self, prompt: str, system_msgs: Optional[list[str]] = None) -> str: """Append default prefix""" if not system_msgs: system_msgs = [] system_msgs.append(self.prefix) return await self.llm.aask(prompt, system_msgs) - + @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) async def _aask_v1( - self, - prompt: str, - output_class_name: str, - output_data_mapping: dict, - system_msgs: Optional[list[str]] = None, - format="markdown", # compatible to original format + self, + prompt: str, + output_class_name: str, + output_data_mapping: dict, + system_msgs: Optional[list[str]] = None, + format="markdown", # compatible to original format ) -> ActionOutput: """Append default prefix""" if not system_msgs: @@ -65,25 +66,25 @@ class Action(ABC): content = await self.llm.aask(prompt, system_msgs) logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - + if format == "json": pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" matches = re.findall(pattern, content, re.DOTALL) - + for match in matches: if match: content = match break - + parsed_data = CustomDecoder(strict=False).decode(content) - + else: # using markdown parser parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) - + logger.debug(parsed_data) instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) - + async def run(self, *args, **kwargs): """Run action""" raise NotImplementedError("The run method should be implemented in a subclass.") diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 75df8b909..30df70ce7 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -7,9 +7,12 @@ """ import shutil from pathlib import Path -from typing import List +from typing import List, Optional, Any + +from pydantic import Field from metagpt.actions import Action, ActionOutput +from metagpt.llm import LLM from metagpt.config import CONFIG from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger @@ -150,13 +153,13 @@ OUTPUT_MAPPING = { class WriteDesign(Action): - def __init__(self, name, context=None, llm=None): - super().__init__(name, context, llm) - self.desc = ( - "Based on the PRD, think about the system design, and design the corresponding APIs, " - "data structures, library tables, processes, and paths. Please provide your design, feedback " - "clearly and in detail." - ) + name: str = "" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) + desc: str = "Based on the PRD, think about the system design, and design the corresponding APIs, " + "data structures, library tables, processes, and paths. Please provide your design, feedback " + "clearly and in detail." + def recreate_workspace(self, workspace: Path): try: @@ -165,16 +168,18 @@ class WriteDesign(Action): pass # Folder does not exist, but we don't care workspace.mkdir(parents=True, exist_ok=True) + async def _save_prd(self, docs_path, resources_path, context): prd_file = docs_path / "prd.md" if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") - + if context[-1].instruct_content: logger.info(f"Saving PRD to {prd_file}") prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict())) + async def _save_system_design(self, docs_path, resources_path, system_design): data_api_design = system_design.instruct_content.dict()[ "Data structures and interface definitions" @@ -188,6 +193,7 @@ class WriteDesign(Action): logger.info(f"Saving System Designs to {system_design_file}") system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict()))) + async def _save(self, context, system_design): if isinstance(system_design, ActionOutput): ws_name = system_design.instruct_content.dict()["Python package name"] @@ -199,9 +205,13 @@ class WriteDesign(Action): resources_path = workspace / "resources" docs_path.mkdir(parents=True, exist_ok=True) resources_path.mkdir(parents=True, exist_ok=True) - await self._save_prd(docs_path, resources_path, context) + try: + await self._save_prd(docs_path, resources_path, context) + except Exception as e: + logger.error(f"Failed to save PRD {e}") await self._save_system_design(docs_path, resources_path, system_design) + async def run(self, context, format=CONFIG.prompt_format): prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format(context=context, format_example=format_example) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index b395fa64e..b72507ee3 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -5,9 +5,12 @@ @Author : alexanderwu @File : project_management.py """ -from typing import List +from typing import List, Optional, Any + +from pydantic import Field from metagpt.actions.action import Action +from metagpt.llm import LLM from metagpt.config import CONFIG from metagpt.const import WORKSPACE_ROOT from metagpt.utils.common import CodeParser @@ -163,21 +166,25 @@ OUTPUT_MAPPING = { class WriteTasks(Action): - def __init__(self, name="CreateTasks", context=None, llm=None): - super().__init__(name, context, llm) - + name: str = "CreateTasks" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) + def _save(self, context, rsp): - if context[-1].instruct_content: - ws_name = context[-1].instruct_content.dict()["Python package name"] - else: - ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + try: + if context[-1].instruct_content: + ws_name = context[-1].instruct_content.dict()["Python package name"] + else: + ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + except: + ws_name = "cli_snake_game" # fixme: 应该透传 file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md" file_path.write_text(json_to_markdown(rsp.instruct_content.dict())) - + # Write requirements.txt requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages"))) - + async def run(self, context, format=CONFIG.prompt_format): prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format(context=context, format_example=format_example) diff --git a/metagpt/actions/search_and_summarize.py b/metagpt/actions/search_and_summarize.py index 069f2a977..0580303e6 100644 --- a/metagpt/actions/search_and_summarize.py +++ b/metagpt/actions/search_and_summarize.py @@ -6,12 +6,16 @@ @File : search_google.py """ import pydantic +from typing import Optional, Any +from pydantic import BaseModel, Field from metagpt.actions import Action +from metagpt.llm import LLM from metagpt.config import Config from metagpt.logs import logger from metagpt.schema import Message from metagpt.tools.search_engine import SearchEngine +from pydantic import root_validator SEARCH_AND_SUMMARIZE_SYSTEM = """### Requirements 1. Please summarize the latest dialogue based on the reference information (secondary) and dialogue history (primary). Do not include text that is irrelevant to the conversation. @@ -54,7 +58,6 @@ SEARCH_AND_SUMMARIZE_PROMPT = """ """ - SEARCH_AND_SUMMARIZE_SALES_SYSTEM = """## Requirements 1. Please summarize the latest dialogue based on the reference information (secondary) and dialogue history (primary). Do not include text that is irrelevant to the conversation. - The context is for reference only. If it is irrelevant to the user's search request history, please reduce its reference and usage. @@ -101,23 +104,41 @@ You are a member of a professional butler team and will provide helpful suggesti class SearchAndSummarize(Action): - def __init__(self, name="", context=None, llm=None, engine=None, search_func=None): - self.config = Config() - self.engine = engine or self.config.search_engine + name: str = "" + content: Optional[str] = None + llm: None = Field(default_factory=LLM) + config: None = Field(default_factory=Config) + engine: Optional[str] = None + search_func: Optional[str] = None - try: - self.search_engine = SearchEngine(self.engine, run_func=search_func) - except pydantic.ValidationError: - self.search_engine = None + result = "" + - self.result = "" - super().__init__(name, context, llm) + @root_validator + def validate_engine_and_run_func(cls, values): + engine = values.get('engine') + search_func = values.get('search_func') + config = Config() + + if engine is None: + engine = config.search_engine + config_data = { + 'engine': engine, + 'run_func': search_func + } + search_engine = SearchEngine(**config_data) + values['search_engine'] = search_engine + return values + + + async def run(self, context: list[Message], system_text=SEARCH_AND_SUMMARIZE_SYSTEM) -> str: + print(context) if self.search_engine is None: logger.warning("Configure one of SERPAPI_API_KEY, SERPER_API_KEY, GOOGLE_API_KEY to unlock full feature") return "" - + query = context[-1].content # logger.debug(query) rsp = await self.search_engine.run(query) @@ -126,9 +147,9 @@ class SearchAndSummarize(Action): logger.error("empty rsp...") return "" # logger.info(rsp) - + system_prompt = [system_text] - + prompt = SEARCH_AND_SUMMARIZE_PROMPT.format( # PREFIX = self.prefix, ROLE=self.profile, @@ -140,4 +161,7 @@ class SearchAndSummarize(Action): logger.debug(prompt) logger.debug(result) return result - \ No newline at end of file + + +if __name__ == "__main__": + action = SearchAndSummarize() diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index c000805c5..2dc240591 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -5,13 +5,18 @@ @Author : alexanderwu @File : write_code.py """ +from typing import List, Optional, Any + +from pydantic import Field +from tenacity import retry, stop_after_attempt, wait_fixed + from metagpt.actions import WriteDesign from metagpt.actions.action import Action +from metagpt.llm import LLM 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 @@ -43,9 +48,10 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc class WriteCode(Action): - def __init__(self, name="WriteCode", context: list[Message] = None, llm=None): - super().__init__(name, context, llm) - + name: str = "WriteCode" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) + def _is_invalid(self, filename): return any(i in filename for i in ["mp3", "wav"]) @@ -79,4 +85,3 @@ class WriteCode(Action): # 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/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 4ff4d6cf6..3d86d7c63 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -5,12 +5,15 @@ @Author : alexanderwu @File : write_code_review.py """ +from typing import List, Optional, Any +from pydantic import Field +from tenacity import retry, stop_after_attempt, wait_fixed +from metagpt.llm import LLM from metagpt.actions.action import Action 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 @@ -62,9 +65,10 @@ FORMAT_EXAMPLE = """ class WriteCodeReview(Action): - def __init__(self, name="WriteCodeReview", context: list[Message] = None, llm=None): - super().__init__(name, context, llm) - + name: str = "WriteCodeReview" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) async def write_code(self, prompt): code_rsp = await self._aask(prompt) @@ -79,4 +83,3 @@ class WriteCodeReview(Action): # 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/actions/write_prd.py b/metagpt/actions/write_prd.py index bd04ca79e..660d7fb95 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -5,9 +5,12 @@ @Author : alexanderwu @File : write_prd.py """ -from typing import List +from typing import List, Optional, Any + +from pydantic import BaseModel, Field from metagpt.actions import Action, ActionOutput +from metagpt.llm import LLM from metagpt.actions.search_and_summarize import SearchAndSummarize from metagpt.config import CONFIG from metagpt.logs import logger @@ -219,18 +222,25 @@ OUTPUT_MAPPING = { class WritePRD(Action): - def __init__(self, name="", context=None, llm=None): - super().__init__(name, context, llm) - + name: str = "" + content: Optional[str] = None + llm: LLM = Field(default_factory=LLM) + assistant_search_action: Action = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + async def run(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput: - sas = SearchAndSummarize() - # rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US) - rsp = "" - info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" - if sas.result: - logger.info(sas.result) + # self.assistant_search_action = SearchAndSummarize() + if self.assistant_search_action is None: + self.assistant_search_action = SearchAndSummarize() + # self.assistant_search_action = SearchAndSummarize() + rsp = await self.assistant_search_action.run(context=requirements) + info = f"### Search Results\n{self.assistant_search_action.result}\n\n### Search Summary\n{rsp}" + if self.assistant_search_action.result: + logger.info(self.assistant_search_action.result) logger.info(rsp) - + prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format( requirements=requirements, search_information=info, format_example=format_example diff --git a/metagpt/environment.py b/metagpt/environment.py index 24e6ada2f..88ff145e0 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -29,11 +29,12 @@ class Environment(BaseModel): arbitrary_types_allowed = True def add_role(self, role: Role): - """增加一个在当前环境的角色 + """增加一个在当前环境的角色, 默认为profile/role_profile Add a role in the current environment """ role.set_env(self) - self.roles[role.profile] = role + # use alias + self.roles[role.role_profile] = role def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index 15d5fe5b1..face22a68 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -5,10 +5,11 @@ @Author : alexanderwu @File : architect.py """ +from pydantic import Field from metagpt.actions import WritePRD from metagpt.actions.design_api import WriteDesign -from metagpt.roles import Role +from metagpt.roles.role import Role class Architect(Role): @@ -21,17 +22,16 @@ class Architect(Role): goal (str): Primary goal or responsibility of the architect. constraints (str): Constraints or guidelines for the architect. """ + name: str = "Bob" + role_profile: str = Field(default="Architect" , alias='profile') + goal: str = "Design a concise, usable, complete python system" + constraints: str = "Try to specify good open source tools as much as possible" def __init__( - self, - name: str = "Bob", - profile: str = "Architect", - goal: str = "Design a concise, usable, complete python system", - constraints: str = "Try to specify good open source tools as much as possible", + self, + **kwargs ) -> None: - """Initializes the Architect with given attributes.""" - super().__init__(name, profile, goal, constraints) - + super().__init__(**kwargs) # Initialize actions specific to the Architect role self._init_actions([WriteDesign]) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 1f6685b38..129bedeb8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -9,11 +9,12 @@ import asyncio import shutil from collections import OrderedDict from pathlib import Path +from pydantic import Field 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.role import Role from metagpt.schema import Message from metagpt.utils.common import CodeParser from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP @@ -23,7 +24,7 @@ async def gather_ordered_k(coros, k) -> list: tasks = OrderedDict() results = [None] * len(coros) done_queue = asyncio.Queue() - + for i, coro in enumerate(coros): if len(tasks) >= k: done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED) @@ -32,17 +33,17 @@ async def gather_ordered_k(coros, k) -> list: await done_queue.put((index, task.result())) task = asyncio.create_task(coro) tasks[task] = i - + if tasks: done, _ = await asyncio.wait(tasks.keys()) for task in done: index = tasks[task] await done_queue.put((index, task.result())) - + while not done_queue.empty(): index, result = await done_queue.get() results[index] = result - + return results @@ -59,42 +60,42 @@ class Engineer(Role): use_code_review (bool): Whether to use code review. todos (list): List of tasks. """ - + name: str = "Alex" + role_profile: str = Field(default="Engineer", alias='profile') + goal: str = "Write elegant, readable, extensible, efficient code" + constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable" + n_borg: int = 1 + use_code_review: bool = False + todos: list = [] + def __init__( - self, - name: str = "Alex", - profile: str = "Engineer", - goal: str = "Write elegant, readable, extensible, efficient code", - constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable", - n_borg: int = 1, - use_code_review: bool = False, + self, + **kwargs ) -> None: - """Initializes the Engineer role with given attributes.""" - super().__init__(name, profile, goal, constraints) - self._init_actions([WriteCode]) - self.use_code_review = use_code_review + super().__init__(**kwargs) + + actions = [WriteCode] if self.use_code_review: - self._init_actions([WriteCode, WriteCodeReview]) + actions = [WriteCode, WriteCodeReview] + self._init_actions(actions) self._watch([WriteTasks]) - self.todos = [] - self.n_borg = n_borg - + @classmethod def parse_tasks(self, task_msg: Message) -> list[str]: if task_msg.instruct_content: return task_msg.instruct_content.dict().get("Task list") return CodeParser.parse_file_list(block="Task list", text=task_msg.content) - + @classmethod def parse_code(self, code_text: str) -> str: return CodeParser.parse_code(block="", text=code_text) - + @classmethod def parse_workspace(cls, system_design_msg: Message) -> str: if system_design_msg.instruct_content: return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip('"') 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] if not msg: @@ -102,7 +103,7 @@ class Engineer(Role): workspace = self.parse_workspace(msg) # Codes are written in workspace/{package_name}/{package_name} return WORKSPACE_ROOT / workspace / workspace - + def recreate_workspace(self): workspace = self.get_workspace() try: @@ -110,7 +111,7 @@ class Engineer(Role): except FileNotFoundError: pass # The folder does not exist, but we don't care workspace.mkdir(parents=True, exist_ok=True) - + def write_file(self, filename: str, code: str): workspace = self.get_workspace() filename = filename.replace('"', "").replace("\n", "") @@ -118,12 +119,12 @@ class Engineer(Role): file.parent.mkdir(parents=True, exist_ok=True) 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 = [] @@ -132,7 +133,7 @@ class Engineer(Role): context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo ) todo_coros.append(todo_coro) - + rsps = await gather_ordered_k(todo_coros, self.n_borg) for todo, code_rsp in zip(self.todos, rsps): _ = self.parse_code(code_rsp) @@ -142,11 +143,11 @@ class Engineer(Role): msg = Message(content=code_rsp, role=self.profile, cause_by=type(self._rc.todo)) self._rc.memory.add(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)) return msg - + async def _act_sp(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later for todo in self.todos: @@ -157,16 +158,16 @@ class Engineer(Role): file_path = self.write_file(todo, code) msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo)) self._rc.memory.add(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" ) return msg - + async def _act_sp_precision(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later for todo in self.todos: @@ -195,19 +196,18 @@ 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) - + 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" ) return msg - + async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" - logger.info(f"{self._setting}: ready to WriteCode") if self.use_code_review: return await self._act_sp_precision() return await self._act_sp() diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index a58ea5385..b099fb4d9 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -5,37 +5,33 @@ @Author : alexanderwu @File : product_manager.py """ +from pydantic import Field + from metagpt.actions import BossRequirement, WritePRD -from metagpt.roles import Role +from metagpt.roles.role import Role class ProductManager(Role): """ - Represents a Product Manager role responsible for product development and management. + Initializes the ProductManager role with given attributes. - Attributes: + Args: name (str): Name of the product manager. - profile (str): Role profile, default is 'Product Manager'. + profile (str): Role profile. goal (str): Goal of the product manager. constraints (str): Constraints or limitations for the product manager. """ - + name: str = "Alice" + role_profile: str = Field(default="Product Manager", alias='profile') + goal: str = "Efficiently create a successful product" + constraints: str = "" + """ + Represents a Product Manager role responsible for product development and management. + """ def __init__( - self, - name: str = "Alice", - profile: str = "Product Manager", - goal: str = "Efficiently create a successful product", - constraints: str = "", + self, + **kwargs ) -> None: - """ - Initializes the ProductManager role with given attributes. - - Args: - name (str): Name of the product manager. - profile (str): Role profile. - goal (str): Goal of the product manager. - constraints (str): Constraints or limitations for the product manager. - """ - super().__init__(name, profile, goal, constraints) + super().__init__(**kwargs) self._init_actions([WritePRD]) self._watch([BossRequirement]) diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 7e7c5699d..a2b227f22 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -5,9 +5,11 @@ @Author : alexanderwu @File : project_manager.py """ +from pydantic import Field + from metagpt.actions import WriteTasks from metagpt.actions.design_api import WriteDesign -from metagpt.roles import Role +from metagpt.roles.role import Role class ProjectManager(Role): @@ -20,23 +22,16 @@ class ProjectManager(Role): goal (str): Goal of the project manager. constraints (str): Constraints or limitations for the project manager. """ + name: str = "Eve" + role_profile: str = Field(default="Project Manager", alias='profile') + + goal: str = "Improve team efficiency and deliver with quality and quantity" + constraints: str = "" def __init__( - self, - name: str = "Eve", - profile: str = "Project Manager", - goal: str = "Improve team efficiency and deliver with quality and quantity", - constraints: str = "", + self, + **kwargs ) -> None: - """ - Initializes the ProjectManager role with given attributes. - - Args: - name (str): Name of the project manager. - profile (str): Role profile. - goal (str): Goal of the project manager. - constraints (str): Constraints or limitations for the project manager. - """ - super().__init__(name, profile, goal, constraints) + super().__init__(**kwargs) self._init_actions([WriteTasks]) self._watch([WriteDesign]) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b96c361c0..9aae64188 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -5,17 +5,26 @@ @Author : alexanderwu @File : role.py """ + from __future__ import annotations -from typing import Iterable, Type, Union -from enum import Enum - +import sys +from types import SimpleNamespace +from typing import ( + Dict, + Optional, + Union, + Iterable, + Type +) +import re from pydantic import BaseModel, Field +from importlib import import_module # from metagpt.environment import Environment from metagpt.config import CONFIG from metagpt.actions import Action, ActionOutput -from metagpt.llm import LLM, HumanProvider +from metagpt.llm import LLM from metagpt.logs import logger from metagpt.memory import Memory, LongTermMemory from metagpt.schema import Message @@ -28,14 +37,12 @@ Please note that only the text between the first and second "===" is information {history} === -Your previous stage: {previous_state} - -Now choose one of the following stages you need to go to in the next step: +You can now choose one of the following stages to decide the stage you need to go in the next step: {states} Just answer a number between 0-{n_states}, choose the most suitable stage according to the understanding of the conversation. Please note that the answer only needs a number, no need to add any other text. -If you think you have completed your goal and don't need to go to any of the stages, return -1. +If there is no conversation record, choose 0. Do not answer anything else, and do not add any other information in your answer. """ @@ -49,27 +56,18 @@ ROLE_TEMPLATE = """Your response should be based on the previous conversation hi {name}: {result} """ -class RoleReactMode(str, Enum): - REACT = "react" - BY_ORDER = "by_order" - PLAN_AND_ACT = "plan_and_act" - - @classmethod - def values(cls): - return [item.value for item in cls] class RoleSetting(BaseModel): """Role Settings""" - name: str - profile: str - goal: str - constraints: str - desc: str - is_human: bool - + name: str = "" + profile: str = "" + goal: str = "" + constraints: str = "" + desc: str = "" + def __str__(self): return f"{self.name}({self.profile})" - + def __repr__(self): return self.__str__() @@ -79,109 +77,128 @@ class RoleContext(BaseModel): env: 'Environment' = Field(default=None) memory: Memory = Field(default_factory=Memory) long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory) - state: int = Field(default=-1) # -1 indicates initial or termination state where todo is None + state: int = Field(default=0) todo: Action = Field(default=None) watch: set[Type[Action]] = Field(default_factory=set) news: list[Type[Message]] = Field(default=[]) - react_mode: RoleReactMode = RoleReactMode.REACT # see `Role._set_react_mode` for definitions of the following two attributes - max_react_loop: int = 1 - + class Config: arbitrary_types_allowed = True - + def check(self, role_id: str): if hasattr(CONFIG, "long_term_memory") and CONFIG.long_term_memory: self.long_term_memory.recover_memory(role_id, self) self.memory = self.long_term_memory # use memory to act as long_term_memory for unify operation - + @property def important_memory(self) -> list[Message]: """Get the information corresponding to the watched actions""" return self.memory.get_by_actions(self.watch) - + @property def history(self) -> list[Message]: return self.memory.get() -class Role: +class Role(BaseModel): """Role/Agent""" - - def __init__(self, name="", profile="", goal="", constraints="", desc="", is_human=False): - self._llm = LLM() if not is_human else HumanProvider() - self._setting = RoleSetting(name=name, profile=profile, goal=goal, - constraints=constraints, desc=desc, is_human=is_human) - self._states = [] - self._actions = [] - self._role_id = str(self._setting) - self._rc = RoleContext() - + name: str = "" + profile: str = "" + goal: str = "" + constraints: str = "" + desc: str = "" + _setting: RoleSetting = Field(default_factory=RoleSetting, alias="_setting") + _setting = RoleSetting(name=name, profile=profile, goal=goal, constraints=constraints) + _role_id: str = "" + _states: list = Field(default=[]) + _actions: list = Field(default=[]) + _actions_type: list = Field(default=[]) + _rc: RoleContext = RoleContext() + + _private_attributes = { + '_setting': _setting, + '_role_id': _role_id, + '_states': [], + '_actions': [], + '_actions_type': [] # 用于记录和序列化 + } + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # 关于私有变量的初始化 https://github.com/pydantic/pydantic/issues/655 + for key in self._private_attributes.keys(): + if key in kwargs: + object.__setattr__(self, key, kwargs[key]) + if key =="_setting": + _setting = RoleSetting(**kwargs[key]) + object.__setattr__(self, '_setting', _setting) + elif key == "_rc": + _rc = RoleContext + object.__setattr__(self, '_rc', _rc) + else: + object.__setattr__(self, key, self._private_attributes[key]) + def _reset(self): - self._states = [] - self._actions = [] + object.__setattr__(self, '_states', []) + object.__setattr__(self, '_actions', []) + + + @staticmethod + def _process_class(class_str, module_name): + cleaned_string = re.sub(r"[<>']", "", class_str).replace("class ", "") + package_name = "metagpt" + file_name = cleaned_string.replace(package_name, "").replace("." + module_name, "") + print(file_name) + # print("\n", sys.modules) + module_file = import_module(file_name, package=package_name) + module = getattr(module_file, module_name) + return module + def _init_actions(self, actions): self._reset() for idx, action in enumerate(actions): if not isinstance(action, Action): - i = action("", llm=self._llm) + ## 默认初始化 + i = action() else: - if self._setting.is_human and not isinstance(action.llm, HumanProvider): - logger.warning(f"is_human attribute does not take effect," - f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances") i = action i.set_prefix(self._get_prefix(), self.profile) self._actions.append(i) self._states.append(f"{idx}. {action}") - - def _set_react_mode(self, react_mode: str, max_react_loop: int = 1): - """Set strategy of the Role reacting to observed Message. Variation lies in how - this Role elects action to perform during the _think stage, especially if it is capable of multiple Actions. - - Args: - react_mode (str): Mode for choosing action during the _think stage, can be one of: - "react": standard think-act loop in the ReAct paper, alternating thinking and acting to solve the task, i.e. _think -> _act -> _think -> _act -> ... - Use llm to select actions in _think dynamically; - "by_order": switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ...; - "plan_and_act": first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... - Use llm to come up with the plan dynamically. - Defaults to "react". - max_react_loop (int): Maximum react cycles to execute, used to prevent the agent from reacting forever. - Take effect only when react_mode is react, in which we use llm to choose actions, including termination. - Defaults to 1, i.e. _think -> _act (-> return result and end) - """ - assert react_mode in RoleReactMode.values(), f"react_mode must be one of {RoleReactMode.values()}" - self._rc.react_mode = react_mode - if react_mode == RoleReactMode.REACT: - self._rc.max_react_loop = max_react_loop - + action_title = action.schema()["title"] + self._actions_type.append(action_title) + def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" self._rc.watch.update(actions) # check RoleContext after adding watch actions self._rc.check(self._role_id) - - def _set_state(self, state: int): + + def _set_state(self, state): """Update the current state.""" self._rc.state = state logger.debug(self._actions) - self._rc.todo = self._actions[self._rc.state] if state >= 0 else None - + 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.""" self._rc.env = env - + @property def profile(self): """Get the role description (position)""" return self._setting.profile - + def _get_prefix(self): """Get the role prefix""" if self._setting.desc: return self._setting.desc return PREFIX_TEMPLATE.format(**self._setting.dict()) - + async def _think(self) -> None: """Think about what to do and decide on the next action""" if len(self._actions) == 1: @@ -190,104 +207,60 @@ class Role: return prompt = self._get_prefix() prompt += STATE_TEMPLATE.format(history=self._rc.history, states="\n".join(self._states), - n_states=len(self._states) - 1, previous_state=self._rc.state) - # print(prompt) + n_states=len(self._states) - 1) next_state = await self._llm.aask(prompt) logger.debug(f"{prompt=}") - if (not next_state.isdigit() and next_state != "-1") \ - or int(next_state) not in range(-1, len(self._states)): - logger.warning(f'Invalid answer of state, {next_state=}, will be set to -1') - next_state = -1 - else: - next_state = int(next_state) - if next_state == -1: - logger.info(f"End actions with {next_state=}") - self._set_state(next_state) - + if not next_state.isdigit() or int(next_state) not in range(len(self._states)): + 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)) + role=self.profile, cause_by=type(self._rc.todo)) else: msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) self._rc.memory.add(msg) # logger.debug(f"{response}") - + 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 - + 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) - + 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}') return len(self._rc.news) - + def _publish_message(self, msg): """If the role belongs to env, then the role's messages will be broadcast to env""" if not self._rc.env: # If env does not exist, do not publish the message return self._rc.env.publish_message(msg) - + async def _react(self) -> Message: - """Think first, then act, until the Role _think it is time to stop and requires no more todo. - This is the standard think-act loop in the ReAct paper, which alternates thinking and acting in task solving, i.e. _think -> _act -> _think -> _act -> ... - Use llm to select actions in _think dynamically - """ - actions_taken = 0 - rsp = Message("No actions taken yet") # will be overwritten after Role _act - while actions_taken < self._rc.max_react_loop: - # think - await self._think() - if self._rc.todo is None: - break - # act - logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}") - rsp = await self._act() - actions_taken += 1 - return rsp # return output from the last action - - async def _act_by_order(self) -> Message: - """switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ...""" - for i in range(len(self._states)): - self._set_state(i) - rsp = await self._act() - return rsp # return output from the last action - - async def _plan_and_act(self) -> Message: - """first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically.""" - # TODO: to be implemented - return Message("") - - async def react(self) -> Message: - """Entry to one of three strategies by which Role reacts to the observed Message""" - if self._rc.react_mode == RoleReactMode.REACT: - rsp = await self._react() - elif self._rc.react_mode == RoleReactMode.BY_ORDER: - rsp = await self._act_by_order() - elif self._rc.react_mode == RoleReactMode.PLAN_AND_ACT: - rsp = await self._plan_and_act() - self._set_state(state=-1) # current reaction is complete, reset state to -1 and todo back to None - return rsp - + """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}" @@ -295,18 +268,14 @@ class Role: 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() - - def get_memories(self, k=0) -> list[Message]: - """A wrapper to return the most recent k memories of this role, return all when k=0""" - return self._rc.memory.get(k=k) - + async def run(self, message=None): """Observe, and think and act based on the results of the observation""" if message: @@ -320,8 +289,8 @@ class Role: # If there is no new information, suspend and wait logger.debug(f"{self._setting}: no news. waiting.") return - - rsp = await self.react() + + rsp = await self._react() # Publish the reply to the environment, waiting for the next subscriber to process self._publish_message(rsp) return rsp From 0dd63e4b2363d30d6c7e5db1705e749f00c9f82f Mon Sep 17 00:00:00 2001 From: stellahsr Date: Mon, 27 Nov 2023 21:13:19 +0800 Subject: [PATCH 3/3] update test cases for serialize_deserialize --- .../metagpt/serialize_deserialize/__init__.py | 4 ++ .../serialize_deserialize/test_actions.py | 24 ++++++++++ .../test_architect_deserialize.py | 26 ++++++++++ .../test_product_manager.py | 21 +++++++++ .../test_project_manager.py | 26 ++++++++++ .../serialize_deserialize/test_role.py | 41 ++++++++++++++++ .../serialize_deserialize/test_team.py | 47 +++++++++++++++++++ .../serialize_deserialize/test_wrire_prd.py | 28 +++++++++++ .../serialize_deserialize/test_write_code.py | 42 +++++++++++++++++ .../test_write_design.py | 39 +++++++++++++++ 10 files changed, 298 insertions(+) create mode 100644 tests/metagpt/serialize_deserialize/__init__.py create mode 100644 tests/metagpt/serialize_deserialize/test_actions.py create mode 100644 tests/metagpt/serialize_deserialize/test_architect_deserialize.py create mode 100644 tests/metagpt/serialize_deserialize/test_product_manager.py create mode 100644 tests/metagpt/serialize_deserialize/test_project_manager.py create mode 100644 tests/metagpt/serialize_deserialize/test_role.py create mode 100644 tests/metagpt/serialize_deserialize/test_team.py create mode 100644 tests/metagpt/serialize_deserialize/test_wrire_prd.py create mode 100644 tests/metagpt/serialize_deserialize/test_write_code.py create mode 100644 tests/metagpt/serialize_deserialize/test_write_design.py diff --git a/tests/metagpt/serialize_deserialize/__init__.py b/tests/metagpt/serialize_deserialize/__init__.py new file mode 100644 index 000000000..78f454fb5 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# @Date : 11/22/2023 11:48 AM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : diff --git a/tests/metagpt/serialize_deserialize/test_actions.py b/tests/metagpt/serialize_deserialize/test_actions.py new file mode 100644 index 000000000..e2efa982b --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_actions.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# @Date : 11/22/2023 11:48 AM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.actions import Action +from metagpt.llm import LLM + +def test_action_serialize(): + action = Action() + ser_action_dict = action.dict() + assert "name" in ser_action_dict + assert "llm" in ser_action_dict + +@pytest.mark.asyncio +async def test_action_deserialize(): + action = Action() + serialized_data = action.dict() + + new_action = Action(**serialized_data) + assert new_action.name == "" + assert new_action.llm == LLM() + assert len(await new_action._aask("who are you")) > 0 diff --git a/tests/metagpt/serialize_deserialize/test_architect_deserialize.py b/tests/metagpt/serialize_deserialize/test_architect_deserialize.py new file mode 100644 index 000000000..cff1bbadd --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_architect_deserialize.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# @Date : 11/26/2023 2:04 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.roles.architect import Architect +from metagpt.actions.action import Action + +def test_architect_serialize(): + role = Architect() + ser_role_dict = role.dict(by_alias=True) + assert "name" in ser_role_dict + assert "_states" in ser_role_dict + assert "_actions" in ser_role_dict + +@pytest.mark.asyncio +async def test_architect_deserialize(): + role = Architect() + ser_role_dict = role.dict(by_alias=True) + new_role = Architect(**ser_role_dict) + # new_role = Architect.deserialize(ser_role_dict) + assert new_role.name == "Bob" + assert len(new_role._actions) == 1 + assert isinstance(new_role._actions[0], Action) + await new_role._actions[0].run(context="write a cli snake game") \ No newline at end of file diff --git a/tests/metagpt/serialize_deserialize/test_product_manager.py b/tests/metagpt/serialize_deserialize/test_product_manager.py new file mode 100644 index 000000000..978c50e5e --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_product_manager.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# @Date : 11/26/2023 2:07 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.roles.product_manager import ProductManager +from metagpt.actions.action import Action +from metagpt.schema import Message + +@pytest.mark.asyncio +async def test_product_manager_deserialize(): + role = ProductManager() + ser_role_dict = role.dict(by_alias=True) + new_role = ProductManager(**ser_role_dict) + # new_role = ProductManager().deserialize(ser_role_dict) + + assert new_role.name == "Alice" + assert len(new_role._actions) == 1 + assert isinstance(new_role._actions[0], Action) + await new_role._actions[0].run([Message(content="write a cli snake game")]) \ No newline at end of file diff --git a/tests/metagpt/serialize_deserialize/test_project_manager.py b/tests/metagpt/serialize_deserialize/test_project_manager.py new file mode 100644 index 000000000..590bd8109 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_project_manager.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# @Date : 11/26/2023 2:06 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.roles.project_manager import ProjectManager +from metagpt.actions.action import Action + +def test_project_manager_serialize(): + role = ProjectManager() + ser_role_dict = role.dict(by_alias=True) + assert "name" in ser_role_dict + assert "_states" in ser_role_dict + assert "_actions" in ser_role_dict + +@pytest.mark.asyncio +async def test_project_manager_deserialize(): + role = ProjectManager() + ser_role_dict = role.dict(by_alias=True) + new_role = ProjectManager(**ser_role_dict) + # new_role = ProjectManager().deserialize(ser_role_dict) + assert new_role.name == "Eve" + assert len(new_role._actions) == 1 + assert isinstance(new_role._actions[0], Action) + await new_role._actions[0].run(context="write a cli snake game") \ No newline at end of file diff --git a/tests/metagpt/serialize_deserialize/test_role.py b/tests/metagpt/serialize_deserialize/test_role.py new file mode 100644 index 000000000..432c9acb7 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_role.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# @Date : 11/23/2023 4:49 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.roles.role import Role +from metagpt.roles.engineer import Engineer + +from metagpt.actions.action import Action + + +def test_role_serialize(): + role = Role() + ser_role_dict = role.dict(by_alias=True) + assert "name" in ser_role_dict + assert "_states" in ser_role_dict + assert "_actions" in ser_role_dict + + +def test_engineer_serialize(): + role = Engineer() + ser_role_dict = role.dict(by_alias=True) + assert "name" in ser_role_dict + assert "_states" in ser_role_dict + assert "_actions" in ser_role_dict + + +@pytest.mark.asyncio +async def test_engineer_deserialize(): + role = Engineer(use_code_review=True) + ser_role_dict = role.dict(by_alias=True) + # new_role = Engineer().deserialize(ser_role_dict) + # also can be deserialized in this way: + new_role = Engineer(**ser_role_dict) + assert new_role.name == "Alex" + assert new_role.use_code_review == True + assert len(new_role._actions) == 2 + assert isinstance(new_role._actions[0], Action) + assert isinstance(new_role._actions[1], Action) + await new_role._actions[0].run(context="write a cli snake game", filename="test_code") diff --git a/tests/metagpt/serialize_deserialize/test_team.py b/tests/metagpt/serialize_deserialize/test_team.py new file mode 100644 index 000000000..44a75d262 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_team.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# @Date : 11/27/2023 10:07 AM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.environment import Environment +from metagpt.schema import Message +from metagpt.software_company import SoftwareCompany +from metagpt.roles import ProjectManager, ProductManager, Architect + + +def test_env_serialize(): + env = Environment() + ser_env_dict = env.dict() + assert "roles" in ser_env_dict + assert "memory" in ser_env_dict + assert "memory" in ser_env_dict + + +def test_env_deserialize(): + env = Environment() + env.publish_message(message=Message(content="test env serialize")) + ser_env_dict = env.dict() + new_env = Environment(**ser_env_dict) + assert len(new_env.roles) == 0 + assert new_env.memory.storage[0].content == "test env serialize" + assert len(new_env.history) == 25 + + +def test_softwarecompany_deserialize(): + team = SoftwareCompany() + team.hire( + [ + ProductManager(), + Architect(), + ProjectManager(), + ] + ) + assert len(team.environment.get_roles()) == 3 + ser_team_dict = team.dict() + new_team = SoftwareCompany(**ser_team_dict) + + assert len(new_team.environment.get_roles()) == 3 + assert new_team.environment.get_role('Product Manager') is not None + assert new_team.environment.get_role('Product Manager') is not None + assert new_team.environment.get_role('Architect') is not None diff --git a/tests/metagpt/serialize_deserialize/test_wrire_prd.py b/tests/metagpt/serialize_deserialize/test_wrire_prd.py new file mode 100644 index 000000000..9b2653820 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_wrire_prd.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# @Date : 11/22/2023 1:47 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.actions import WritePRD +from metagpt.llm import LLM +from metagpt.schema import Message + + +def test_action_serialize(): + action = WritePRD() + ser_action_dict = action.dict() + assert "name" in ser_action_dict + assert "llm" in ser_action_dict + + +@pytest.mark.asyncio +async def test_action_deserialize(): + action = WritePRD() + serialized_data = action.dict() + new_action = WritePRD(**serialized_data) + # new_action = WritePRD().deserialize(serialized_data) + assert new_action.name == "" + assert new_action.llm == LLM() + assert len(await new_action.run([Message(content="write a cli snake game")]))>0 + diff --git a/tests/metagpt/serialize_deserialize/test_write_code.py b/tests/metagpt/serialize_deserialize/test_write_code.py new file mode 100644 index 000000000..0b1f1dc7c --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_write_code.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Date : 11/23/2023 10:56 AM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.actions import WriteCode, WriteCodeReview +from metagpt.llm import LLM + +def test_write_design_serialize(): + action = WriteCode() + ser_action_dict = action.dict() + assert ser_action_dict["name"] == "WriteCode" + assert "llm" in ser_action_dict + +def test_write_task_serialize(): + action = WriteCodeReview() + ser_action_dict = action.dict() + assert ser_action_dict["name"] == "WriteCodeReview" + assert "llm" in ser_action_dict + +@pytest.mark.asyncio +async def test_write_code_deserialize(): + action = WriteCode() + serialized_data = action.dict() + new_action = WriteCode(**serialized_data) + # new_action = WriteCode().deserialize(serialized_data) + assert new_action.name == "WriteCode" + assert new_action.llm == LLM() + await new_action.run(context="write a cli snake game", filename="test_code") + +@pytest.mark.asyncio +async def test_write_code_review_deserialize(): + action = WriteCodeReview() + serialized_data = action.dict() + new_action = WriteCodeReview(**serialized_data) + # new_action = WriteCodeReview().deserialize(serialized_data) + code = await WriteCode().run(context="write a cli snake game", filename="test_code") + + assert new_action.name == "WriteCodeReview" + assert new_action.llm == LLM() + await new_action.run(context="write a cli snake game", code =code, filename="test_rewrite_code") \ No newline at end of file diff --git a/tests/metagpt/serialize_deserialize/test_write_design.py b/tests/metagpt/serialize_deserialize/test_write_design.py new file mode 100644 index 000000000..56bf78a63 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_write_design.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# @Date : 11/22/2023 8:19 PM +# @Author : stellahong (stellahong@fuzhi.ai) +# @Desc : +import pytest + +from metagpt.actions import WriteDesign, WriteTasks +from metagpt.llm import LLM + +def test_write_design_serialize(): + action = WriteDesign() + ser_action_dict = action.dict() + assert "name" in ser_action_dict + assert "llm" in ser_action_dict + +def test_write_task_serialize(): + action = WriteTasks() + ser_action_dict = action.dict() + assert "name" in ser_action_dict + assert "llm" in ser_action_dict + +@pytest.mark.asyncio +async def test_write_design_deserialize(): + action = WriteDesign() + serialized_data = action.dict() + new_action = WriteDesign().deserialize(serialized_data) + assert new_action.name == "" + assert new_action.llm == LLM() + await new_action.run(context="write a cli snake game") + +@pytest.mark.asyncio +async def test_write_task_deserialize(): + action = WriteTasks() + serialized_data = action.dict() + new_action = WriteTasks(**serialized_data) + # new_action = WriteTasks().deserialize(serialized_data) + assert new_action.name == "CreateTasks" + assert new_action.llm == LLM() + await new_action.run(context="write a cli snake game") \ No newline at end of file