From c8570036fc92be30d2513a95c72ed9d0dc73bc55 Mon Sep 17 00:00:00 2001 From: stellahsr Date: Mon, 27 Nov 2023 21:12:50 +0800 Subject: [PATCH] update basic code for serialize --- metagpt/actions/action.py | 57 ++++--- metagpt/actions/design_api.py | 20 ++- metagpt/actions/project_management.py | 13 +- metagpt/actions/search_and_summarize.py | 44 ++++-- metagpt/actions/write_code.py | 14 +- metagpt/actions/write_code_review.py | 8 +- metagpt/actions/write_prd.py | 14 +- metagpt/const.py | 2 +- metagpt/environment.py | 26 ++-- metagpt/roles/architect.py | 21 ++- metagpt/roles/engineer.py | 33 ++-- metagpt/roles/product_manager.py | 41 +++-- metagpt/roles/project_manager.py | 30 ++-- metagpt/roles/role.py | 193 +++++++++++++----------- 14 files changed, 270 insertions(+), 246 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 3bfb69de4..e890ef76a 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -6,10 +6,9 @@ @File : action.py """ -from abc import ABC -from typing import Optional - +from typing import Optional, Any from tenacity import retry, stop_after_attempt, wait_random_exponential +from pydantic import BaseModel, Field from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM @@ -20,25 +19,22 @@ from metagpt.utils.utils import general_after_log 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 = "" # aask*时会加上prefix,作为system_message - self.profile = "" # FIXME: USELESS - self.desc = "" # for skill manager - self.nodes = ... +action_subclass_registry = {} - # Output, useless - # self.content = "" - # self.instruct_content = None - # self.env = None - # def set_env(self, env): - # self.env = env +class Action(BaseModel): + name: str = "" + llm: LLM = Field(default_factory=LLM) + context = None + prefix = "" # aask*时会加上prefix,作为system_message + profile = "" # FIXME: USELESS + desc = "" # for skill manager + nodes = None + # 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""" @@ -95,27 +91,26 @@ class Action(ABC): after=general_after_log(logger), ) 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: content = await self.llm.aask(prompt, system_msgs) logger.debug(f"llm raw output:\n{content}") output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - + if format == "json": parsed_data = llm_output_postprecess(output=content, schema=output_class.schema(), req_key="[/CONTENT]") - else: # using markdown parser parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) - - logger.debug(f"parsed_data:\n{parsed_data}") + + 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 5a5f52de7..a10ff1c9a 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -11,9 +11,12 @@ """ import json from pathlib import Path +from typing import Optional +from pydantic import Field from metagpt.actions import Action, ActionOutput from metagpt.actions.design_api_an import DESIGN_API_NODE +from metagpt.llm import LLM from metagpt.config import CONFIG from metagpt.const import ( DATA_API_DESIGN_FILE_REPO, @@ -25,12 +28,8 @@ from metagpt.const import ( from metagpt.logs import logger from metagpt.schema import Document, Documents from metagpt.utils.file_repository import FileRepository - -# from metagpt.utils.get_template import get_template from metagpt.utils.mermaid import mermaid_to_file -# from typing import List - NEW_REQ_TEMPLATE = """ ### Legacy Content @@ -42,13 +41,12 @@ NEW_REQ_TEMPLATE = """ 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." async def run(self, with_messages, format=CONFIG.prompt_format): # Use `git diff` to identify which PRD documents have been modified in the `docs/prds` directory. diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 1f14e7944..d830a4c15 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -9,11 +9,15 @@ 2. Move the document storage operations related to WritePRD from the save operation of WriteDesign. 3. According to the design in Section 2.2.3.5.4 of RFC 135, add incremental iteration functionality. """ + import json +from typing import List, Optional, Any +from pydantic import Field from metagpt.actions import ActionOutput from metagpt.actions.action import Action from metagpt.actions.project_management_an import PM_NODE +from metagpt.llm import LLM from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -24,10 +28,8 @@ from metagpt.const import ( from metagpt.logs import logger from metagpt.schema import Document, Documents from metagpt.utils.file_repository import FileRepository +from metagpt.provider.base_gpt_api import BaseGPTAPI -# from typing import List - -# from metagpt.utils.get_template import get_template NEW_REQ_TEMPLATE = """ ### Legacy Content @@ -39,8 +41,9 @@ NEW_REQ_TEMPLATE = """ 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: BaseGPTAPI = Field(default_factory=LLM) async def run(self, with_messages, format=CONFIG.prompt_format): system_design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) diff --git a/metagpt/actions/search_and_summarize.py b/metagpt/actions/search_and_summarize.py index 5e4cdaea0..7b549518e 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,38 @@ 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 + search_engine: SearchEngine = 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: 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 +144,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, diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 5960e2621..2d155e6bf 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -14,10 +14,17 @@ 3. Encapsulate the input of RunCode into RunCodeContext and encapsulate the output of RunCode into RunCodeResult to standardize and unify parameter passing between WriteCode, RunCode, and DebugError. """ + import json from tenacity import retry, stop_after_attempt, wait_random_exponential + + +from typing import List, Optional, Any +from pydantic import Field +from tenacity import retry, stop_after_attempt, wait_fixed + from metagpt.actions.action import Action from metagpt.config import CONFIG from metagpt.const import ( @@ -27,6 +34,8 @@ from metagpt.const import ( TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, ) +from metagpt.actions import WriteDesign +from metagpt.llm import LLM from metagpt.logs import logger from metagpt.schema import CodingContext, Document, RunCodeResult from metagpt.utils.common import CodeParser @@ -84,8 +93,9 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc class WriteCode(Action): - def __init__(self, name="WriteCode", context=None, llm=None): - super().__init__(name, context, llm) + name: str = "WriteCode" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) async def write_code(self, prompt) -> str: diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 4b3e9aece..bf07d0a93 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -8,9 +8,12 @@ WriteCode object, rather than passing them in when calling the run function. """ +from typing import List, Optional, Any +from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions import WriteCode +from metagpt.llm import LLM from metagpt.actions.action import Action from metagpt.config import CONFIG from metagpt.logs import logger @@ -119,8 +122,9 @@ REWRITE_CODE_TEMPLATE = """ class WriteCodeReview(Action): - def __init__(self, name="WriteCodeReview", context=None, llm=None): - super().__init__(name, context, llm) + name: str = "WriteCodeReview" + context: Optional[str] = None + llm: LLM = Field(default_factory=LLM) @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) async def write_code_review_and_rewrite(self, context_prompt, cr_prompt, filename): diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index bb0cf8fb9..7f9089763 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -10,10 +10,13 @@ 3. Move the document storage operations related to WritePRD from the save operation of WriteDesign. @Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD. """ + from __future__ import annotations import json from pathlib import Path +from typing import List, Optional, Any +from pydantic import BaseModel, Field from metagpt.actions import Action, ActionOutput from metagpt.actions.action_node import ActionNode @@ -23,6 +26,8 @@ from metagpt.actions.write_prd_an import ( WP_ISSUE_TYPE_NODE, WRITE_PRD_NODE, ) +from metagpt.llm import LLM +from metagpt.actions.search_and_summarize import SearchAndSummarize from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, @@ -36,12 +41,8 @@ from metagpt.logs import logger from metagpt.schema import BugFixContext, Document, Documents, Message from metagpt.utils.common import CodeParser from metagpt.utils.file_repository import FileRepository - -# from metagpt.utils.get_template import get_template from metagpt.utils.mermaid import mermaid_to_file -# from typing import List - CONTEXT_TEMPLATE = """ ### Project Name @@ -64,8 +65,9 @@ NEW_REQ_TEMPLATE = """ 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) async def run(self, with_messages, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput | Message: # Determine which requirement documents need to be rewritten: Use LLM to assess whether new requirements are diff --git a/metagpt/const.py b/metagpt/const.py index b46bc15a4..9cf9726fc 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -60,7 +60,7 @@ SWAGGER_PATH = UT_PATH / "files/api/" UT_PY_PATH = UT_PATH / "files/ut/" API_QUESTIONS_PATH = UT_PATH / "files/question/" -SERDES_PATH = DEFAULT_WORKSPACE_ROOT / "storage" # TODO to store `storage` under the individual generated project +SERDESER_PATH = DEFAULT_WORKSPACE_ROOT / "storage" # TODO to store `storage` under the individual generated project TMP = METAGPT_ROOT / "tmp" diff --git a/metagpt/environment.py b/metagpt/environment.py index 14da6cd95..19197bd10 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -54,31 +54,33 @@ class Environment(BaseModel): write_json_file(history_path, {"content": self.history}) def deserialize(self, stg_path: Path): + """ stg_path: ./storage/team/environment/ """ """ stg_path: ./storage/team/environment/ """ roles_path = stg_path.joinpath("roles.json") roles_info = read_json_file(roles_path) + roles = [] for role_info in roles_info: - role_class = role_info.get("role_class") - role_name = role_info.get("role_name") - - role_path = stg_path.joinpath(f"roles/{role_class}_{role_name}") + # role stored in ./environment/roles/{role_class}_{role_name} + role_path = stg_path.joinpath(f'roles/{role_info.get("role_class")}_{role_info.get("role_name")}') role = Role.deserialize(role_path) + roles.append(role) - self.add_role(role) + history = read_json_file(stg_path.joinpath("history.json")) + history = history.get("content") - memory = Memory.deserialize(stg_path) - self.memory = memory - - history_path = stg_path.joinpath("history.json") - history = read_json_file(history_path) - self.history = history.get("content") + environment = Environment(**{ + "history": history + }) + environment.add_roles(roles) + return environment def add_role(self, role: Role): """增加一个在当前环境的角色 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 fa91d393d..377531c8d 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,18 +22,14 @@ class Architect(Role): goal (str): Primary goal or responsibility of the architect. constraints (str): Constraints or guidelines for the architect. """ + name: str = "Bob" + profile: str = Field(default="Architect", alias='profile') + goal: str = "design a concise, usable, complete software system" + constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries." \ + "Use same language as user requirement" - def __init__( - self, - name: str = "Bob", - profile: str = "Architect", - goal: str = "design a concise, usable, complete software system", - constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries." - "Use same language as user requirement" - ) -> None: - """Initializes the Architect with given attributes.""" - super().__init__(name, profile, goal, constraints) - + def __init__(self, **kwargs) -> None: + 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 f1e65b177..59ca18a17 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -16,8 +16,9 @@ @Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results of SummarizeCode. """ -from __future__ import annotations +from __future__ import annotations +from pydantic import Field import json from collections import defaultdict from pathlib import Path @@ -44,9 +45,11 @@ from metagpt.schema import ( ) from metagpt.utils.common import any_to_str, any_to_str_set + IS_PASS_PROMPT = """ {context} +<<<<<<< HEAD ---- Does the above log indicate anything that needs to be done? If there are any tasks to be completed, please answer 'NO' along with the to-do list in JSON format; @@ -66,25 +69,21 @@ class Engineer(Role): n_borg (int): Number of borgs. use_code_review (bool): Whether to use code review. """ + 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 google-style and be modular and maintainable. " \ + "Use same language as user requirement", + n_borg: int = 1 + use_code_review: bool = False + code_todos: list = [] + summarize_todos = [] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) - 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 google-style and be modular and maintainable. " - "Use same language as user requirement", - n_borg: int = 1, - use_code_review: bool = False, - ) -> None: - """Initializes the Engineer role with given attributes.""" - super().__init__(name, profile, goal, constraints) - self.use_code_review = use_code_review self._init_actions([WriteCode]) self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug]) - self.code_todos = [] - self.summarize_todos = [] - self.n_borg = n_borg @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index e5e9f2b5e..a49459fca 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -7,40 +7,33 @@ @Modified By: mashenquan, 2023/11/27. Add `PrepareDocuments` action according to Section 2.2.3.5.1 of RFC 135. """ +from pydantic import Field + from metagpt.actions import UserRequirement, WritePRD from metagpt.actions.prepare_documents import PrepareDocuments from metagpt.config import CONFIG -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. + Represents a Project Manager role responsible for overseeing project execution and team efficiency. Attributes: - name (str): Name of the product manager. - profile (str): Role profile, default is 'Product Manager'. - goal (str): Goal of the product manager. - constraints (str): Constraints or limitations for the product manager. + name (str): Name of the project manager. + profile (str): Role profile, default is 'Project Manager'. + goal (str): Goal of the project manager. + constraints (str): Constraints or limitations for the project manager. """ - - def __init__( - self, - name: str = "Alice", - profile: str = "Product Manager", - goal: str = "efficiently create a successful product", - constraints: str = "use same language as user requirement", - ) -> 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) + name: str = "Alice" + role_profile: str = Field(default="Product Manager", alias='profile') + goal: str = "efficiently create a successful product" + constraints: str = "use same language as user requiremen" + """ + Represents a Product Manager role responsible for product development and management. + """ + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) self._init_actions([PrepareDocuments, WritePRD]) self._watch([UserRequirement, PrepareDocuments]) diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 5a2b9be50..211e41d3b 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,24 +22,14 @@ class ProjectManager(Role): goal (str): Goal of the project manager. constraints (str): Constraints or limitations for the project manager. """ + name: str = "Eve" + profile: str = Field(default="Project Manager") + + goal: str = "reak down tasks according to PRD/technical design, generate a task list, and analyze task " \ + "dependencies to start with the prerequisite modules" + constraints: str = "use same language as user requirement" - def __init__( - self, - name: str = "Eve", - profile: str = "Project Manager", - goal: str = "break down tasks according to PRD/technical design, generate a task list, and analyze task " - "dependencies to start with the prerequisite modules", - constraints: str = "use same language as user requirement", - ) -> 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) + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) self._init_actions([WriteTasks]) self._watch([WriteDesign]) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index bb3b2acfe..07a78e4bb 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -18,14 +18,16 @@ @Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing functionality is to be consolidated into the `Environment` class. """ + from __future__ import annotations + from enum import Enum from typing import Iterable, Set, Type from pathlib import Path from pydantic import BaseModel, Field -from metagpt.actions import Action, ActionOutput +from metagpt.actions.action import Action, ActionOutput, action_subclass_registry from metagpt.actions.action_node import ActionNode from metagpt.actions.add_requirement import UserRequirement from metagpt.llm import LLM, HumanProvider @@ -35,6 +37,8 @@ from metagpt.utils.common import any_to_str from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output from metagpt.memory import Memory from metagpt.utils.utils import read_json_file, write_json_file, import_class +from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.const import SERDESER_PATH PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -45,14 +49,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. """ @@ -89,7 +91,7 @@ class RoleSetting(BaseModel): def __str__(self): return f"{self.name}({self.profile})" - + def __repr__(self): return self.__str__() @@ -112,7 +114,7 @@ class RoleContext(BaseModel): 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) @@ -123,7 +125,7 @@ class RoleContext(BaseModel): 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() @@ -139,56 +141,99 @@ class _RoleInjector(type): return instance -class Role(metaclass=_RoleInjector): - """Role/Agent""" +role_subclass_registry = {} - 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._llm.system_prompt = self._get_prefix() - self._states = [] - self._actions = [] - self._role_id = str(self._setting) - self._rc = RoleContext() + +class Role(BaseModel): + """Role/Agent""" + name: str = "" + profile: str = "" + goal: str = "" + constraints: str = "" + desc: str = "" + is_human: bool = False + + _llm: BaseGPTAPI = Field(default_factory=LLM) + _role_id: str = "" + _states: list[str] = Field(default=[]) + _actions: list[Action] = Field(default=[]) + _rc: RoleContext = Field(default=RoleContext) + _subscription: tuple = set() + + # builtin variables + recovered: bool = False # to tag if a recovered role + builtin_class_name: str = "" + + _private_attributes = { + "_llm": LLM() if not is_human else HumanProvider(), + "_role_id": _role_id, + "_states": [], + "_actions": [], + "_rc": RoleContext() + } + + class Config: + arbitrary_types_allowed = True + exclude = ["_llm"] + + def __init__(self, **kwargs): + for index in range(len(kwargs.get("_actions", []))): + current_action = kwargs["_actions"][index] + if isinstance(current_action, dict): + item_class_name = current_action.get("builtin_class_name", None) + for name, subclass in action_subclass_registry.items(): + registery_class_name = subclass.__fields__["builtin_class_name"].default + if item_class_name == registery_class_name: + current_action = subclass(**current_action) + break + kwargs["_actions"][index] = current_action + + super().__init__(**kwargs) + + # 关于私有变量的初始化 https://github.com/pydantic/pydantic/issues/655 + self._private_attributes["_llm"] = LLM() if not self.is_human else HumanProvider() + self._private_attributes["_role_id"] = str(self._setting) self._subscription = {any_to_str(self), name} if name else {any_to_str(self)} - self._recovered = False + for key in self._private_attributes.keys(): + if key in kwargs: + object.__setattr__(self, key, kwargs[key]) + if key == "_rc": + _rc = RoleContext(**kwargs["_rc"]) + object.__setattr__(self, "_rc", _rc) + else: + if key == "_rc": + # # Warning, if use self._private_attributes["_rc"], + # # self._rc will be a shared object between roles, so init one or reset it inside `_reset` + object.__setattr__(self, key, RoleContext()) + else: + object.__setattr__(self, key, self._private_attributes[key]) + + # deserialize child classes dynamically for inherited `role` + object.__setattr__(self, "builtin_class_name", self.__class__.__name__) + self.__fields__["builtin_class_name"].default = self.__class__.__name__ + + def _reset(self): + object.__setattr__(self, '_states', []) + object.__setattr__(self, '_actions', []) + + @property + def _setting(self): + return f"{self.name}({self.profile})" def serialize(self, stg_path: Path): - role_info_path = stg_path.joinpath("role_info.json") - role_info = { + stg_path = SERDESER_PATH.joinpath(f"team/environment/roles/{self.__class__.__name__}_{self.name}") \ + if stg_path is None else stg_path + + role_info = self.dict(exclude={"_rc": {"memory": True}, "_llm": True}) + role_info.update({ "role_class": self.__class__.__name__, "module_name": self.__module__ - } - setting = self._setting.dict() - setting.pop("desc") - setting.pop("is_human") # not all inherited roles have this atrr - role_info.update(setting) + }) + role_info_path = stg_path.joinpath("role_info.json") write_json_file(role_info_path, role_info) - actions_info_path = stg_path.joinpath("actions/actions_info.json") - actions_info = [] - for action in self._actions: - actions_info.append(action.serialize()) - write_json_file(actions_info_path, actions_info) - - watches_info_path = stg_path.joinpath("watches/watches_info.json") - watches_info = [] - for watch in self._rc.watch: - watches_info.append(watch.ser_class()) - write_json_file(watches_info_path, watches_info) - - actions_todo_path = stg_path.joinpath("actions/todo.json") - actions_todo = { - "cur_state": self._rc.state, - "react_mode": self._rc.react_mode.value, - "max_react_loop": self._rc.max_react_loop - } - write_json_file(actions_todo_path, actions_todo) - - self._rc.memory.serialize(stg_path) + self._rc.memory.serialize(stg_path) # serialize role's memory alone @classmethod def deserialize(cls, stg_path: Path) -> "Role": @@ -201,45 +246,13 @@ class Role(metaclass=_RoleInjector): role_class = import_class(class_name=role_class_str, module_name=module_name) role = role_class(**role_info) # initiate particular Role - actions_info_path = stg_path.joinpath("actions/actions_info.json") - actions = [] - actions_info = read_json_file(actions_info_path) - for action_info in actions_info: - action = Action.deserialize(action_info) - actions.append(action) - - watches_info_path = stg_path.joinpath("watches/watches_info.json") - watches = [] - watches_info = read_json_file(watches_info_path) - for watch_info in watches_info: - action = Action.deser_class(watch_info) - watches.append(action) - - role.init_actions(actions) - role.watch(watches) - - actions_todo_path = stg_path.joinpath("actions/todo.json") - # recover self._rc.state - actions_todo = read_json_file(actions_todo_path) - max_react_loop = actions_todo.get("max_react_loop", 1) - cur_state = actions_todo.get("cur_state", -1) - role.set_state(cur_state) - role.set_recovered(True) - react_mode_str = actions_todo.get("react_mode", RoleReactMode.REACT.value) - if react_mode_str not in RoleReactMode.values(): - logger.warning(f"ReactMode: {react_mode_str} not in {RoleReactMode.values()}, use react as default") - react_mode_str = RoleReactMode.REACT.value - role.set_react_mode(RoleReactMode(react_mode_str), max_react_loop) + role.set_recovered(True) # set True to make a tag role_memory = Memory.deserialize(stg_path) role.set_memory(role_memory) return role - def _reset(self): - self._states = [] - self._actions = [] - def _init_action_system_message(self, action: Action): action.set_prefix(self._get_prefix(), self.profile) @@ -256,7 +269,8 @@ class Role(metaclass=_RoleInjector): 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( @@ -331,10 +345,6 @@ class Role(metaclass=_RoleInjector): if env: env.set_subscription(self, self._subscription) - @property - def name(self): - return self._setting.name - @property def profile(self): """Get the role description (position)""" @@ -355,7 +365,7 @@ class Role(metaclass=_RoleInjector): 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: @@ -378,6 +388,7 @@ class Role(metaclass=_RoleInjector): next_state = await self._llm.aask(prompt) next_state = extract_state_value_from_output(next_state) 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 @@ -423,8 +434,8 @@ class Role(metaclass=_RoleInjector): if news_text: logger.debug(f"{self._setting} observed: {news_text}") return len(self._rc.news) - - def publish_message(self, msg): + + def _publish_message(self, msg): """If the role belongs to env, then the role's messages will be broadcast to env""" if not msg: return @@ -501,7 +512,7 @@ class Role(metaclass=_RoleInjector): 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, with_message=None): """Observe, and think and act based on the results of the observation""" if with_message: