diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 8b28ffd8e..c941d44b6 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -64,11 +64,10 @@ class Action(BaseModel): """Set prefix for later usage""" self.prefix = prefix self.profile = profile - return self def __str__(self): return self.__class__.__name__ - + def __repr__(self): return self.__str__() diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 504328582..a13c5873a 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -16,6 +16,11 @@ from pydantic import Field from metagpt.actions import Action, ActionOutput from metagpt.actions.design_api_an import DESIGN_API_NODE +from typing import List, Optional, Any + +from pydantic import Field + +from metagpt.actions import Action, ActionOutput from metagpt.llm import LLM from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.config import CONFIG diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 83225060a..636f3f12a 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -7,6 +7,9 @@ @Modified By: mashenquan, 2023/11/27. Following the think-act principle, solidify the task parameters when creating the WriteCode object, rather than passing them in when calling the run function. """ +from typing import List, Optional, Any +from pydantic import Field +from tenacity import retry, stop_after_attempt, wait_fixed from typing import List, Optional, Any from pydantic import Field diff --git a/metagpt/const.py b/metagpt/const.py index 9cf9726fc..3b4f2ae4b 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -55,6 +55,7 @@ DATA_PATH = METAGPT_ROOT / "data" RESEARCH_PATH = DATA_PATH / "research" TUTORIAL_PATH = DATA_PATH / "tutorial_docx" INVOICE_OCR_TABLE_PATH = DATA_PATH / "invoice_table" + UT_PATH = DATA_PATH / "ut" SWAGGER_PATH = UT_PATH / "files/api/" UT_PY_PATH = UT_PATH / "files/ut/" diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index e8a5be395..d36188f0c 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -7,6 +7,9 @@ from typing import Optional from pydantic import Field +from typing import Optional +from pydantic import Field + from metagpt.logs import logger from metagpt.memory import Memory from metagpt.memory.memory_storage import MemoryStorage diff --git a/metagpt/memory/memory.py b/metagpt/memory/memory.py index adef0d283..b647198e3 100644 --- a/metagpt/memory/memory.py +++ b/metagpt/memory/memory.py @@ -8,6 +8,7 @@ """ import copy from collections import defaultdict + from typing import Iterable, Type, Union, Optional, Set from pathlib import Path from pydantic import BaseModel, Field diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index 377531c8d..266ffc256 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -22,6 +22,7 @@ class Architect(Role): goal (str): Primary goal or responsibility of the architect. constraints (str): Constraints or guidelines for the architect. """ + name: str = "Bob" profile: str = Field(default="Architect", alias='profile') goal: str = "design a concise, usable, complete software system" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 59ca18a17..ad3d0f66a 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -49,7 +49,6 @@ from metagpt.utils.common import any_to_str, any_to_str_set IS_PASS_PROMPT = """ {context} -<<<<<<< HEAD ---- Does the above log indicate anything that needs to be done? If there are any tasks to be completed, please answer 'NO' along with the to-do list in JSON format; diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index b7ee1ed53..d885f2ee6 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -25,6 +25,7 @@ class ProjectManager(Role): name: str = Field(default="Eve") profile: str = Field(default="Project Manager") + goal: str = "reak down tasks according to PRD/technical design, generate a task list, and analyze task " \ "dependencies to start with the prerequisite modules" constraints: str = "use same language as user requirement" diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index e63404939..bed5a38e7 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -124,7 +124,7 @@ class RoleContext(BaseModel): news: list[Type[Message]] = Field(default=[], exclude=True) # TODO not used react_mode: RoleReactMode = RoleReactMode.REACT # see `Role._set_react_mode` for definitions of the following two attributes max_react_loop: int = 1 - + class Config: arbitrary_types_allowed = True @@ -175,7 +175,7 @@ class _RoleInjector(type): role_subclass_registry = {} -class Role(BaseModel): +class Role(BaseModel, metaclass=_RoleInjector): """Role/Agent""" name: str = "" profile: str = "" @@ -248,6 +248,62 @@ class Role(BaseModel): super().__init_subclass__(**kwargs) role_subclass_registry[cls.__name__] = cls + # builtin variables + recovered: bool = False # to tag if a recovered role + builtin_class_name: str = "" + + _private_attributes = { + "_llm": LLM() if not is_human else HumanProvider(), + "_role_id": _role_id, + "_states": [], + "_actions": [], + "_rc": RoleContext() + } + + class Config: + arbitrary_types_allowed = True + exclude = ["_llm"] + + def __init__(self, **kwargs: 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 + + super().__init__(**kwargs) + + # 关于私有变量的初始化 https://github.com/pydantic/pydantic/issues/655 + self._private_attributes["_llm"] = LLM() if not self.is_human else HumanProvider() + self._private_attributes["_role_id"] = str(self._setting) + + for key in self._private_attributes.keys(): + if key in kwargs: + object.__setattr__(self, key, kwargs[key]) + if key == "_rc": + _rc = RoleContext(**kwargs["_rc"]) + object.__setattr__(self, "_rc", _rc) + else: + if key == "_rc": + # # Warning, if use self._private_attributes["_rc"], + # # self._rc will be a shared object between roles, so init one or reset it inside `_reset` + object.__setattr__(self, key, RoleContext()) + else: + object.__setattr__(self, key, self._private_attributes[key]) + + # deserialize child classes dynamically for inherited `role` + object.__setattr__(self, "builtin_class_name", self.__class__.__name__) + self.__fields__["builtin_class_name"].default = self.__class__.__name__ + + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + role_subclass_registry[cls.__name__] = cls + def _reset(self): object.__setattr__(self, "_states", []) object.__setattr__(self, "_actions", []) @@ -442,7 +498,7 @@ class Role(BaseModel): if next_state == -1: logger.info(f"End actions with {next_state=}") self._set_state(next_state) - + async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") response = await self._rc.todo.run(self._rc.important_memory) @@ -574,7 +630,7 @@ class Role(BaseModel): # If there is no new information, suspend and wait logger.debug(f"{self._setting}: no news. waiting.") return - + rsp = await self.react() # Reset the next action to be taken. diff --git a/metagpt/schema.py b/metagpt/schema.py index 15dfb579c..962850547 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -39,6 +39,9 @@ from metagpt.const import ( TASK_FILE_REPO, ) from metagpt.logs import logger +from metagpt.utils.serialize import actionoutout_schema_to_mapping, actionoutput_mapping_to_str, \ + actionoutput_str_to_mapping +from metagpt.utils.utils import import_class from metagpt.utils.common import any_to_str, any_to_str_set # from metagpt.utils.serialize import actionoutout_schema_to_mapping @@ -55,6 +58,7 @@ class RawMessage(TypedDict): role: str + class Document(BaseModel): """ Represents a document. diff --git a/metagpt/team.py b/metagpt/team.py index 87a6766f6..bd02508c4 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -99,6 +99,7 @@ class Team(BaseModel): n_round -= 1 logger.debug(f"max {n_round=} left.") self._check_balance() + await self.env.run() if CONFIG.git_repo: CONFIG.git_repo.archive() diff --git a/metagpt/utils/utils.py b/metagpt/utils/utils.py index 33ca16944..35df654d7 100644 --- a/metagpt/utils/utils.py +++ b/metagpt/utils/utils.py @@ -8,6 +8,9 @@ import json from pathlib import Path import importlib from tenacity import _utils +import traceback + +from metagpt.logs import logger def general_after_log(logger: "loguru.Logger", sec_format: str = "%0.3f") -> typing.Callable[["RetryCallState"], None]: diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index 8aacdd77b..ee322368e 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -61,6 +61,7 @@ async def test_publish_and_process_message(env: Environment): constraints="资源有限,需要节省成本") env.add_roles([product_manager, architect]) + env.set_manager(Manager()) env.publish_message(Message(role="User", content="需要一个基于LLM做总结的搜索引擎", cause_by=UserRequirement)) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 5eea789ea..ca8b9043f 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -8,16 +8,22 @@ the utilization of the new feature of `Message` class. """ +<<<<<<< HEAD import json import pytest from metagpt.actions import Action +======= +>>>>>>> a69be36abf7beef1a989a707d1aa027948c07fee from metagpt.schema import AIMessage, Message, SystemMessage, UserMessage from metagpt.actions.action_output import ActionOutput from metagpt.actions.write_code import WriteCode from metagpt.utils.serialize import serialize_general_message, deserialize_general_message +<<<<<<< HEAD from metagpt.utils.common import get_class_name +======= +>>>>>>> a69be36abf7beef1a989a707d1aa027948c07fee @pytest.mark.asyncio @@ -110,7 +116,3 @@ def test_message_serdeser(): new_message = deserialize_general_message(message_dict) assert new_message.instruct_content is None assert new_message.cause_by == "" - - -if __name__ == "__main__": - pytest.main([__file__, "-s"])