update ser&deser after env_refactor

This commit is contained in:
better629 2023-12-19 14:22:52 +08:00
parent 35ac28c30e
commit ebc4fe4b17
15 changed files with 152 additions and 200 deletions

View file

@ -7,23 +7,21 @@
"""
from __future__ import annotations
import re
from typing import Optional, Any
from typing import Optional, Any
from tenacity import retry, stop_after_attempt, wait_random_exponential
from pydantic import BaseModel, Field
from tenacity import retry, stop_after_attempt, wait_random_exponential
from metagpt.actions.action_output import ActionOutput
from metagpt.llm import LLM
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.provider.postprecess.llm_output_postprecess import llm_output_postprecess
from metagpt.utils.common import OutputParser
from metagpt.utils.utils import general_after_log
from metagpt.utils.utils import import_class
action_subclass_registry = {}
@ -31,9 +29,10 @@ class Action(BaseModel):
name: str = ""
llm: BaseGPTAPI = Field(default_factory=LLM, exclude=True)
context = ""
prefix = "" # aask*时会加上prefix作为system_message
prefix = "" # aask*时会加上prefix作为system_message
profile = "" # FIXME: USELESS
desc = "" # for skill manager
desc = "" # for skill manager
nodes = []
# content: Optional[str] = None
# instruct_content: Optional[str] = None
@ -42,7 +41,7 @@ class Action(BaseModel):
class Config:
arbitrary_types_allowed = True
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
@ -64,10 +63,11 @@ 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__()
@ -110,16 +110,16 @@ class Action(BaseModel):
content = await self.llm.aask(prompt, system_msgs)
logger.debug(f"llm raw output:\n{content}")
output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping)
if format == "json":
parsed_data = llm_output_postprecess(output=content, schema=output_class.schema(), req_key="[/CONTENT]")
else: # using markdown parser
parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping)
logger.debug(parsed_data)
instruct_content = output_class(**parsed_data)
return ActionOutput(content, instruct_content)
async def run(self, *args, **kwargs):
"""Run action"""
raise NotImplementedError("The run method should be implemented in a subclass.")

View file

@ -19,8 +19,6 @@ from metagpt.utils.git_repository import GitRepository
class PrepareDocuments(Action):
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
async def run(self, with_messages, **kwargs):
if not CONFIG.git_repo:

View file

@ -16,9 +16,10 @@
"""
import json
from tenacity import retry, stop_after_attempt, wait_random_exponential
from typing import List, Optional, Any
from typing import Optional
from pydantic import Field
from tenacity import retry, stop_after_attempt, wait_random_exponential
from metagpt.actions.action import Action
from metagpt.config import CONFIG
@ -30,8 +31,8 @@ from metagpt.const import (
TEST_OUTPUTS_FILE_REPO,
)
from metagpt.llm import LLM
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import CodingContext, Document, RunCodeResult
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
@ -89,7 +90,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
class WriteCode(Action):
name: str = "WriteCode"
context: Optional[str] = None
context: Optional[Document] = None
llm: BaseGPTAPI = Field(default_factory=LLM)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
@ -131,7 +132,9 @@ class WriteCode(Action):
logger.info(f"Writing {coding_context.filename}..")
code = await self.write_code(prompt)
if not coding_context.code_doc:
coding_context.code_doc = Document(filename=coding_context.filename, root_path=CONFIG.src_workspace)
# avoid root_path pydantic ValidationError if use WriteCode alone
root_path = CONFIG.src_workspace if CONFIG.src_workspace else ""
coding_context.code_doc = Document(filename=coding_context.filename, root_path=root_path)
coding_context.code_doc.content = code
return coding_context

View file

@ -7,21 +7,19 @@
@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 typing import Optional
from pydantic import Field
from tenacity import retry, stop_after_attempt, wait_random_exponential
from metagpt.actions import WriteCode
from metagpt.llm import LLM
from metagpt.actions.action import Action
from metagpt.config import CONFIG
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.schema import CodingContext
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import CodingContext
from metagpt.utils.common import CodeParser
PROMPT_TEMPLATE = """
@ -39,7 +37,6 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
```
"""
EXAMPLE_AND_INSTRUCTION = """
{format_example}
@ -127,7 +124,7 @@ REWRITE_CODE_TEMPLATE = """
class WriteCodeReview(Action):
name: str = "WriteCodeReview"
context: Optional[str] = None
context: Optional[CodingContext] = None
llm: BaseGPTAPI = Field(default_factory=LLM)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
@ -147,9 +144,15 @@ class WriteCodeReview(Action):
iterative_code = self.context.code_doc.content
k = CONFIG.code_review_k_times or 1
for i in range(k):
format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename)
task_content = self.context.task_doc.content if self.context.task_doc else ""
code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename)
format_example = FORMAT_EXAMPLE.format(
filename=self.context.code_doc.filename
)
task_content = (
self.context.task_doc.content if self.context.task_doc else ""
)
code_context = await WriteCode.get_codes(
self.context.task_doc, exclude=self.context.filename
)
context = "\n".join(
[
"## System Design\n" + str(self.context.design_doc) + "\n",
@ -162,11 +165,16 @@ class WriteCodeReview(Action):
code=iterative_code,
filename=self.context.code_doc.filename,
)
cr_prompt = EXAMPLE_AND_INSTRUCTION.format(format_example=format_example, )
logger.info(
f"Code review and rewrite {self.context.code_doc.filename}: {i+1}/{k} | {len(iterative_code)=}, {len(self.context.code_doc.content)=}"
cr_prompt = EXAMPLE_AND_INSTRUCTION.format(
format_example=format_example,
)
logger.info(
f"Code review and rewrite {self.context.code_doc.filename}: {i + 1}/{k} | {len(iterative_code)=}, "
f"{len(self.context.code_doc.content)=}"
)
result, rewrited_code = await self.write_code_review_and_rewrite(
context_prompt, cr_prompt, self.context.code_doc.filename
)
result, rewrited_code = await self.write_code_review_and_rewrite(context_prompt, cr_prompt, self.context.code_doc.filename)
if "LBTM" in result:
iterative_code = rewrited_code
elif "LGTM" in result:

View file

@ -15,8 +15,9 @@ from __future__ import annotations
import json
from pathlib import Path
from typing import List, Optional, Any
from pydantic import BaseModel, Field
from typing import Optional
from pydantic import Field
from metagpt.actions import Action, ActionOutput
from metagpt.actions.action_node import ActionNode
@ -26,9 +27,6 @@ from metagpt.actions.write_prd_an import (
WP_ISSUE_TYPE_NODE,
WRITE_PRD_NODE,
)
from metagpt.llm import LLM
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.actions.search_and_summarize import SearchAndSummarize
from metagpt.config import CONFIG
from metagpt.const import (
BUGFIX_FILENAME,
@ -38,13 +36,14 @@ from metagpt.const import (
PRDS_FILE_REPO,
REQUIREMENT_FILENAME,
)
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import BugFixContext, Document, Documents, Message
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.mermaid import mermaid_to_file
CONTEXT_TEMPLATE = """
### Project Name
{project_name}
@ -75,7 +74,7 @@ class WritePRD(Action):
# related to the PRD. If they are related, rewrite the PRD.
docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO)
requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME)
if await self._is_bugfix(requirement_doc.content):
if requirement_doc and await self._is_bugfix(requirement_doc.content):
await docs_file_repo.save(filename=BUGFIX_FILENAME, content=requirement_doc.content)
await docs_file_repo.save(filename=REQUIREMENT_FILENAME, content="")
bug_fix = BugFixContext(filename=BUGFIX_FILENAME)
@ -144,7 +143,8 @@ class WritePRD(Action):
async def _update_prd(self, requirement_doc, prd_doc, prds_file_repo, *args, **kwargs) -> Document | None:
if not prd_doc:
prd = await self._run_new_requirement(requirements=[requirement_doc.content], *args, **kwargs)
prd = await self._run_new_requirement(requirements=[requirement_doc.content if requirement_doc else ""],
*args, **kwargs)
new_prd_doc = Document(
root_path=PRDS_FILE_REPO,
filename=FileRepository.new_filename() + ".json",
@ -166,7 +166,7 @@ class WritePRD(Action):
if not quadrant_chart:
return
pathname = (
CONFIG.git_repo.workdir / Path(COMPETITIVE_ANALYSIS_FILE_REPO) / Path(prd_doc.filename).with_suffix("")
CONFIG.git_repo.workdir / Path(COMPETITIVE_ANALYSIS_FILE_REPO) / Path(prd_doc.filename).with_suffix("")
)
if not pathname.parent.exists():
pathname.parent.mkdir(parents=True, exist_ok=True)

View file

@ -12,14 +12,12 @@
functionality is to be consolidated into the `Environment` class.
"""
import asyncio
from typing import Iterable, Set
from pathlib import Path
from typing import Iterable, Set
from pydantic import BaseModel, Field
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.memory import Memory
from metagpt.roles.role import Role, role_subclass_registry
from metagpt.schema import Message
from metagpt.utils.common import is_subscribed
@ -29,7 +27,6 @@ from metagpt.utils.utils import read_json_file, write_json_file
class Environment(BaseModel):
"""环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到
Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles
"""
roles: dict[str, Role] = Field(default_factory=dict)
@ -63,12 +60,11 @@ class Environment(BaseModel):
roles_info.append({
"role_class": role.__class__.__name__,
"module_name": role.__module__,
"role_name": role.name
"role_name": role.name,
})
role.serialize(stg_path=stg_path.joinpath(f"roles/{role.__class__.__name__}_{role.name}"))
write_json_file(roles_path, roles_info)
self.memory.serialize(stg_path)
history_path = stg_path.joinpath("history.json")
write_json_file(history_path, {"content": self.history})
@ -92,6 +88,7 @@ class Environment(BaseModel):
"history": history
})
environment.add_roles(roles)
return environment
def add_role(self, role: Role):

View file

@ -8,16 +8,14 @@
"""
import copy
from collections import defaultdict
from typing import Iterable, Type, Union, Optional, Set
from pathlib import Path
from typing import Iterable, Set
from pydantic import BaseModel, Field
import json
from metagpt.schema import Message
from metagpt.utils.common import any_to_str, any_to_str_set
from metagpt.utils.utils import read_json_file, write_json_file
from metagpt.utils.utils import import_class
class Memory(BaseModel):
@ -30,10 +28,7 @@ class Memory(BaseModel):
index = kwargs.get("index", {})
new_index = defaultdict(list)
for action_str, value in index.items():
action_dict = json.loads(action_str)
action_class = import_class("Action", "metagpt.actions.action")
action_obj = action_class.deser_class(action_dict)
new_index[action_obj] = [Message(**item_dict) for item_dict in value]
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
@ -43,9 +38,8 @@ class Memory(BaseModel):
obj_dict = super(Memory, self).dict(*args, **kwargs)
new_obj_dict = copy.deepcopy(obj_dict)
new_obj_dict["index"] = {}
for action, value in obj_dict["index"].items():
action_ser = json.dumps(action.ser_class())
new_obj_dict["index"][action_ser] = value
for action_str, value in obj_dict["index"].items():
new_obj_dict["index"][action_str] = value
return new_obj_dict
def serialize(self, stg_path: Path):

View file

@ -23,11 +23,11 @@ class Architect(Role):
constraints (str): Constraints or guidelines for the architect.
"""
name: str = "Bob"
profile: str = Field(default="Architect", alias='profile')
goal: str = "design a concise, usable, complete software system"
constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries." \
"Use same language as user requirement"
name: str = Field(default="Bob")
profile: str = Field(default="Architect")
goal: str = Field(default="design a concise, usable, complete software system")
constraints: str = Field(default="make sure the architecture is simple enough and use appropriate open source "
"libraries. Use same language as user requirement")
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)

