feat: Implement Chapter 3 of RFC 236.

This commit is contained in:
莘权 马 2024-05-31 17:06:46 +08:00
parent 49ffb79433
commit f3b839847b
4 changed files with 302 additions and 17 deletions

View file

@ -10,8 +10,9 @@
@Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD.
"""
import json
import uuid
from pathlib import Path
from typing import Optional
from typing import List, Optional
from pydantic import BaseModel, Field
@ -27,6 +28,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 AIMessage, Document, Documents, Message
from metagpt.utils.common import aread, awrite, to_markdown_code_block
from metagpt.utils.mermaid import mermaid_to_file
from metagpt.utils.project_repo import ProjectRepo
from metagpt.utils.report import DocsReporter, GalleryReporter
@ -51,7 +53,116 @@ class WriteDesign(Action):
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
async def run(self, with_messages: Message, schema: str = None):
async def run(
self,
with_messages: List[Message] = None,
*,
user_requirement: str = "",
prd_filename: str = "",
exists_design_filename: str = "",
extra_info: str = "",
output_path: str = "",
**kwargs,
) -> AIMessage:
"""
Write a system design.
Args:
user_requirement (str): The user's requirements for the system design.
prd_filename (str, optional): The filename of the Product Requirement Document (PRD).
exists_design_filename (str, optional): The filename of the existing design document.
extra_info (str, optional): Additional information to be included in the system design.
output_path (str, optional): The output path where the system design should be saved.
Returns:
AIMessage: An AIMessage object containing the system design.
Example:
# Write a new system design.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info)
>>> print(result.content)
The design is balabala...
# Modify an exists system design.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> exists_design_filename = "/path/to/exists/design/filename"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, exists_design_filename=exists_design_filename)
>>> print(result.content)
The design is balabala...
# Write a new system design with the given PRD(Product Requirement Document).
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> prd_filename = "/path/to/prd/filename"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, prd_filename=prd_filename)
>>> print(result.content)
The design is balabala...
# Modify an exists system design with the given PRD(Product Requirement Document).
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> prd_filename = "/path/to/prd/filename"
>>> exists_design_filename = "/path/to/exists/design/filename"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, exists_design_filename=exists_design_filename, prd_filename=prd_filename)
>>> print(result.content)
The design is balabala...
# Write a new system design and save to the directory.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> output_path = "/path/to/save/"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, output_path=output_path)
>>> print(result.content)
System Design filename: "/path/to/design/filename"
# Modify an exists system design and save to the directory.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> exists_design_filename = "/path/to/exists/design/filename"
>>> output_path = "/path/to/save/"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, exists_design_filename=exists_design_filename)
>>> print(result.content)
System Design filename: "/path/to/design/filename"
# Write a new system design with the given PRD(Product Requirement Document) and save to the directory.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> prd_filename = "/path/to/prd/filename"
>>> output_path = "/path/to/save/"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, prd_filename=prd_filename)
>>> print(result.content)
System Design filename: "/path/to/design/filename"
# Modify an exists system design with the given PRD(Product Requirement Document) and save to the directory.
>>> user_requirement = "Your user requirements"
>>> extra_info = "Your extra information"
>>> prd_filename = "/path/to/prd/filename"
>>> exists_design_filename = "/path/to/exists/design/filename"
>>> output_path = "/path/to/save/"
>>> action = WriteDesign()
>>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, exists_design_filename=exists_design_filename, prd_filename=prd_filename)
>>> print(result.content)
System Design filename: "/path/to/design/filename"
"""
if not with_messages:
return await self._execute_api(
user_requirement=user_requirement,
prd_filename=prd_filename,
exists_design_filename=exists_design_filename,
extra_info=extra_info,
output_path=output_path,
)
self.input_args = with_messages[0].instruct_content
self.repo = ProjectRepo(self.input_args.project_path)
changed_prds = self.input_args.changed_prd_filenames
@ -147,3 +258,32 @@ class WriteDesign(Action):
image_path = pathname.parent / f"{pathname.name}.png"
if image_path.exists():
await GalleryReporter().async_report(image_path, "path")
async def _execute_api(
self,
user_requirement: str = "",
prd_filename: str = "",
exists_design_filename: str = "",
extra_info: str = "",
output_path: str = "",
) -> AIMessage:
context = to_markdown_code_block(user_requirement)
if extra_info:
context = to_markdown_code_block(extra_info)
if prd_filename:
prd_content = await aread(filename=prd_filename)
context += to_markdown_code_block(prd_content)
if not exists_design_filename:
node = await self._new_system_design(context=context)
design = Document(content=node.instruct_content.model_dump_json())
else:
old_design_content = await aread(filename=exists_design_filename)
design = await self._merge(
prd_doc=Document(content=context), system_design_doc=Document(content=old_design_content)
)
if not output_path:
return AIMessage(content=design.instruct_content.model_dump_json())
output_filename = Path(output_path) / f"{uuid.uuid4().hex}.json"
await awrite(filename=output_filename, data=design.content)
return AIMessage(content=f'System Design filename: "{str(output_filename)}"')

View file

@ -12,7 +12,7 @@
import json
from pathlib import Path
from typing import Optional
from typing import List, Optional
from pydantic import BaseModel, Field
@ -20,7 +20,8 @@ from metagpt.actions.action import Action
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 AIMessage, Document, Documents
from metagpt.schema import AIMessage, Document, Documents, Message
from metagpt.utils.common import aread, to_markdown_code_block
from metagpt.utils.project_repo import ProjectRepo
from metagpt.utils.report import DocsReporter
@ -39,7 +40,39 @@ class WriteTasks(Action):
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
async def run(self, with_messages):
async def run(
self, with_messages: List[Message] = None, *, user_requirement: str = "", design_filename: str = "", **kwargs
) -> AIMessage:
"""
Write a project schedule given a project system design file.
Args:
user_requirement (str, optional): A string specifying the user's requirements. Defaults to an empty string.
design_filename (str): The filename of the project system design file. Defaults to an empty string.
**kwargs: Additional keyword arguments.
Returns:
AIMessage: The generated project schedule.
Example:
# Write a new project schedule.
>>> design_filename = "/path/to/design/filename"
>>> action = WriteTasks()
>>> result = await action.run(design_filename=design_filename)
>>> print(result.content)
The project schedule is balabala...
# Write a new project schedule with the user requirement.
>>> design_filename = "/path/to/design/filename"
>>> user_requirement = "Your user requirements"
>>> action = WriteTasks()
>>> result = await action.run(design_filename=design_filename, user_requirement=user_requirement)
>>> print(result.content)
The project schedule is balabala...
"""
if not with_messages:
return await self._execute_api(user_requirement=user_requirement, design_filename=design_filename)
self.input_args = with_messages[0].instruct_content
self.repo = ProjectRepo(self.input_args.project_path)
changed_system_designs = self.input_args.changed_system_design_filenames
@ -99,7 +132,7 @@ class WriteTasks(Action):
await reporter.async_report(self.repo.workdir / md.root_relative_path, "path")
return task_doc
async def _run_new_tasks(self, context):
async def _run_new_tasks(self, context: str):
node = await PM_NODE.fill(context, self.llm, schema=self.prompt_schema)
return node
@ -121,3 +154,11 @@ class WriteTasks(Action):
continue
packages.add(pkg)
await self.repo.save(filename=PACKAGE_REQUIREMENTS_FILENAME, content="\n".join(packages))
async def _execute_api(self, user_requirement: str = "", design_filename: str = ""):
context = to_markdown_code_block(user_requirement)
if not design_filename:
content = await aread(filename=design_filename)
context += to_markdown_code_block(content)
node = await self._run_new_tasks(context)
return AIMessage(content=node.instruct_content.model_dump_json())

View file

@ -14,8 +14,9 @@
from __future__ import annotations
import json
import uuid
from pathlib import Path
from typing import Optional
from typing import List, Optional
from pydantic import BaseModel, Field
@ -37,7 +38,7 @@ from metagpt.const import (
)
from metagpt.logs import logger
from metagpt.schema import AIMessage, Document, Documents, Message
from metagpt.utils.common import CodeParser
from metagpt.utils.common import CodeParser, aread, awrite, to_markdown_code_block
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.mermaid import mermaid_to_file
from metagpt.utils.project_repo import ProjectRepo
@ -73,8 +74,75 @@ class WritePRD(Action):
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
async def run(self, with_messages, *args, **kwargs) -> Message:
"""Run the action."""
async def run(
self,
with_messages: List[Message] = None,
*,
user_requirement: str = "",
output_path: str = "",
exists_prd_filename: str = "",
extra_info: str = "",
**kwargs,
) -> AIMessage:
"""
Write a Product Requirement Document.
Args:
user_requirement (str): A string detailing the user's requirements.
output_path (str, optional): The file path where the output document should be saved. Defaults to "".
exists_prd_filename (str, optional): The file path of an existing Product Requirement Document to use as a reference. Defaults to "".
extra_info (str, optional): Additional information to include in the document. Defaults to "".
**kwargs: Additional keyword arguments.
Returns:
AIMessage: The resulting message after generating the Product Requirement Document.
Example:
# Write a new PRD(Product Requirement Document)
>>> user_requirement = "YOUR REQUIREMENTS"
>>> extra_info = "YOUR EXTRA INFO"
>>> write_prd = WritePRD()
>>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info)
>>> print(result.content)
The PRD is about balabala...
# Modify a exists PRD(Product Requirement Document)
>>> user_requirement = "YOUR REQUIREMENTS"
>>> extra_info = "YOUR EXTRA INFO"
>>> exists_prd_filename = "/path/to/exists/prd_filename"
>>> write_prd = WritePRD()
>>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, exists_prd_filename=exists_prd_filename)
>>> print(result.content)
The PRD is about balabala...
# Write and save a new PRD(Product Requirement Document) to the directory.
>>> user_requirement = "YOUR REQUIREMENTS"
>>> extra_info = "YOUR EXTRA INFO"
>>> output_path = "/path/to/prd/directory/"
>>> write_prd = WritePRD()
>>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, output_path=output_path)
>>> print(result.content)
PRD filename: "/path/to/prd/directory/213434ad.json"
# Modify a exists PRD(Product Requirement Document) and save to the directory.
>>> user_requirement = "YOUR REQUIREMENTS"
>>> extra_info = "YOUR EXTRA INFO"
>>> exists_prd_filename = "/path/to/exists/prd_filename"
>>> output_path = "/path/to/prd/directory/"
>>> write_prd = WritePRD()
>>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, exists_prd_filename=exists_prd_filename, output_path=output_path)
>>> print(result.content)
PRD filename: "/path/to/prd/directory/213434ad.json"
"""
if not with_messages:
return await self._execute_api(
user_requirement=user_requirement,
output_path=output_path,
exists_prd_filename=exists_prd_filename,
extra_info=extra_info,
)
self.input_args = with_messages[-1].instruct_content
if not self.input_args:
self.repo = ProjectRepo(self.config.project_path)
@ -110,6 +178,7 @@ class WritePRD(Action):
else:
logger.info(f"New requirement detected: {req.content}")
await self._handle_new_requirement(req)
kvs = self.input_args.model_dump()
kvs["changed_prd_filenames"] = [
str(self.repo.docs.prd.workdir / i) for i in list(self.repo.docs.prd.changed_files.keys())
@ -143,16 +212,20 @@ class WritePRD(Action):
send_to="Alex", # the name of Engineer
)
async def _new_prd(self, requirement: str) -> ActionNode:
project_name = self.project_name
context = CONTEXT_TEMPLATE.format(requirements=requirement, project_name=project_name)
exclude = [PROJECT_NAME.key] if project_name else []
node = await WRITE_PRD_NODE.fill(
context=context, llm=self.llm, exclude=exclude, schema=self.prompt_schema
) # schema=schema
return node
async def _handle_new_requirement(self, req: Document) -> ActionOutput:
"""handle new requirement"""
async with DocsReporter(enable_llm_stream=True) as reporter:
await reporter.async_report({"type": "prd"}, "meta")
project_name = self.project_name
context = CONTEXT_TEMPLATE.format(requirements=req, project_name=project_name)
exclude = [PROJECT_NAME.key] if project_name else []
node = await WRITE_PRD_NODE.fill(
context=context, llm=self.llm, exclude=exclude, schema=self.prompt_schema
) # schema=schema
node = await self._new_prd(req.content)
await self._rename_workspace(node)
new_prd_doc = await self.repo.docs.prd.save(
filename=FileRepository.new_filename() + ".json", content=node.instruct_content.model_dump_json()
@ -223,4 +296,28 @@ class WritePRD(Action):
ws_name = CodeParser.parse_str(block="Project Name", text=prd)
if ws_name:
self.project_name = ws_name
self.repo.git_repo.rename_root(self.project_name)
if self.repo:
self.repo.git_repo.rename_root(self.project_name)
async def _execute_api(
self, user_requirement: str, output_path: str, exists_prd_filename: str, extra_info: str
) -> AIMessage:
content = to_markdown_code_block(val=user_requirement)
if extra_info:
content += to_markdown_code_block(val=extra_info)
req = Document(content=content)
if not exists_prd_filename:
node = await self._new_prd(requirement=req.content)
new_prd = Document(content=node.instruct_content.model_dump_json())
else:
content = await aread(filename=exists_prd_filename)
old_prd = Document(content=content)
new_prd = await self._merge(req=req, related_doc=old_prd)
if not output_path:
return AIMessage(content=new_prd.content)
output_filename = Path(output_path) / f"{uuid.uuid4().hex}.json"
await awrite(filename=output_filename, data=new_prd.content)
return AIMessage(content=f'PRD filename: "{str(output_filename)}"')

View file

@ -667,6 +667,8 @@ def role_raise_decorator(func):
@handle_exception
async def aread(filename: str | Path, encoding="utf-8") -> str:
"""Read file asynchronously."""
if not filename or not Path(filename).exists():
return ""
try:
async with aiofiles.open(str(filename), mode="r", encoding=encoding) as reader:
content = await reader.read()
@ -940,3 +942,8 @@ def get_markdown_code_block_type(filename: str) -> str:
# Add more file extensions and corresponding code block types as needed
}
return types.get(ext, "")
def to_markdown_code_block(val: str, type_: str = "") -> str:
val = val.replace("```", "\\`\\`\\`")
return f"\n```{type_}\n{val}\n```\n"