feat: +user requirement to Architect, ProjectManager

This commit is contained in:
莘权 马 2024-04-29 15:07:21 +08:00
parent df36fcc929
commit 125d043464
15 changed files with 264 additions and 911 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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()