From 7d92e3eb12191355511f31acc9e65bfad98aef6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 15 Jun 2024 22:33:11 +0800 Subject: [PATCH] feat: +software tools --- examples/mgx_write_project_framework.py | 3 +- metagpt/roles/architect.py | 25 ++- metagpt/strategy/experience_retriever.py | 58 +++++++ metagpt/tools/libs/software_development.py | 183 ++++++++++++++++++++- 4 files changed, 261 insertions(+), 8 deletions(-) diff --git a/examples/mgx_write_project_framework.py b/examples/mgx_write_project_framework.py index 359b90080..2537e9b00 100644 --- a/examples/mgx_write_project_framework.py +++ b/examples/mgx_write_project_framework.py @@ -24,6 +24,7 @@ from metagpt.logs import logger from metagpt.roles import Architect from metagpt.roles.di.team_leader import TeamLeader from metagpt.schema import AIMessage, UserMessage +from metagpt.strategy.experience_retriever import TRDToolExpRetriever from metagpt.utils.common import aread, to_markdown_code_block app = typer.Typer(add_completion=False) @@ -40,7 +41,7 @@ class EnvBuilder(BaseModel): def build(self) -> Environment: env = MGXEnv(context=self.context) team_leader = TeamLeader() - architect = Architect() + architect = Architect(experience_retriever=TRDToolExpRetriever()) # Prepare context use_case_actors = "".join([f"- {v}: {k}\n" for k, v in self.actors.items()]) diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index 90781abef..786368d8a 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -18,6 +18,11 @@ from metagpt.actions.requirement_analysis.trd import ( WriteTRD, ) from metagpt.roles.di.role_zero import RoleZero +from metagpt.tools.libs.software_development import ( + extract_external_interfaces, + write_framework, + write_trd, +) from metagpt.utils.common import tool2name @@ -42,16 +47,24 @@ class Architect(RoleZero): instruction: str = """Use WriteDesign tool to write a system design document if a system design is required; Use WriteTRD tool to write a TRD if a TRD is required;""" max_react_loop: int = 1 # FIXME: Read and edit files requires more steps, consider later + # tools: list[str] = [ + # "Editor:write,read,write_content", + # "RoleZero", + # "WriteDesign", + # "CompressExternalInterfaces", + # "DetectInteraction", + # "EvaluateTRD", + # "WriteTRD", + # "WriteFramework", + # "EvaluateFramework", + # ] tools: list[str] = [ "Editor:write,read,write_content", "RoleZero", "WriteDesign", - "CompressExternalInterfaces", - "DetectInteraction", - "EvaluateTRD", - "WriteTRD", - "WriteFramework", - "EvaluateFramework", + extract_external_interfaces.__name__, + write_trd.__name__, + write_framework.__name__, ] def __init__(self, **kwargs) -> None: diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index d04f0b5a1..956525cf9 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -13,6 +13,64 @@ class ExpRetriever(BaseModel): class DummyExpRetriever(ExpRetriever): """A dummy experience retriever that returns empty string.""" + def retrieve(self, context: str = "") -> str: + return self.EXAMPLE + + EXAMPLE: str = "" + + +class TRDToolExpRetriever(ExpRetriever): + """A TRD-related experience retriever that returns empty string.""" + + def retrieve(self, context: str = "") -> str: + return self.EXAMPLE + + EXAMPLE: str = """ + ## example 1 + User Requirement: Given some user requirements, write a TRD, and implement the TRD within a software framework. + Explanation: Given a complete user requirement, to write the TRD (Technical Requirements Document), follow these steps: 1. Call `extract_external_interfaces` to extract information about external interfaces from the acknowledgment; 2. Call `write_trd` to generate the TRD; 3. Call `write_framework` to generate the software framework code. + ```json + [ + { + "command_name": "extract_external_interfaces", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Execute `extract_external_interfaces` to extract external interfaces information from acknowledgement.", + "acknowledge": "## Interfaces\n balabala..." + } + }, + { + "command_name": "write_trd", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Execute `write_trd` to write the TRD based on user requirements", + "user_requirements": "This is user requirement balabala...", + "use_case_actors": "These are actors involved in the use case, balabala...", + "external_interfaces": " returned by `extract_external_interfaces`", + } + }, + { + "command_name": "write_framework", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "Execute `write_framework` to write the framework based on the TRD", + "use_case_actors": "These are actors involved in the use case, balabala...", + "trd": " returned by `write_trd`", + "acknowledge": "## Interfaces\n balabala...", + "additional_technical_requirements": "These are additional technical requirements, balabala..." + } + } + ] + ``` + """ + + +class TRDExpRetriever(ExpRetriever): + """A TRD-related experience retriever that returns empty string.""" + def retrieve(self, context: str = "") -> str: return self.EXAMPLE diff --git a/metagpt/tools/libs/software_development.py b/metagpt/tools/libs/software_development.py index a48e4a191..331e7cf12 100644 --- a/metagpt/tools/libs/software_development.py +++ b/metagpt/tools/libs/software_development.py @@ -3,9 +3,24 @@ from __future__ import annotations from pathlib import Path +from typing import Optional +from metagpt.actions.requirement_analysis.framework import ( + EvaluateFramework, + WriteFramework, + save_framework, +) +from metagpt.actions.requirement_analysis.trd import ( + CompressExternalInterfaces, + DetectInteraction, + EvaluateTRD, + WriteTRD, +) from metagpt.const import ASSISTANT_ALIAS -from metagpt.logs import ToolLogItem, log_tool_output +from metagpt.context import Context +from metagpt.logs import ToolLogItem, log_tool_output, logger +from metagpt.tools.tool_registry import register_tool +from metagpt.utils.cost_manager import CostManager async def import_git_repo(url: str) -> Path: @@ -42,3 +57,169 @@ async def import_git_repo(url: str) -> Path: log_tool_output(output=outputs, tool_name=import_git_repo.__name__) return ctx.repo.workdir + + +@register_tool(tags=["system design", "Extracts and compresses the information about external system interfaces"]) +async def extract_external_interfaces(acknowledge: str) -> str: + """ + Extracts and compresses information about external system interfaces from a given acknowledgement text. + + Args: + acknowledge (str): A natural text of acknowledgement containing details about external system interfaces. + + Returns: + str: A compressed version of the information about external system interfaces. + + Example: + >>> acknowledge = "## Interfaces\\n..." + >>> external_interfaces = await extract_external_interfaces(acknowledge=acknowledge) + >>> print(external_interfaces) + ```json\n[\n{\n"id": 1,\n"inputs": {... + """ + compress_acknowledge = CompressExternalInterfaces() + return await compress_acknowledge.run(acknowledge=acknowledge) + + +@register_tool(tags=["system design", "write trd", "Write a TRD"]) +async def write_trd( + use_case_actors: str, + user_requirements: str, + external_interfaces: str, + investment: float = 10, + context: Optional[Context] = None, +) -> (str, str): + """ + Handles the writing of a Technical Requirements Document (TRD) based on user requirements. + + Args: + user_requirements (str): The new/incremental user requirements. + use_case_actors (str): Description of the actors involved in the use case. + external_interfaces (str): List of available external interfaces. + investment (float): Budget. Automatically stops optimizing TRD when the budget is overdrawn. + context (Context, optional): The context configuration. Default is None. + Returns: + str: The newly created TRD. + + Example: + >>> # Given a new user requirements, write out a new TRD. + >>> user_requirements = "Write a 'snake game' TRD." + >>> use_case_actors = "- Actor: game player;\\n- System: snake game; \\n- External System: game center;" + >>> external_interfaces = "The available external interfaces returned by `CompressExternalInterfaces.run` are ..." + >>> investment = 10.0 + >>> trd = await write_trd( + >>> user_requirements=user_requirements, + >>> use_case_actors=use_case_actors, + >>> external_interfaces=external_interfaces, + >>> investment=investment, + >>> ) + >>> print(trd) + ## Technical Requirements Document\n ... + """ + context = context or Context(cost_manager=CostManager(max_budget=investment)) + detect_interaction = DetectInteraction(context=context) + w_trd = WriteTRD(context=context) + evaluate_trd = EvaluateTRD(context=context) + is_pass = False + evaluation_conclusion = "" + interaction_events = "" + trd = "" + while not is_pass and (context.cost_manager.total_cost < context.cost_manager.max_budget): + interaction_events = await detect_interaction.run( + user_requirements=user_requirements, + use_case_actors=use_case_actors, + legacy_interaction_events=interaction_events, + evaluation_conclusion=evaluation_conclusion, + ) + trd = await w_trd.run( + user_requirements=user_requirements, + use_case_actors=use_case_actors, + available_external_interfaces=external_interfaces, + evaluation_conclusion=evaluation_conclusion, + interaction_events=interaction_events, + previous_version_trd=trd, + ) + evaluation = await evaluate_trd.run( + user_requirements=user_requirements, + use_case_actors=use_case_actors, + trd=trd, + interaction_events=interaction_events, + ) + is_pass = evaluation.is_pass + evaluation_conclusion = evaluation.conclusion + + return trd + + +@register_tool(tags=["system design", "write software framework", "Write a software framework based on a TRD"]) +async def write_framework( + use_case_actors: str, + trd: str, + acknowledge: str, + additional_technical_requirements: str, + output_dir: Optional[str] = "", + investment: float = 10, + context: Optional[Context] = None, +) -> str: + """ + Run the action to generate a software framework based on the provided TRD and related information. + + Args: + use_case_actors (str): Description of the use case actors involved. + trd (str): Technical Requirements Document detailing the requirements. + acknowledge (str): External acknowledgements or acknowledgements required. + additional_technical_requirements (str): Any additional technical requirements. + output_dir (str, optional): Path to save the software framework files. Default is en empty string. + investment (float): Budget. Automatically stops optimizing TRD when the budget is overdrawn. + context (Context, optional): The context configuration. Default is None. + + Returns: + str: The generated software framework as a string of pathnames. + + Example: + >>> use_case_actors = "- Actor: game player;\\n- System: snake game; \\n- External System: game center;" + >>> trd = "## TRD\\n..." + >>> acknowledge = "## Interfaces\\n..." + >>> additional_technical_requirements = "Using Java language, ..." + >>> investment = 10.0 + >>> framework = await write_framework( + >>> use_case_actors=use_case_actors, + >>> trd=trd, + >>> acknowledge=acknowledge, + >>> additional_technical_requirements=constraint, + >>> investment=investment, + >>> ) + >>> print(framework) + [{"path":"balabala", "filename":"...", ... + """ + context = context or Context(cost_manager=CostManager(max_budget=investment)) + write_framework = WriteFramework(context=context) + evaluate_framework = EvaluateFramework(context=context) + is_pass = False + framework = "" + evaluation_conclusion = "" + while not is_pass and (context.cost_manager.total_cost < context.cost_manager.max_budget): + try: + framework = await write_framework.run( + use_case_actors=use_case_actors, + trd=trd, + acknowledge=acknowledge, + legacy_output=framework, + evaluation_conclusion=evaluation_conclusion, + additional_technical_requirements=additional_technical_requirements, + ) + except Exception as e: + logger.info(f"{e}") + break + evaluation = await evaluate_framework.run( + use_case_actors=use_case_actors, + trd=trd, + acknowledge=acknowledge, + legacy_output=framework, + additional_technical_requirements=additional_technical_requirements, + ) + is_pass = evaluation.is_pass + evaluation_conclusion = evaluation.conclusion + + file_list = await save_framework(dir_data=framework, output_dir=output_dir) + logger.info(f"Output:\n{file_list}") + return "\n".join(file_list)