diff --git a/README.md b/README.md index fc8781014..e80082a3a 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 6721ca9ca..038925184 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 a38b92f5b..411d190b4 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 を準備する diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index a538baa77..aefe6d39d 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -7,9 +7,9 @@ """ import re from abc import ABC -from typing import Optional +from typing import Optional, Any -import importlib +from pydantic import BaseModel, Field from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions.action_output import ActionOutput @@ -20,27 +20,27 @@ from metagpt.utils.custom_decoder import CustomDecoder from metagpt.utils.utils import import_class -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__() @@ -80,15 +80,15 @@ class Action(ABC): 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: @@ -97,25 +97,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 d1fa561f0..e867ad6fc 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -67,11 +67,12 @@ class Environment(BaseModel): self.history = history.get("content") 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 9b0613fd5..eb5539f43 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -5,18 +5,28 @@ @Author : alexanderwu @File : role.py """ + +import sys +from enum import Enum +import importlib from __future__ import annotations -from typing import Iterable, Type, Union -from enum import Enum -from pathlib import Path +from types import SimpleNamespace +from typing import ( + Dict, + Optional, + Union, + Iterable, + Type +) +import re from pydantic import BaseModel, Field -import importlib +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 @@ -60,18 +70,18 @@ class RoleReactMode(str, Enum): 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__() @@ -81,43 +91,87 @@ 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() - self._recovered = False + 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): + 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 serialize(self, stg_path: Path): role_info_path = stg_path.joinpath("role_info.json") @@ -216,6 +270,7 @@ class Role: self._reset() for idx, action in enumerate(actions): if not isinstance(action, Action): + ## 默认初始化 i = action("", llm=self._llm) else: if self._setting.is_human and not isinstance(action.llm, HumanProvider): @@ -225,6 +280,8 @@ class Role: i.set_prefix(self._get_prefix(), self.profile) self._actions.append(i) self._states.append(f"{idx}. {action}") + action_title = action.schema()["title"] + self._actions_type.append(action_title) def set_react_mode(self, react_mode: RoleReactMode, max_react_loop: int = 1): self._set_react_mode(react_mode, max_react_loop) @@ -267,11 +324,11 @@ class Role: self._rc.state = state logger.debug(self._actions) self._rc.todo = self._actions[self._rc.state] if state >= 0 else None - + 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 name(self): return self._setting.name @@ -280,13 +337,13 @@ class Role: 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: @@ -301,7 +358,6 @@ class Role: 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) next_state = await self._llm.aask(prompt) logger.debug(f"{prompt=}") if (not next_state.isdigit() and next_state != "-1") \ @@ -313,50 +369,46 @@ class Role: if next_state == -1: logger.info(f"End actions with {next_state=}") self._set_state(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 - + 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 -> ... @@ -406,18 +458,18 @@ 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: @@ -431,7 +483,7 @@ class Role: # 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) 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