diff --git a/examples/agent_creator.py b/examples/agent_creator.py index d4d7de3be..340dfafa4 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -17,7 +17,7 @@ MULTI_ACTION_AGENT_CODE_EXAMPLE = EXAMPLE_CODE_FILE.read_text() class CreateAgent(Action): - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ ### BACKGROUND You are using an agent framework called metagpt to write agents capable of different actions, the usage of metagpt can be illustrated by the following example: @@ -64,9 +64,9 @@ class AgentCreator(Role): self._init_actions([CreateAgent]) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - todo = self._rc.todo - msg = self._rc.memory.get()[-1] + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + todo = self.rc.todo + msg = self.rc.memory.get()[-1] instruction = msg.content code_text = await CreateAgent().run(example=self.agent_template, instruction=instruction) diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index 7a7fa6b56..6c3219efc 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -16,7 +16,7 @@ from metagpt.schema import Message class SimpleWriteCode(Action): - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ Write a python function that can {instruction} and provide two runnnable test cases. Return ```python your_code_here ``` with NO other texts, your code: @@ -60,8 +60,8 @@ class SimpleCoder(Role): self._init_actions([SimpleWriteCode]) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - todo = self._rc.todo # todo will be SimpleWriteCode() + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + todo = self.rc.todo # todo will be SimpleWriteCode() msg = self.get_memories(k=1)[0] # find the most recent messages code_text = await todo.run(msg.content) @@ -80,16 +80,16 @@ class RunnableCoder(Role): self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") # By choosing the Action by order under the hood # todo will be first SimpleWriteCode() then SimpleRunCode() - todo = self._rc.todo + todo = self.rc.todo msg = self.get_memories(k=1)[0] # find the most k recent messages result = await todo.run(msg.content) msg = Message(content=result, role=self.profile, cause_by=type(todo)) - self._rc.memory.add(msg) + self.rc.memory.add(msg) return msg diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py index 70ad71c6b..73278c08c 100644 --- a/examples/build_customized_multi_agents.py +++ b/examples/build_customized_multi_agents.py @@ -22,7 +22,7 @@ def parse_code(rsp): class SimpleWriteCode(Action): - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ Write a python function that can {instruction}. Return ```python your_code_here ``` with NO other texts, your code: @@ -50,7 +50,7 @@ class SimpleCoder(Role): class SimpleWriteTest(Action): - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ Context: {context} Write {k} unit tests using pytest for the given function, assuming you have imported it. Return ```python your_code_here ``` with NO other texts, @@ -80,8 +80,8 @@ class SimpleTester(Role): self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - todo = self._rc.todo + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + todo = self.rc.todo # context = self.get_memories(k=1)[0].content # use the most recent memory as context context = self.get_memories() # use all memories as context @@ -93,7 +93,7 @@ class SimpleTester(Role): class SimpleWriteReview(Action): - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ Context: {context} Review the test cases and provide one critical comments: """ diff --git a/examples/debate.py b/examples/debate.py index b3d287079..eb0a09839 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -7,6 +7,7 @@ Author: garylin2099 """ import asyncio import platform +from typing import Any import fire @@ -20,7 +21,7 @@ from metagpt.team import Team class SpeakAloud(Action): """Action: Speak out aloud in a debate (quarrel)""" - PROMPT_TEMPLATE = """ + PROMPT_TEMPLATE: str = """ ## BACKGROUND Suppose you are {name}, you are in a debate with {opponent_name}. ## DEBATE HISTORY @@ -30,9 +31,7 @@ class SpeakAloud(Action): Now it's your turn, you should closely respond to your opponent's latest argument, state your position, defend your arguments, and attack your opponent's arguments, craft a strong and emotional response in 80 words, in {name}'s rhetoric and viewpoints, your will argue: """ - - def __init__(self, name="SpeakAloud", context=None, llm=None): - super().__init__(name, context, llm) + name: str = "SpeakAloud" async def run(self, context: str, name: str, opponent_name: str): prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name) @@ -44,27 +43,24 @@ class SpeakAloud(Action): class Debator(Role): - def __init__( - self, - name: str, - profile: str, - opponent_name: str, - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "" + profile: str = "" + opponent_name: str = "" + + def __init__(self, **data: Any): + super().__init__(**data) self._init_actions([SpeakAloud]) self._watch([UserRequirement, SpeakAloud]) - self.opponent_name = opponent_name async def _observe(self) -> int: await super()._observe() # accept messages sent (from opponent) to self, disregard own messages from the last round - self._rc.news = [msg for msg in self._rc.news if msg.send_to == {self.name}] - return len(self._rc.news) + self.rc.news = [msg for msg in self.rc.news if msg.send_to == {self.name}] + return len(self.rc.news) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - todo = self._rc.todo # An instance of SpeakAloud + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + todo = self.rc.todo # An instance of SpeakAloud memories = self.get_memories() context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories) @@ -79,7 +75,7 @@ class Debator(Role): sent_from=self.name, send_to=self.opponent_name, ) - self._rc.memory.add(msg) + self.rc.memory.add(msg) return msg diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 576990a83..4136d7599 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -10,7 +10,7 @@ from __future__ import annotations from typing import Any, Optional, Union -from pydantic import BaseModel, Field +from pydantic import ConfigDict, Field from metagpt.actions.action_node import ActionNode from metagpt.llm import LLM @@ -19,50 +19,31 @@ from metagpt.schema import ( CodeSummarizeContext, CodingContext, RunCodeContext, + SerDeserMixin, TestingContext, ) -action_subclass_registry = {} +class Action(SerDeserMixin, is_polymorphic_base=True): + model_config = ConfigDict(arbitrary_types_allowed=True, exclude=["llm"]) -class Action(BaseModel): name: str = "" llm: BaseLLM = Field(default_factory=LLM, exclude=True) context: Union[dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, str, None] = "" - prefix = "" # aask*时会加上prefix,作为system_message - desc = "" # for skill manager + prefix: str = "" # aask*时会加上prefix,作为system_message + desc: str = "" # for skill manager node: ActionNode = Field(default=None, exclude=True) - # builtin variables - builtin_class_name: str = "" - - class Config: - arbitrary_types_allowed = True - def __init_with_instruction(self, instruction: str): """Initialize action with instruction""" self.node = ActionNode(key=self.name, expected_type=str, instruction=instruction, example="", schema="raw") return self - def __init__(self, **kwargs: Any): - super().__init__(**kwargs) + def __init__(self, **data: Any): + super().__init__(**data) - # deserialize child classes dynamically for inherited `action` - object.__setattr__(self, "builtin_class_name", self.__class__.__name__) - self.__fields__["builtin_class_name"].default = self.__class__.__name__ - - if "instruction" in kwargs: - self.__init_with_instruction(kwargs["instruction"]) - - def __init_subclass__(cls, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) - action_subclass_registry[cls.__name__] = cls - - def dict(self, *args, **kwargs) -> "DictStrAny": - obj_dict = super().dict(*args, **kwargs) - if "llm" in obj_dict: - obj_dict.pop("llm") - return obj_dict + if "instruction" in data: + self.__init_with_instruction(data["instruction"]) def set_prefix(self, prefix): """Set prefix for later usage""" diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 9534e91c5..3389b8964 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -11,7 +11,7 @@ NOTE: You should use typing.List instead of list to do type annotation. Because import json from typing import Any, Dict, List, Optional, Tuple, Type -from pydantic import BaseModel, create_model, root_validator, validator +from pydantic import BaseModel, create_model, field_validator, model_validator from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.config import CONFIG @@ -137,13 +137,15 @@ class ActionNode: """基于pydantic v1的模型动态生成,用来检验结果类型正确性""" new_class = create_model(class_name, **mapping) - @validator("*", allow_reuse=True) + @field_validator("*", mode="before") + @classmethod def check_name(v, field): if field.name not in mapping.keys(): raise ValueError(f"Unrecognized block: {field.name}") return v - @root_validator(pre=True, allow_reuse=True) + @model_validator(mode="before") + @classmethod def check_missing_fields(values): required_fields = set(mapping.keys()) missing_fields = required_fields - set(values.keys()) @@ -273,7 +275,9 @@ class ActionNode: output_class = self.create_model_class(output_class_name, output_data_mapping) if schema == "json": - parsed_data = llm_output_postprecess(output=content, schema=output_class.schema(), req_key=f"[/{TAG}]") + parsed_data = llm_output_postprecess( + output=content, schema=output_class.model_json_schema(), req_key=f"[/{TAG}]" + ) else: # using markdown parser parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) @@ -282,7 +286,7 @@ class ActionNode: return content, instruct_content def get(self, key): - return self.instruct_content.dict()[key] + return self.instruct_content.model_dump()[key] def set_recursive(self, name, value): setattr(self, name, value) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 8535d63b1..32e2a2a19 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -79,7 +79,7 @@ class WriteDesign(Action): logger.info("Nothing has changed.") # Wait until all files under `docs/system_designs/` are processed before sending the publish message, # leaving room for global optimization in subsequent steps. - return ActionOutput(content=changed_files.json(), instruct_content=changed_files) + return ActionOutput(content=changed_files.model_dump_json(), instruct_content=changed_files) async def _new_system_design(self, context, schema=CONFIG.prompt_schema): node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=schema) @@ -88,7 +88,7 @@ class WriteDesign(Action): async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=schema) - system_design_doc.content = node.instruct_content.json(ensure_ascii=False) + system_design_doc.content = node.instruct_content.model_dump_json() return system_design_doc async def _update_system_design(self, filename, prds_file_repo, system_design_file_repo) -> Document: @@ -99,7 +99,7 @@ class WriteDesign(Action): doc = Document( root_path=SYSTEM_DESIGN_FILE_REPO, filename=filename, - content=system_design.instruct_content.json(ensure_ascii=False), + content=system_design.instruct_content.model_dump_json(), ) else: doc = await self._merge(prd_doc=prd, system_design_doc=old_system_design_doc) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 7eda89130..a53f13e4c 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -73,7 +73,7 @@ class WriteTasks(Action): logger.info("Nothing has changed.") # Wait until all files under `docs/tasks/` are processed before sending the publish_message, leaving room for # global optimization in subsequent steps. - return ActionOutput(content=change_files.json(), instruct_content=change_files) + return ActionOutput(content=change_files.model_dump_json(), instruct_content=change_files) async def _update_tasks(self, filename, system_design_file_repo, tasks_file_repo): system_design_doc = await system_design_file_repo.get(filename) @@ -83,7 +83,7 @@ class WriteTasks(Action): else: rsp = await self._run_new_tasks(context=system_design_doc.content) task_doc = Document( - root_path=TASK_FILE_REPO, filename=filename, content=rsp.instruct_content.json(ensure_ascii=False) + root_path=TASK_FILE_REPO, filename=filename, content=rsp.instruct_content.model_dump_json() ) await tasks_file_repo.save( filename=filename, content=task_doc.content, dependencies={system_design_doc.root_relative_path} @@ -102,7 +102,7 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) node = await PM_NODE.fill(context, self.llm, schema) - task_doc.content = node.instruct_content.json(ensure_ascii=False) + task_doc.content = node.instruct_content.model_dump_json() return task_doc @staticmethod diff --git a/metagpt/actions/rebuild_class_view.py b/metagpt/actions/rebuild_class_view.py index 2a6a6a6d9..66bc2c7ab 100644 --- a/metagpt/actions/rebuild_class_view.py +++ b/metagpt/actions/rebuild_class_view.py @@ -50,7 +50,7 @@ class RebuildClassView(Action): # try: # node = await REBUILD_CLASS_VIEW_NODE.fill(context=f"```{code_type}\n{src_code}\n```", llm=self.llm, to=format) - # class_view = node.instruct_content.dict()["Class View"] + # class_view = node.instruct_content.model_dump()["Class View"] # except Exception as e: # class_view = RepoParser.rebuild_class_view(src_code, code_type) # await graph_db.insert(subject=concat_namespace(filename, class_name), predicate=GraphKeyword.HAS_CLASS_VIEW, object_=class_view) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index a6cc7cc22..a1535a723 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -84,6 +84,7 @@ class CollectLinks(Action): context: Optional[str] = None llm: BaseLLM = Field(default_factory=LLM) desc: str = "Collect links from a search engine." + search_engine: SearchEngine = Field(default_factory=SearchEngine) rank_func: Union[Callable[[list[str]], None], None] = None diff --git a/metagpt/actions/search_and_summarize.py b/metagpt/actions/search_and_summarize.py index 615576d76..cd3ef7d77 100644 --- a/metagpt/actions/search_and_summarize.py +++ b/metagpt/actions/search_and_summarize.py @@ -8,7 +8,7 @@ from typing import Any, Optional import pydantic -from pydantic import Field, root_validator +from pydantic import Field, model_validator from metagpt.actions import Action from metagpt.config import CONFIG, Config @@ -114,10 +114,10 @@ class SearchAndSummarize(Action): engine: Optional[SearchEngineType] = CONFIG.search_engine search_func: Optional[Any] = None search_engine: SearchEngine = None + result: str = "" - result = "" - - @root_validator + @model_validator(mode="before") + @classmethod def validate_engine_and_run_func(cls, values): engine = values.get("engine") search_func = values.get("search_func") diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index de647f167..9cefb70d8 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -80,7 +80,7 @@ class WritePRD(Action): await docs_file_repo.save(filename=REQUIREMENT_FILENAME, content="") bug_fix = BugFixContext(filename=BUGFIX_FILENAME) return Message( - content=bug_fix.json(), + content=bug_fix.model_dump_json(), instruct_content=bug_fix, role="", cause_by=FixBug, @@ -112,7 +112,7 @@ class WritePRD(Action): # Once all files under 'docs/prds/' have been compared with the newly added requirements, trigger the # 'publish' message to transition the workflow to the next stage. This design allows room for global # optimization in subsequent steps. - return ActionOutput(content=change_files.json(), instruct_content=change_files) + return ActionOutput(content=change_files.model_dump_json(), instruct_content=change_files) async def _run_new_requirement(self, requirements, schema=CONFIG.prompt_schema) -> ActionOutput: # sas = SearchAndSummarize() @@ -139,7 +139,7 @@ class WritePRD(Action): CONFIG.project_name = Path(CONFIG.project_path).name prompt = NEW_REQ_TEMPLATE.format(requirements=new_requirement_doc.content, old_prd=prd_doc.content) node = await WRITE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema) - prd_doc.content = node.instruct_content.json(ensure_ascii=False) + prd_doc.content = node.instruct_content.model_dump_json() await self._rename_workspace(node) return prd_doc @@ -151,7 +151,7 @@ class WritePRD(Action): new_prd_doc = Document( root_path=PRDS_FILE_REPO, filename=FileRepository.new_filename() + ".json", - content=prd.instruct_content.json(ensure_ascii=False), + content=prd.instruct_content.model_dump_json(), ) elif await self._is_relative(requirement_doc, prd_doc): new_prd_doc = await self._merge(requirement_doc, prd_doc) @@ -189,7 +189,7 @@ class WritePRD(Action): if not CONFIG.project_name: if isinstance(prd, (ActionOutput, ActionNode)): - ws_name = prd.instruct_content.dict()["Project Name"] + ws_name = prd.instruct_content.model_dump()["Project Name"] else: ws_name = CodeParser.parse_str(block="Project Name", text=prd) CONFIG.project_name = ws_name diff --git a/metagpt/actions/write_prd_review.py b/metagpt/actions/write_prd_review.py index 0241f192f..9199e7536 100644 --- a/metagpt/actions/write_prd_review.py +++ b/metagpt/actions/write_prd_review.py @@ -19,6 +19,7 @@ class WritePRDReview(Action): name: str = "" context: Optional[str] = None llm: BaseLLM = Field(default_factory=LLM) + prd: Optional[str] = None desc: str = "Based on the PRD, conduct a PRD Review, providing clear and detailed feedback" prd_review_prompt_template: str = """ diff --git a/metagpt/document.py b/metagpt/document.py index 0af3a915c..022e5d6f1 100644 --- a/metagpt/document.py +++ b/metagpt/document.py @@ -17,7 +17,7 @@ from langchain.document_loaders import ( UnstructuredWordDocumentLoader, ) from langchain.text_splitter import CharacterTextSplitter -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from tqdm import tqdm from metagpt.config import CONFIG @@ -117,13 +117,12 @@ class IndexableDocument(Document): Advanced document handling: For vector databases or search engines. """ + model_config = ConfigDict(arbitrary_types_allowed=True) + data: Union[pd.DataFrame, list] content_col: Optional[str] = Field(default="") meta_col: Optional[str] = Field(default="") - class Config: - arbitrary_types_allowed = True - @classmethod def from_path(cls, data_path: Path, content_col="content", meta_col="metadata"): if not data_path.exists(): diff --git a/metagpt/environment.py b/metagpt/environment.py index 0ee85f707..ddb9ad9dd 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -15,11 +15,11 @@ import asyncio from pathlib import Path from typing import Iterable, Set -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator from metagpt.config import CONFIG from metagpt.logs import logger -from metagpt.roles.role import Role, role_subclass_registry +from metagpt.roles.role import Role from metagpt.schema import Message from metagpt.utils.common import is_subscribed, read_json_file, write_json_file @@ -29,30 +29,17 @@ class Environment(BaseModel): Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles """ + model_config = ConfigDict(arbitrary_types_allowed=True) + desc: str = Field(default="") # 环境描述 - roles: dict[str, Role] = Field(default_factory=dict) - members: dict[Role, Set] = Field(default_factory=dict) + roles: dict[str, SerializeAsAny[Role]] = Field(default_factory=dict, validate_default=True) + members: dict[Role, Set] = Field(default_factory=dict, exclude=True) history: str = "" # For debug - class Config: - arbitrary_types_allowed = True - - def __init__(self, **kwargs): - roles = [] - for role_key, role in kwargs.get("roles", {}).items(): - current_role = kwargs["roles"][role_key] - if isinstance(current_role, dict): - item_class_name = current_role.get("builtin_class_name", None) - for name, subclass in role_subclass_registry.items(): - registery_class_name = subclass.__fields__["builtin_class_name"].default - if item_class_name == registery_class_name: - current_role = subclass(**current_role) - break - kwargs["roles"][role_key] = current_role - roles.append(current_role) - super().__init__(**kwargs) - - self.add_roles(roles) # add_roles again to init the Role.set_env + @model_validator(mode="after") + def init_roles(self): + self.add_roles(self.roles.values()) + return self def serialize(self, stg_path: Path): roles_path = stg_path.joinpath("roles.json") diff --git a/metagpt/management/skill_manager.py b/metagpt/management/skill_manager.py index e4892e3d9..5ab6273fb 100644 --- a/metagpt/management/skill_manager.py +++ b/metagpt/management/skill_manager.py @@ -4,7 +4,7 @@ @Time : 2023/6/5 01:44 @Author : alexanderwu @File : skill_manager.py -@Modified By: mashenquan, 2023/8/20. Remove useless `_llm` +@Modified By: mashenquan, 2023/8/20. Remove useless `llm` """ from metagpt.actions import Action from metagpt.const import PROMPT_PATH diff --git a/metagpt/memory/brain_memory.py b/metagpt/memory/brain_memory.py index c882859d8..b82ac1210 100644 --- a/metagpt/memory/brain_memory.py +++ b/metagpt/memory/brain_memory.py @@ -73,7 +73,7 @@ class BrainMemory(BaseModel): redis = Redis(conf=redis_conf) if not redis.is_valid() or not redis_key: return False - v = self.json(ensure_ascii=False) + v = self.model_dump_json() if self.cacheable: await redis.set(key=redis_key, data=v, timeout_sec=timeout_sec) logger.debug(f"REDIS SET {redis_key} {v}") @@ -99,7 +99,7 @@ class BrainMemory(BaseModel): if msg.id: if self.to_int(msg.id, 0) <= self.to_int(self.last_history_id, -1): return - self.history.append(msg.dict()) + self.history.append(msg.model_dump()) self.last_history_id = str(msg.id) self.is_dirty = True @@ -156,7 +156,7 @@ class BrainMemory(BaseModel): if left == 0: break m.content = m.content[0:left] - msgs.append(m.dict()) + msgs.append(m.model_dump()) break msgs.append(m) total_length += delta diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index 1497b8910..8da6ed84a 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -7,7 +7,7 @@ from typing import Optional -from pydantic import Field +from pydantic import ConfigDict, Field from metagpt.logs import logger from metagpt.memory import Memory @@ -22,13 +22,12 @@ class LongTermMemory(Memory): - update memory when it changed """ + model_config = ConfigDict(arbitrary_types_allowed=True) + memory_storage: MemoryStorage = Field(default_factory=MemoryStorage) rc: Optional["RoleContext"] = None msg_from_recover: bool = False - class Config: - arbitrary_types_allowed = True - def recover_memory(self, role_id: str, rc: "RoleContext"): messages = self.memory_storage.recover_memory(role_id) self.rc = rc diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index bd03786ad..593409648 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -8,9 +8,9 @@ """ from collections import defaultdict from pathlib import Path -from typing import Iterable, Set +from typing import DefaultDict, Iterable, Set -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SerializeAsAny from metagpt.const import IGNORED_MESSAGE_ID from metagpt.schema import Message @@ -25,23 +25,14 @@ from metagpt.utils.common import ( class Memory(BaseModel): """The most basic memory: super-memory""" - storage: list[Message] = [] - index: dict[str, list[Message]] = Field(default_factory=defaultdict(list)) + storage: list[SerializeAsAny[Message]] = [] + index: DefaultDict[str, list[SerializeAsAny[Message]]] = Field(default_factory=lambda: defaultdict(list)) ignore_id: bool = False - def __init__(self, **kwargs): - index = kwargs.get("index", {}) - new_index = defaultdict(list) - for action_str, value in index.items(): - new_index[action_str] = [Message(**item_dict) for item_dict in value] - kwargs["index"] = new_index - super(Memory, self).__init__(**kwargs) - self.index = new_index - def serialize(self, stg_path: Path): """stg_path = ./storage/team/environment/ or ./storage/team/environment/roles/{role_class}_{role_name}/""" memory_path = stg_path.joinpath("memory.json") - storage = self.dict() + storage = self.model_dump() write_json_file(memory_path, storage) @classmethod diff --git a/metagpt/roles/assistant.py b/metagpt/roles/assistant.py index 00a576089..89965f3bd 100644 --- a/metagpt/roles/assistant.py +++ b/metagpt/roles/assistant.py @@ -65,22 +65,20 @@ class Assistant(Role): prompt += f"If the text explicitly want you to {desc}, return `[SKILL]: {name}` brief and clear. For instance: [SKILL]: {name}\n" prompt += 'Otherwise, return `[TALK]: {talk}` brief and clear. For instance: if {talk} is "xxxx" return [TALK]: xxxx\n\n' prompt += f"Now what specific action is explicitly mentioned in the text: {last_talk}\n" - rsp = await self._llm.aask(prompt, []) + rsp = await self.llm.aask(prompt, []) logger.info(f"THINK: {prompt}\n, THINK RESULT: {rsp}\n") return await self._plan(rsp, last_talk=last_talk) async def act(self) -> Message: - result = await self._rc.todo.run() + result = await self.rc.todo.run() if not result: return None if isinstance(result, str): - msg = Message(content=result, role="assistant", cause_by=self._rc.todo) + msg = Message(content=result, role="assistant", cause_by=self.rc.todo) elif isinstance(result, Message): msg = result else: - msg = Message( - content=result.content, instruct_content=result.instruct_content, cause_by=type(self._rc.todo) - ) + msg = Message(content=result.content, instruct_content=result.instruct_content, cause_by=type(self.rc.todo)) self.memory.add_answer(msg) return msg @@ -99,8 +97,8 @@ class Assistant(Role): async def talk_handler(self, text, **kwargs) -> bool: history = self.memory.history_text text = kwargs.get("last_talk") or text - self._rc.todo = TalkAction( - context=text, knowledge=self.memory.get_knowledge(), history_summary=history, llm=self._llm, **kwargs + self.rc.todo = TalkAction( + context=text, knowledge=self.memory.get_knowledge(), history_summary=history, llm=self.llm, **kwargs ) return True @@ -110,13 +108,11 @@ class Assistant(Role): if not skill: logger.info(f"skill not found: {text}") return await self.talk_handler(text=last_talk, **kwargs) - action = ArgumentsParingAction(skill=skill, llm=self._llm, ask=last_talk, **kwargs) + action = ArgumentsParingAction(skill=skill, llm=self.llm, ask=last_talk, **kwargs) await action.run(**kwargs) if action.args is None: return await self.talk_handler(text=last_talk, **kwargs) - self._rc.todo = SkillAction( - skill=skill, args=action.args, llm=self._llm, name=skill.name, desc=skill.description - ) + self.rc.todo = SkillAction(skill=skill, args=action.args, llm=self.llm, name=skill.name, desc=skill.description) return True async def refine_memory(self) -> str: @@ -125,16 +121,16 @@ class Assistant(Role): return None if not self.memory.is_history_available: return last_talk - history_summary = await self.memory.summarize(max_words=800, keep_language=True, llm=self._llm) - if last_talk and await self.memory.is_related(text1=last_talk, text2=history_summary, llm=self._llm): + history_summary = await self.memory.summarize(max_words=800, keep_language=True, llm=self.llm) + if last_talk and await self.memory.is_related(text1=last_talk, text2=history_summary, llm=self.llm): # Merge relevant content. - merged = await self.memory.rewrite(sentence=last_talk, context=history_summary, llm=self._llm) + merged = await self.memory.rewrite(sentence=last_talk, context=history_summary, llm=self.llm) return f"{merged} {last_talk}" return last_talk def get_memory(self) -> str: - return self.memory.json() + return self.memory.model_dump_json() def load_memory(self, jsn): try: diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 76c3d96b3..b8866e055 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -109,7 +109,7 @@ class Engineer(Role): coding_context = await todo.run() # Code review if review: - action = WriteCodeReview(context=coding_context, llm=self._llm) + action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) coding_context = await action.run() await src_file_repo.save( @@ -118,9 +118,12 @@ class Engineer(Role): content=coding_context.code_doc.content, ) msg = Message( - content=coding_context.json(), instruct_content=coding_context, role=self.profile, cause_by=WriteCode + content=coding_context.model_dump_json(), + instruct_content=coding_context, + role=self.profile, + cause_by=WriteCode, ) - self._rc.memory.add(msg) + self.rc.memory.add(msg) changed_files.add(coding_context.code_doc.filename) if not changed_files: @@ -129,12 +132,12 @@ class Engineer(Role): async def _act(self) -> Message | None: """Determines the mode of action based on whether code review is used.""" - if self._rc.todo is None: + if self.rc.todo is None: return None - if isinstance(self._rc.todo, WriteCode): + if isinstance(self.rc.todo, WriteCode): self.next_todo_action = any_to_name(SummarizeCode) return await self._act_write_code() - if isinstance(self._rc.todo, SummarizeCode): + if isinstance(self.rc.todo, SummarizeCode): self.next_todo_action = any_to_name(WriteCode) return await self._act_summarize() return None @@ -170,7 +173,7 @@ class Engineer(Role): tasks.append(todo.context.dict()) await code_summaries_file_repo.save( filename=Path(todo.context.design_filename).name, - content=todo.context.json(), + content=todo.context.model_dump_json(), dependencies=dependencies, ) else: @@ -193,7 +196,7 @@ class Engineer(Role): ) async def _is_pass(self, summary) -> (str, str): - rsp = await self._llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False) + rsp = await self.llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False) logger.info(rsp) if "YES" in rsp: return True, rsp @@ -204,17 +207,17 @@ class Engineer(Role): CONFIG.src_workspace = CONFIG.git_repo.workdir / CONFIG.git_repo.workdir.name write_code_filters = any_to_str_set([WriteTasks, SummarizeCode, FixBug]) summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview]) - if not self._rc.news: + if not self.rc.news: return None - msg = self._rc.news[0] + msg = self.rc.news[0] if msg.cause_by in write_code_filters: - logger.debug(f"TODO WriteCode:{msg.json()}") + logger.debug(f"TODO WriteCode:{msg.model_dump_json()}") await self._new_code_actions(bug_fix=msg.cause_by == any_to_str(FixBug)) - return self._rc.todo + return self.rc.todo if msg.cause_by in summarize_code_filters and msg.sent_from == any_to_str(self): - logger.debug(f"TODO SummarizeCode:{msg.json()}") + logger.debug(f"TODO SummarizeCode:{msg.model_dump_json()}") await self._new_summarize_actions() - return self._rc.todo + return self.rc.todo return None @staticmethod @@ -241,7 +244,9 @@ class Engineer(Role): context = await Engineer._new_coding_context( filename, src_file_repo, task_file_repo, design_file_repo, dependency ) - coding_doc = Document(root_path=str(src_file_repo.root_path), filename=filename, content=context.json()) + coding_doc = Document( + root_path=str(src_file_repo.root_path), filename=filename, content=context.model_dump_json() + ) return coding_doc async def _new_code_actions(self, bug_fix=False): @@ -266,15 +271,15 @@ class Engineer(Role): filename=task_filename, design_doc=design_doc, task_doc=task_doc, code_doc=old_code_doc ) coding_doc = Document( - root_path=str(src_file_repo.root_path), filename=task_filename, content=context.json() + root_path=str(src_file_repo.root_path), filename=task_filename, content=context.model_dump_json() ) if task_filename in changed_files.docs: logger.warning( - f"Log to expose potential conflicts: {coding_doc.json()} & " - f"{changed_files.docs[task_filename].json()}" + f"Log to expose potential conflicts: {coding_doc.model_dump_json()} & " + f"{changed_files.docs[task_filename].model_dump_json()}" ) changed_files.docs[task_filename] = coding_doc - self.code_todos = [WriteCode(context=i, llm=self._llm) for i in changed_files.docs.values()] + self.code_todos = [WriteCode(context=i, llm=self.llm) for i in changed_files.docs.values()] # Code directly modified by the user. dependency = await CONFIG.git_repo.get_dependency() for filename in changed_src_files: @@ -288,10 +293,10 @@ class Engineer(Role): dependency=dependency, ) changed_files.docs[filename] = coding_doc - self.code_todos.append(WriteCode(context=coding_doc, llm=self._llm)) + self.code_todos.append(WriteCode(context=coding_doc, llm=self.llm)) if self.code_todos: - self._rc.todo = self.code_todos[0] + self.rc.todo = self.code_todos[0] async def _new_summarize_actions(self): src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) @@ -304,9 +309,9 @@ class Engineer(Role): summarizations[ctx].append(filename) for ctx, filenames in summarizations.items(): ctx.codes_filenames = filenames - self.summarize_todos.append(SummarizeCode(context=ctx, llm=self._llm)) + self.summarize_todos.append(SummarizeCode(context=ctx, llm=self.llm)) if self.summarize_todos: - self._rc.todo = self.summarize_todos[0] + self.rc.todo = self.summarize_todos[0] @property def todo(self) -> str: diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index 3349a498f..f5588974b 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -69,8 +69,8 @@ class InvoiceOCRAssistant(Role): Returns: A message containing the result of the action. """ - msg = self._rc.memory.get(k=1)[0] - todo = self._rc.todo + msg = self.rc.memory.get(k=1)[0] + todo = self.rc.todo if isinstance(todo, InvoiceOCR): self.origin_query = msg.content invoice_path: InvoicePath = msg.instruct_content @@ -87,11 +87,11 @@ class InvoiceOCRAssistant(Role): else: self._init_actions([GenerateTable]) - self._rc.todo = None + self.rc.todo = None content = INVOICE_OCR_SUCCESS resp = OCRResults(ocr_result=json.dumps(resp)) msg = Message(content=content, instruct_content=resp) - self._rc.memory.add(msg) + self.rc.memory.add(msg) return await super().react() elif isinstance(todo, GenerateTable): ocr_results: OCRResults = msg.instruct_content @@ -108,5 +108,5 @@ class InvoiceOCRAssistant(Role): resp = ReplyData(content=resp) msg = Message(content=content, instruct_content=resp) - self._rc.memory.add(msg) + self.rc.memory.add(msg) return msg diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index 5412dc2b5..10b30b976 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -45,7 +45,7 @@ class ProductManager(Role): else: self._set_state(0) self.todo_action = any_to_name(WritePRD) - return bool(self._rc.todo) + return bool(self.rc.todo) async def _observe(self, ignore_memory=False) -> int: return await super()._observe(ignore_memory=True) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 39246364e..b1d06d122 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -69,7 +69,7 @@ class QaEngineer(Role): ) logger.info(f"Writing {test_doc.filename}..") context = TestingContext(filename=test_doc.filename, test_doc=test_doc, code_doc=code_doc) - context = await WriteTest(context=context, llm=self._llm).run() + context = await WriteTest(context=context, llm=self.llm).run() await tests_file_repo.save( filename=context.test_doc.filename, content=context.test_doc.content, @@ -86,7 +86,7 @@ class QaEngineer(Role): ) self.publish_message( Message( - content=run_code_context.json(), + content=run_code_context.model_dump_json(), role=self.profile, cause_by=WriteTest, sent_from=self, @@ -106,11 +106,11 @@ class QaEngineer(Role): return run_code_context.code = src_doc.content run_code_context.test_code = test_doc.content - result = await RunCode(context=run_code_context, llm=self._llm).run() + result = await RunCode(context=run_code_context, llm=self.llm).run() run_code_context.output_filename = run_code_context.test_filename + ".json" await CONFIG.git_repo.new_file_repository(TEST_OUTPUTS_FILE_REPO).save( filename=run_code_context.output_filename, - content=result.json(), + content=result.model_dump_json(), dependencies={src_doc.root_relative_path, test_doc.root_relative_path}, ) run_code_context.code = None @@ -120,7 +120,7 @@ class QaEngineer(Role): mappings = {"Engineer": "Alex", "QaEngineer": "Edward"} self.publish_message( Message( - content=run_code_context.json(), + content=run_code_context.model_dump_json(), role=self.profile, cause_by=RunCode, sent_from=self, @@ -130,14 +130,14 @@ class QaEngineer(Role): async def _debug_error(self, msg): run_code_context = RunCodeContext.loads(msg.content) - code = await DebugError(context=run_code_context, llm=self._llm).run() + code = await DebugError(context=run_code_context, llm=self.llm).run() await FileRepository.save_file( filename=run_code_context.test_filename, content=code, relative_path=TEST_CODES_FILE_REPO ) run_code_context.output = None self.publish_message( Message( - content=run_code_context.json(), + content=run_code_context.model_dump_json(), role=self.profile, cause_by=DebugError, sent_from=self, @@ -159,7 +159,7 @@ class QaEngineer(Role): code_filters = any_to_str_set({SummarizeCode}) test_filters = any_to_str_set({WriteTest, DebugError}) run_filters = any_to_str_set({RunCode}) - for msg in self._rc.news: + for msg in self.rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself if msg.cause_by in code_filters: diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index f981d72a7..9705e71bb 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -41,20 +41,20 @@ class Researcher(Role): logger.warning(f"The language `{self.language}` has not been tested, it may not work.") async def _think(self) -> bool: - if self._rc.todo is None: + if self.rc.todo is None: self._set_state(0) return True - if self._rc.state + 1 < len(self._states): - self._set_state(self._rc.state + 1) + if self.rc.state + 1 < len(self.states): + self._set_state(self.rc.state + 1) else: - self._rc.todo = None + self.rc.todo = None return False async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - todo = self._rc.todo - msg = self._rc.memory.get(k=1)[0] + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + todo = self.rc.todo + msg = self.rc.memory.get(k=1)[0] if isinstance(msg.instruct_content, Report): instruct_content = msg.instruct_content topic = instruct_content.topic @@ -78,14 +78,14 @@ class Researcher(Role): else: summaries = instruct_content.summaries summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries) - content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text) + content = await self.rc.todo.run(topic, summary_text, system_text=research_system_text) ret = Message( content="", instruct_content=Report(topic=topic, content=content), role=self.profile, - cause_by=self._rc.todo, + cause_by=self.rc.todo, ) - self._rc.memory.add(ret) + self.rc.memory.add(ret) return ret def research_system_text(self, topic, current_task: Action) -> str: diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index d6e874ffe..2b8209758 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -10,8 +10,8 @@ consolidated within the `_observe` function. 2. Standardize the message filtering for string label matching. Role objects can access the message labels they've subscribed to through the `subscribed_tags` property. - 3. Move the message receive buffer from the global variable `self._rc.env.memory` to the role's private variable - `self._rc.msg_buffer` for easier message identification and asynchronous appending of messages. + 3. Move the message receive buffer from the global variable `self.rc.env.memory` to the role's private variable + `self.rc.msg_buffer` for easier message identification and asynchronous appending of messages. 4. Standardize the way messages are passed: `publish_message` sends messages out, while `put_message` places messages into the Role object's private message receive buffer. There are no other message transmit methods. 5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes @@ -24,12 +24,11 @@ from __future__ import annotations from enum import Enum from pathlib import Path -from typing import Any, Iterable, Set, Type +from typing import Any, Iterable, Optional, Set, Type -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator from metagpt.actions import Action, ActionOutput -from metagpt.actions.action import action_subclass_registry from metagpt.actions.action_node import ActionNode from metagpt.actions.add_requirement import UserRequirement from metagpt.const import SERDESER_PATH @@ -37,7 +36,7 @@ from metagpt.llm import LLM, HumanProvider from metagpt.logs import logger from metagpt.memory import Memory from metagpt.provider.base_llm import BaseLLM -from metagpt.schema import Message, MessageQueue +from metagpt.schema import Message, MessageQueue, SerDeserMixin from metagpt.utils.common import ( any_to_name, any_to_str, @@ -92,8 +91,10 @@ class RoleReactMode(str, Enum): class RoleContext(BaseModel): """Role Runtime Context""" + model_config = ConfigDict(arbitrary_types_allowed=True) + # # env exclude=True to avoid `RecursionError: maximum recursion depth exceeded in comparison` - env: "Environment" = Field(default=None, exclude=True) + env: "Environment" = Field(default=None, exclude=True) # # avoid circular import # TODO judge if ser&deser msg_buffer: MessageQueue = Field( default_factory=MessageQueue, exclude=True @@ -109,9 +110,6 @@ class RoleContext(BaseModel): ) # 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) @@ -128,12 +126,11 @@ class RoleContext(BaseModel): return self.memory.get() -role_subclass_registry = {} - - -class Role(BaseModel): +class Role(SerDeserMixin, is_polymorphic_base=True): """Role/Agent""" + model_config = ConfigDict(arbitrary_types_allowed=True, exclude=["llm"]) + name: str = "" profile: str = "" goal: str = "" @@ -141,84 +138,40 @@ class Role(BaseModel): desc: str = "" is_human: bool = False - _llm: BaseLLM = Field(default_factory=LLM) # Each role has its own LLM, use different system message - _role_id: str = "" - _states: list[str] = [] - _actions: list[Action] = [] - _rc: RoleContext = Field(default_factory=RoleContext) + llm: BaseLLM = Field(default_factory=LLM, exclude=True) # Each role has its own LLM, use different system message + role_id: str = "" + states: list[str] = [] + actions: list[SerializeAsAny[Action]] = Field(default=[], validate_default=True) + rc: RoleContext = Field(default_factory=RoleContext) subscription: set[str] = set() # builtin variables recovered: bool = False # to tag if a recovered role - latest_observed_msg: Message = None # record the latest observed message when interrupted - builtin_class_name: str = "" - - _private_attributes = { - "_llm": None, - "_role_id": _role_id, - "_states": [], - "_actions": [], - "_rc": RoleContext(), - "_subscription": set(), - } + latest_observed_msg: Optional[Message] = None # record the latest observed message when interrupted __hash__ = object.__hash__ # support Role as hashable type in `Environment.members` - class Config: - arbitrary_types_allowed = True - exclude = ["_llm"] + @model_validator(mode="after") + def check_subscription(self) -> set: + if not self.subscription: + self.subscription = {any_to_str(self), self.name} if self.name else {any_to_str(self)} + return self - def __init__(self, **kwargs: Any): - 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 + def __init__(self, **data: Any): + # --- avoid PydanticUndefinedAnnotation name 'Environment' is not defined # + from metagpt.environment import Environment - super().__init__(**kwargs) + Environment + # ------ + Role.model_rebuild() + super().__init__(**data) - # 关于私有变量的初始化 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), self.name} if self.name else {any_to_str(self)} - - 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]) - - self._llm.system_prompt = self._get_prefix() - - # 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__ - - if "actions" in kwargs: - self._init_actions(kwargs["actions"]) - - self._watch(kwargs.get("watch") or [UserRequirement]) - - def __init_subclass__(cls, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) - role_subclass_registry[cls.__name__] = cls + self.llm.system_prompt = self._get_prefix() + self._watch(data.get("watch") or [UserRequirement]) def _reset(self): - object.__setattr__(self, "_states", []) - object.__setattr__(self, "_actions", []) + self.states = [] + self.actions = [] @property def _setting(self): @@ -231,12 +184,12 @@ class Role(BaseModel): else stg_path ) - role_info = self.dict(exclude={"_rc": {"memory": True, "msg_buffer": True}, "_llm": True}) + role_info = self.model_dump(exclude={"rc": {"memory": True, "msg_buffer": True}, "llm": True}) role_info.update({"role_class": self.__class__.__name__, "module_name": self.__module__}) role_info_path = stg_path.joinpath("role_info.json") write_json_file(role_info_path, role_info) - self._rc.memory.serialize(stg_path) # serialize role's memory alone + self.rc.memory.serialize(stg_path) # serialize role's memory alone @classmethod def deserialize(cls, stg_path: Path) -> "Role": @@ -260,13 +213,13 @@ class Role(BaseModel): action.set_prefix(self._get_prefix()) def refresh_system_message(self): - self._llm.system_prompt = self._get_prefix() + self.llm.system_prompt = self._get_prefix() def set_recovered(self, recovered: bool = False): self.recovered = recovered def set_memory(self, memory: Memory): - self._rc.memory = memory + self.rc.memory = memory def init_actions(self, actions): self._init_actions(actions) @@ -276,7 +229,7 @@ class Role(BaseModel): for idx, action in enumerate(actions): if not isinstance(action, Action): ## 默认初始化 - i = action(name="", llm=self._llm) + i = action(name="", llm=self.llm) else: if self.is_human and not isinstance(action.llm, HumanProvider): logger.warning( @@ -285,10 +238,9 @@ class Role(BaseModel): f"try passing in Action classes instead of initialized instances" ) i = action - # i.set_env(self._rc.env) self._init_action_system_message(i) - self._actions.append(i) - self._states.append(f"{idx}. {action}") + 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 @@ -307,20 +259,20 @@ class Role(BaseModel): 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 + self.rc.react_mode = react_mode if react_mode == RoleReactMode.REACT: - self._rc.max_react_loop = max_react_loop + self.rc.max_react_loop = max_react_loop def _watch(self, actions: Iterable[Type[Action]] | Iterable[Action]): """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message buffer during _observe. """ - self._rc.watch = {any_to_str(t) for t in actions} + self.rc.watch = {any_to_str(t) for t in actions} # check RoleContext after adding watch actions - self._rc.check(self._role_id) + self.rc.check(self.role_id) def is_watch(self, caused_by: str): - return caused_by in self._rc.watch + return caused_by in self.rc.watch def subscribe(self, tags: Set[str]): """Used to receive Messages with certain tags from the environment. Message will be put into personal message @@ -328,19 +280,19 @@ class Role(BaseModel): or profile. """ self.subscription = tags - if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - self._rc.env.set_subscription(self, self.subscription) + if self.rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 + self.rc.env.set_subscription(self, self.subscription) def _set_state(self, state: int): """Update the current state.""" - self._rc.state = state - logger.debug(f"actions={self._actions}, state={state}") - self._rc.todo = self._actions[self._rc.state] if state >= 0 else None + self.rc.state = state + logger.debug(f"actions={self.actions}, state={state}") + 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 + self.rc.env = env if env: env.set_subscription(self, self.subscription) self.refresh_system_message() # add env message to system message @@ -348,7 +300,7 @@ class Role(BaseModel): @property def action_count(self): """Return number of action""" - return len(self._actions) + return len(self.actions) def _get_prefix(self): """Get the role prefix""" @@ -360,38 +312,38 @@ class Role(BaseModel): if self.constraints: prefix += CONSTRAINT_TEMPLATE.format(**{"constraints": self.constraints}) - if self._rc.env and self._rc.env.desc: - other_role_names = ", ".join(self._rc.env.role_names()) - env_desc = f"You are in {self._rc.env.desc} with roles({other_role_names})." + if self.rc.env and self.rc.env.desc: + other_role_names = ", ".join(self.rc.env.role_names()) + env_desc = f"You are in {self.rc.env.desc} with roles({other_role_names})." prefix += env_desc return prefix async def _think(self) -> bool: """Consider what to do and decide on the next course of action. Return false if nothing can be done.""" - if len(self._actions) == 1: + if len(self.actions) == 1: # If there is only one action, then only this one can be performed self._set_state(0) return True - if self.recovered and self._rc.state >= 0: - self._set_state(self._rc.state) # action to run from recovered state + if self.recovered and self.rc.state >= 0: + self._set_state(self.rc.state) # action to run from recovered state self.set_recovered(False) # avoid max_react_loop out of work return True 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, + history=self.rc.history, + states="\n".join(self.states), + n_states=len(self.states) - 1, + previous_state=self.rc.state, ) - next_state = await self._llm.aask(prompt) + 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)): + 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: @@ -402,21 +354,21 @@ class Role(BaseModel): return True async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - response = await self._rc.todo.run(self._rc.history) + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + response = await self.rc.todo.run(self.rc.history) if isinstance(response, (ActionOutput, ActionNode)): msg = Message( content=response.content, instruct_content=response.instruct_content, role=self._setting, - cause_by=self._rc.todo, + cause_by=self.rc.todo, sent_from=self, ) elif isinstance(response, Message): msg = response else: - msg = Message(content=response, role=self.profile, cause_by=self._rc.todo, sent_from=self) - self._rc.memory.add(msg) + msg = Message(content=response, role=self.profile, cause_by=self.rc.todo, sent_from=self) + self.rc.memory.add(msg) return msg @@ -426,7 +378,7 @@ class Role(BaseModel): observed_pure = [msg.dict(exclude={"id": True}) for msg in observed] existed_pure = [msg.dict(exclude={"id": True}) for msg in existed] for idx, new in enumerate(observed_pure): - if (new["cause_by"] in self._rc.watch or self.name in new["send_to"]) and new not in existed_pure: + if (new["cause_by"] in self.rc.watch or self.name in new["send_to"]) and new not in existed_pure: news.append(observed[idx]) return news @@ -437,59 +389,59 @@ class Role(BaseModel): if self.recovered: news = [self.latest_observed_msg] if self.latest_observed_msg else [] if not news: - news = self._rc.msg_buffer.pop_all() + news = self.rc.msg_buffer.pop_all() # Store the read messages in your own memory to prevent duplicate processing. - old_messages = [] if ignore_memory else self._rc.memory.get() - self._rc.memory.add_batch(news) + old_messages = [] if ignore_memory else self.rc.memory.get() + self.rc.memory.add_batch(news) # Filter out messages of interest. - self._rc.news = [n for n in news if n.cause_by in self._rc.watch and n not in old_messages] - self.latest_observed_msg = self._rc.news[-1] if self._rc.news else None # record the latest observed msg + self.rc.news = [n for n in news if n.cause_by in self.rc.watch and n not in old_messages] + self.latest_observed_msg = self.rc.news[-1] if self.rc.news else None # record the latest observed msg # Design Rules: # If you need to further categorize Message objects, you can do so using the Message.set_meta function. # msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer. - news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news] + 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) + return len(self.rc.news) # async def _observe(self, ignore_memory=False) -> int: # """Prepare new messages for processing from the message buffer and other sources.""" # # Read unprocessed messages from the msg buffer. - # news = self._rc.msg_buffer.pop_all() + # news = self.rc.msg_buffer.pop_all() # if self.recovered: # news = [self.latest_observed_msg] if self.latest_observed_msg else [] # else: # self.latest_observed_msg = news[-1] if len(news) > 0 else None # record the latest observed msg # # # Store the read messages in your own memory to prevent duplicate processing. - # old_messages = [] if ignore_memory else self._rc.memory.get() - # self._rc.memory.add_batch(news) + # old_messages = [] if ignore_memory else self.rc.memory.get() + # self.rc.memory.add_batch(news) # # Filter out messages of interest. - # self._rc.news = self._find_news(news, old_messages) + # self.rc.news = self._find_news(news, old_messages) # # # Design Rules: # # If you need to further categorize Message objects, you can do so using the Message.set_meta function. # # msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer. - # news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news] + # 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) + # 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 msg: return - if not self._rc.env: + if not self.rc.env: # If env does not exist, do not publish the message return - self._rc.env.publish_message(msg) + self.rc.env.publish_message(msg) def put_message(self, message): """Place the message into the Role object's private message buffer.""" if not message: return - self._rc.msg_buffer.push(message) + self.rc.msg_buffer.push(message) async def _react(self) -> Message: """Think first, then act, until the Role _think it is time to stop and requires no more todo. @@ -498,22 +450,22 @@ class Role(BaseModel): """ actions_taken = 0 rsp = Message(content="No actions taken yet") # will be overwritten after Role _act - while actions_taken < self._rc.max_react_loop: + while actions_taken < self.rc.max_react_loop: # think await self._think() - if self._rc.todo is None: + if self.rc.todo is None: break # act - logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}") + logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}") rsp = await self._act() # 这个rsp是否需要publish_message? 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) -> ...""" - start_idx = self._rc.state if self._rc.state >= 0 else 0 # action to run from recovered state - rsp = Message(content="No actions taken yet") # return default message if _actions=[] - for i in range(start_idx, len(self._states)): + start_idx = self.rc.state if self.rc.state >= 0 else 0 # action to run from recovered state + rsp = Message(content="No actions taken yet") # return default message if actions=[] + for i in range(start_idx, len(self.states)): self._set_state(i) rsp = await self._act() return rsp # return output from the last action @@ -525,18 +477,18 @@ class Role(BaseModel): 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: + if self.rc.react_mode == RoleReactMode.REACT: rsp = await self._react() - elif self._rc.react_mode == RoleReactMode.BY_ORDER: + elif self.rc.react_mode == RoleReactMode.BY_ORDER: rsp = await self._act_by_order() - elif self._rc.react_mode == RoleReactMode.PLAN_AND_ACT: + 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 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) + return self.rc.memory.get(k=k) @role_raise_decorator async def run(self, with_message=None) -> Message | None: @@ -561,7 +513,7 @@ class Role(BaseModel): rsp = await self.react() # Reset the next action to be taken. - self._rc.todo = None + self.rc.todo = None # Send the response message to the Environment object to have it relay the message to the subscribers. self.publish_message(rsp) return rsp @@ -569,12 +521,12 @@ class Role(BaseModel): @property def is_idle(self) -> bool: """If true, all actions have been executed.""" - return not self._rc.news and not self._rc.todo and self._rc.msg_buffer.empty() + return not self.rc.news and not self.rc.todo and self.rc.msg_buffer.empty() async def think(self) -> Action: """The exported `think` function""" await self._think() - return self._rc.todo + return self.rc.todo async def act(self) -> ActionOutput: """The exported `act` function""" @@ -584,6 +536,6 @@ class Role(BaseModel): @property def todo(self) -> str: """AgentStore uses this attribute to display to the user what actions the current role should take.""" - if self._actions: - return any_to_name(self._actions[0]) + if self.actions: + return any_to_name(self.actions[0]) return "" diff --git a/metagpt/roles/searcher.py b/metagpt/roles/searcher.py index 6e2bd8bc9..e713f7697 100644 --- a/metagpt/roles/searcher.py +++ b/metagpt/roles/searcher.py @@ -57,19 +57,19 @@ class Searcher(Role): async def _act_sp(self) -> Message: """Performs the search action in a single process.""" - logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - response = await self._rc.todo.run(self._rc.memory.get(k=0)) + logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})") + response = await self.rc.todo.run(self.rc.memory.get(k=0)) if isinstance(response, (ActionOutput, ActionNode)): msg = Message( content=response.content, instruct_content=response.instruct_content, role=self.profile, - cause_by=self._rc.todo, + cause_by=self.rc.todo, ) else: - msg = Message(content=response, role=self.profile, cause_by=self._rc.todo) - self._rc.memory.add(msg) + msg = Message(content=response, role=self.profile, cause_by=self.rc.todo) + self.rc.memory.add(msg) return msg async def _act(self) -> Message: diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index d982ebb68..f7d229adb 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -7,13 +7,13 @@ @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message distribution feature for message filtering. """ -from typing import Any, Type +from typing import Any, Callable, Union from pydantic import Field from semantic_kernel import Kernel from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner -from semantic_kernel.planning.basic_planner import BasicPlanner +from semantic_kernel.planning.basic_planner import BasicPlanner, Plan from metagpt.actions import UserRequirement from metagpt.actions.execute_task import ExecuteTask @@ -41,17 +41,17 @@ class SkAgent(Role): goal: str = "Execute task based on passed in task description" constraints: str = "" - plan: Any = None + plan: Plan = None planner_cls: Any = None - planner: Any = None + planner: Union[BasicPlanner, SequentialPlanner, ActionPlanner] = None llm: BaseLLM = Field(default_factory=LLM) kernel: Kernel = Field(default_factory=Kernel) - import_semantic_skill_from_directory: Type[Kernel.import_semantic_skill_from_directory] = None - import_skill: Type[Kernel.import_skill] = None + import_semantic_skill_from_directory: Callable = None + import_skill: Callable = None - def __init__(self, **kwargs) -> None: + def __init__(self, **data: Any) -> None: """Initializes the Engineer role with given attributes.""" - super().__init__(**kwargs) + super().__init__(**data) self._init_actions([ExecuteTask()]) self._watch([UserRequirement]) self.kernel = make_sk_kernel() @@ -71,10 +71,10 @@ class SkAgent(Role): self._set_state(0) # how funny the interface is inconsistent if isinstance(self.planner, BasicPlanner): - self.plan = await self.planner.create_plan_async(self._rc.important_memory[-1].content, self.kernel) + self.plan = await self.planner.create_plan_async(self.rc.important_memory[-1].content, self.kernel) logger.info(self.plan.generated_plan) elif any(isinstance(self.planner, cls) for cls in [SequentialPlanner, ActionPlanner]): - self.plan = await self.planner.create_plan_async(self._rc.important_memory[-1].content) + self.plan = await self.planner.create_plan_async(self.rc.important_memory[-1].content) async def _act(self) -> Message: # how funny the interface is inconsistent @@ -85,6 +85,6 @@ class SkAgent(Role): result = (await self.plan.invoke_async()).result logger.info(result) - msg = Message(content=result, role=self.profile, cause_by=self._rc.todo) - self._rc.memory.add(msg) + msg = Message(content=result, role=self.profile, cause_by=self.rc.todo) + self.rc.memory.add(msg) return msg diff --git a/metagpt/roles/teacher.py b/metagpt/roles/teacher.py index 3f70200ea..5449fe828 100644 --- a/metagpt/roles/teacher.py +++ b/metagpt/roles/teacher.py @@ -42,34 +42,34 @@ class Teacher(Role): async def _think(self) -> bool: """Everything will be done part by part.""" - if not self._actions: - if not self._rc.news or self._rc.news[0].cause_by != any_to_str(UserRequirement): + if not self.actions: + if not self.rc.news or self.rc.news[0].cause_by != any_to_str(UserRequirement): raise ValueError("Lesson content invalid.") actions = [] print(TeachingPlanBlock.TOPICS) for topic in TeachingPlanBlock.TOPICS: - act = WriteTeachingPlanPart(context=self._rc.news[0].content, topic=topic, llm=self._llm) + act = WriteTeachingPlanPart(context=self.rc.news[0].content, topic=topic, llm=self.llm) actions.append(act) self._init_actions(actions) - if self._rc.todo is None: + if self.rc.todo is None: self._set_state(0) return True - if self._rc.state + 1 < len(self._states): - self._set_state(self._rc.state + 1) + if self.rc.state + 1 < len(self.states): + self._set_state(self.rc.state + 1) return True - self._rc.todo = None + self.rc.todo = None return False async def _react(self) -> Message: ret = Message(content="") while True: await self._think() - if self._rc.todo is None: + if self.rc.todo is None: break - logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}") + logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}") msg = await self._act() if ret.content != "": ret.content += "\n\n\n" @@ -104,7 +104,7 @@ class Teacher(Role): def course_title(self): """Return course title of teaching plan""" default_title = "teaching_plan" - for act in self._actions: + for act in self.actions: if act.topic != TeachingPlanBlock.COURSE_TITLE: continue if act.rsp is None: diff --git a/metagpt/roles/tutorial_assistant.py b/metagpt/roles/tutorial_assistant.py index bedf8b3be..10bd82c60 100644 --- a/metagpt/roles/tutorial_assistant.py +++ b/metagpt/roles/tutorial_assistant.py @@ -34,9 +34,9 @@ class TutorialAssistant(Role): constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout" language: str = "Chinese" - topic = "" - main_title = "" - total_content = "" + topic: str = "" + main_title: str = "" + total_content: str = "" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -71,9 +71,9 @@ class TutorialAssistant(Role): Returns: A message containing the result of the action. """ - todo = self._rc.todo + todo = self.rc.todo if type(todo) is WriteDirectory: - msg = self._rc.memory.get(k=1)[0] + msg = self.rc.memory.get(k=1)[0] self.topic = msg.content resp = await todo.run(topic=self.topic) logger.info(resp) diff --git a/metagpt/schema.py b/metagpt/schema.py index c60247aa1..46064472f 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -23,9 +23,17 @@ from abc import ABC from asyncio import Queue, QueueEmpty, wait_for from json import JSONDecodeError from pathlib import Path -from typing import Any, Dict, List, Optional, Set, Type, TypeVar +from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union -from pydantic import BaseModel, Field +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PrivateAttr, + field_serializer, + field_validator, +) +from pydantic_core import core_schema from metagpt.config import CONFIG from metagpt.const import ( @@ -46,6 +54,64 @@ from metagpt.utils.serialize import ( ) +class SerDeserMixin(BaseModel): + """SereDeserMixin for subclass' ser&deser""" + + __is_polymorphic_base = False + __subclasses_map__ = {} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type["SerDeserMixin"], handler: Callable[[Any], core_schema.CoreSchema] + ) -> core_schema.CoreSchema: + schema = handler(source) + og_schema_ref = schema["ref"] + schema["ref"] += ":mixin" + + return core_schema.no_info_before_validator_function( + cls.__deserialize_with_real_type__, + schema=schema, + ref=og_schema_ref, + serialization=core_schema.wrap_serializer_function_ser_schema(cls.__serialize_add_class_type__), + ) + + @classmethod + def __serialize_add_class_type__( + cls, + value, + handler: core_schema.SerializerFunctionWrapHandler, + ) -> Any: + ret = handler(value) + if not len(cls.__subclasses__()): + # only subclass add `__module_class_name` + ret["__module_class_name"] = f"{cls.__module__}.{cls.__qualname__}" + return ret + + @classmethod + def __deserialize_with_real_type__(cls, value: Any): + if not isinstance(value, dict): + return value + + if not cls.__is_polymorphic_base or (len(cls.__subclasses__()) and "__module_class_name" not in value): + # add right condition to init BaseClass like Action() + return value + module_class_name = value.get("__module_class_name", None) + if module_class_name is None: + raise ValueError("Missing field: __module_class_name") + + class_type = cls.__subclasses_map__.get(module_class_name, None) + + if class_type is None: + raise TypeError("Trying to instantiate {module_class_name} which not defined yet.") + + return class_type(**value) + + def __init_subclass__(cls, is_polymorphic_base: bool = False, **kwargs): + cls.__is_polymorphic_base = is_polymorphic_base + cls.__subclasses_map__[f"{cls.__module__}.{cls.__qualname__}"] = cls + super().__init_subclass__(**kwargs) + + class SimpleMessage(BaseModel): content: str role: str @@ -102,33 +168,64 @@ class Documents(BaseModel): class Message(BaseModel): """list[: ]""" - id: str # According to Section 2.2.3.1.1 of RFC 135 + id: str = Field(default="", validate_default=True) # According to Section 2.2.3.1.1 of RFC 135 content: str - instruct_content: BaseModel = None + instruct_content: Optional[BaseModel] = Field(default=None, validate_default=True) role: str = "user" # system / user / assistant - cause_by: str = "" - sent_from: str = "" - send_to: Set = Field(default_factory={MESSAGE_ROUTE_TO_ALL}) + cause_by: str = Field(default="", validate_default=True) + sent_from: str = Field(default="", validate_default=True) + send_to: set = Field(default={MESSAGE_ROUTE_TO_ALL}, validate_default=True) - def __init__(self, content: str = "", **kwargs): - ic = kwargs.get("instruct_content", None) + @field_validator("id", mode="before") + @classmethod + def check_id(cls, id: str) -> str: + return id if id else uuid.uuid4().hex + + @field_validator("instruct_content", mode="before") + @classmethod + def check_instruct_content(cls, ic: Any) -> BaseModel: if ic and not isinstance(ic, BaseModel) and "class" in ic: # compatible with custom-defined ActionOutput mapping = actionoutput_str_to_mapping(ic["mapping"]) actionnode_class = import_class("ActionNode", "metagpt.actions.action_node") # avoid circular import ic_obj = actionnode_class.create_model_class(class_name=ic["class"], mapping=mapping) - ic_new = ic_obj(**ic["value"]) - kwargs["instruct_content"] = ic_new + ic = ic_obj(**ic["value"]) + return ic - kwargs["id"] = kwargs.get("id", uuid.uuid4().hex) - kwargs["content"] = kwargs.get("content", content) - kwargs["cause_by"] = any_to_str( - kwargs.get("cause_by", import_class("UserRequirement", "metagpt.actions.add_requirement")) - ) - kwargs["sent_from"] = any_to_str(kwargs.get("sent_from", "")) - kwargs["send_to"] = any_to_str_set(kwargs.get("send_to", {MESSAGE_ROUTE_TO_ALL})) - super(Message, self).__init__(**kwargs) + @field_validator("cause_by", mode="before") + @classmethod + def check_cause_by(cls, cause_by: Any) -> str: + return any_to_str(cause_by if cause_by else import_class("UserRequirement", "metagpt.actions.add_requirement")) + + @field_validator("sent_from", mode="before") + @classmethod + def check_sent_from(cls, sent_from: Any) -> str: + return any_to_str(sent_from if sent_from else "") + + @field_validator("send_to", mode="before") + @classmethod + def check_send_to(cls, send_to: Any) -> set: + return any_to_str_set(send_to if send_to else {MESSAGE_ROUTE_TO_ALL}) + + @field_serializer("instruct_content", mode="plain") + def ser_instruct_content(self, ic: BaseModel) -> Union[str, None]: + ic_dict = None + if ic: + # compatible with custom-defined ActionOutput + schema = ic.model_json_schema() + # `Documents` contain definitions + if "definitions" not in schema: + # TODO refine with nested BaseModel + mapping = actionoutout_schema_to_mapping(schema) + mapping = actionoutput_mapping_to_str(mapping) + + ic_dict = {"class": schema["title"], "mapping": mapping, "value": ic.model_dump()} + return ic_dict + + def __init__(self, content: str = "", **data: Any): + data["content"] = data.get("content", content) + super().__init__(**data) def __setattr__(self, key, val): """Override `@property.setter`, convert non-string parameters into string parameters.""" @@ -142,26 +239,10 @@ class Message(BaseModel): new_val = val super().__setattr__(key, new_val) - def dict(self, *args, **kwargs) -> "DictStrAny": - """overwrite the `dict` to dump dynamic pydantic model""" - obj_dict = super(Message, self).dict(*args, **kwargs) - ic = self.instruct_content - if ic: - # compatible with custom-defined ActionOutput - schema = ic.schema() - # `Documents` contain definitions - if "definitions" not in schema: - # TODO refine with nested BaseModel - mapping = actionoutout_schema_to_mapping(schema) - mapping = actionoutput_mapping_to_str(mapping) - - obj_dict["instruct_content"] = {"class": schema["title"], "mapping": mapping, "value": ic.dict()} - return obj_dict - def __str__(self): # prefix = '-'.join([self.role, str(self.cause_by)]) if self.instruct_content: - return f"{self.role}: {self.instruct_content.dict()}" + return f"{self.role}: {self.instruct_content.model_dump()}" return f"{self.role}: {self.content}" def __repr__(self): @@ -173,7 +254,7 @@ class Message(BaseModel): def dump(self) -> str: """Convert the object to json string""" - return self.json(exclude_none=True) + return self.model_dump_json(exclude_none=True, warnings=False) @staticmethod @handle_exception(exception_type=JSONDecodeError, default_return=None) @@ -224,19 +305,9 @@ class AIMessage(Message): class MessageQueue(BaseModel): """Message queue which supports asynchronous updates.""" - _queue: Queue = Field(default_factory=Queue) + model_config = ConfigDict(arbitrary_types_allowed=True) - _private_attributes = {"_queue": Queue()} - - class Config: - arbitrary_types_allowed = True - - def __init__(self, **kwargs: Any): - for key in self._private_attributes.keys(): - if key in kwargs: - object.__setattr__(self, key, kwargs[key]) - else: - object.__setattr__(self, key, Queue()) + _queue: Queue = PrivateAttr(default_factory=Queue) def pop(self) -> Message | None: """Pop one message from the queue.""" @@ -312,28 +383,28 @@ class BaseContext(BaseModel, ABC): class CodingContext(BaseContext): filename: str - design_doc: Optional[Document] - task_doc: Optional[Document] - code_doc: Optional[Document] + design_doc: Optional[Document] = None + task_doc: Optional[Document] = None + code_doc: Optional[Document] = None class TestingContext(BaseContext): filename: str code_doc: Document - test_doc: Optional[Document] + test_doc: Optional[Document] = None class RunCodeContext(BaseContext): mode: str = "script" - code: Optional[str] + code: Optional[str] = None code_filename: str = "" - test_code: Optional[str] + test_code: Optional[str] = None test_filename: str = "" command: List[str] = Field(default_factory=list) working_directory: str = "" additional_python_paths: List[str] = Field(default_factory=list) - output_filename: Optional[str] - output: Optional[str] + output_filename: Optional[str] = None + output: Optional[str] = None class RunCodeResult(BaseContext): diff --git a/metagpt/subscription.py b/metagpt/subscription.py index 607cbdb8d..e2b0916ac 100644 --- a/metagpt/subscription.py +++ b/metagpt/subscription.py @@ -1,7 +1,7 @@ import asyncio from typing import AsyncGenerator, Awaitable, Callable -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from metagpt.logs import logger from metagpt.roles import Role @@ -33,10 +33,9 @@ class SubscriptionRunner(BaseModel): >>> asyncio.run(main()) """ - tasks: dict[Role, asyncio.Task] = Field(default_factory=dict) + model_config = ConfigDict(arbitrary_types_allowed=True) - class Config: - arbitrary_types_allowed = True + tasks: dict[Role, asyncio.Task] = Field(default_factory=dict) async def subscribe( self, diff --git a/metagpt/team.py b/metagpt/team.py index fd9af9045..b98fc2efb 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -10,8 +10,9 @@ import warnings from pathlib import Path +from typing import Any -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from metagpt.actions import UserRequirement from metagpt.config import CONFIG @@ -34,32 +35,27 @@ class Team(BaseModel): dedicated to env any multi-agent activity, such as collaboratively writing executable code. """ + model_config = ConfigDict(arbitrary_types_allowed=True) + env: Environment = Field(default_factory=Environment) investment: float = Field(default=10.0) idea: str = Field(default="") - def __init__(self, **kwargs): - super().__init__(**kwargs) - if "roles" in kwargs: - self.hire(kwargs["roles"]) - if "env_desc" in kwargs: - self.env.desc = kwargs["env_desc"] - - class Config: - arbitrary_types_allowed = True + def __init__(self, **data: Any): + super(Team, self).__init__(**data) + if "roles" in data: + self.hire(data["roles"]) + if "env_desc" in data: + self.env.desc = data["env_desc"] def serialize(self, stg_path: Path = None): stg_path = SERDESER_PATH.joinpath("team") if stg_path is None else stg_path team_info_path = stg_path.joinpath("team_info.json") - write_json_file(team_info_path, self.dict(exclude={"env": True})) + write_json_file(team_info_path, self.model_dump(exclude={"env": True})) self.env.serialize(stg_path.joinpath("environment")) # save environment alone - @classmethod - def recover(cls, stg_path: Path) -> "Team": - return cls.deserialize(stg_path) - @classmethod def deserialize(cls, stg_path: Path) -> "Team": """stg_path = ./storage/team""" @@ -76,7 +72,6 @@ class Team(BaseModel): # recover environment environment = Environment.deserialize(stg_path=stg_path.joinpath("environment")) team_info.update({"env": environment}) - team = Team(**team_info) return team @@ -121,7 +116,7 @@ class Team(BaseModel): return self.run_project(idea=idea, send_to=send_to) def _save(self): - logger.info(self.json(ensure_ascii=False)) + logger.info(self.model_dump_json()) @serialize_decorator async def run(self, n_round=3, idea="", send_to="", auto_archive=True): diff --git a/metagpt/tools/search_engine_googleapi.py b/metagpt/tools/search_engine_googleapi.py index b9faf2ced..8aca3aee2 100644 --- a/metagpt/tools/search_engine_googleapi.py +++ b/metagpt/tools/search_engine_googleapi.py @@ -9,7 +9,7 @@ from typing import Optional from urllib.parse import urlparse import httplib2 -from pydantic import BaseModel, validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from metagpt.config import CONFIG from metagpt.logs import logger @@ -25,15 +25,14 @@ except ImportError: class GoogleAPIWrapper(BaseModel): - google_api_key: Optional[str] = None - google_cse_id: Optional[str] = None + model_config = ConfigDict(arbitrary_types_allowed=True) + + google_api_key: Optional[str] = Field(default=None, validate_default=True) + google_cse_id: Optional[str] = Field(default=None, validate_default=True) loop: Optional[asyncio.AbstractEventLoop] = None executor: Optional[futures.Executor] = None - class Config: - arbitrary_types_allowed = True - - @validator("google_api_key", always=True) + @field_validator("google_api_key", mode="before") @classmethod def check_google_api_key(cls, val: str): val = val or CONFIG.google_api_key @@ -45,7 +44,7 @@ class GoogleAPIWrapper(BaseModel): ) return val - @validator("google_cse_id", always=True) + @field_validator("google_cse_id", mode="before") @classmethod def check_google_cse_id(cls, val: str): val = val or CONFIG.google_cse_id diff --git a/metagpt/tools/search_engine_serpapi.py b/metagpt/tools/search_engine_serpapi.py index 750184198..ecbeac336 100644 --- a/metagpt/tools/search_engine_serpapi.py +++ b/metagpt/tools/search_engine_serpapi.py @@ -8,13 +8,15 @@ from typing import Any, Dict, Optional, Tuple import aiohttp -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from metagpt.config import CONFIG class SerpAPIWrapper(BaseModel): - search_engine: Any #: :meta private: + model_config = ConfigDict(arbitrary_types_allowed=True) + + search_engine: Any = None #: :meta private: params: dict = Field( default={ "engine": "google", @@ -23,13 +25,11 @@ class SerpAPIWrapper(BaseModel): "hl": "en", } ) - serpapi_api_key: Optional[str] = None + # should add `validate_default=True` to check with default value + serpapi_api_key: Optional[str] = Field(default=None, validate_default=True) aiosession: Optional[aiohttp.ClientSession] = None - class Config: - arbitrary_types_allowed = True - - @validator("serpapi_api_key", always=True) + @field_validator("serpapi_api_key", mode="before") @classmethod def check_serpapi_api_key(cls, val: str): val = val or CONFIG.serpapi_api_key diff --git a/metagpt/tools/search_engine_serper.py b/metagpt/tools/search_engine_serper.py index 0eec2694b..3707d905d 100644 --- a/metagpt/tools/search_engine_serper.py +++ b/metagpt/tools/search_engine_serper.py @@ -9,21 +9,18 @@ import json from typing import Any, Dict, Optional, Tuple import aiohttp -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, field_validator from metagpt.config import CONFIG class SerperWrapper(BaseModel): - search_engine: Any #: :meta private: + search_engine: Any = None #: :meta private: payload: dict = Field(default={"page": 1, "num": 10}) - serper_api_key: Optional[str] = None + serper_api_key: Optional[str] = Field(default=None, validate_default=True) aiosession: Optional[aiohttp.ClientSession] = None - class Config: - arbitrary_types_allowed = True - - @validator("serper_api_key", always=True) + @field_validator("serper_api_key", mode="before") @classmethod def check_serper_api_key(cls, val: str): val = val or CONFIG.serper_api_key diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index f03de1da1..d20607d92 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -27,7 +27,7 @@ from typing import Any, Callable, List, Tuple, Union, get_args, get_origin import aiofiles import loguru -from pydantic.json import pydantic_encoder +from pydantic_core import to_jsonable_python from tenacity import RetryCallState, _utils from metagpt.const import MESSAGE_ROUTE_TO_ALL @@ -472,7 +472,7 @@ def write_json_file(json_file: str, data: list, encoding=None): folder_path.mkdir(parents=True, exist_ok=True) with open(json_file, "w", encoding=encoding) as fout: - json.dump(data, fout, ensure_ascii=False, indent=4, default=pydantic_encoder) + json.dump(data, fout, ensure_ascii=False, indent=4, default=to_jsonable_python) def import_class(class_name: str, module_name: str) -> type: @@ -512,7 +512,7 @@ def role_raise_decorator(func): except KeyboardInterrupt as kbi: logger.error(f"KeyboardInterrupt: {kbi} occurs, start to serialize the project") if self.latest_observed_msg: - self._rc.memory.delete(self.latest_observed_msg) + self.rc.memory.delete(self.latest_observed_msg) # raise again to make it captured outside raise Exception(format_trackback_info(limit=None)) except Exception: @@ -522,7 +522,7 @@ def role_raise_decorator(func): "we delete the newest role communication message in the role's memory." ) # remove role newest observed msg to make it observed again - self._rc.memory.delete(self.latest_observed_msg) + self.rc.memory.delete(self.latest_observed_msg) # raise again to make it captured outside raise Exception(format_trackback_info(limit=None)) diff --git a/metagpt/utils/parse_html.py b/metagpt/utils/parse_html.py index f2395026f..65aa3f236 100644 --- a/metagpt/utils/parse_html.py +++ b/metagpt/utils/parse_html.py @@ -5,7 +5,7 @@ from typing import Generator, Optional from urllib.parse import urljoin, urlparse from bs4 import BeautifulSoup -from pydantic import BaseModel +from pydantic import BaseModel, PrivateAttr class WebPage(BaseModel): @@ -13,11 +13,8 @@ class WebPage(BaseModel): html: str url: str - class Config: - underscore_attrs_are_private = True - - _soup: Optional[BeautifulSoup] = None - _title: Optional[str] = None + _soup: Optional[BeautifulSoup] = PrivateAttr(default=None) + _title: Optional[str] = PrivateAttr(default=None) @property def soup(self) -> BeautifulSoup: diff --git a/metagpt/utils/serialize.py b/metagpt/utils/serialize.py index 3939b1306..c6bd8ad75 100644 --- a/metagpt/utils/serialize.py +++ b/metagpt/utils/serialize.py @@ -62,10 +62,10 @@ def serialize_message(message: "Message"): ic = message_cp.instruct_content if ic: # model create by pydantic create_model like `pydantic.main.prd`, can't pickle.dump directly - schema = ic.schema() + schema = ic.model_json_schema() mapping = actionoutout_schema_to_mapping(schema) - message_cp.instruct_content = {"class": schema["title"], "mapping": mapping, "value": ic.dict()} + message_cp.instruct_content = {"class": schema["title"], "mapping": mapping, "value": ic.model_dump()} msg_ser = pickle.dumps(message_cp) return msg_ser diff --git a/requirements.txt b/requirements.txt index a65e1f5b1..81d81ba9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ fire==0.4.0 typer # godot==0.1.1 # google_api_python_client==2.93.0 # Used by search_engine.py -lancedb==0.1.16 +lancedb==0.4.0 langchain==0.0.352 loguru==0.6.0 meilisearch==0.21.0 @@ -19,7 +19,7 @@ openai==1.6.0 openpyxl beautifulsoup4==4.12.2 pandas==2.0.3 -pydantic==1.10.8 +pydantic==2.5.3 #pygame==2.1.3 #pymilvus==2.2.8 pytest==7.2.2 @@ -33,16 +33,15 @@ tqdm==4.64.0 #unstructured[local-inference] # selenium>4 # webdriver_manager<3.9 -anthropic==0.3.6 +anthropic==0.8.1 typing-inspect==0.8.0 -aiofiles -typing_extensions==4.7.0 +typing_extensions==4.9.0 libcst==1.0.1 -qdrant-client==1.4.0 +qdrant-client==1.7.0 pytest-mock==3.11.1 # open-interpreter==0.1.7; python_version>"3.9" # Conflict with openai 1.x ta==0.10.2 -semantic-kernel==0.4.0.dev0 +semantic-kernel==0.4.3.dev0 wrapt==1.15.0 #aiohttp_jinja2 # azure-cognitiveservices-speech~=1.31.0 # Used by metagpt/tools/azure_tts.py diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py index 92d8a1bbc..4e5bf5439 100644 --- a/tests/metagpt/actions/test_action_node.py +++ b/tests/metagpt/actions/test_action_node.py @@ -125,7 +125,7 @@ def test_create_model_class(): def test_create_model_class_with_mapping(): t = ActionNode.create_model_class("test_class_1", WRITE_TASKS_OUTPUT_MAPPING) t1 = t(**t_dict) - value = t1.dict()["Task list"] + value = t1.model_dump()["Task list"] assert value == ["game.py", "app.py", "static/css/styles.css", "static/js/script.js", "templates/index.html"] diff --git a/tests/metagpt/actions/test_debug_error.py b/tests/metagpt/actions/test_debug_error.py index 8289fe41b..6258aa6d4 100644 --- a/tests/metagpt/actions/test_debug_error.py +++ b/tests/metagpt/actions/test_debug_error.py @@ -142,7 +142,7 @@ async def test_debug_error(): "Ran 5 tests in 0.007s\n\nFAILED (failures=1)\n;\n", ) await FileRepository.save_file( - filename=ctx.output_filename, content=output_data.json(), relative_path=TEST_OUTPUTS_FILE_REPO + filename=ctx.output_filename, content=output_data.model_dump_json(), relative_path=TEST_OUTPUTS_FILE_REPO ) debug_error = DebugError(context=ctx) diff --git a/tests/metagpt/actions/test_write_code.py b/tests/metagpt/actions/test_write_code.py index 40a3b44ed..d6c878d8b 100644 --- a/tests/metagpt/actions/test_write_code.py +++ b/tests/metagpt/actions/test_write_code.py @@ -20,11 +20,11 @@ async def test_write_code(): context = CodingContext( filename="task_filename.py", design_doc=Document(content="设计一个名为'add'的函数,该函数接受两个整数作为输入,并返回它们的和。") ) - doc = Document(content=context.json()) + doc = Document(content=context.model_dump_json()) write_code = WriteCode(context=doc) code = await write_code.run() - logger.info(code.json()) + logger.info(code.model_dump_json()) # 我们不能精确地预测生成的代码,但我们可以检查某些关键字 assert "def add" in code.code_doc.content diff --git a/tests/metagpt/actions/test_write_test.py b/tests/metagpt/actions/test_write_test.py index 9c6971ad3..9649b9abb 100644 --- a/tests/metagpt/actions/test_write_test.py +++ b/tests/metagpt/actions/test_write_test.py @@ -29,7 +29,7 @@ async def test_write_test(): write_test = WriteTest(context=context) context = await write_test.run() - logger.info(context.json()) + logger.info(context.model_dump_json()) # We cannot exactly predict the generated test cases, but we can check if it is a string and if it is not empty assert isinstance(context.test_doc.content, str) diff --git a/tests/metagpt/memory/test_brain_memory.py b/tests/metagpt/memory/test_brain_memory.py index 32e58c70e..67f9fc583 100644 --- a/tests/metagpt/memory/test_brain_memory.py +++ b/tests/metagpt/memory/test_brain_memory.py @@ -28,16 +28,16 @@ # bm = BrainMemory() # for h in v.history: # msg = Message(content=h) -# bm.history.append(msg.dict()) +# bm.history.append(msg.model_dump()) # for h in v.solution: # msg = Message(content=h) -# bm.solution.append(msg.dict()) +# bm.solution.append(msg.model_dump()) # for h in v.knowledge: # msg = Message(content=h) -# bm.knowledge.append(msg.dict()) +# bm.knowledge.append(msg.model_dump()) # for h in v.stack: # msg = Message(content=h) -# bm.stack.append(msg.dict()) +# bm.stack.append(msg.model_dump()) # s = bm.json() # m = json.loads(s) # bm = BrainMemory(**m) diff --git a/tests/metagpt/roles/test_role.py b/tests/metagpt/roles/test_role.py index 72cd84a9a..d45b6bd8d 100644 --- a/tests/metagpt/roles/test_role.py +++ b/tests/metagpt/roles/test_role.py @@ -8,4 +8,4 @@ from metagpt.roles.role import Role def test_role_desc(): role = Role(profile="Sales", desc="Best Seller") assert role.profile == "Sales" - assert role._setting.desc == "Best Seller" + assert role.desc == "Best Seller" diff --git a/tests/metagpt/serialize_deserialize/test_action.py b/tests/metagpt/serialize_deserialize/test_action.py index 14d558c13..b3206696b 100644 --- a/tests/metagpt/serialize_deserialize/test_action.py +++ b/tests/metagpt/serialize_deserialize/test_action.py @@ -10,15 +10,20 @@ from metagpt.llm import LLM def test_action_serialize(): action = Action() - ser_action_dict = action.dict() + ser_action_dict = action.model_dump() assert "name" in ser_action_dict - # assert "llm" not in ser_action_dict # not export + assert "llm" not in ser_action_dict # not export + assert "__module_class_name" not in ser_action_dict + + action = Action(name="test") + ser_action_dict = action.model_dump() + assert "test" in ser_action_dict["name"] @pytest.mark.asyncio async def test_action_deserialize(): action = Action() - serialized_data = action.dict() + serialized_data = action.model_dump() new_action = Action(**serialized_data) diff --git a/tests/metagpt/serialize_deserialize/test_architect_deserialize.py b/tests/metagpt/serialize_deserialize/test_architect_deserialize.py index b92eba8a1..b113912a7 100644 --- a/tests/metagpt/serialize_deserialize/test_architect_deserialize.py +++ b/tests/metagpt/serialize_deserialize/test_architect_deserialize.py @@ -10,19 +10,19 @@ from metagpt.roles.architect import Architect def test_architect_serialize(): role = Architect() - ser_role_dict = role.dict(by_alias=True) + ser_role_dict = role.model_dump(by_alias=True) assert "name" in ser_role_dict - assert "_states" in ser_role_dict - assert "_actions" 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) + ser_role_dict = role.model_dump(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(with_messages="write a cli snake game") + assert len(new_role.actions) == 1 + assert isinstance(new_role.actions[0], Action) + await new_role.actions[0].run(with_messages="write a cli snake game") diff --git a/tests/metagpt/serialize_deserialize/test_environment.py b/tests/metagpt/serialize_deserialize/test_environment.py index 096c1dd68..557c3f4cd 100644 --- a/tests/metagpt/serialize_deserialize/test_environment.py +++ b/tests/metagpt/serialize_deserialize/test_environment.py @@ -20,14 +20,15 @@ from tests.metagpt.serialize_deserialize.test_serdeser_base import ( def test_env_serialize(): env = Environment() - ser_env_dict = env.dict() + ser_env_dict = env.model_dump() assert "roles" in ser_env_dict + assert len(ser_env_dict["roles"]) == 0 def test_env_deserialize(): env = Environment() env.publish_message(message=Message(content="test env serialize")) - ser_env_dict = env.dict() + ser_env_dict = env.model_dump() new_env = Environment(**ser_env_dict) assert len(new_env.roles) == 0 assert len(new_env.history) == 25 @@ -47,16 +48,16 @@ def test_environment_serdeser(): environment.add_role(role_c) environment.publish_message(message) - ser_data = environment.dict() + ser_data = environment.model_dump() assert ser_data["roles"]["Role C"]["name"] == "RoleC" new_env: Environment = Environment(**ser_data) assert len(new_env.roles) == 1 - assert list(new_env.roles.values())[0]._states == list(environment.roles.values())[0]._states - assert list(new_env.roles.values())[0]._actions == list(environment.roles.values())[0]._actions - assert isinstance(list(environment.roles.values())[0]._actions[0], ActionOK) - assert type(list(new_env.roles.values())[0]._actions[0]) == ActionOK + assert list(new_env.roles.values())[0].states == list(environment.roles.values())[0].states + assert list(new_env.roles.values())[0].actions == list(environment.roles.values())[0].actions + assert isinstance(list(environment.roles.values())[0].actions[0], ActionOK) + assert type(list(new_env.roles.values())[0].actions[0]) == ActionOK def test_environment_serdeser_v2(): @@ -64,13 +65,13 @@ def test_environment_serdeser_v2(): pm = ProjectManager() environment.add_role(pm) - ser_data = environment.dict() + ser_data = environment.model_dump() new_env: Environment = Environment(**ser_data) role = new_env.get_role(pm.profile) assert isinstance(role, ProjectManager) - assert isinstance(role._actions[0], WriteTasks) - assert isinstance(list(new_env.roles.values())[0]._actions[0], WriteTasks) + assert isinstance(role.actions[0], WriteTasks) + assert isinstance(list(new_env.roles.values())[0].actions[0], WriteTasks) def test_environment_serdeser_save(): @@ -85,4 +86,4 @@ def test_environment_serdeser_save(): new_env: Environment = Environment.deserialize(stg_path) assert len(new_env.roles) == 1 - assert type(list(new_env.roles.values())[0]._actions[0]) == ActionOK + assert type(list(new_env.roles.values())[0].actions[0]) == ActionOK diff --git a/tests/metagpt/serialize_deserialize/test_memory.py b/tests/metagpt/serialize_deserialize/test_memory.py index 5a40f5c3b..aa3e2a465 100644 --- a/tests/metagpt/serialize_deserialize/test_memory.py +++ b/tests/metagpt/serialize_deserialize/test_memory.py @@ -25,7 +25,7 @@ def test_memory_serdeser(): memory = Memory() memory.add_batch([msg1, msg2]) - ser_data = memory.dict() + ser_data = memory.model_dump() new_memory = Memory(**ser_data) assert new_memory.count() == 2 @@ -35,6 +35,9 @@ def test_memory_serdeser(): assert new_memory.storage[-1].cause_by == any_to_str(WriteDesign) assert new_msg2.role == "Boss" + memory = Memory(storage=[msg1, msg2], index={msg1.cause_by: [msg1], msg2.cause_by: [msg2]}) + assert memory.count() == 2 + def test_memory_serdeser_save(): msg1 = Message(role="User", content="write a 2048 game", cause_by=UserRequirement) diff --git a/tests/metagpt/serialize_deserialize/test_polymorphic.py b/tests/metagpt/serialize_deserialize/test_polymorphic.py new file mode 100644 index 000000000..ed0482c34 --- /dev/null +++ b/tests/metagpt/serialize_deserialize/test_polymorphic.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : unittest of polymorphic conditions + +from pydantic import BaseModel, ConfigDict, SerializeAsAny + +from metagpt.actions import Action +from tests.metagpt.serialize_deserialize.test_serdeser_base import ( + ActionOKV2, + ActionPass, +) + + +class ActionSubClasses(BaseModel): + actions: list[SerializeAsAny[Action]] = [] + + +class ActionSubClassesNoSAA(BaseModel): + """without SerializeAsAny""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + actions: list[Action] = [] + + +def test_serialize_as_any(): + """test subclasses of action with different fields in ser&deser""" + # ActionOKV2 with a extra field `extra_field` + action_subcls = ActionSubClasses(actions=[ActionOKV2(), ActionPass()]) + action_subcls_dict = action_subcls.model_dump() + assert action_subcls_dict["actions"][0]["extra_field"] == ActionOKV2().extra_field + + +def test_no_serialize_as_any(): + # ActionOKV2 with a extra field `extra_field` + action_subcls = ActionSubClassesNoSAA(actions=[ActionOKV2(), ActionPass()]) + action_subcls_dict = action_subcls.model_dump() + # without `SerializeAsAny`, it will serialize as Action + assert "extra_field" not in action_subcls_dict["actions"][0] + + +def test_polymorphic(): + _ = ActionOKV2( + **{"name": "ActionOKV2", "context": "", "prefix": "", "desc": "", "extra_field": "ActionOKV2 Extra Info"} + ) + + action_subcls = ActionSubClasses(actions=[ActionOKV2(), ActionPass()]) + action_subcls_dict = action_subcls.model_dump() + + assert "__module_class_name" in action_subcls_dict["actions"][0] + + new_action_subcls = ActionSubClasses(**action_subcls_dict) + assert isinstance(new_action_subcls.actions[0], ActionOKV2) + assert isinstance(new_action_subcls.actions[1], ActionPass) + + new_action_subcls = ActionSubClasses.model_validate(action_subcls_dict) + assert isinstance(new_action_subcls.actions[0], ActionOKV2) + assert isinstance(new_action_subcls.actions[1], ActionPass) diff --git a/tests/metagpt/serialize_deserialize/test_product_manager.py b/tests/metagpt/serialize_deserialize/test_product_manager.py index b65e329d1..5e1624503 100644 --- a/tests/metagpt/serialize_deserialize/test_product_manager.py +++ b/tests/metagpt/serialize_deserialize/test_product_manager.py @@ -12,10 +12,10 @@ from metagpt.schema import Message @pytest.mark.asyncio async def test_product_manager_deserialize(): role = ProductManager() - ser_role_dict = role.dict(by_alias=True) + ser_role_dict = role.model_dump(by_alias=True) new_role = ProductManager(**ser_role_dict) assert new_role.name == "Alice" - assert len(new_role._actions) == 2 - assert isinstance(new_role._actions[0], Action) - await new_role._actions[0].run([Message(content="write a cli snake game")]) + assert len(new_role.actions) == 2 + assert isinstance(new_role.actions[0], Action) + await new_role.actions[0].run([Message(content="write a cli snake game")]) diff --git a/tests/metagpt/serialize_deserialize/test_project_manager.py b/tests/metagpt/serialize_deserialize/test_project_manager.py index e52e3f247..1088a4461 100644 --- a/tests/metagpt/serialize_deserialize/test_project_manager.py +++ b/tests/metagpt/serialize_deserialize/test_project_manager.py @@ -11,20 +11,20 @@ from metagpt.roles.project_manager import ProjectManager def test_project_manager_serialize(): role = ProjectManager() - ser_role_dict = role.dict(by_alias=True) + ser_role_dict = role.model_dump(by_alias=True) assert "name" in ser_role_dict - assert "_states" in ser_role_dict - assert "_actions" 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) + ser_role_dict = role.model_dump(by_alias=True) new_role = ProjectManager(**ser_role_dict) assert new_role.name == "Eve" - assert len(new_role._actions) == 1 - assert isinstance(new_role._actions[0], Action) - assert isinstance(new_role._actions[0], WriteTasks) - # await new_role._actions[0].run(context="write a cli snake game") + assert len(new_role.actions) == 1 + assert isinstance(new_role.actions[0], Action) + assert isinstance(new_role.actions[0], WriteTasks) + # await new_role.actions[0].run(context="write a cli snake game") diff --git a/tests/metagpt/serialize_deserialize/test_role.py b/tests/metagpt/serialize_deserialize/test_role.py index 343f01ace..d38797baf 100644 --- a/tests/metagpt/serialize_deserialize/test_role.py +++ b/tests/metagpt/serialize_deserialize/test_role.py @@ -6,6 +6,7 @@ import shutil import pytest +from pydantic import BaseModel, SerializeAsAny from metagpt.actions import WriteCode from metagpt.actions.add_requirement import UserRequirement @@ -17,48 +18,67 @@ from metagpt.roles.role import Role from metagpt.schema import Message from metagpt.utils.common import format_trackback_info from tests.metagpt.serialize_deserialize.test_serdeser_base import ( + ActionOK, RoleA, RoleB, RoleC, + RoleD, serdeser_path, ) def test_roles(): role_a = RoleA() - assert len(role_a._rc.watch) == 1 + assert len(role_a.rc.watch) == 1 role_b = RoleB() - assert len(role_a._rc.watch) == 1 - assert len(role_b._rc.watch) == 1 + assert len(role_a.rc.watch) == 1 + assert len(role_b.rc.watch) == 1 + + role_d = RoleD(actions=[ActionOK()]) + assert len(role_d.actions) == 1 + + +def test_role_subclasses(): + """test subclasses of role with same fields in ser&deser""" + + class RoleSubClasses(BaseModel): + roles: list[SerializeAsAny[Role]] = [] + + role_subcls = RoleSubClasses(roles=[RoleA(), RoleB()]) + role_subcls_dict = role_subcls.model_dump() + + new_role_subcls = RoleSubClasses(**role_subcls_dict) + assert isinstance(new_role_subcls.roles[0], RoleA) + assert isinstance(new_role_subcls.roles[1], RoleB) def test_role_serialize(): role = Role() - ser_role_dict = role.dict(by_alias=True) + ser_role_dict = role.model_dump() assert "name" in ser_role_dict - assert "_states" in ser_role_dict - assert "_actions" 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) + ser_role_dict = role.model_dump() assert "name" in ser_role_dict - assert "_states" in ser_role_dict - assert "_actions" 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) + ser_role_dict = role.model_dump() new_role = Engineer(**ser_role_dict) assert new_role.name == "Alex" assert new_role.use_code_review is True - assert len(new_role._actions) == 1 - assert isinstance(new_role._actions[0], WriteCode) - # await new_role._actions[0].run(context="write a cli snake game", filename="test_code") + assert len(new_role.actions) == 1 + assert isinstance(new_role.actions[0], WriteCode) + # await new_role.actions[0].run(context="write a cli snake game", filename="test_code") def test_role_serdeser_save(): @@ -87,10 +107,10 @@ async def test_role_serdeser_interrupt(): logger.error(f"Exception in `role_a.run`, detail: {format_trackback_info()}") role_c.serialize(stg_path) - assert role_c._rc.memory.count() == 1 + assert role_c.rc.memory.count() == 1 new_role_a: Role = Role.deserialize(stg_path) - assert new_role_a._rc.state == 1 + assert new_role_a.rc.state == 1 with pytest.raises(Exception): await new_role_a.run(with_message=Message(content="demo", cause_by=UserRequirement)) diff --git a/tests/metagpt/serialize_deserialize/test_schema.py b/tests/metagpt/serialize_deserialize/test_schema.py index 0358265a9..e793079f0 100644 --- a/tests/metagpt/serialize_deserialize/test_schema.py +++ b/tests/metagpt/serialize_deserialize/test_schema.py @@ -4,9 +4,12 @@ from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode -from metagpt.schema import Message +from metagpt.schema import Document, Documents, Message from metagpt.utils.common import any_to_str -from tests.metagpt.serialize_deserialize.test_serdeser_base import MockMessage +from tests.metagpt.serialize_deserialize.test_serdeser_base import ( + MockICMessage, + MockMessage, +) def test_message_serdeser(): @@ -15,14 +18,24 @@ def test_message_serdeser(): ic_obj = ActionNode.create_model_class("code", out_mapping) message = Message(content="code", instruct_content=ic_obj(**out_data), role="engineer", cause_by=WriteCode) - ser_data = message.dict() + ser_data = message.model_dump() assert ser_data["cause_by"] == "metagpt.actions.write_code.WriteCode" assert ser_data["instruct_content"]["class"] == "code" new_message = Message(**ser_data) assert new_message.cause_by == any_to_str(WriteCode) assert new_message.cause_by in [any_to_str(WriteCode)] - assert new_message.instruct_content == ic_obj(**out_data) + assert new_message.instruct_content != ic_obj(**out_data) # TODO find why `!=` + assert new_message.instruct_content.model_dump() == ic_obj(**out_data).model_dump() + + message = Message(content="test_ic", instruct_content=MockICMessage()) + ser_data = message.model_dump() + new_message = Message(**ser_data) + assert new_message.instruct_content != MockICMessage() # TODO + + message = Message(content="test_documents", instruct_content=Documents(docs={"doc1": Document(content="test doc")})) + ser_data = message.model_dump() + assert "class" in ser_data["instruct_content"] def test_message_without_postprocess(): @@ -31,8 +44,9 @@ def test_message_without_postprocess(): out_data = {"field1": ["field1 value1", "field1 value2"]} ic_obj = ActionNode.create_model_class("code", out_mapping) message = MockMessage(content="code", instruct_content=ic_obj(**out_data)) - ser_data = message.dict() - assert ser_data["instruct_content"] == {"field1": ["field1 value1", "field1 value2"]} + ser_data = message.model_dump() + assert ser_data["instruct_content"] == {} + ser_data["instruct_content"] = None new_message = MockMessage(**ser_data) assert new_message.instruct_content != ic_obj(**out_data) diff --git a/tests/metagpt/serialize_deserialize/test_serdeser_base.py b/tests/metagpt/serialize_deserialize/test_serdeser_base.py index 23c14e851..daa46c99c 100644 --- a/tests/metagpt/serialize_deserialize/test_serdeser_base.py +++ b/tests/metagpt/serialize_deserialize/test_serdeser_base.py @@ -4,6 +4,7 @@ import asyncio from pathlib import Path +from typing import Optional from pydantic import BaseModel, Field @@ -15,15 +16,19 @@ from metagpt.roles.role import Role, RoleReactMode serdeser_path = Path(__file__).absolute().parent.joinpath("..", "..", "data", "serdeser_storage") +class MockICMessage(BaseModel): + content: str = "test_ic" + + class MockMessage(BaseModel): """to test normal dict without postprocess""" content: str = "" - instruct_content: BaseModel = Field(default=None) + instruct_content: Optional[BaseModel] = Field(default=None) class ActionPass(Action): - name: str = Field(default="ActionPass") + name: str = "ActionPass" async def run(self, messages: list["Message"]) -> ActionOutput: await asyncio.sleep(5) # sleep to make other roles can watch the executed Message @@ -35,7 +40,7 @@ class ActionPass(Action): class ActionOK(Action): - name: str = Field(default="ActionOK") + name: str = "ActionOK" async def run(self, messages: list["Message"]) -> str: await asyncio.sleep(5) @@ -43,12 +48,17 @@ class ActionOK(Action): class ActionRaise(Action): - name: str = Field(default="ActionRaise") + name: str = "ActionRaise" async def run(self, messages: list["Message"]) -> str: raise RuntimeError("parse error in ActionRaise") +class ActionOKV2(Action): + name: str = "ActionOKV2" + extra_field: str = "ActionOKV2 Extra Info" + + class RoleA(Role): name: str = Field(default="RoleA") profile: str = Field(default="Role A") @@ -71,7 +81,7 @@ class RoleB(Role): super(RoleB, self).__init__(**kwargs) self._init_actions([ActionOK, ActionRaise]) self._watch([ActionPass]) - self._rc.react_mode = RoleReactMode.BY_ORDER + self.rc.react_mode = RoleReactMode.BY_ORDER class RoleC(Role): @@ -84,5 +94,12 @@ class RoleC(Role): super(RoleC, self).__init__(**kwargs) self._init_actions([ActionOK, ActionRaise]) self._watch([UserRequirement]) - self._rc.react_mode = RoleReactMode.BY_ORDER - self._rc.memory.ignore_id = True + self.rc.react_mode = RoleReactMode.BY_ORDER + self.rc.memory.ignore_id = True + + +class RoleD(Role): + name: str = Field(default="RoleD") + profile: str = Field(default="Role D") + goal: str = "RoleD's goal" + constraints: str = "RoleD's constraints" diff --git a/tests/metagpt/serialize_deserialize/test_team.py b/tests/metagpt/serialize_deserialize/test_team.py index dc41fa4ed..566f63c3d 100644 --- a/tests/metagpt/serialize_deserialize/test_team.py +++ b/tests/metagpt/serialize_deserialize/test_team.py @@ -33,8 +33,8 @@ def test_team_deserialize(): ] ) assert len(company.env.get_roles()) == 3 - ser_company = company.dict() - new_company = Team(**ser_company) + ser_company = company.model_dump() + new_company = Team.model_validate(ser_company) assert len(new_company.env.get_roles()) == 3 assert new_company.env.get_role(pm.profile) is not None @@ -47,6 +47,7 @@ def test_team_deserialize(): def test_team_serdeser_save(): company = Team() + company.hire([RoleC()]) stg_path = serdeser_path.joinpath("team") @@ -71,13 +72,13 @@ async def test_team_recover(): company.run_project(idea) await company.run(n_round=4) - ser_data = company.dict() + ser_data = company.model_dump() new_company = Team(**ser_data) - new_role_c = new_company.env.get_role(role_c.profile) - # assert new_role_c._rc.memory == role_c._rc.memory # TODO - assert new_role_c._rc.env != role_c._rc.env # TODO - assert type(list(new_company.env.roles.values())[0]._actions[0]) == ActionOK + new_company.env.get_role(role_c.profile) + # assert new_role_c.rc.memory == role_c.rc.memory # TODO + # assert new_role_c.rc.env != role_c.rc.env # TODO + assert type(list(new_company.env.roles.values())[0].actions[0]) == ActionOK new_company.run_project(idea) await new_company.run(n_round=4) @@ -97,11 +98,11 @@ async def test_team_recover_save(): new_company = Team.deserialize(stg_path) new_role_c = new_company.env.get_role(role_c.profile) - # assert new_role_c._rc.memory == role_c._rc.memory - assert new_role_c._rc.env != role_c._rc.env + # assert new_role_c.rc.memory == role_c.rc.memory + # assert new_role_c.rc.env != role_c.rc.env assert new_role_c.recovered != role_c.recovered # here cause previous ut is `!=` - assert new_role_c._rc.todo != role_c._rc.todo # serialize exclude `_rc.todo` - assert new_role_c._rc.news != role_c._rc.news # serialize exclude `_rc.news` + assert new_role_c.rc.todo != role_c.rc.todo # serialize exclude `rc.todo` + assert new_role_c.rc.news != role_c.rc.news # serialize exclude `rc.news` new_company.run_project(idea) await new_company.run(n_round=4) @@ -116,10 +117,6 @@ async def test_team_recover_multi_roles_save(): role_a = RoleA() role_b = RoleB() - assert role_a.subscription == {"tests.metagpt.serialize_deserialize.test_serdeser_base.RoleA", "RoleA"} - assert role_b.subscription == {"tests.metagpt.serialize_deserialize.test_serdeser_base.RoleB", "RoleB"} - assert role_b._rc.watch == {"tests.metagpt.serialize_deserialize.test_serdeser_base.ActionPass"} - company = Team() company.hire([role_a, role_b]) company.run_project(idea) @@ -130,6 +127,6 @@ async def test_team_recover_multi_roles_save(): new_company = Team.deserialize(stg_path) new_company.run_project(idea) - assert new_company.env.get_role(role_b.profile)._rc.state == 1 + assert new_company.env.get_role(role_b.profile).rc.state == 1 await new_company.run(n_round=4) diff --git a/tests/metagpt/serialize_deserialize/test_write_code.py b/tests/metagpt/serialize_deserialize/test_write_code.py index 65b8f456a..2fb669a6b 100644 --- a/tests/metagpt/serialize_deserialize/test_write_code.py +++ b/tests/metagpt/serialize_deserialize/test_write_code.py @@ -12,9 +12,9 @@ from metagpt.schema import CodingContext, Document def test_write_design_serialize(): action = WriteCode() - ser_action_dict = action.dict() + ser_action_dict = action.model_dump() assert ser_action_dict["name"] == "WriteCode" - # assert "llm" in ser_action_dict # not export + assert "llm" not in ser_action_dict # not export @pytest.mark.asyncio @@ -22,9 +22,9 @@ async def test_write_code_deserialize(): context = CodingContext( filename="test_code.py", design_doc=Document(content="write add function to calculate two numbers") ) - doc = Document(content=context.json()) + doc = Document(content=context.model_dump_json()) action = WriteCode(context=doc) - serialized_data = action.dict() + serialized_data = action.model_dump() new_action = WriteCode(**serialized_data) assert new_action.name == "WriteCode" diff --git a/tests/metagpt/serialize_deserialize/test_write_code_review.py b/tests/metagpt/serialize_deserialize/test_write_code_review.py index 01026590c..e9ad4b858 100644 --- a/tests/metagpt/serialize_deserialize/test_write_code_review.py +++ b/tests/metagpt/serialize_deserialize/test_write_code_review.py @@ -22,7 +22,7 @@ def div(a: int, b: int = 0): ) action = WriteCodeReview(context=context) - serialized_data = action.dict() + serialized_data = action.model_dump() assert serialized_data["name"] == "WriteCodeReview" new_action = WriteCodeReview(**serialized_data) diff --git a/tests/metagpt/serialize_deserialize/test_write_design.py b/tests/metagpt/serialize_deserialize/test_write_design.py index 4e768ddd7..d556c144d 100644 --- a/tests/metagpt/serialize_deserialize/test_write_design.py +++ b/tests/metagpt/serialize_deserialize/test_write_design.py @@ -10,22 +10,22 @@ from metagpt.llm import LLM def test_write_design_serialize(): action = WriteDesign() - ser_action_dict = action.dict() + ser_action_dict = action.model_dump() assert "name" in ser_action_dict - # assert "llm" in ser_action_dict # not export + assert "llm" not in ser_action_dict # not export def test_write_task_serialize(): action = WriteTasks() - ser_action_dict = action.dict() + ser_action_dict = action.model_dump() assert "name" in ser_action_dict - # assert "llm" in ser_action_dict # not export + assert "llm" not in ser_action_dict # not export @pytest.mark.asyncio async def test_write_design_deserialize(): action = WriteDesign() - serialized_data = action.dict() + serialized_data = action.model_dump() new_action = WriteDesign(**serialized_data) assert new_action.name == "" assert new_action.llm == LLM() @@ -35,7 +35,7 @@ async def test_write_design_deserialize(): @pytest.mark.asyncio async def test_write_task_deserialize(): action = WriteTasks() - serialized_data = action.dict() + serialized_data = action.model_dump() new_action = WriteTasks(**serialized_data) assert new_action.name == "CreateTasks" assert new_action.llm == LLM() diff --git a/tests/metagpt/serialize_deserialize/test_write_prd.py b/tests/metagpt/serialize_deserialize/test_write_prd.py index d6d14f99a..79b9a8677 100644 --- a/tests/metagpt/serialize_deserialize/test_write_prd.py +++ b/tests/metagpt/serialize_deserialize/test_write_prd.py @@ -12,15 +12,15 @@ from metagpt.schema import Message def test_action_serialize(): action = WritePRD() - ser_action_dict = action.dict() + ser_action_dict = action.model_dump() assert "name" in ser_action_dict - # assert "llm" in ser_action_dict # not export + assert "llm" not in ser_action_dict # not export @pytest.mark.asyncio async def test_action_deserialize(): action = WritePRD() - serialized_data = action.dict() + serialized_data = action.model_dump() new_action = WritePRD(**serialized_data) assert new_action.name == "" assert new_action.llm == LLM() diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index dbe45130d..6589f6ade 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -33,6 +33,15 @@ class MockRole(Role): self._init_actions([MockAction()]) +def test_basic(): + mock_role = MockRole() + assert mock_role.subscription == {"tests.metagpt.test_role.MockRole"} + assert mock_role.rc.watch == {"metagpt.actions.add_requirement.UserRequirement"} + + mock_role = MockRole(name="mock_role") + assert mock_role.subscription == {"tests.metagpt.test_role.MockRole", "mock_role"} + + @pytest.mark.asyncio async def test_react(): class Input(BaseModel): @@ -60,12 +69,12 @@ async def test_react(): name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints, desc=seed.desc ) role.subscribe({seed.subscription}) - assert role._rc.watch == {any_to_str(UserRequirement)} + assert role.rc.watch == {any_to_str(UserRequirement)} assert role.name == seed.name assert role.profile == seed.profile - assert role._setting.goal == seed.goal - assert role._setting.constraints == seed.constraints - assert role._setting.desc == seed.desc + assert role.goal == seed.goal + assert role.constraints == seed.constraints + assert role.desc == seed.desc assert role.is_idle env = Environment() env.add_role(role) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 897d203c7..a6316733a 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -31,6 +31,8 @@ def test_messages(): def test_message(): + Message("a", role="v1") + m = Message(content="a", role="v1") v = m.dump() d = json.loads(v) @@ -74,22 +76,22 @@ def test_message_serdeser(): ic_obj = ActionNode.create_model_class("code", out_mapping) message = Message(content="code", instruct_content=ic_obj(**out_data), role="engineer", cause_by=WriteCode) - message_dict = message.dict() + message_dict = message.model_dump() assert message_dict["cause_by"] == "metagpt.actions.write_code.WriteCode" assert message_dict["instruct_content"] == { "class": "code", "mapping": {"field3": "(, Ellipsis)", "field4": "(list[str], Ellipsis)"}, "value": {"field3": "field3 value3", "field4": ["field4 value1", "field4 value2"]}, } - - new_message = Message(**message_dict) + new_message = Message.model_validate(message_dict) assert new_message.content == message.content - assert new_message.instruct_content == message.instruct_content + assert new_message.instruct_content.model_dump() == message.instruct_content.model_dump() + assert new_message.instruct_content != message.instruct_content # TODO assert new_message.cause_by == message.cause_by assert new_message.instruct_content.field3 == out_data["field3"] message = Message(content="code") - message_dict = message.dict() + message_dict = message.model_dump() new_message = Message(**message_dict) assert new_message.instruct_content is None assert new_message.cause_by == "metagpt.actions.add_requirement.UserRequirement" diff --git a/tests/metagpt/utils/test_common.py b/tests/metagpt/utils/test_common.py index 53708527f..2440e04ab 100644 --- a/tests/metagpt/utils/test_common.py +++ b/tests/metagpt/utils/test_common.py @@ -56,7 +56,7 @@ class TestGetProjectRoot: def test_any_to_str(self): class Input(BaseModel): - x: Any + x: Any = None want: str inputs = [ @@ -74,7 +74,7 @@ class TestGetProjectRoot: def test_any_to_str_set(self): class Input(BaseModel): - x: Any + x: Any = None want: Set inputs = [ diff --git a/tests/metagpt/utils/test_dependency_file.py b/tests/metagpt/utils/test_dependency_file.py index ae4d40ea5..0ff5e97b0 100644 --- a/tests/metagpt/utils/test_dependency_file.py +++ b/tests/metagpt/utils/test_dependency_file.py @@ -21,8 +21,8 @@ from metagpt.utils.dependency_file import DependencyFile async def test_dependency_file(): class Input(BaseModel): x: Union[Path, str] - deps: Optional[Set[Union[Path, str]]] - key: Optional[Union[Path, str]] + deps: Optional[Set[Union[Path, str]]] = None + key: Optional[Union[Path, str]] = None want: Set[str] inputs = [