mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-14 16:32:16 +02:00
feat: +user requirement to Architect, ProjectManager
This commit is contained in:
parent
df36fcc929
commit
125d043464
15 changed files with 264 additions and 911 deletions
|
|
@ -6,9 +6,11 @@
|
|||
@File : architect.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.utils.common import any_to_str
|
||||
|
||||
|
||||
class Architect(Role):
|
||||
|
|
@ -33,7 +35,25 @@ class Architect(Role):
|
|||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# Initialize actions specific to the Architect role
|
||||
self.set_actions([WriteDesign])
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self), context=self.context), WriteDesign])
|
||||
|
||||
# Set events or actions the Architect should watch or be aware of
|
||||
self._watch({WritePRD})
|
||||
self._watch({UserRequirement, PrepareDocuments, WritePRD})
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
mappings = {
|
||||
any_to_str(UserRequirement): 0,
|
||||
any_to_str(PrepareDocuments): 1,
|
||||
any_to_str(WritePRD): 1,
|
||||
}
|
||||
for i in self.rc.news:
|
||||
idx = mappings.get(i.cause_by, -1)
|
||||
if idx < 0:
|
||||
continue
|
||||
self.rc.todo = self.actions[idx]
|
||||
return bool(self.rc.todo)
|
||||
return False
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
return await super()._observe(ignore_memory=True)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ from collections import defaultdict
|
|||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks
|
||||
from metagpt.actions import (
|
||||
Action,
|
||||
UserRequirement,
|
||||
WriteCode,
|
||||
WriteCodeReview,
|
||||
WriteTasks,
|
||||
)
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange
|
||||
|
|
@ -95,7 +102,18 @@ class Engineer(Role):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
self.set_actions([WriteCode])
|
||||
self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug, WriteCodePlanAndChange])
|
||||
self._watch(
|
||||
[
|
||||
UserRequirement,
|
||||
PrepareDocuments,
|
||||
WriteTasks,
|
||||
SummarizeCode,
|
||||
WriteCode,
|
||||
WriteCodeReview,
|
||||
FixBug,
|
||||
WriteCodePlanAndChange,
|
||||
]
|
||||
)
|
||||
self.code_todos = []
|
||||
self.summarize_todos = []
|
||||
self.next_todo_action = any_to_name(WriteCode)
|
||||
|
|
@ -245,14 +263,25 @@ class Engineer(Role):
|
|||
return False, rsp
|
||||
|
||||
async def _think(self) -> Action | None:
|
||||
if not self.src_workspace:
|
||||
self.src_workspace = get_project_srcs_path(self.project_repo.workdir)
|
||||
write_plan_and_change_filters = any_to_str_set([WriteTasks, FixBug])
|
||||
write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode])
|
||||
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
|
||||
if not self.rc.news:
|
||||
return None
|
||||
msg = self.rc.news[0]
|
||||
if msg.cause_by == any_to_str(UserRequirement):
|
||||
self.rc.todo = PrepareDocuments(
|
||||
key_descriptions={
|
||||
"project_path": 'the project path if exists in "Original Requirement"',
|
||||
"src_file_path": 'the path of the source code file explicitly requested for modification if exists in "Original Requirement"',
|
||||
},
|
||||
context=self.context,
|
||||
send_to=any_to_str(self),
|
||||
)
|
||||
return self.rc.todo
|
||||
|
||||
if not self.src_workspace:
|
||||
self.src_workspace = get_project_srcs_path(self.project_repo.workdir)
|
||||
write_plan_and_change_filters = any_to_str_set([PrepareDocuments, WriteTasks, FixBug])
|
||||
write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode])
|
||||
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
|
||||
if self.config.inc and msg.cause_by in write_plan_and_change_filters:
|
||||
logger.debug(f"TODO WriteCodePlanAndChange:{msg.model_dump_json()}")
|
||||
await self._new_code_plan_and_change_action(cause_by=msg.cause_by)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role, RoleReactMode
|
||||
from metagpt.utils.common import any_to_name
|
||||
from metagpt.utils.common import any_to_name, any_to_str
|
||||
|
||||
|
||||
class ProductManager(Role):
|
||||
|
|
@ -33,7 +33,7 @@ class ProductManager(Role):
|
|||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.set_actions([PrepareDocuments, WritePRD])
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self)), WritePRD])
|
||||
self._watch([UserRequirement, PrepareDocuments])
|
||||
self.rc.react_mode = RoleReactMode.BY_ORDER
|
||||
self.todo_action = any_to_name(WritePRD)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
@File : project_manager.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.actions import UserRequirement, WriteTasks
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.utils.common import any_to_str
|
||||
|
||||
|
||||
class ProjectManager(Role):
|
||||
|
|
@ -33,5 +35,23 @@ class ProjectManager(Role):
|
|||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.set_actions([WriteTasks])
|
||||
self._watch([WriteDesign])
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self), context=self.context), WriteTasks])
|
||||
self._watch([UserRequirement, PrepareDocuments, WriteDesign])
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
mappings = {
|
||||
any_to_str(UserRequirement): 0,
|
||||
any_to_str(PrepareDocuments): 1,
|
||||
any_to_str(WriteDesign): 1,
|
||||
}
|
||||
for i in self.rc.news:
|
||||
idx = mappings.get(i.cause_by, -1)
|
||||
if idx < 0:
|
||||
continue
|
||||
self.rc.todo = self.actions[idx]
|
||||
return bool(self.rc.todo)
|
||||
return False
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
return await super()._observe(ignore_memory=True)
|
||||
|
|
|
|||
|
|
@ -16,12 +16,18 @@
|
|||
"""
|
||||
|
||||
from metagpt.actions import DebugError, RunCode, 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.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Document, Message, RunCodeContext, TestingContext
|
||||
from metagpt.utils.common import any_to_str_set, init_python_folder, parse_recipient
|
||||
from metagpt.schema import AIMessage, Document, Message, RunCodeContext, TestingContext
|
||||
from metagpt.utils.common import (
|
||||
any_to_str,
|
||||
any_to_str_set,
|
||||
init_python_folder,
|
||||
parse_recipient,
|
||||
)
|
||||
|
||||
|
||||
class QaEngineer(Role):
|
||||
|
|
@ -40,8 +46,20 @@ class QaEngineer(Role):
|
|||
|
||||
# FIXME: a bit hack here, only init one action to circumvent _think() logic,
|
||||
# will overwrite _think() in future updates
|
||||
self.set_actions([WriteTest])
|
||||
self._watch([SummarizeCode, WriteTest, RunCode, DebugError])
|
||||
self.set_actions(
|
||||
[
|
||||
PrepareDocuments(
|
||||
send_to=any_to_str(self),
|
||||
key_descriptions={
|
||||
"project_path": 'the project path if exists in "Original Requirement"',
|
||||
"reqa_file": 'the path of the source code file explicitly requested for unit test if exists in "Original Requirement"',
|
||||
},
|
||||
context=self.context,
|
||||
),
|
||||
WriteTest,
|
||||
]
|
||||
)
|
||||
self._watch([PrepareDocuments, SummarizeCode, WriteTest, RunCode, DebugError])
|
||||
self.test_round = 0
|
||||
|
||||
async def _write_test(self, message: Message) -> None:
|
||||
|
|
@ -80,9 +98,8 @@ class QaEngineer(Role):
|
|||
additional_python_paths=[str(self.context.src_workspace)],
|
||||
)
|
||||
self.publish_message(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
|
|
@ -116,9 +133,8 @@ class QaEngineer(Role):
|
|||
recipient = parse_recipient(result.summary)
|
||||
mappings = {"Engineer": "Alex", "QaEngineer": "Edward"}
|
||||
self.publish_message(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=RunCode,
|
||||
sent_from=self,
|
||||
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
|
||||
|
|
@ -131,9 +147,8 @@ 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(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=DebugError,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
|
|
@ -143,16 +158,15 @@ class QaEngineer(Role):
|
|||
async def _act(self) -> Message:
|
||||
await init_python_folder(self.project_repo.tests.workdir)
|
||||
if self.test_round > self.test_round_allowed:
|
||||
result_msg = Message(
|
||||
result_msg = AIMessage(
|
||||
content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)",
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
return result_msg
|
||||
|
||||
code_filters = any_to_str_set({SummarizeCode})
|
||||
code_filters = any_to_str_set({PrepareDocuments, SummarizeCode})
|
||||
test_filters = any_to_str_set({WriteTest, DebugError})
|
||||
run_filters = any_to_str_set({RunCode})
|
||||
for msg in self.rc.news:
|
||||
|
|
@ -168,9 +182,8 @@ class QaEngineer(Role):
|
|||
# I ran my test code, time to fix bugs, if any
|
||||
await self._debug_error(msg)
|
||||
self.test_round += 1
|
||||
return Message(
|
||||
return AIMessage(
|
||||
content=f"Round {self.test_round} of tests done",
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from metagpt.context_mixin import ContextMixin
|
|||
from metagpt.logs import logger
|
||||
from metagpt.memory import Memory
|
||||
from metagpt.provider import HumanProvider
|
||||
from metagpt.schema import Message, MessageQueue, SerializationMixin
|
||||
from metagpt.schema import AIMessage, Message, MessageQueue, SerializationMixin
|
||||
from metagpt.strategy.planner import Planner
|
||||
from metagpt.utils.common import any_to_name, any_to_str, role_raise_decorator
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
|
@ -390,17 +390,16 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
|
||||
response = await self.rc.todo.run(self.rc.history)
|
||||
if isinstance(response, (ActionOutput, ActionNode)):
|
||||
msg = Message(
|
||||
msg = AIMessage(
|
||||
content=response.content,
|
||||
instruct_content=response.instruct_content,
|
||||
role=self._setting,
|
||||
cause_by=self.rc.todo,
|
||||
sent_from=self,
|
||||
)
|
||||
elif isinstance(response, Message):
|
||||
msg = response
|
||||
else:
|
||||
msg = Message(content=response, role=self.profile, cause_by=self.rc.todo, sent_from=self)
|
||||
msg = AIMessage(content=response, cause_by=self.rc.todo, sent_from=self)
|
||||
self.rc.memory.add(msg)
|
||||
|
||||
return msg
|
||||
|
|
@ -451,7 +450,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
Use llm to select actions in _think dynamically
|
||||
"""
|
||||
actions_taken = 0
|
||||
rsp = Message(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
|
||||
rsp = AIMessage(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
|
||||
while actions_taken < self.rc.max_react_loop:
|
||||
# think
|
||||
has_todo = await self._think()
|
||||
|
|
@ -466,7 +465,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
async def _act_by_order(self) -> Message:
|
||||
"""switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ..."""
|
||||
start_idx = self.rc.state if self.rc.state >= 0 else 0 # action to run from recovered state
|
||||
rsp = Message(content="No actions taken yet") # return default message if actions=[]
|
||||
rsp = AIMessage(content="No actions taken yet") # return default message if actions=[]
|
||||
for i in range(start_idx, len(self.states)):
|
||||
self._set_state(i)
|
||||
rsp = await self._act()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue