Merge branch 'feature/refine_msg_content' into 'mgx_ops'

refactor: 清空了软件公司message中的无用内容

See merge request pub/MetaGPT!95
This commit is contained in:
林义章 2024-05-11 11:41:23 +00:00
commit 5783835364
11 changed files with 76 additions and 64 deletions

View file

@ -13,7 +13,7 @@ import json
from pathlib import Path
from typing import Optional
from metagpt.actions import Action, ActionOutput
from metagpt.actions import Action
from metagpt.actions.design_api_an import (
DATA_STRUCTURES_AND_INTERFACES,
DESIGN_API_NODE,
@ -24,7 +24,7 @@ from metagpt.actions.design_api_an import (
)
from metagpt.const import DATA_API_DESIGN_FILE_REPO, SEQ_FLOW_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import Document, Documents, Message
from metagpt.schema import AIMessage, Document, Documents, Message
from metagpt.utils.mermaid import mermaid_to_file
from metagpt.utils.report import DocsReporter, GalleryReporter
@ -68,7 +68,15 @@ 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.model_dump_json(), instruct_content=changed_files)
return AIMessage(
content="Designing is complete. "
+ "\n".join(
list(self.repo.docs.system_design.changed_files.keys())
+ list(self.repo.resources.data_api_design.changed_files.keys())
+ list(self.repo.resources.seq_flow.changed_files.keys())
),
cause_by=self,
)
async def _new_system_design(self, context):
node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=self.prompt_schema)

View file

@ -13,6 +13,7 @@ from typing import Dict, Optional
from metagpt.actions import Action, UserRequirement
from metagpt.const import REQUIREMENT_FILENAME
from metagpt.logs import logger
from metagpt.schema import AIMessage
from metagpt.utils.common import any_to_str
from metagpt.utils.file_repository import FileRepository
@ -56,6 +57,7 @@ class PrepareDocuments(Action):
if not v or k in ["resources", "reason"]:
continue
self.context.kwargs.set(k, v)
logger.info(f"{k}={v}")
if self.context.kwargs.project_path:
self.config.update_via_cli(
project_path=self.context.kwargs.project_path,

View file

@ -14,11 +14,10 @@ import json
from typing import Optional
from metagpt.actions.action import Action
from metagpt.actions.action_output import ActionOutput
from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODE
from metagpt.const import PACKAGE_REQUIREMENTS_FILENAME
from metagpt.logs import logger
from metagpt.schema import Document, Documents
from metagpt.schema import AIMessage, Document, Documents
from metagpt.utils.report import DocsReporter
NEW_REQ_TEMPLATE = """
@ -55,7 +54,15 @@ 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.model_dump_json(), instruct_content=change_files)
return AIMessage(
content="WBS is completed. "
+ "\n".join(
[PACKAGE_REQUIREMENTS_FILENAME]
+ list(self.repo.docs.task.changed_files.keys())
+ list(self.repo.resources.api_spec_and_task.changed_files.keys())
),
cause_by=self,
)
async def _update_tasks(self, filename):
system_design_doc = await self.repo.docs.system_design.get(filename)

View file

@ -33,7 +33,7 @@ from metagpt.const import (
REQUIREMENT_FILENAME,
)
from metagpt.logs import logger
from metagpt.schema import BugFixContext, Document, Documents, Message
from metagpt.schema import AIMessage, Document, Documents, Message
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.mermaid import mermaid_to_file
@ -66,7 +66,7 @@ class WritePRD(Action):
3. Requirement update: If the requirement is an update, the PRD document will be updated.
"""
async def run(self, with_messages, *args, **kwargs) -> ActionOutput | Message:
async def run(self, with_messages, *args, **kwargs) -> Message:
"""Run the action."""
req: Document = await self.repo.requirement
docs: list[Document] = await self.repo.docs.prd.get_all()
@ -82,22 +82,27 @@ class WritePRD(Action):
# if requirement is related to other documents, update them, otherwise create a new one
if related_docs := await self.get_related_docs(req, docs):
logger.info(f"Requirement update detected: {req.content}")
return await self._handle_requirement_update(req, related_docs)
await self._handle_requirement_update(req, related_docs)
else:
logger.info(f"New requirement detected: {req.content}")
return await self._handle_new_requirement(req)
await self._handle_new_requirement(req)
return AIMessage(
content="PRD is completed. "
+ "\n".join(
list(self.repo.docs.prd.changed_files.keys())
+ list(self.repo.resources.prd.changed_files.keys())
+ list(self.repo.resources.competitive_analysis.changed_files.keys())
),
cause_by=self,
)
async def _handle_bugfix(self, req: Document) -> Message:
# ... bugfix logic ...
await self.repo.docs.save(filename=BUGFIX_FILENAME, content=req.content)
await self.repo.docs.save(filename=REQUIREMENT_FILENAME, content="")
bug_fix = BugFixContext(filename=BUGFIX_FILENAME)
return Message(
content=bug_fix.model_dump_json(),
instruct_content=bug_fix,
role="",
return AIMessage(
content=f"A new issue is received: {BUGFIX_FILENAME}",
cause_by=FixBug,
sent_from=self,
send_to="Alex", # the name of Engineer
)

View file

@ -81,6 +81,7 @@ MESSAGE_ROUTE_CAUSE_BY = "cause_by"
MESSAGE_META_ROLE = "role"
MESSAGE_ROUTE_TO_ALL = "<all>"
MESSAGE_ROUTE_TO_NONE = "<none>"
MESSAGE_ROUTE_TO_SELF = "<self>" # Add this tag to replace `ActionOutput`
REQUIREMENT_FILENAME = "requirement.txt"
BUGFIX_FILENAME = "bugfix.txt"

View file

@ -202,10 +202,13 @@ class Environment(ExtEnv):
for _ in range(k):
futures = []
for role in self.roles.values():
if role.is_idle:
continue
future = role.run()
futures.append(future)
await asyncio.gather(*futures)
if futures:
await asyncio.gather(*futures)
logger.debug(f"is idle: {self.is_idle}")
def get_roles(self) -> dict[str, "Role"]:

View file

@ -39,6 +39,7 @@ from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange
from metagpt.const import (
BUGFIX_FILENAME,
CODE_PLAN_AND_CHANGE_FILE_REPO,
MESSAGE_ROUTE_TO_SELF,
REQUIREMENT_FILENAME,
SYSTEM_DESIGN_FILE_REPO,
TASK_FILE_REPO,
@ -150,12 +151,6 @@ class Engineer(Role):
dependencies=list(dependencies),
content=coding_context.code_doc.content,
)
AIMessage(
content=coding_context.model_dump_json(),
instruct_content=coding_context,
cause_by=WriteCode,
)
changed_files.add(coding_context.code_doc.filename)
if not changed_files:
logger.info("Nothing has changed.")
@ -177,17 +172,16 @@ class Engineer(Role):
return await self.rc.todo.run(self.rc.history)
async def _act_write_code(self):
changed_files = await self._act_sp_with_cr(review=self.use_code_review)
await self._act_sp_with_cr(review=self.use_code_review)
return AIMessage(
content="\n".join(changed_files),
cause_by=WriteCodeReview if self.use_code_review else WriteCode,
send_to=self,
sent_from=self,
content="", cause_by=WriteCodeReview if self.use_code_review else WriteCode, send_to=MESSAGE_ROUTE_TO_SELF
)
async def _act_summarize(self):
tasks = []
for todo in self.summarize_todos:
if self.n_summarize >= self.config.max_auto_summarize_code:
break
summary = await todo.run()
summary_filename = Path(todo.i_context.design_filename).with_suffix(".md").name
dependencies = {todo.i_context.design_filename, todo.i_context.task_filename}
@ -209,19 +203,23 @@ class Engineer(Role):
)
else:
await self.project_repo.docs.code_summary.delete(filename=Path(todo.i_context.design_filename).name)
self.summarize_todos = []
logger.info(f"--max-auto-summarize-code={self.config.max_auto_summarize_code}")
if not tasks or self.config.max_auto_summarize_code == 0:
self.n_summarize = 0
return AIMessage(
content="",
content="Coding is complete. "
"\n".join(
list(self.project_repo.resources.code_summary.changed_files.keys())
+ list(self.project_repo.srcs.changed_files.keys())
),
cause_by=SummarizeCode,
sent_from=self,
send_to="Edward", # The name of QaEngineer
)
# The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited.
# This parameter is used for debugging the workflow.
self.n_summarize += 1 if self.config.max_auto_summarize_code > self.n_summarize else 0
return AIMessage(content=json.dumps(tasks), cause_by=SummarizeCode, send_to=self, sent_from=self)
return AIMessage(content="", cause_by=SummarizeCode, send_to=MESSAGE_ROUTE_TO_SELF)
async def _act_code_plan_and_change(self):
"""Write code plan and change that guides subsequent WriteCode and WriteCodeReview"""
@ -243,12 +241,7 @@ class Engineer(Role):
dependencies=dependencies,
)
return AIMessage(
content=code_plan_and_change,
cause_by=WriteCodePlanAndChange,
send_to=self,
sent_from=self,
)
return AIMessage(content="", cause_by=WriteCodePlanAndChange, send_to=MESSAGE_ROUTE_TO_SELF)
async def _is_pass(self, summary) -> (str, str):
rsp = await self.llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False)
@ -416,7 +409,6 @@ class Engineer(Role):
self.summarize_todos.append(new_summarize)
if self.summarize_todos:
self.set_todo(self.summarize_todos[0])
self.summarize_todos.pop(0)
async def _new_code_plan_and_change_action(self, cause_by: str):
"""Create a WriteCodePlanAndChange action for subsequent to-do actions."""