View file

@ -18,12 +18,14 @@
"""
from __future__ import annotations
from pydantic import Field
import json
from collections import defaultdict
from pathlib import Path
from typing import Set
from pydantic import Field
from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks
from metagpt.actions.fix_bug import FixBug
from metagpt.actions.summarize_code import SummarizeCode
@ -45,7 +47,6 @@ from metagpt.schema import (
)
from metagpt.utils.common import any_to_str, any_to_str_set
IS_PASS_PROMPT = """
{context}
@ -69,15 +70,15 @@ class Engineer(Role):
use_code_review (bool): Whether to use code review.
"""
name: str = "Alex"
role_profile: str = Field(default="Engineer", alias='profile')
profile: str = Field(default="Engineer")
goal: str = "write elegant, readable, extensible, efficient code"
constraints: str = "the code should conform to standards like google-style and be modular and maintainable. " \
"Use same language as user requirement",
"Use same language as user requirement"
n_borg: int = 1
use_code_review: bool = False
code_todos: list = []
summarize_todos = []
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
@ -211,7 +212,7 @@ class Engineer(Role):
@staticmethod
async def _new_coding_context(
filename, src_file_repo, task_file_repo, design_file_repo, dependency
filename, src_file_repo, task_file_repo, design_file_repo, dependency
) -> CodingContext:
old_code_doc = await src_file_repo.get(filename)
if not old_code_doc:

View file

@ -26,13 +26,14 @@ class ProductManager(Role):
constraints (str): Constraints or limitations for the project manager.
"""
name: str = "Alice"
role_profile: str = Field(default="Product Manager", alias='profile')
profile: str = Field(default="Product Manager")
goal: str = "efficiently create a successful product"
constraints: str = "use same language as user requiremen"
"""
Represents a Product Manager role responsible for product development and management.
"""
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)

