diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index c34c72ed2..99a4175f6 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -14,6 +14,7 @@ from metagpt.actions.debug_error import DebugError from metagpt.actions.design_api import WriteDesign from metagpt.actions.design_api_review import DesignReview from metagpt.actions.project_management import AssignTasks, WriteTasks +from metagpt.actions.refine import Refine from metagpt.actions.research import CollectLinks, WebBrowseAndSummarize, ConductResearch from metagpt.actions.run_code import RunCode from metagpt.actions.search_and_summarize import SearchAndSummarize diff --git a/metagpt/actions/refine.py b/metagpt/actions/refine.py new file mode 100644 index 000000000..beea40fc8 --- /dev/null +++ b/metagpt/actions/refine.py @@ -0,0 +1,10 @@ +from metagpt.actions import Action + + +# 增量开发动作的基类 +class Refine(Action): + def __init__(self, name="Refine", context=None, llm=None): + super().__init__(name, context, llm) + + def run(self, *args, **kwargs): + raise NotImplementedError diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py new file mode 100644 index 000000000..d6a948b43 --- /dev/null +++ b/metagpt/actions/refine_design_api.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import re +import shutil +from pathlib import Path +from typing import List, Union + +from metagpt.actions import Action, ActionOutput +from metagpt.config import CONFIG +from metagpt.const import WORKSPACE_ROOT +from metagpt.logs import logger +from metagpt.utils.common import CodeParser +from metagpt.utils.get_template import get_template +from metagpt.utils.json_to_markdown import json_to_markdown +from metagpt.utils.mermaid import mermaid_to_file + +templates = { + "json": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to perform incremental development and design a state-of-the-art (SOTA) PEP8-compliant Python system based on the context and legacy design. Make the best use of good open source tools. +Requirement: Fill in the following missing information based on the context, each section name is a key in json. Output exactly as shown in the example, including single and double quotes. +Max Output: 8192 chars or 2048 tokens. Try to use them up. + +## Incremental implementation approach: Provide as Python list[str]. Analyze the difficult points of the requirements, select the appropriate open-source framework. Up to 5. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## Data structures and interface definitions: Use single quotes to wrap content. Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design. + +## Program call flow: Use single quotes to wrap content. Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT. + +output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example. +Output exactly as shown in the example, including single and double quotes, and only output the json inside this tag, nothing else +""", + "FORMAT_EXAMPLE": """ +[CONTENT] +{ + "Incremental implementation approach": ["We will ...",], + "Python package name": "new_name", + "Data structures and interface definitions": ' + classDiagram + class Game{ + +int score + } + ... + Game "1" -- "1" Food: has + ', + "Program call flow": ' + sequenceDiagram + participant M as Main + ... + G->>M: end game + ' +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to perform incremental development and design a state-of-the-art (SOTA) PEP8-compliant Python system based on the context and legacy design. Make the best use of good open source tools. +Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately. Output exactly as shown in the example, including single and double quotes. +Max Output: 8192 chars or 2048 tokens. Try to use them up. +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Incremental implementation approach: Provide as Python list[str]. Analyze the difficult points of the requirements, select the appropriate open-source framework. Up to 5. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## Data structures and interface definitions: Use single quotes to wrap content. Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design. + +## Program call flow: Use single quotes to wrap content. Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT. + +""", + "FORMAT_EXAMPLE": """ +--- + +## Incremental implementation approach +```python +[ + "We will ...", +] +``` + +## Python package name +```python +"new_name" +``` + +## Data structures and interface definitions +```mermaid +classDiagram + class Game{ + +int score + } + ... + Game "1" -- "1" Food: has +``` + +## Program call flow +```mermaid +sequenceDiagram + participant M as Main + ... + G->>M: end game +``` +--- +""", + }, +} + +OUTPUT_MAPPING = { + # "Incremental Requirements": (str, ...), + # "Difference Description": (Union[List[str], str], ...), + "Incremental implementation approach": (Union[List[str], str], ...), + "Python package name": (str, ...), + # "File list": (List[str], ...), + "Data structures and interface definitions": (str, ...), + "Program call flow": (str, ...) +} + + +class RefineDesign(Action): + def __init__(self, name, context=None, llm=None): + super().__init__(name, context, llm) + self.desc = ( + "Based on the PRD, think about the system design, and design the corresponding APIs, " + "data structures, library tables, processes, and paths. Please provide your design, feedback " + "clearly and in detail." + ) + + def recreate_workspace(self, workspace: Path): + try: + shutil.rmtree(workspace) + except FileNotFoundError: + pass # Folder does not exist, but we don't care + workspace.mkdir(parents=True, exist_ok=True) + + def create_or_increment_workspace(self, workspace: Path): + # 如果工作空间已存在,添加数字以区分 + original_workspace = workspace + index = 1 + while workspace.exists(): + ws_name_match = re.match(r'^(.*)_([\d]+)$', original_workspace.name) + if ws_name_match: + base_name, existing_index = ws_name_match.groups() + index = int(existing_index) + index += 1 + workspace = original_workspace.parent / f"{base_name}_{index}" + else: + workspace = original_workspace.parent / f"{original_workspace.name}_{index}" + index += 1 + + # 创建工作空间,包括所有必要的父文件夹 + workspace.mkdir(parents=True, exist_ok=True) + return workspace + + async def _save_prd(self, docs_path, resources_path, context): + prd_file = docs_path / "prd.md" + + if context[-1].instruct_content: + logger.info(f"Saving PRD to {prd_file}") + prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict())) + + async def _save_system_design(self, docs_path, resources_path, system_design): + data_api_design = system_design.instruct_content.dict()[ + "Data structures and interface definitions" + ] # CodeParser.parse_code(block="Data structures and interface definitions", text=content) + seq_flow = system_design.instruct_content.dict()[ + "Program call flow" + ] # CodeParser.parse_code(block="Program call flow", text=content) + await mermaid_to_file(data_api_design, resources_path / "data_api_design") + await mermaid_to_file(seq_flow, resources_path / "seq_flow") + system_design_file = docs_path / "system_design.md" + logger.info(f"Saving System Designs to {system_design_file}") + system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict()))) + + async def _save(self, context, system_design): + if isinstance(system_design, ActionOutput): + ws_name = system_design.instruct_content.dict()["Python package name"] + else: + ws_name = CodeParser.parse_str(block="Python package name", text=system_design) + workspace = WORKSPACE_ROOT / ws_name + # workspace = self.create_or_increment_workspace(workspace) + self.recreate_workspace(workspace) + docs_path = workspace / "docs" + resources_path = workspace / "resources" + docs_path.mkdir(parents=True, exist_ok=True) + resources_path.mkdir(parents=True, exist_ok=True) + await self._save_prd(docs_path, resources_path, context) + await self._save_system_design(docs_path, resources_path, system_design) + + async def run(self, context, legacy, format=CONFIG.prompt_format): + prompt_template, format_example = get_template(templates, format) + prompt = prompt_template.format(context=context, legacy=legacy, format_example=format_example) + # system_design = await self._aask(prompt) + system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format) + # fix Python package name, we can't system_design.instruct_content.python_package_name = "xxx" since "Python package name" contain space, have to use setattr + setattr( + system_design.instruct_content, + "Python package name", + system_design.instruct_content.dict()["Python package name"].strip().strip("'").strip('"'), + ) + await self._save(context, system_design) + return system_design diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py new file mode 100644 index 000000000..1d8bab5f8 --- /dev/null +++ b/metagpt/actions/refine_prd.py @@ -0,0 +1,95 @@ +from typing import List, Union + +from metagpt.actions import Refine, ActionOutput, SearchAndSummarize +from metagpt.config import CONFIG +from metagpt.logs import logger +from metagpt.utils.get_template import get_template + +increment_template = { + "json": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Search Information +{search_information} + +## Format example +{format_example} +----- +Role: You are a professional Product Manager tasked with overseeing incremental development and crafting Product Requirements Documents (PRDs) for a concise, usable, and efficient product. +Requirements: According to the context, fill in the following missing information, each section name is a key in json ,If the requirements are unclear, ensure minimum viability and avoid excessive design. Only output one json, nothing else. + +## Incremental Development Analysis: Provide as Python list[str], up to 5. incremental development analysis and plans based on the context and the legacy. If the requirement itself is simple, the Incremental Development Analysis should also be simple. + +output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, +and only output the json inside this tag, nothing else +""", + "FORMAT_EXAMPLE": """ +[CONTENT] +{ + "Incremental Development Analysis": [], +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Search Information +{search_information} + +## Format example +{format_example} +----- +Role: You are a professional Product Manager tasked with overseeing incremental development and crafting Product Requirements Documents (PRDs) for a concise, usable, and efficient product. +Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD WRITE BEFORE the code and triple quote. Output carefully referenced "Format example" in format.Only output one json, nothing else. + +## Incremental Development Analysis: Provide as Python list[str], up to 5. Incremental development analysis and plans based on the context and the legacy. If the requirement itself is simple, the Incremental Development Analysis should also be simple. +""", + "FORMAT_EXAMPLE": """ +--- + +## Incremental Development Analysis +[ + "We will ...", +] +""", + }, +} + +INCREMENT_OUTPUT_MAPPING = { + "Incremental Development Analysis": (List[str], ...), +} + + +class RefinePRD(Refine): + + def __init__(self, name="RefinePRD", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context, legacy, format=CONFIG.prompt_format, *args, **kwargs): + sas = SearchAndSummarize() + rsp = "" + info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" + if sas.result: + logger.info(sas.result) + logger.info(rsp) + + prompt_template, format_example = get_template(increment_template, format) + prompt = prompt_template.format( + context=context, legacy=legacy, search_information=info, + format_example=format_example + ) + logger.debug(prompt) + prd = await self._aask_v1(prompt, "prd", INCREMENT_OUTPUT_MAPPING, format=format) + return prd diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py new file mode 100644 index 000000000..2775b1cb6 --- /dev/null +++ b/metagpt/actions/refine_project_management.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from typing import List, Union + +from metagpt.actions.action import Action +from metagpt.config import CONFIG +from metagpt.const import WORKSPACE_ROOT +from metagpt.utils.common import CodeParser +from metagpt.utils.get_template import get_template +from metagpt.utils.json_to_markdown import json_to_markdown + +templates = { + "json": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to perform incremental development based on the context and the legacy. Break down tasks according to PRD/technical design, provide a Task list, and analyze task dependencies to start with the prerequisite modules. +Requirements: Based on the context and the Legacy Project Management and Legacy Code, fill in the following missing information. Note that Please try your best to reuse legacy code, and all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file that need to modified. +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. +Output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT]. The following is the attribute description of the JSON object. + +## Required Python third-party packages: Provided as a python list, the requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. + +## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first. + +Output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, +and only output the json inside this tag, nothing else +""", + "FORMAT_EXAMPLE": ''' +{ + "Required Python third-party packages": [ + "flask==1.1.2", + "bcrypt==3.2.0" + ], + "Full API spec": """ + openapi: 3.0.0 + ... + description: A JSON object ... + """, + "Task list": [ + "game.py" + ] +} +''', + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to perform incremental development based on the context and the legacy. Break down tasks according to PRD/technical design, provide a Task list need to modified files, and analyze task dependencies to start with the prerequisite modules. +Requirements: Based on the context and the Legacy Project Management and Legacy Code, fill in the following missing information. Note that Please try your best to reuse legacy code, and all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file that need to modified. +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided as a python list, the requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. + +## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first. +""", + "FORMAT_EXAMPLE": ''' +--- + +## Required Python third-party packages +```python +[ + "flask==1.1.2", + "bcrypt==3.2.0" +] +``` + +## Full API spec +```python +""" +openapi: 3.0.0 +... +description: A JSON object ... +""" +``` + +## Task list +```python +[ + "game.py", +] +``` +--- +''', + }, +} +OUTPUT_MAPPING = { + # "Incremental Requirements": (str, ...), + # ## Incremental Requirements: Provided as a str, the foremost incremental requirements for project management here based on the previous. + # "Difference Analysis": (Union[List[str], str], ...), + "Required Python third-party packages": (Union[List[str], str], ...), + "Full API spec": (str, ...), + # "Logic Analysis": (List[List[str]], ...), + "Task list": (List[str], ...), +} + + +class RefineTasks(Action): + def __init__(self, name="RefineTasks", context=None, llm=None): + super().__init__(name, context, llm) + + def _save(self, context, rsp): + if context[-1].instruct_content: + ws_name = context[-1].instruct_content.dict()["Python package name"] + else: + ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md" + file_path.write_text(json_to_markdown(rsp.instruct_content.dict())) + + # Write requirements.txt + requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" + requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages"))) + + async def run(self, context, legacy, format=CONFIG.prompt_format): + prompt_template, format_example = get_template(templates, format) + prompt = prompt_template.format(context=context, + legacy=legacy, + format_example=format_example) + rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format) + self._save(context, rsp) + return rsp + + +class AssignTasks(Action): + async def run(self, *args, **kwargs): + # Here you should implement the actual action + pass diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py new file mode 100644 index 000000000..3e51f0d2d --- /dev/null +++ b/metagpt/actions/write_code_guide.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from metagpt.actions.action import Action +from metagpt.logs import logger +from metagpt.schema import Message +from metagpt.utils.common import CodeParser +from tenacity import retry, stop_after_attempt, wait_fixed + + +PROMPT_TEMPLATE = """ +NOTICE +Role: You are a professional software engineer, and your main task is to conduct incremental development, proposing incremental development plans and code guideance based on context and legacy code. Existing code and logic that need to be retained must also appear in the code after incremental development, do not omit it. Ensure that the code conforms to the PEP8 standards, is elegantly designed and modularized, easy to read and maintain, and is written in Python 3.9 (or in another programming language). Output format carefully referenced "Format example". + +## Regulations Review: To make the software directly operable without further coding, follow the regulations below during incremental development: +0) Determine the scope of responsibilities of each file and what classes and methods need to be implemented. +1) Import all referenced classes. +2) Implement all methods. +3) Add necessary explanation to all methods. +4) Ensure there are no potential bugs. +5) Confirm that the entire project conforms to the tasks proposed by the user. +6) Review the code thoroughly, checking for errors and validating the logic to ensure seamless user interaction without compromising any specified requirements. + +## Incremental Development Plan: Provided as a Python list containing `filename.py`. Proposed the detail and essential incremental development plan, based on the following context and legacy code by thinking and analyzing step by step. All incremental modules/functions need to be added to the corresponding code files. + +## Code Guidance: Propose the foremost guidelines that how to implement code of modification part for incremental development based on the above context, legacy code and incremental development plan. + +----- +# Context +{context} + +## Legacy Code +You are tasked with conducting incremental development in the existing code based on the provided legacy code and above information. +``` +{legacy} +``` +----- + +## Format example +----- +## Incremental Development Guide +[ + "`game.py` Contains `Game` and ...", +] + + +## Code Guidance +### Implementation `xx` in `xxx.py` ..., else retain the original xxx.py code. +```python +## xxx.py +... +``` +--- +### Implementation of the `Game` in `game.py` ..., else retain the original game.py code. +```python +## game.py +class Game: + ... +``` +----- +""" + + +class WriteCodeGuide(Action): + def __init__(self, name="WriteCodeGuide", context: list[Message] = None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context, legacy): + prompt = PROMPT_TEMPLATE.format(context=context, legacy=legacy) + logger.info(f'Write Code Guide ..') + code_guide = await self._aask(prompt) + # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) + return code_guide + \ No newline at end of file diff --git a/metagpt/actions/write_code_refine.py b/metagpt/actions/write_code_refine.py new file mode 100644 index 000000000..5a70b2c68 --- /dev/null +++ b/metagpt/actions/write_code_refine.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from metagpt.actions.action import Action +from metagpt.logs import logger +from metagpt.schema import Message +from metagpt.utils.common import CodeParser +from tenacity import retry, stop_after_attempt, wait_fixed + +PROMPT_TEMPLATE = """ +NOTICE +Role: You are a professional engineer; your primary goal is to write PEP8 compliant, elegant, modular, easy-to-read, and maintainable Python 3.9 code (or any other programming language of your choice). +Requirements: Rewrite the complete code based on the Legacy Code so that it can be executed and avoid any potential bugs. You should modify the corresponding code based on the guidance. Output the complete code, fixing all errors according to the context. Ensure that you adhere to the specified guidelines for incremental development and modification of legacy code. +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format should be carefully referenced using the "Format example". Only output the current modified code, nothing else. In the modified code, if unchanged, you should output it, the complete code. + +## Rewrite Complete Code: Only Write one file {filename}, Write code using triple quotes, based on the following list, context, guidelines and legacy code. +1. Important: Do your best to implement ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT. +2. Implement one of the following code files based on the provided context. Return the code in the specified format. Your code will be part of the entire project, so ensure it is complete, reliable, and reusable. +3. Attention1: Implement the functions required by the current file scope of responsibility. For example, main only needs to focus on the basic functions of main.py in the legacy code and the incremental functions to be implemented. Reuse existing code as much as possible. You can import functions from other codes instead of reimplementing the function. If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. +4. Attention2: Make modifications and additions to the legacy code in accordance with the provided guidelines and API. Ensure that the complete code is implemented without any omissions, taking into account the guidelines, context, and existing legacy code. Retain the basic function methods from the legacy code, and make sure to preserve the existing code and logic that needs to be retained throughout the incremental development process. Avoid omitting any essential components. +5. Think before writing: What should be implemented and provided in this document? +6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. +7. Do not use public member functions that do not exist in your design. +8. The Modified Code is implemented according to the requirements, and there are no issues with the code logic. All functions in the Modified Code are fully implemented; none are omitted or incomplete. + +----- +# Context +{context} + +----- +## Guidelines: The foremost guidelines of modification for incremental development. +{guide} + +----- +## Legacy Code: The Legacy Code that needs to be modified. '===' is the separator of each code file in the legacy code. Basic function methods need to be retained in {filename}. +{legacy} + +----- +## Format example +----- +## Rewrite Complete Code: {filename} +```python +# {filename} +... +``` +----- +""" + + +class WriteCodeRefine(Action): + def __init__(self, name="WriteCodeRefine", context: list[Message] = None, llm=None): + super().__init__(name, context, llm) + + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + async def write_code(self, prompt): + code_rsp = await self._aask(prompt) + code = CodeParser.parse_code(block="", text=code_rsp) + return code + + async def run(self, context, legacy, filename, guide): + prompt = PROMPT_TEMPLATE.format(context=context, legacy=legacy, filename=filename, guide=guide) + logger.info(f'Code refine {filename}..') + code = await self.write_code(prompt) + # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) + # self._save(context, filename, code) + return code + \ No newline at end of file