View file

@ -18,10 +18,9 @@
from metagpt.actions import DebugError, RunCode, UserRequirement, WriteTest
from metagpt.actions.prepare_documents import PrepareDocuments
from metagpt.actions.summarize_code import SummarizeCode
from metagpt.const import MESSAGE_ROUTE_TO_NONE
from metagpt.const import MESSAGE_ROUTE_TO_NONE, MESSAGE_ROUTE_TO_SELF
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.utils.report import EditorReporter
from metagpt.schema import AIMessage, Document, Message, RunCodeContext, TestingContext
from metagpt.utils.common import (
any_to_str,
@ -29,6 +28,7 @@ from metagpt.utils.common import (
init_python_folder,
parse_recipient,
)
from metagpt.utils.report import EditorReporter
class QaEngineer(Role):
@ -95,12 +95,7 @@ class QaEngineer(Role):
additional_python_paths=[str(self.context.src_workspace)],
)
self.publish_message(
AIMessage(
content=run_code_context.model_dump_json(),
cause_by=WriteTest,
sent_from=self,
send_to=self,
)
AIMessage(content=run_code_context.model_dump_json(), cause_by=WriteTest, send_to=MESSAGE_ROUTE_TO_SELF)
)
logger.info(f"Done {str(self.project_repo.tests.workdir)} generating.")
@ -133,7 +128,6 @@ class QaEngineer(Role):
AIMessage(
content=run_code_context.model_dump_json(),
cause_by=RunCode,
sent_from=self,
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
)
)
@ -144,12 +138,7 @@ class QaEngineer(Role):
await self.project_repo.tests.save(filename=run_code_context.test_filename, content=code)
run_code_context.output = None
self.publish_message(
AIMessage(
content=run_code_context.model_dump_json(),
cause_by=DebugError,
sent_from=self,
send_to=self,
)
AIMessage(content=run_code_context.model_dump_json(), cause_by=DebugError, send_to=MESSAGE_ROUTE_TO_SELF)
)
async def _act(self) -> Message:
@ -157,9 +146,9 @@ class QaEngineer(Role):
await init_python_folder(self.project_repo.tests.workdir)
if self.test_round > self.test_round_allowed:
result_msg = AIMessage(
content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)",
content=f"Exceeding {self.test_round_allowed} rounds of tests, stop. "
+ "\n".join(list(self.project_repo.tests.changed_files.keys())),
cause_by=WriteTest,
sent_from=self.profile,
send_to=MESSAGE_ROUTE_TO_NONE,
)
return result_msg
@ -185,7 +174,6 @@ class QaEngineer(Role):
return AIMessage(
content=f"Round {self.test_round} of tests done",
cause_by=WriteTest,
sent_from=self.profile,
send_to=MESSAGE_ROUTE_TO_NONE,
)

View file

@ -30,6 +30,7 @@ from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validat
from metagpt.actions import Action, ActionOutput
from metagpt.actions.action_node import ActionNode
from metagpt.actions.add_requirement import UserRequirement
from metagpt.const import MESSAGE_ROUTE_TO_SELF
from metagpt.context_mixin import ContextMixin
from metagpt.logs import logger
from metagpt.memory import Memory
@ -408,7 +409,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
elif isinstance(response, Message):
msg = response
else:
msg = AIMessage(content=response, cause_by=self.rc.todo, sent_from=self)
msg = AIMessage(content=response or "", cause_by=self.rc.todo, sent_from=self)
if self.enable_memory:
self.rc.memory.add(msg)
@ -443,12 +444,19 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
"""If the role belongs to env, then the role's messages will be broadcast to env"""
if not msg:
return
if MESSAGE_ROUTE_TO_SELF in msg.send_to:
msg.send_to.add(any_to_str(self))
msg.send_to.remove(MESSAGE_ROUTE_TO_SELF)
if not msg.sent_from or msg.sent_from == MESSAGE_ROUTE_TO_SELF:
msg.sent_from = any_to_str(self)
if all(to in {any_to_str(self), self.name} for to in msg.send_to): # Message to myself
self.put_message(msg)
return
if not self.rc.env:
# If env does not exist, do not publish the message
return
if isinstance(msg, AIMessage) and not msg.agent:
msg.with_agent(self._setting)
self.rc.env.publish_message(msg)
def put_message(self, message):

View file

@ -200,7 +200,7 @@ class Message(BaseModel):
"""list[<role>: <content>]"""
id: str = Field(default="", validate_default=True) # According to Section 2.2.3.1.1 of RFC 135
content: str
content: str # natural language for user or agent
instruct_content: Optional[BaseModel] = Field(default=None, validate_default=True)
role: str = "user" # system / user / assistant
cause_by: str = Field(default="", validate_default=True)
@ -748,10 +748,6 @@ class CodeSummarizeContext(BaseModel):
return hash((self.design_filename, self.task_filename))
class BugFixContext(BaseContext):
filename: str = ""
class CodePlanAndChangeContext(BaseModel):
requirement: str = ""
issue: str = ""

View file

@ -31,6 +31,7 @@ serdeser_path = Path(__file__).absolute().parent.joinpath("../data/serdeser_stor
class MockEnv(Environment):
def publish_message(self, message: Message, peekable: bool = True) -> bool:
logger.info(f"{message.metadata}:{message.content}")
consumers = []
for role, addrs in self.member_addrs.items():
if is_send_to(message, addrs):
@ -87,6 +88,7 @@ async def test_publish_and_process_message(env: Environment):
assert len(env.history.storage) == 0
@pytest.mark.skip
@pytest.mark.asyncio
@pytest.mark.parametrize(
("content", "send_to"),
@ -105,7 +107,7 @@ async def test_publish_and_process_message(env: Environment):
any_to_str(ProjectManager),
),
(
"Rewrite 'main.py' of the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
"src filename is 'game.py', Uncaught SyntaxError: Identifier 'Position' has already been declared (at game.js:1:1), the project at '/Users/iorishinier/github/bak/MetaGPT/workspace/snake_game'",
any_to_str(Engineer),
),
(