View file

@ -24,8 +24,6 @@ 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"

View file

@ -20,42 +20,26 @@
"""
from __future__ import annotations
from enum import Enum
from typing import Iterable, Set, Type
from pathlib import Path
from typing import Iterable, Set, Type, Any
from pydantic import BaseModel, Field
from metagpt.actions.action import Action, ActionOutput, action_subclass_registry
from metagpt.actions.action_node import ActionNode
from metagpt.actions.add_requirement import UserRequirement
from pathlib import Path
from typing import (
Iterable,
Type,
Any
)
from pydantic import BaseModel, Field, validator
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.actions.action import Action, ActionOutput, action_subclass_registry
from metagpt.const import SERDESER_PATH
from metagpt.llm import LLM
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.logs import logger
from metagpt.memory import Memory
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.provider.human_provider import HumanProvider
from metagpt.schema import Message, MessageQueue
from metagpt.utils.common import any_to_str
from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output
from metagpt.memory import Memory
from metagpt.provider.human_provider import HumanProvider
from metagpt.utils.utils import read_json_file, write_json_file, import_class
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.utils.utils import read_json_file, write_json_file, import_class, role_raise_decorator
from metagpt.const import SERDESER_PATH
PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """
@ -65,12 +49,14 @@ Please note that only the text between the first and second "===" is information
{history}
===
You can now choose one of the following stages to decide the stage you need to go in the next step:
Your previous stage: {previous_state}
Now choose one of the following stages you need to go to in the next step:
{states}
Just answer a number between 0-{n_states}, choose the most suitable stage according to the understanding of the conversation.
Please note that the answer only needs a number, no need to add any other text.
If there is no conversation record, choose 0.
If you think you have completed your goal and don't need to go to any of the stages, return -1.
Do not answer anything else, and do not add any other information in your answer.
"""
@ -106,7 +92,7 @@ class RoleSetting(BaseModel):
def __str__(self):
return f"{self.name}({self.profile})"
def __repr__(self):
return self.__str__()
@ -115,37 +101,21 @@ class RoleContext(BaseModel):
"""Role Runtime Context"""
# # env exclude=True to avoid `RecursionError: maximum recursion depth exceeded in comparison`
env: "Environment" = Field(default=None, exclude=True)
msg_buffer: MessageQueue = Field(default_factory=MessageQueue) # Message Buffer with Asynchronous Updates
# TODO judge if ser&deser
msg_buffer: MessageQueue = Field(default_factory=MessageQueue,
exclude=True) # Message Buffer with Asynchronous Updates
memory: Memory = Field(default_factory=Memory)
# long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
state: int = Field(default=-1) # -1 indicates initial or termination state where todo is None
todo: Action = Field(default=None, exclude=True)
watch: set[Type[Action]] = Field(default_factory=set)
watch: set[str] = Field(default_factory=set)
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
react_mode: RoleReactMode = RoleReactMode.REACT # see `Role._set_react_mode` for definitions of the following two attributes
max_react_loop: int = 1
class Config:
arbitrary_types_allowed = True
def __init__(self, **kwargs):
watch_info = kwargs.get("watch", set())
watch = set()
for item in watch_info:
action = Action.deser_class(item)
watch.update([action])
kwargs["watch"] = watch
super(RoleContext, self).__init__(**kwargs)
def dict(self, *args, **kwargs) -> "DictStrAny":
obj_dict = super(RoleContext, self).dict(*args, **kwargs)
watch = obj_dict.get("watch", set())
watch_info = []
for item in watch:
watch_info.append(item.ser_class())
obj_dict["watch"] = watch_info
return obj_dict
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)
@ -156,26 +126,16 @@ class RoleContext(BaseModel):
def important_memory(self) -> list[Message]:
"""Get the information corresponding to the watched actions"""
return self.memory.get_by_actions(self.watch)
@property
def history(self) -> list[Message]:
return self.memory.get()
class _RoleInjector(type):
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
if not instance._rc.watch:
instance._watch([UserRequirement])
return instance
role_subclass_registry = {}
class Role(BaseModel, metaclass=_RoleInjector):
class Role(BaseModel):
"""Role/Agent"""
name: str = ""
profile: str = ""
@ -189,7 +149,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
_states: list[str] = Field(default=[])
_actions: list[Action] = Field(default=[])
_rc: RoleContext = Field(default=RoleContext)
_subscription: tuple = set()
_subscription: tuple[str] = set()
# builtin variables
recovered: bool = False # to tag if a recovered role
@ -203,6 +163,8 @@ class Role(BaseModel, metaclass=_RoleInjector):
"_rc": RoleContext()
}
__hash__ = object.__hash__ # support Role as hashable type in `Environment.members`
class Config:
arbitrary_types_allowed = True
exclude = ["_llm"]
@ -240,6 +202,9 @@ class Role(BaseModel, metaclass=_RoleInjector):
else:
object.__setattr__(self, key, self._private_attributes[key])
if not self._rc.watch:
self._watch([UserRequirement])
# 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__
@ -303,7 +268,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
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", [])
@ -338,7 +303,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
role_class = import_class(class_name=role_class_str, module_name=module_name)
role = role_class(**role_info) # initiate particular Role
role.set_recovered(True) # set True to make a tag
role.set_recovered(True) # set True to make a tag
role_memory = Memory.deserialize(stg_path)
role.set_memory(role_memory)
@ -362,7 +327,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
for idx, action in enumerate(actions):
if not isinstance(action, Action):
## 默认初始化
i = action(llm=self._llm)
i = action(name="", llm=self._llm)
else:
if self._setting.is_human and not isinstance(action.llm, HumanProvider):
logger.warning(
@ -437,24 +402,10 @@ class Role(BaseModel, metaclass=_RoleInjector):
if env:
env.set_subscription(self, self._subscription)
@property
def profile(self):
"""Get the role description (position)"""
return self._setting.profile
@property
def name(self):
"""Get virtual user name"""
return self._setting.name
@property
def subscription(self) -> Set:
"""The labels for messages to be consumed by the Role object."""
return self._subscription
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
def _get_prefix(self):
"""Get the role prefix"""
@ -466,7 +417,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
"goal": self.goal,
"constraints": self.constraints
})
async def _think(self) -> None:
"""Think about what to do and decide on the next action"""
if len(self._actions) == 1:
@ -475,7 +426,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
return
if self.recovered and self._rc.state >= 0:
self._set_state(self._rc.state) # action to run from recovered state
self.recovered = False # avoid max_react_loop out of work
self.recovered = False # avoid max_react_loop out of work
return
prompt = self._get_prefix()
@ -498,7 +449,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
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)
@ -535,8 +486,8 @@ class Role(BaseModel, metaclass=_RoleInjector):
if news_text:
logger.debug(f"{self._setting} observed: {news_text}")
return len(self._rc.news)
def _publish_message(self, msg):
def publish_message(self, msg):
"""If the role belongs to env, then the role's messages will be broadcast to env"""
if not msg:
return
@ -557,7 +508,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
Use llm to select actions in _think dynamically
"""
actions_taken = 0
rsp = Message("No actions taken yet") # will be overwritten after Role _act
rsp = Message(content="No actions taken yet") # will be overwritten after Role _act
while actions_taken < self._rc.max_react_loop:
# think
await self._think()
@ -580,7 +531,7 @@ class Role(BaseModel, metaclass=_RoleInjector):
async def _plan_and_act(self) -> Message:
"""first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically."""
# TODO: to be implemented
return Message("")
return Message(content="")
async def react(self) -> Message:
"""Entry to one of three strategies by which Role reacts to the observed Message"""
@ -613,24 +564,24 @@ class Role(BaseModel, metaclass=_RoleInjector):
def get_memories(self, k=0) -> list[Message]:
"""A wrapper to return the most recent k memories of this role, return all when k=0"""
return self._rc.memory.get(k=k)
async def run(self, with_message=None):
"""Observe, and think and act based on the results of the observation"""
if with_message:
msg = None
if isinstance(with_message, str):
msg = Message(with_message)
msg = Message(content=with_message)
elif isinstance(with_message, Message):
msg = with_message
elif isinstance(with_message, list):
msg = Message("\n".join(with_message))
msg = Message(content="\n".join(with_message))
self.put_message(msg)
if not await self._observe():
# 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.

View file

@ -13,6 +13,8 @@
3. Add `id` to `Message` according to Section 2.2.3.1.1 of RFC 135.
"""
from __future__ import annotations
import asyncio
import json
import os.path
@ -20,14 +22,9 @@ import uuid
from asyncio import Queue, QueueEmpty, wait_for
from json import JSONDecodeError
from pathlib import Path
from typing import Dict, List, Optional, Set, TypedDict
from pydantic import BaseModel, Field
from dataclasses import dataclass, field
from typing import Type, TypedDict, Union, Optional
from typing import Dict, List, Set, TypedDict, Optional, Any
from pydantic import BaseModel, Field
from pydantic.main import ModelMetaclass
from metagpt.config import CONFIG
from metagpt.const import (
@ -39,15 +36,7 @@ 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
# from metagpt.actions.action_output import ActionOutput
# from metagpt.actions.action import Action
from metagpt.utils.serialize import actionoutout_schema_to_mapping, actionoutput_mapping_to_str, \
actionoutput_str_to_mapping
from metagpt.utils.utils import import_class
@ -58,7 +47,6 @@ class RawMessage(TypedDict):
role: str
class Document(BaseModel):
"""
Represents a document.
@ -68,7 +56,7 @@ class Document(BaseModel):
filename: str = ""
content: str = ""
def get_meta(self) -> "Document":
def get_meta(self) -> Document:
"""Get metadata of the document.
:return: A new Document instance with the same root path and filename.
@ -120,7 +108,6 @@ class Message(BaseModel):
def __init__(self, **kwargs):
instruct_content = kwargs.get("instruct_content", None)
cause_by = kwargs.get("cause_by", None)
if instruct_content and not isinstance(instruct_content, BaseModel):
ic = instruct_content
mapping = actionoutput_str_to_mapping(ic["mapping"])
@ -129,9 +116,11 @@ class Message(BaseModel):
ic_obj = actionoutput_class.create_model_class(class_name=ic["class"], mapping=mapping)
ic_new = ic_obj(**ic["value"])
kwargs["instruct_content"] = ic_new
if cause_by and not isinstance(cause_by, ModelMetaclass):
action_class = import_class("Action", "metagpt.actions.action")
kwargs["cause_by"] = action_class.deser_class(cause_by)
kwargs["id"] = uuid.uuid4().hex
kwargs["cause_by"] = any_to_str(kwargs.get("cause_by", ""))
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)
def __setattr__(self, key, val):
@ -156,9 +145,6 @@ class Message(BaseModel):
mapping = actionoutput_mapping_to_str(mapping)
obj_dict["instruct_content"] = {"class": schema["title"], "mapping": mapping, "value": ic.dict()}
cb = self.cause_by
if cb:
obj_dict["cause_by"] = cb.ser_class()
return obj_dict
def __str__(self):
@ -214,11 +200,24 @@ class AIMessage(Message):
super().__init__(content=content, role="assistant")
class MessageQueue:
class MessageQueue(BaseModel):
"""Message queue which supports asynchronous updates."""
def __init__(self):
self._queue = Queue()
_queue: Queue = Field(default_factory=Queue)
_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, self._private_attributes[key])
def pop(self) -> Message | None:
"""Pop one message from the queue."""
@ -266,7 +265,7 @@ class MessageQueue:
return json.dumps(lst)
@staticmethod
def load(self, v) -> "MessageQueue":
def load(self, v) -> MessageQueue:
"""Convert the json string to the `MessageQueue` object."""
q = MessageQueue()
try:
@ -287,7 +286,7 @@ class CodingContext(BaseModel):
code_doc: Optional[Document]
@staticmethod
def loads(val: str) -> "CodingContext" | None:
def loads(val: str) -> CodingContext | None:
try:
m = json.loads(val)
return CodingContext(**m)
@ -301,7 +300,7 @@ class TestingContext(BaseModel):
test_doc: Optional[Document]
@staticmethod
def loads(val: str) -> "TestingContext" | None:
def loads(val: str) -> TestingContext | None:
try:
m = json.loads(val)
return TestingContext(**m)
@ -322,7 +321,7 @@ class RunCodeContext(BaseModel):
output: Optional[str]
@staticmethod
def loads(val: str) -> "RunCodeContext" | None:
def loads(val: str) -> RunCodeContext | None:
try:
m = json.loads(val)
return RunCodeContext(**m)
@ -336,7 +335,7 @@ class RunCodeResult(BaseModel):
stderr: str
@staticmethod
def loads(val: str) -> "RunCodeResult" | None:
def loads(val: str) -> RunCodeResult | None:
try:
m = json.loads(val)
return RunCodeResult(**m)
@ -351,7 +350,7 @@ class CodeSummarizeContext(BaseModel):
reason: str = ""
@staticmethod
def loads(filenames: List) -> "CodeSummarizeContext":
def loads(filenames: List) -> CodeSummarizeContext:
ctx = CodeSummarizeContext()
for filename in filenames:
if Path(filename).is_relative_to(SYSTEM_DESIGN_FILE_REPO):

View file

@ -8,18 +8,19 @@
Section 2.2.3.3 of RFC 135.
"""
from pathlib import Path
from pydantic import BaseModel, Field
from metagpt.actions import UserRequirement
from metagpt.config import CONFIG
from metagpt.const import MESSAGE_ROUTE_TO_ALL
from metagpt.const import SERDESER_PATH
from metagpt.environment import Environment
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import NoMoneyException
from metagpt.utils.utils import read_json_file, write_json_file, serialize_decorator
from metagpt.const import SERDESER_PATH
class Team(BaseModel):
@ -39,9 +40,9 @@ class Team(BaseModel):
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={"environment": True}))
write_json_file(team_info_path, self.dict(exclude={"env": True}))
self.environment.serialize(stg_path.joinpath("environment")) # save environment alone
self.env.serialize(stg_path.joinpath("environment")) # save environment alone
@classmethod
def recover(cls, stg_path: Path) -> "Team":
@ -60,7 +61,7 @@ class Team(BaseModel):
# recover environment
environment = Environment.deserialize(stg_path=stg_path.joinpath("environment"))
team_info.update({"environment": environment})
team_info.update({"env": environment})
team = Team(**team_info)
return team

View file

@ -9,6 +9,7 @@ from pathlib import Path
import importlib
from tenacity import _utils
import traceback
from pydantic.json import pydantic_encoder
from metagpt.logs import logger
@ -46,7 +47,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)
json.dump(data, fout, ensure_ascii=False, indent=4, default=pydantic_encoder)
def import_class(class_name: str, module_name: str) -> type: