mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-05-21 14:05:17 +02:00
update ser&deser after env_refactor
This commit is contained in:
parent
35ac28c30e
commit
ebc4fe4b17
15 changed files with 152 additions and 200 deletions
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue