mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-05-27 14:25:20 +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
|
|
@ -9,10 +9,12 @@
|
|||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions import Action, UserRequirement
|
||||
from metagpt.const import REQUIREMENT_FILENAME
|
||||
from metagpt.schema import AIMessage
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
|
||||
|
|
@ -21,6 +23,13 @@ class PrepareDocuments(Action):
|
|||
|
||||
name: str = "PrepareDocuments"
|
||||
i_context: Optional[str] = None
|
||||
key_descriptions: Optional[Dict[str, str]] = None
|
||||
send_to: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if not self.key_descriptions:
|
||||
self.key_descriptions = {"project_path": 'the project path if exists in "Original Requirement"'}
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
|
|
@ -40,10 +49,26 @@ class PrepareDocuments(Action):
|
|||
|
||||
async def run(self, with_messages, **kwargs):
|
||||
"""Create and initialize the workspace folder, initialize the Git environment."""
|
||||
user_requirements = [i for i in with_messages if i.cause_by == any_to_str(UserRequirement)]
|
||||
if not self.config.project_path and user_requirements and self.key_descriptions:
|
||||
args = await user_requirements[0].parse_resources(llm=self.llm, key_descriptions=self.key_descriptions)
|
||||
for k, v in args.items():
|
||||
if not v or k in ["resources", "reason"]:
|
||||
continue
|
||||
self.context.kwargs.set(k, v)
|
||||
if self.context.kwargs.project_path:
|
||||
self.config.update_via_cli(
|
||||
project_path=self.context.kwargs.project_path,
|
||||
project_name="",
|
||||
inc=False,
|
||||
reqa_file=self.context.kwargs.reqa_file or "",
|
||||
max_auto_summarize_code=0,
|
||||
)
|
||||
|
||||
self._init_repo()
|
||||
|
||||
# Write the newly added requirements from the main parameter idea to `docs/requirement.txt`.
|
||||
doc = await self.repo.docs.save(filename=REQUIREMENT_FILENAME, content=with_messages[0].content)
|
||||
# Send a Message notification to the WritePRD action, instructing it to process requirements using
|
||||
# `docs/requirement.txt` and `docs/prd/`.
|
||||
return ActionOutput(content=doc.content, instruct_content=doc)
|
||||
return AIMessage(content="", instruct_content=doc, cause_by=self, send_to=self.send_to)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -351,8 +351,9 @@ class UserMessage(Message):
|
|||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="user")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="user", **kwargs)
|
||||
|
||||
|
||||
class SystemMessage(Message):
|
||||
|
|
@ -360,8 +361,9 @@ class SystemMessage(Message):
|
|||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="system")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="system", **kwargs)
|
||||
|
||||
|
||||
class AIMessage(Message):
|
||||
|
|
@ -369,8 +371,9 @@ class AIMessage(Message):
|
|||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="assistant")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="assistant", **kwargs)
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||
import typer
|
||||
|
||||
from metagpt.const import CONFIG_ROOT
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
||||
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
||||
|
|
@ -65,7 +66,7 @@ def generate_repo(
|
|||
idea = company.idea
|
||||
|
||||
company.invest(investment)
|
||||
company.run_project(idea)
|
||||
company.run_project(idea, send_to=any_to_str(ProductManager))
|
||||
asyncio.run(company.run(n_round=n_round))
|
||||
|
||||
return ctx.repo
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from metagpt.context import Context
|
|||
from metagpt.environment import Environment
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.schema import UserMessage
|
||||
from metagpt.utils.common import (
|
||||
NoMoneyException,
|
||||
read_json_file,
|
||||
|
|
@ -102,7 +102,7 @@ class Team(BaseModel):
|
|||
|
||||
# Human requirement.
|
||||
self.env.publish_message(
|
||||
Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL),
|
||||
UserMessage(content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL),
|
||||
peekable=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ from metagpt.tools.libs import (
|
|||
deployer,
|
||||
)
|
||||
from metagpt.tools.libs.env import get_env, set_get_env_entry, default_get_env, get_env_description
|
||||
from metagpt.tools.libs.software_development import (
|
||||
git_archive,
|
||||
)
|
||||
|
||||
_ = (
|
||||
data_preprocess,
|
||||
|
|
@ -28,7 +25,6 @@ _ = (
|
|||
gpt_v_generator,
|
||||
web_scraping,
|
||||
email_login,
|
||||
git_archive,
|
||||
terminal,
|
||||
file_manager,
|
||||
browser,
|
||||
|
|
|
|||
|
|
@ -3,384 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from metagpt.const import ASSISTANT_ALIAS, BUGFIX_FILENAME, REQUIREMENT_FILENAME
|
||||
from metagpt.const import ASSISTANT_ALIAS
|
||||
from metagpt.logs import ToolLogItem, log_tool_output
|
||||
from metagpt.schema import BugFixContext, Message
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "ProductManager"])
|
||||
async def write_prd(idea: str, project_path: Optional[str | Path] = None) -> Path:
|
||||
"""Writes a PRD based on user requirements.
|
||||
|
||||
Args:
|
||||
idea (str): The idea or concept for the PRD.
|
||||
project_path (Optional[str|Path], optional): The path to an existing project directory.
|
||||
If it's None, a new project path will be created. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Path: The path to the PRD files under the project directory
|
||||
|
||||
Example:
|
||||
>>> # Create a new project:
|
||||
>>> from metagpt.tools.libs.software_development import write_prd
|
||||
>>> prd_path = await write_prd("Create a new feature for the application")
|
||||
>>> print(prd_path)
|
||||
'/path/to/project_path/docs/prd/'
|
||||
|
||||
>>> # Add user requirements to the exists project:
|
||||
>>> from metagpt.tools.libs.software_development import write_prd
|
||||
>>> project_path = '/path/to/exists_project_path'
|
||||
>>> prd_path = await write_prd("Create a new feature for the application", project_path=project_path)
|
||||
>>> print(prd_path = )
|
||||
'/path/to/project_path/docs/prd/'
|
||||
"""
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import ProductManager
|
||||
|
||||
log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_prd.__name__)], tool_name=write_prd.__name__)
|
||||
|
||||
ctx = Context()
|
||||
if project_path and Path(project_path).exists():
|
||||
ctx.config.project_path = Path(project_path)
|
||||
ctx.config.inc = True
|
||||
|
||||
role = ProductManager(context=ctx)
|
||||
msg = await role.run(with_message=Message(content=idea, cause_by=UserRequirement))
|
||||
await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia PRD File", value=str(ctx.repo.docs.prd.workdir / i))
|
||||
for i in ctx.repo.docs.prd.changed_files.keys()
|
||||
]
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="PRD File", value=str(ctx.repo.resources.prd.workdir / i))
|
||||
for i in ctx.repo.resources.prd.changed_files.keys()
|
||||
]
|
||||
)
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="Competitive Analysis", value=str(ctx.repo.resources.competitive_analysis.workdir / i))
|
||||
for i in ctx.repo.resources.competitive_analysis.changed_files.keys()
|
||||
]
|
||||
)
|
||||
log_tool_output(output=outputs, tool_name=write_prd.__name__)
|
||||
|
||||
return ctx.repo.docs.prd.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["Design", "software development", "Architect"])
|
||||
async def write_design(prd_path: str | Path) -> Path:
|
||||
"""Writes a system design to the project repository, based on the PRD of the project.
|
||||
|
||||
Args:
|
||||
prd_path (str|Path): The path to the PRD files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to the system design files under the project directory.
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import write_design
|
||||
>>> prd_path = '/path/to/project_path/docs/prd' # Returned by `write_prd`
|
||||
>>> system_design_path = await write_desgin(prd_path)
|
||||
>>> print(system_design_path)
|
||||
'/path/to/project_path/docs/system_design/'
|
||||
|
||||
"""
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Architect
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_design.__name__)], tool_name=write_design.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
prd_path = Path(prd_path)
|
||||
project_path = (Path(prd_path) if not prd_path.is_file() else prd_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = Architect(context=ctx)
|
||||
await role.run(with_message=Message(content="", cause_by=WritePRD))
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia Design File", value=str(ctx.repo.docs.system_design.workdir / i))
|
||||
for i in ctx.repo.docs.system_design.changed_files.keys()
|
||||
]
|
||||
for i in ctx.repo.resources.system_design.changed_files.keys():
|
||||
outputs.append(ToolLogItem(name="Design File", value=str(ctx.repo.resources.system_design.workdir / i)))
|
||||
for i in ctx.repo.resources.data_api_design.changed_files.keys():
|
||||
outputs.append(
|
||||
ToolLogItem(name="Class Diagram File", value=str(ctx.repo.resources.data_api_design.workdir / i))
|
||||
)
|
||||
for i in ctx.repo.resources.seq_flow.changed_files.keys():
|
||||
outputs.append(ToolLogItem(name="Sequence Diagram File", value=str(ctx.repo.resources.seq_flow.workdir / i)))
|
||||
log_tool_output(output=outputs, tool_name=write_design.__name__)
|
||||
|
||||
return ctx.repo.docs.system_design.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Architect"])
|
||||
async def write_project_plan(system_design_path: str | Path) -> Path:
|
||||
"""Writes a project plan to the project repository, based on the design of the project.
|
||||
|
||||
Args:
|
||||
system_design_path (str|Path): The path to the system design files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to task files under the project directory.
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import write_project_plan
|
||||
>>> system_design_path = '/path/to/project_path/docs/system_design/' # Returned by `write_design`
|
||||
>>> task_path = await write_project_plan(system_design_path)
|
||||
>>> print(task_path)
|
||||
'/path/to/project_path/docs/task'
|
||||
|
||||
"""
|
||||
from metagpt.actions import WriteDesign
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import ProjectManager
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_project_plan.__name__)],
|
||||
tool_name=write_project_plan.__name__,
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
system_design_path = Path(system_design_path)
|
||||
project_path = (system_design_path if not system_design_path.is_file() else system_design_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = ProjectManager(context=ctx)
|
||||
await role.run(with_message=Message(content="", cause_by=WriteDesign))
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia Project Plan", value=str(ctx.repo.docs.task.workdir / i))
|
||||
for i in ctx.repo.docs.task.changed_files.keys()
|
||||
]
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="Project Plan", value=str(ctx.repo.resources.api_spec_and_task.workdir / i))
|
||||
for i in ctx.repo.resources.api_spec_and_task.changed_files.keys()
|
||||
]
|
||||
)
|
||||
log_tool_output(output=outputs, tool_name=write_project_plan.__name__)
|
||||
|
||||
return ctx.repo.docs.task.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Engineer"])
|
||||
async def write_codes(task_path: str | Path, inc: bool = False) -> Path:
|
||||
"""Writes code to implement designed features according to the project plan and adds them to the project repository.
|
||||
In code writing tasks, prioritize calling this tool against writing code from scratch directly.
|
||||
|
||||
Args:
|
||||
task_path (str|Path): The path to task files under the project directory.
|
||||
inc (bool, optional): Whether to write incremental codes. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Path: The path to the source code files under the project directory.
|
||||
|
||||
Example:
|
||||
# Write codes to a new project
|
||||
>>> from metagpt.tools.libs.software_development import write_codes
|
||||
>>> task_path = '/path/to/project_path/docs/task' # Returned by `write_project_plan`
|
||||
>>> src_path = await write_codes(task_path)
|
||||
>>> print(src_path)
|
||||
'/path/to/project_path/src/'
|
||||
|
||||
# Write increment codes to the exists project
|
||||
>>> from metagpt.tools.libs.software_development import write_codes
|
||||
>>> task_path = '/path/to/project_path/docs/task' # Returned by `write_prd`
|
||||
>>> src_path = await write_codes(task_path, inc=True)
|
||||
>>> print(src_path)
|
||||
'/path/to/project_path/src/'
|
||||
"""
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Engineer
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_codes.__name__)], tool_name=write_codes.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
ctx.config.inc = inc
|
||||
task_path = Path(task_path)
|
||||
project_path = (task_path if not task_path.is_file() else task_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = Engineer(context=ctx)
|
||||
msg = Message(content="", cause_by=WriteTasks, send_to=role)
|
||||
me = {any_to_str(role), role.name}
|
||||
while me.intersection(msg.send_to):
|
||||
msg = await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Source File", value=str(ctx.repo.srcs.workdir / i))
|
||||
for i in ctx.repo.srcs.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=write_codes.__name__)
|
||||
|
||||
return ctx.repo.srcs.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "QaEngineer"])
|
||||
async def run_qa_test(src_path: str | Path) -> Path:
|
||||
"""Run QA test on the project repository.
|
||||
|
||||
Args:
|
||||
src_path (str|Path): The path to the source code files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to the unit tests under the project directory
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import run_qa_test
|
||||
>>> src_path = '/path/to/project_path/src/' # Returned by `write_codes`
|
||||
>>> test_path = await run_qa_test(src_path)
|
||||
>>> print(test_path)
|
||||
'/path/to/project_path/tests'
|
||||
"""
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.context import Context
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.roles import QaEngineer
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=run_qa_test.__name__)], tool_name=run_qa_test.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
src_path = Path(src_path)
|
||||
project_path = (src_path if not src_path.is_file() else src_path.parent) / ".."
|
||||
ctx.set_repo_dir(project_path)
|
||||
ctx.src_workspace = ctx.git_repo.workdir / ctx.git_repo.workdir.name
|
||||
|
||||
env = Environment(context=ctx)
|
||||
role = QaEngineer(context=ctx)
|
||||
env.add_role(role)
|
||||
|
||||
msg = Message(content="", cause_by=SummarizeCode, send_to=role)
|
||||
env.publish_message(msg)
|
||||
|
||||
while not env.is_idle:
|
||||
await env.run()
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Unit Test File", value=str(ctx.repo.tests.workdir / i))
|
||||
for i in ctx.repo.tests.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=run_qa_test.__name__)
|
||||
|
||||
return ctx.repo.tests.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Engineer"])
|
||||
async def fix_bug(project_path: str | Path, issue: str) -> Path:
|
||||
"""Fix bugs in the project repository.
|
||||
|
||||
Args:
|
||||
project_path (str|Path): The path to the project repository.
|
||||
issue (str): Description of the bug or issue.
|
||||
|
||||
Returns:
|
||||
Path: The path to the project directory
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import fix_bug
|
||||
>>> project_path = '/path/to/project_path' # Returned by `write_codes`
|
||||
>>> issue = 'Exception: exception about ...; Bug: bug about ...; Issue: issue about ...'
|
||||
>>> project_path = await fix_bug(project_path=project_path, issue=issue)
|
||||
>>> print(project_path)
|
||||
'/path/to/project_path'
|
||||
"""
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Engineer
|
||||
|
||||
log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=fix_bug.__name__)], tool_name=fix_bug.__name__)
|
||||
|
||||
ctx = Context()
|
||||
ctx.set_repo_dir(project_path)
|
||||
ctx.src_workspace = ctx.git_repo.workdir / ctx.git_repo.workdir.name
|
||||
await ctx.repo.docs.save(filename=BUGFIX_FILENAME, content=issue)
|
||||
await ctx.repo.docs.save(filename=REQUIREMENT_FILENAME, content="")
|
||||
|
||||
role = Engineer(context=ctx)
|
||||
bug_fix = BugFixContext(filename=BUGFIX_FILENAME)
|
||||
msg = Message(
|
||||
content=bug_fix.model_dump_json(),
|
||||
instruct_content=bug_fix,
|
||||
role="",
|
||||
cause_by=FixBug,
|
||||
sent_from=role,
|
||||
send_to=role,
|
||||
)
|
||||
me = {any_to_str(role), role.name}
|
||||
while me.intersection(msg.send_to):
|
||||
msg = await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Changed File", value=str(ctx.repo.srcs.workdir / i))
|
||||
for i in ctx.repo.srcs.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=fix_bug.__name__)
|
||||
|
||||
return project_path
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "git"])
|
||||
async def git_archive(project_path: str | Path) -> str:
|
||||
"""Stage and commit changes for the project repository using Git.
|
||||
|
||||
Args:
|
||||
project_path (str|Path): The path to the project repository.
|
||||
|
||||
|
||||
Returns:
|
||||
git log
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import git_archive
|
||||
>>> project_path = '/path/to/project_path' # Returned by `write_prd`
|
||||
>>> git_log = await git_archive(project_path=project_path)
|
||||
>>> print(git_log)
|
||||
commit a221d1c418c07f2b4fc07001e486285ead1a520a (HEAD -> feature/toollib/software_company, geekan/main)
|
||||
Merge: e01afd09 4a72f398
|
||||
Author: Sirui Hong <x@xx.github.com>
|
||||
Date: Tue Mar 19 15:16:03 2024 +0800
|
||||
Merge pull request #1037 from iorisa/fixbug/issues/1018
|
||||
fixbug: #1018
|
||||
|
||||
"""
|
||||
from metagpt.context import Context
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=git_archive.__name__)], tool_name=git_archive.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
project_dir = ProjectRepo.search_project_path(project_path)
|
||||
if not project_dir:
|
||||
ValueError(f"{project_path} is not a valid git repository.")
|
||||
ctx.set_repo_dir(project_dir)
|
||||
files = " ".join(ctx.git_repo.changed_files.keys())
|
||||
outputs = [ToolLogItem(name="cmd", value=f"git add {files}")]
|
||||
log_tool_output(output=outputs, tool_name=git_archive.__name__)
|
||||
ctx.git_repo.archive()
|
||||
|
||||
outputs = [ToolLogItem(name="cmd", value="git commit -m 'Archive'")]
|
||||
log_tool_output(output=outputs, tool_name=git_archive.__name__)
|
||||
|
||||
return ctx.git_repo.log()
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "import git repo"])
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,53 +11,123 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.context import Context
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Architect, ProductManager, Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.roles import (
|
||||
Architect,
|
||||
Engineer,
|
||||
ProductManager,
|
||||
ProjectManager,
|
||||
QaEngineer,
|
||||
)
|
||||
from metagpt.schema import Message, UserMessage
|
||||
from metagpt.utils.common import any_to_str, is_send_to
|
||||
|
||||
serdeser_path = Path(__file__).absolute().parent.joinpath("../data/serdeser_storage")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
return Environment()
|
||||
# @pytest.fixture
|
||||
# def env():
|
||||
# return Environment()
|
||||
#
|
||||
#
|
||||
# def test_add_role(env: Environment):
|
||||
# role = ProductManager(
|
||||
# name="Alice", profile="product manager", goal="create a new product", constraints="limited resources"
|
||||
# )
|
||||
# env.add_role(role)
|
||||
# assert env.get_role(role.profile) == role
|
||||
#
|
||||
#
|
||||
# def test_get_roles(env: Environment):
|
||||
# role1 = Role(name="Alice", profile="product manager", goal="create a new product", constraints="limited resources")
|
||||
# role2 = Role(name="Bob", profile="engineer", goal="develop the new product", constraints="short deadline")
|
||||
# env.add_role(role1)
|
||||
# env.add_role(role2)
|
||||
# roles = env.get_roles()
|
||||
# assert roles == {role1.profile: role1, role2.profile: role2}
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_publish_and_process_message(env: Environment):
|
||||
# if env.context.git_repo:
|
||||
# env.context.git_repo.delete_repository()
|
||||
# env.context.git_repo = None
|
||||
#
|
||||
# product_manager = ProductManager(name="Alice", profile="Product Manager", goal="做AI Native产品", constraints="资源有限")
|
||||
# architect = Architect(
|
||||
# name="Bob", profile="Architect", goal="设计一个可用、高效、较低成本的系统,包括数据结构与接口", constraints="资源有限,需要节省成本"
|
||||
# )
|
||||
#
|
||||
# env.add_roles([product_manager, architect])
|
||||
#
|
||||
# env.publish_message(Message(role="User", content="需要一个基于LLM做总结的搜索引擎", cause_by=UserRequirement))
|
||||
# await env.run(k=2)
|
||||
# logger.info(f"{env.history}")
|
||||
# assert len(env.history.storage) > 10
|
||||
|
||||
|
||||
def test_add_role(env: Environment):
|
||||
role = ProductManager(
|
||||
name="Alice", profile="product manager", goal="create a new product", constraints="limited resources"
|
||||
)
|
||||
env.add_role(role)
|
||||
assert env.get_role(role.profile) == role
|
||||
class MockEnv(Environment):
|
||||
def publish_message(self, message: Message, peekable: bool = True) -> bool:
|
||||
consumers = []
|
||||
for role, addrs in self.member_addrs.items():
|
||||
if is_send_to(message, addrs):
|
||||
role.put_message(message)
|
||||
consumers.append(role)
|
||||
if not consumers:
|
||||
logger.warning(f"Message no recipients: {message.dump()}")
|
||||
if message.cause_by in [any_to_str(UserRequirement), any_to_str(PrepareDocuments)]:
|
||||
assert len(consumers) == 1
|
||||
|
||||
|
||||
def test_get_roles(env: Environment):
|
||||
role1 = Role(name="Alice", profile="product manager", goal="create a new product", constraints="limited resources")
|
||||
role2 = Role(name="Bob", profile="engineer", goal="develop the new product", constraints="short deadline")
|
||||
env.add_role(role1)
|
||||
env.add_role(role2)
|
||||
roles = env.get_roles()
|
||||
assert roles == {role1.profile: role1, role2.profile: role2}
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_and_process_message(env: Environment):
|
||||
if env.context.git_repo:
|
||||
env.context.git_repo.delete_repository()
|
||||
env.context.git_repo = None
|
||||
|
||||
product_manager = ProductManager(name="Alice", profile="Product Manager", goal="做AI Native产品", constraints="资源有限")
|
||||
architect = Architect(
|
||||
name="Bob", profile="Architect", goal="设计一个可用、高效、较低成本的系统,包括数据结构与接口", constraints="资源有限,需要节省成本"
|
||||
@pytest.mark.parametrize(
|
||||
("content", "send_to"),
|
||||
[
|
||||
# ("snake game", any_to_str(ProductManager)),
|
||||
# (
|
||||
# "Rewrite the PRD file of the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game', add 'moving enemy' to the original requirement",
|
||||
# any_to_str(ProductManager),
|
||||
# ),
|
||||
# (
|
||||
# "Add 'random moving enemy, and dispears after 10 seconds' design to the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
# any_to_str(Architect),
|
||||
# ),
|
||||
(
|
||||
'Rewrite the tasks file of the project at "/Users/iorishinier/github/MetaGPT/workspace/snake_game"',
|
||||
any_to_str(ProjectManager),
|
||||
),
|
||||
# (
|
||||
# "Rewrite 'main.py' of the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
# any_to_str(Engineer),
|
||||
# ),
|
||||
# (
|
||||
# "Rewrite the unit test of 'test_main.py' at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
# any_to_str(QaEngineer),
|
||||
# ),
|
||||
],
|
||||
)
|
||||
async def test_env(content, send_to):
|
||||
context = Context()
|
||||
env = MockEnv(context=context)
|
||||
env.add_roles(
|
||||
[
|
||||
ProductManager(context=context),
|
||||
Architect(context=context),
|
||||
ProjectManager(context=context),
|
||||
Engineer(n_borg=5, use_code_review=True, context=context),
|
||||
QaEngineer(context=context),
|
||||
]
|
||||
)
|
||||
|
||||
env.add_roles([product_manager, architect])
|
||||
|
||||
env.publish_message(Message(role="User", content="需要一个基于LLM做总结的搜索引擎", cause_by=UserRequirement))
|
||||
await env.run(k=2)
|
||||
logger.info(f"{env.history=}")
|
||||
assert len(env.history) > 10
|
||||
msg = UserMessage(content=content, send_to=send_to)
|
||||
env.publish_message(msg)
|
||||
while not env.is_idle:
|
||||
await env.run()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class MockLLM(OriginalLLM):
|
|||
format_msgs: Optional[list[dict[str, str]]] = None,
|
||||
images: Optional[Union[str, list[str]]] = None,
|
||||
timeout=LLM_API_TIMEOUT,
|
||||
stream=True,
|
||||
stream=False,
|
||||
) -> str:
|
||||
# used to identify it a message has been called before
|
||||
if isinstance(msg, list):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue