From 8a73d5cadbe40f7b275ddba37f680a141259756c Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 21 Nov 2023 14:38:19 +0800 Subject: [PATCH 001/101] add increment development function --- metagpt/actions/__init__.py | 1 + metagpt/actions/refine.py | 10 + metagpt/actions/refine_design_api.py | 231 ++++++++++++++++ metagpt/actions/refine_prd.py | 260 +++++++++++++++++++ metagpt/actions/refine_project_management.py | 208 +++++++++++++++ metagpt/actions/write_code.py | 10 +- metagpt/roles/architect.py | 48 +++- metagpt/roles/engineer.py | 74 +++++- metagpt/roles/product_manager.py | 36 ++- metagpt/roles/project_manager.py | 38 ++- metagpt/roles/role.py | 1 + startup.py | 54 +++- 12 files changed, 943 insertions(+), 28 deletions(-) create mode 100644 metagpt/actions/refine.py create mode 100644 metagpt/actions/refine_design_api.py create mode 100644 metagpt/actions/refine_prd.py create mode 100644 metagpt/actions/refine_project_management.py diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index b004bd58e..a9087e822 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -15,6 +15,7 @@ from metagpt.actions.design_api import WriteDesign from metagpt.actions.design_api_review import DesignReview from metagpt.actions.design_filenames import DesignFilenames 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..591db175a --- /dev/null +++ b/metagpt/actions/refine_design_api.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import shutil +from pathlib import Path +from typing import List + +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 Design +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; 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 +Max Output: 8192 chars or 2048 tokens. Try to use them up. + +## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: 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 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. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +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] +{ + "Difference Description": ["The ..."], + "Implementation approach": "We will ...", + "Python package name": "snake_game", + "File list": ["main.py"], + "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 + ', + "Anything UNCLEAR": "The requirement is clear to me." +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy Design +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; 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 +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. + +## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: 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 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. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +""", + "FORMAT_EXAMPLE": """ +--- +## Difference Description +```python +[ + "The ...", +] +``` + +## Implementation approach +We will ... + +## Python package name +```python +"snake_game" +``` + +## File list +```python +[ + "main.py", +] +``` + +## 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 +``` + +## Anything UNCLEAR +The requirement is clear to me. +--- +""", + }, +} + +OUTPUT_MAPPING = { + "Difference Description": (List[str], ...), + "Implementation approach": (str, ...), + "Python package name": (str, ...), + "File list": (List[str], ...), + "Data structures and interface definitions": (str, ...), + "Program call flow": (str, ...), + "Anything UNCLEAR": (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) + + async def _save_prd(self, docs_path, resources_path, context): + prd_file = docs_path / "prd.md" + if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: + quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] + await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") + + 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 + 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..1e2709c6b --- /dev/null +++ b/metagpt/actions/refine_prd.py @@ -0,0 +1,260 @@ +from typing import List + +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 +## User's New Requirements +{new_requirements} + +## Difference Description +{difference_description} + +## Legacy PRD +{legacy} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, efficient product based on the new requirements, the difference description and Legacy PRD. +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 + +## New Requirements: Provide as Plain text and place the new requirements here + +## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +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] +{ + "New Requirements": "", + "Difference Description": "", + "Search Information": "", + "Requirements": "", + "Product Goals": [], + "User Stories": [], + "Competitive Analysis": [], + "Competitive Quadrant Chart": "quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78]", + "Requirement Analysis": "", + "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], + "UI Design draft": "", + "Anything UNCLEAR": "", +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +You need to refine the requirements based on the new requirements and the existing requirements' output. +## User's New Requirements +{new_requirements} + +## Difference Description +{difference_description} + +## Legacy PRD +{legacy} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, 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. + +## New Requirements: Provide as Plain text and place the new requirements here + +## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. +""", + "FORMAT_EXAMPLE": """ +--- +## New Requirements +The boss ... + +## Difference Description +```python +[ + "...", +] + +## Product Goals +```python +[ + "Create a ...", +] +``` + +## User Stories +```python +[ + "As a user, ...", +] +``` + +## Competitive Analysis +```python +[ + "Python Snake Game: ...", +] +``` + +## Competitive Quadrant Chart +```mermaid +quadrantChart + title Reach and engagement of campaigns + ... + "Our Target Product": [0.6, 0.7] +``` + +## Requirement Analysis +The product should be a ... + +## Requirement Pool +```python +[ + ["End game ...", "P0"] +] +``` + +## UI Design draft +Give a basic function description, and a draft + +## Anything UNCLEAR +There are no unclear points. +--- +""", + }, +} + +INCREMENT_OUTPUT_MAPPING = { + "New Requirements": (str, ...), + # "Major Enhancements": (List[str], ...), + "Difference Description": (List[str], ...), + "Product Goals": (List[str], ...), + "User Stories": (List[str], ...), + "Competitive Analysis": (List[str], ...), + "Competitive Quadrant Chart": (str, ...), + "Requirement Analysis": (str, ...), + "Requirement Pool": (List[List[str]], ...), + "UI Design draft": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +# 对于产品经理,增量开发的动作是:RefinePDR,输出是结合新需求和已有需求输出的新的PDR +class RefinePRD(Refine): + + def __init__(self, name="RefinePRD", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, new_requirements, difference_description, 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( + new_requirements=new_requirements, difference_description=difference_description, 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..354996f3e --- /dev/null +++ b/metagpt/actions/refine_project_management.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from typing import List + +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 Design +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided in requirements.txt format + +## Required Other language third-party packages: Provided in requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first + +## 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 + +## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. + +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. + +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" + ], + "Required Other language third-party packages": [ + "No third-party ..." + ], + "Full API spec": """ + openapi: 3.0.0 + ... + description: A JSON object ... + """, + "Logic Analysis": [ + ["game.py","Contains..."] + ], + "Task list": [ + "game.py" + ], + "Shared Knowledge": """ + 'game.py' contains ... + """, + "Difference Description": """ + The ... + """, + "Anything UNCLEAR": "We need ... how to start." +} +''', + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy Design +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Requirements: Based on the context, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided in requirements.txt format + +## Required Other language third-party packages: Provided in requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first + +## 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 + +## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. + +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. + +""", + "FORMAT_EXAMPLE": ''' +--- +## Required Python third-party packages +```python +""" +flask==1.1.2 +bcrypt==3.2.0 +""" +``` + +## Required Other language third-party packages +```python +""" +No third-party ... +""" +``` + +## Full API spec +```python +""" +openapi: 3.0.0 +... +description: A JSON object ... +""" +``` + +## Logic Analysis +```python +[ + ["game.py", "Contains ..."], +] +``` + +## Task list +```python +[ + "game.py", +] +``` + +## Shared Knowledge +```python +""" +'game.py' contains ... +""" +``` + +## Difference Description +```python +""" +The ... +""" +``` + +## Anything UNCLEAR +We need ... how to start. +--- +''', + }, +} +OUTPUT_MAPPING = { + "Required Python third-party packages": (List[str], ...), + "Required Other language third-party packages": (List[str], ...), + "Full API spec": (str, ...), + "Logic Analysis": (List[List[str]], ...), + "Task list": (List[str], ...), + "Shared Knowledge": (str, ...), + "Difference Description": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +class RefineTasks(Action): + def __init__(self, name="CreateTasks", 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.py b/metagpt/actions/write_code.py index c000805c5..663ae0929 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -7,6 +7,7 @@ """ from metagpt.actions import WriteDesign from metagpt.actions.action import Action +from metagpt.actions.refine_design_api import RefineDesign from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message @@ -55,7 +56,14 @@ class WriteCode(Action): if self._is_invalid(filename): return - design = [i for i in context if i.cause_by == WriteDesign][0] + # FIXME: 需要适配increment + # design = [i for i in context if i.cause_by == WriteDesign][0] + design = [] + for i in context: + if i.cause_by == WriteDesign: + design.append(i) + elif i.cause_by == RefineDesign: + design.append(i) ws_name = CodeParser.parse_str(block="Python package name", text=design.content) ws_path = WORKSPACE_ROOT / ws_name diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index 15d5fe5b1..dddcd2a8b 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -6,9 +6,13 @@ @File : architect.py """ -from metagpt.actions import WritePRD +from metagpt.actions import WritePRD, ActionOutput from metagpt.actions.design_api import WriteDesign +from metagpt.actions.refine_design_api import RefineDesign +from metagpt.actions.refine_prd import RefinePRD +from metagpt.logs import logger from metagpt.roles import Role +from metagpt.schema import Message class Architect(Role): @@ -23,17 +27,43 @@ class Architect(Role): """ def __init__( - self, - name: str = "Bob", - profile: str = "Architect", - goal: str = "Design a concise, usable, complete python system", - constraints: str = "Try to specify good open source tools as much as possible", + self, + name: str = "Bob", + profile: str = "Architect", + goal: str = "Design a concise, usable, complete python system", + constraints: str = "Try to specify good open source tools as much as possible", + legacy: str = "", + increment: bool = False, ) -> None: """Initializes the Architect with given attributes.""" super().__init__(name, profile, goal, constraints) + self.legacy = legacy + self.increment = increment # Initialize actions specific to the Architect role - self._init_actions([WriteDesign]) - # Set events or actions the Architect should watch or be aware of - self._watch({WritePRD}) + if self.increment: + self._init_actions([RefineDesign]) + self._watch({RefinePRD}) + else: + self._init_actions([WriteDesign]) + self._watch({WritePRD}) + + async def _act(self) -> Message: + if self.increment: + logger.info(f"{self._setting}: ready to RefineDesign") + response = await self._rc.todo.run(self._rc.history, self.legacy) + + else: + logger.info(f"{self._setting}: ready to WriteDesign") + response = await self._rc.todo.run(self._rc.history) + + if isinstance(response, ActionOutput): + msg = Message(content=response.content, instruct_content=response.instruct_content, + role=self.profile, cause_by=type(self._rc.todo)) + else: + msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) + self._rc.memory.add(msg) + logger.debug(f"{response}") + + return msg diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 1f6685b38..0a0107636 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -11,6 +11,8 @@ from collections import OrderedDict from pathlib import Path from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks +from metagpt.actions.refine_design_api import RefineDesign +from metagpt.actions.refine_project_management import RefineTasks from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -68,14 +70,21 @@ class Engineer(Role): constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable", n_borg: int = 1, use_code_review: bool = False, + legacy: str = "", + increment: bool = False, ) -> None: """Initializes the Engineer role with given attributes.""" super().__init__(name, profile, goal, constraints) self._init_actions([WriteCode]) self.use_code_review = use_code_review - if self.use_code_review: + self.legacy = legacy + self.increment = increment + if self.use_code_review or self.increment: self._init_actions([WriteCode, WriteCodeReview]) - self._watch([WriteTasks]) + if self.increment: + self._watch([RefineTasks]) + else: + self._watch([WriteTasks]) self.todos = [] self.n_borg = n_borg @@ -96,7 +105,10 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + if self.increment: + msg = self._rc.memory.get_by_action(RefineDesign)[-1] + else: + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) @@ -167,6 +179,55 @@ class Engineer(Role): ) return msg + async def _act_increment(self, legacy) -> Message: + code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + flag = True + for todo in self.todos: + """ + # Select essential information from the historical data to reduce the length of the prompt (summarized from human experience): + 1. All from Architect + 2. All from ProjectManager + 3. Do we need other codes (currently needed)? + TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. + """ + context = [] + + if self.increment: + msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks, WriteCode]) + else: + msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) + + for m in msg: + context.append(m.content) + context_str = "\n".join(context) + # Refine code or Write code + if flag and self.increment: + code = legacy + flag = False + else: + code = await WriteCode().run(context=context_str, filename=todo) + + # Code review + if self.use_code_review: + try: + rewrite_code = await WriteCode().run(context=context_str, code=code, filename=todo) + code = rewrite_code + except Exception as e: + logger.error("code review failed!", e) + pass + file_path = self.write_file(todo, code) + msg = Message(content=code, role=self.profile, cause_by=WriteCode) + self._rc.memory.add(msg) + + code_msg = todo + FILENAME_CODE_SEP + str(file_path) + code_msg_all.append(code_msg) + + logger.info(f"Done {self.get_workspace()} generating.") + msg = Message( + content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" + ) + return msg + async def _act_sp_precision(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later for todo in self.todos: @@ -207,7 +268,12 @@ class Engineer(Role): async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" - logger.info(f"{self._setting}: ready to WriteCode") + if self.increment: + logger.info(f"{self._setting}: ready to RefineWriteCode") + else: + logger.info(f"{self._setting}: ready to WriteCode") if self.use_code_review: return await self._act_sp_precision() + elif self.increment: + return await self._act_increment(self.legacy) return await self._act_sp() diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index a58ea5385..0b2d83ed0 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -5,8 +5,11 @@ @Author : alexanderwu @File : product_manager.py """ -from metagpt.actions import BossRequirement, WritePRD +from metagpt.actions import BossRequirement, WritePRD, ActionOutput +from metagpt.actions.refine_prd import RefinePRD +from metagpt.logs import logger from metagpt.roles import Role +from metagpt.schema import Message class ProductManager(Role): @@ -26,6 +29,9 @@ class ProductManager(Role): profile: str = "Product Manager", goal: str = "Efficiently create a successful product", constraints: str = "", + difference_description: str = "", + legacy: str = "", + increment: bool = False, ) -> None: """ Initializes the ProductManager role with given attributes. @@ -37,5 +43,31 @@ class ProductManager(Role): constraints (str): Constraints or limitations for the product manager. """ super().__init__(name, profile, goal, constraints) - self._init_actions([WritePRD]) + self.difference_description = difference_description + self.legacy = legacy + self.increment = increment + + if self.increment: + self._init_actions([RefinePRD]) + else: + self._init_actions([WritePRD]) self._watch([BossRequirement]) + + async def _act(self) -> Message: + if self.increment: + logger.info(f"{self._setting}: ready to RefinePRD") + response = await self._rc.todo.run(self._rc.history, self.difference_description, self.legacy) + + else: + logger.info(f"{self._setting}: ready to WritePRD") + response = await self._rc.todo.run(self._rc.history) + + if isinstance(response, ActionOutput): + msg = Message(content=response.content, instruct_content=response.instruct_content, + role=self.profile, cause_by=type(self._rc.todo)) + else: + msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) + self._rc.memory.add(msg) + logger.debug(f"{response}") + + return msg diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 7e7c5699d..21a196d21 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -5,9 +5,13 @@ @Author : alexanderwu @File : project_manager.py """ -from metagpt.actions import WriteTasks +from metagpt.actions import WriteTasks, ActionOutput from metagpt.actions.design_api import WriteDesign +from metagpt.actions.refine_design_api import RefineDesign +from metagpt.actions.refine_project_management import RefineTasks +from metagpt.logs import logger from metagpt.roles import Role +from metagpt.schema import Message class ProjectManager(Role): @@ -27,6 +31,8 @@ class ProjectManager(Role): profile: str = "Project Manager", goal: str = "Improve team efficiency and deliver with quality and quantity", constraints: str = "", + increment: bool = False, + legacy: str = "", ) -> None: """ Initializes the ProjectManager role with given attributes. @@ -38,5 +44,31 @@ class ProjectManager(Role): constraints (str): Constraints or limitations for the project manager. """ super().__init__(name, profile, goal, constraints) - self._init_actions([WriteTasks]) - self._watch([WriteDesign]) + self.increment = increment + self.legacy = legacy + + if self.increment: + self._init_actions([RefineTasks]) + self._watch([RefineDesign]) + else: + self._init_actions([WriteTasks]) + self._watch([WriteDesign]) + + async def _act(self) -> Message: + if self.increment: + logger.info(f"{self._setting}: ready to RefineTasks") + response = await self._rc.todo.run(self._rc.history, self.legacy) + + else: + logger.info(f"{self._setting}: ready to WriteTasks") + response = await self._rc.todo.run(self._rc.history) + + if isinstance(response, ActionOutput): + msg = Message(content=response.content, instruct_content=response.instruct_content, + role=self.profile, cause_by=type(self._rc.todo)) + else: + msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo)) + self._rc.memory.add(msg) + logger.debug(f"{response}") + + return msg diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 0251176f7..a39ca1001 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -205,6 +205,7 @@ class Role: # history=self.history) logger.info(f"{self._setting}: ready to {self._rc.todo}") + response = await self._rc.todo.run(self._rc.important_memory) # logger.info(response) if isinstance(response, ActionOutput): diff --git a/startup.py b/startup.py index e9fbf94d3..7e8e0a692 100644 --- a/startup.py +++ b/startup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import asyncio +import os import fire @@ -16,26 +17,57 @@ from metagpt.team import Team async def startup( idea: str, + difference_description: str = "", + path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = False, run_tests: bool = False, implement: bool = True, + increment: bool = False, ): """Run a startup. Be a boss.""" company = Team() - company.hire( - [ - ProductManager(), - Architect(), - ProjectManager(), - ] - ) + + if increment: + # 读取文件 + prd_path = os.path.join(path, 'docs/prd.md') + design_path = os.path.join(path, 'docs/system_design.md') + api_spec_path = os.path.join(path, 'docs/api_spec_and_tasks.md') + code_path = os.path.join(path, os.path.basename(path)) + + with open(prd_path, 'r', encoding='utf-8') as f: + legacy_prd = f.read() + + with open(design_path, 'r', encoding='utf-8') as f: + legacy_design = f.read() + + with open(api_spec_path, 'r', encoding='utf-8') as f: + legacy_api_spec = f.read() + + # 遍历文件夹,获取所有代码文件 + legacy_code = '' + for root, dirs, files in os.walk(code_path): + filenames = [filename for filename in files if filename.endswith('.py')] + legacy_code += f'There are {len(files)} scripts in the current folder: {", ".join(filenames)}\n\n' + for file in files: + if file.endswith('.py'): + with open(os.path.join(root, file), 'r', encoding='utf-8') as f: + legacy_code += f.read() + '\n\n' + + company.hire( + [ProductManager(difference_description=difference_description, legacy=legacy_prd, increment=increment), + Architect(legacy=legacy_design, increment=increment), + ProjectManager(legacy=legacy_api_spec, increment=increment)]) + else: + company.hire([ProductManager(), Architect(), ProjectManager()]) # if implement or code_review - if implement or code_review: + if (implement or code_review) and not increment: # developing features: implement the idea company.hire([Engineer(n_borg=5, use_code_review=code_review)]) + elif (implement or code_review) and increment: + company.hire([Engineer(n_borg=5, use_code_review=code_review, legacy=legacy_code, increment=increment)]) if run_tests: # developing features: run tests on the spot and identify bugs @@ -49,11 +81,14 @@ async def startup( def main( idea: str, + difference_description: str = "", + path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = True, run_tests: bool = False, implement: bool = True, + increment: bool = False, ): """ We are a software startup comprised of AI. By investing in us, @@ -65,7 +100,8 @@ def main( :param code_review: Whether to use code review. :return: """ - asyncio.run(startup(idea, investment, n_round, code_review, run_tests, implement)) + asyncio.run( + startup(idea, difference_description, path, investment, n_round, code_review, run_tests, implement, increment)) if __name__ == "__main__": From 1ea3c0c9f3149d1ccd4cd36ea8a2bd738c683a52 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 21 Nov 2023 14:42:45 +0800 Subject: [PATCH 002/101] add increment development function --- metagpt/roles/role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index a39ca1001..0251176f7 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -205,7 +205,6 @@ class Role: # history=self.history) logger.info(f"{self._setting}: ready to {self._rc.todo}") - response = await self._rc.todo.run(self._rc.important_memory) # logger.info(response) if isinstance(response, ActionOutput): From 8ff833f1e355d5adb5341dbbd8e3fd4265c110f3 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 24 Nov 2023 18:22:06 +0800 Subject: [PATCH 003/101] update increment development, add bug fix function --- metagpt/actions/refine_design_api.py | 25 ++- metagpt/actions/refine_prd.py | 17 +- metagpt/actions/refine_project_management.py | 34 ++-- metagpt/actions/write_code_refine.py | 80 ++++++++++ metagpt/roles/engineer.py | 154 +++++++++++++++---- metagpt/roles/qa_engineer.py | 36 ++++- startup.py | 68 +++++--- 7 files changed, 334 insertions(+), 80 deletions(-) create mode 100644 metagpt/actions/write_code_refine.py diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index 591db175a..f1c231525 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import re import shutil from pathlib import Path from typing import List @@ -25,7 +26,7 @@ templates = { ## Format example {format_example} ----- -Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +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 the provided difference descriptions. 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 Max Output: 8192 chars or 2048 tokens. Try to use them up. @@ -83,7 +84,7 @@ and only output the json inside this tag, nothing else ## Format example {format_example} ----- -Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +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 the provided difference descriptions. 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 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. @@ -179,6 +180,25 @@ class RefineDesign(Action): 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 and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: @@ -208,6 +228,7 @@ class RefineDesign(Action): 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" diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 1e2709c6b..81cb02af8 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -43,13 +43,15 @@ quadrantChart ## Format example {format_example} ----- -Role: You are a professional product manager; the goal is to design a concise, usable, efficient product based on the new requirements, the difference description and Legacy PRD. +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 ## New Requirements: Provide as Plain text and place the new requirements here ## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple + ## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple ## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less @@ -72,9 +74,10 @@ and only output the json inside this tag, nothing else [CONTENT] { "New Requirements": "", - "Difference Description": "", + "Difference Description": [], "Search Information": "", "Requirements": "", + "Incremental Development Plan": [], "Product Goals": [], "User Stories": [], "Competitive Analysis": [], @@ -138,7 +141,7 @@ quadrantChart ## Format example {format_example} ----- -Role: You are a professional product manager; the goal is to design a concise, usable, efficient product +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. @@ -146,6 +149,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD W ## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple + ## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple ## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less @@ -172,6 +177,11 @@ The boss ... "...", ] +## Incremental Development Plan +[ + "It ...", +] + ## Product Goals ```python [ @@ -225,6 +235,7 @@ INCREMENT_OUTPUT_MAPPING = { "New Requirements": (str, ...), # "Major Enhancements": (List[str], ...), "Difference Description": (List[str], ...), + "Incremental Development Plan": (List[str], ...), "Product Goals": (List[str], ...), "User Stories": (List[str], ...), "Competitive Analysis": (List[str], ...), diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index 354996f3e..7d9e118d8 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -21,7 +21,7 @@ templates = { ## Format example {format_example} ----- -Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. @@ -31,14 +31,14 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## 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 ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, @@ -58,6 +58,9 @@ and only output the json inside this tag, nothing else ... description: A JSON object ... """, + "Difference Description": """ + The ... + """, "Logic Analysis": [ ["game.py","Contains..."] ], @@ -67,9 +70,6 @@ and only output the json inside this tag, nothing else "Shared Knowledge": """ 'game.py' contains ... """, - "Difference Description": """ - The ... - """, "Anything UNCLEAR": "We need ... how to start." } ''', @@ -85,7 +85,7 @@ and only output the json inside this tag, nothing else ## Format example {format_example} ----- -Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. @@ -95,14 +95,14 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## 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 ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. """, @@ -132,6 +132,13 @@ description: A JSON object ... """ ``` +## Difference Description +```python +""" +The ... +""" +``` + ## Logic Analysis ```python [ @@ -153,13 +160,6 @@ description: A JSON object ... """ ``` -## Difference Description -```python -""" -The ... -""" -``` - ## Anything UNCLEAR We need ... how to start. --- @@ -170,10 +170,10 @@ OUTPUT_MAPPING = { "Required Python third-party packages": (List[str], ...), "Required Other language third-party packages": (List[str], ...), "Full API spec": (str, ...), + "Difference Description": (str, ...), "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), "Shared Knowledge": (str, ...), - "Difference Description": (str, ...), "Anything UNCLEAR": (str, ...), } diff --git a/metagpt/actions/write_code_refine.py b/metagpt/actions/write_code_refine.py new file mode 100644 index 000000000..527952aed --- /dev/null +++ b/metagpt/actions/write_code_refine.py @@ -0,0 +1,80 @@ +#!/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, which includes reviewing existing code, providing modification suggestions, rewriting code, and optimizing the codebase. 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). +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". + +## Code Review: Based on the following context and legacy code, and following the checklist, provide key, clear, concise, and specific code modification suggestions, up to 5. +``` +1. Check 0: Is the code implemented as per the requirements? +2. Check 1: Are there any issues with the code logic? +3. Check 2: Does the existing code follow the "Data structures and interface definitions"? +4. Check 3: Is there a function in the code that is omitted or not fully implemented that needs to be implemented? +5. Check 4: Does the code have unnecessary or lack dependencies? +``` + +## Incremental Development: {filename} Based on the findings from the "Code Review," context, and the legacy code, conduct incremental development by rewriting, optimizing, and adding new code using triple quotes. +----- +# Context +{context} + +## Legacy Code +You are tasked with conducting incremental development in the existing code and creating a new code file, {filename}, based on the provided legacy code and above information. +``` +{code} +``` +----- + +## Format example +----- +{format_example} +----- + +""" + +FORMAT_EXAMPLE = """ + +## Code Review +1. The code ... +2. ... +3. ... +4. ... +5. ... + +## Incremental Development: {filename} +```python +## {filename} - Incremental Development +... +``` +""" + + + +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, code, filename): + format_example = FORMAT_EXAMPLE.format(filename=filename) + prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example) + 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 diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 0a0107636..13034acaa 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -6,13 +6,16 @@ @File : engineer.py """ import asyncio +import re import shutil from collections import OrderedDict from pathlib import Path +from typing import List -from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks +from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks, BossRequirement from metagpt.actions.refine_design_api import RefineDesign from metagpt.actions.refine_project_management import RefineTasks +from metagpt.actions.write_code_refine import WriteCodeRefine from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -72,6 +75,8 @@ class Engineer(Role): use_code_review: bool = False, legacy: str = "", increment: bool = False, + bug_msgs: List = None, + bug_fix: bool = False, ) -> None: """Initializes the Engineer role with given attributes.""" super().__init__(name, profile, goal, constraints) @@ -79,13 +84,20 @@ class Engineer(Role): self.use_code_review = use_code_review self.legacy = legacy self.increment = increment + self.bug_msgs = bug_msgs + self.bug_fix = bug_fix if self.use_code_review or self.increment: - self._init_actions([WriteCode, WriteCodeReview]) + self._init_actions([WriteCode, WriteCodeReview, WriteCodeRefine]) + if self.increment: self._watch([RefineTasks]) + self.todos = [] + elif self.bug_fix: + self._watch([BossRequirement]) + self.todos = [] else: self._watch([WriteTasks]) - self.todos = [] + self.todos = [] self.n_borg = n_borg @classmethod @@ -94,6 +106,18 @@ class Engineer(Role): return task_msg.instruct_content.dict().get("Task list") return CodeParser.parse_file_list(block="Task list", text=task_msg.content) + @classmethod + def parse_todos(self, bug_context: List) -> list[str]: + for msg in bug_context: + if msg.sent_from == "ProjectManager": + content = msg.content + tasks_section = re.search(r"## Task list\n\n(.*?)(\n\n|$)", content, re.DOTALL) + if tasks_section: + tasks = tasks_section.group(1).split("\n") + tasks = [task.strip("-").strip() for task in tasks] + return tasks + return [] + @classmethod def parse_code(self, code_text: str) -> str: return CodeParser.parse_code(block="", text=code_text) @@ -107,13 +131,17 @@ class Engineer(Role): def get_workspace(self) -> Path: if self.increment: msg = self._rc.memory.get_by_action(RefineDesign)[-1] + elif self.bug_fix: + msg = self._rc.memory.get_by_action(BossRequirement)[-1] else: msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) + workspace = workspace if workspace else "src" # Codes are written in workspace/{package_name}/{package_name} return WORKSPACE_ROOT / workspace / workspace + # return self.create_or_increment_workspace(WORKSPACE_ROOT, workspace) def recreate_workspace(self): workspace = self.get_workspace() @@ -123,8 +151,7 @@ class Engineer(Role): pass # The folder does not exist, but we don't care workspace.mkdir(parents=True, exist_ok=True) - def write_file(self, filename: str, code: str): - workspace = self.get_workspace() + def write_file(self, workspace: Path, filename: str, code: str) -> Path: filename = filename.replace('"', "").replace("\n", "") file = workspace / filename file.parent.mkdir(parents=True, exist_ok=True) @@ -134,7 +161,10 @@ class Engineer(Role): def recv(self, message: Message) -> None: self._rc.memory.add(message) if message in self._rc.important_memory: - self.todos = self.parse_tasks(message) + if not self.bug_fix: + self.todos = self.parse_tasks(message) + else: + self.todos = self.parse_todos(self.bug_msgs) async def _act_mp(self) -> Message: # self.recreate_workspace() @@ -161,12 +191,13 @@ class Engineer(Role): async def _act_sp(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + workspace = self.get_workspace() for todo in self.todos: code = await WriteCode().run(context=self._rc.history, filename=todo) # logger.info(todo) # logger.info(code_rsp) # code = self.parse_code(code_rsp) - file_path = self.write_file(todo, code) + file_path = self.write_file(workspace, todo, code) msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo)) self._rc.memory.add(msg) @@ -181,41 +212,76 @@ class Engineer(Role): async def _act_increment(self, legacy) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + workspace = self.get_workspace() flag = True + # legacy_codes = legacy.split('---') for todo in self.todos: - """ - # Select essential information from the historical data to reduce the length of the prompt (summarized from human experience): - 1. All from Architect - 2. All from ProjectManager - 3. Do we need other codes (currently needed)? - TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. - """ context = [] - if self.increment: - msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks, WriteCode]) - else: - msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) - + msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks, WriteCodeRefine]) for m in msg: context.append(m.content) context_str = "\n".join(context) + code = legacy + # Refine code or Write code - if flag and self.increment: - code = legacy - flag = False - else: - code = await WriteCode().run(context=context_str, filename=todo) + # if self.increment and len(legacy_codes) > 0: + # code = legacy_codes.pop(0) # Code review - if self.use_code_review: - try: - rewrite_code = await WriteCode().run(context=context_str, code=code, filename=todo) - code = rewrite_code - except Exception as e: - logger.error("code review failed!", e) - pass - file_path = self.write_file(todo, code) + try: + rewrite_code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo) + code = rewrite_code + except Exception as e: + logger.error("code review failed!", e) + pass + + # code = await WriteCode().run(context=context_str, filename=todo) + + file_path = self.write_file(workspace, todo, code) + msg = Message(content=code, role=self.profile, cause_by=WriteCode) + self._rc.memory.add(msg) + + code_msg = todo + FILENAME_CODE_SEP + str(file_path) + code_msg_all.append(code_msg) + + logger.info(f"Done {self.get_workspace()} generating.") + msg = Message( + content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" + ) + return msg + + async def _act_bug_fix(self, bug_msgs) -> Message: + code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + workspace = self.get_workspace() + flag = True + # legacy_codes = legacy.split('---') + for todo in self.todos: + context = [] + + for m in bug_msgs: + if m.sent_from != "Engineer": + context.append(m.content) + context.append(m.content) + context_str = "\n".join(context) + code = [m.content for m in bug_msgs if m.sent_from == "Engineer"] + code = "\n".join(code) + + # Refine code or Write code + # if self.increment and len(legacy_codes) > 0: + # code = legacy_codes.pop(0) + + # Code review + try: + rewrite_code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo) + code = rewrite_code + except Exception as e: + logger.error("code review failed!", e) + pass + + # code = await WriteCode().run(context=context_str, filename=todo) + + file_path = self.write_file(workspace, todo, code) msg = Message(content=code, role=self.profile, cause_by=WriteCode) self._rc.memory.add(msg) @@ -230,6 +296,7 @@ class Engineer(Role): async def _act_sp_precision(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + workspace = self.get_workspace() for todo in self.todos: """ # Select essential information from the historical data to reduce the length of the prompt (summarized from human experience): @@ -253,7 +320,7 @@ class Engineer(Role): except Exception as e: logger.error("code review failed!", e) pass - file_path = self.write_file(todo, code) + file_path = self.write_file(workspace, todo, code) msg = Message(content=code, role=self.profile, cause_by=WriteCode) self._rc.memory.add(msg) @@ -266,14 +333,35 @@ class Engineer(Role): ) return msg + async def _observe(self) -> int: + if self.bug_fix: + msg = Message( + content=self.bug_msgs[0].content + "\n---\n" + self.legacy, + role=self.profile, + cause_by=BossRequirement, + sent_from=self.profile, + send_to=self.profile, + ) + self._publish_message(msg) + await super()._observe() + self._rc.news = [ + msg for msg in self._rc.news if msg.send_to == self.profile + ] # only relevant msgs count as observed news + return len(self._rc.news) + async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" if self.increment: logger.info(f"{self._setting}: ready to RefineWriteCode") + elif self.bug_fix: + logger.info(f"{self._setting}: ready to BugFix") else: logger.info(f"{self._setting}: ready to WriteCode") + if self.use_code_review: return await self._act_sp_precision() elif self.increment: return await self._act_increment(self.legacy) + elif self.bug_fix: + return await self._act_bug_fix(self.bug_msgs) return await self._act_sp() diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index a763c2ce8..c67d42b5c 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -14,8 +14,9 @@ from metagpt.actions import ( WriteCode, WriteCodeReview, WriteDesign, - WriteTest, + WriteTest, BossRequirement, ) +from metagpt.actions.write_code_refine import WriteCodeRefine from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -32,12 +33,20 @@ class QaEngineer(Role): goal="Write comprehensive and robust tests to ensure codes will work as expected without bugs", constraints="The test code you write should conform to code standard like PEP8, be modular, easy to read and maintain", test_round_allowed=5, + legacy="", + bug_context="", ): super().__init__(name, profile, goal, constraints) - self._init_actions( - [WriteTest] - ) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates - self._watch([WriteCode, WriteCodeReview, WriteTest, RunCode, DebugError]) + self.legacy = legacy + self.bug_context = bug_context + if self.bug_context: + self._init_actions([WriteTest]) + self._watch([WriteCode, WriteCodeRefine, WriteTest, RunCode, DebugError]) + else: + self._init_actions( + [WriteTest] + ) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates + self._watch([WriteCode, WriteCodeReview, WriteTest, RunCode, DebugError]) self.test_round = 0 self.test_round_allowed = test_round_allowed @@ -48,10 +57,14 @@ class QaEngineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + if self.bug_context: + msg = self._rc.memory.get_by_action(WriteCodeRefine)[-1] + else: + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) + workspace = workspace if workspace else "src" # project directory: workspace/{package_name}, which contains package source code folder, tests folder, resources folder, etc. if return_proj_dir: return WORKSPACE_ROOT / workspace @@ -146,6 +159,15 @@ class QaEngineer(Role): self._publish_message(msg) async def _observe(self) -> int: + if self.bug_context: + msg = Message( + content=self.bug_context + "\n---\n" + self.legacy, + role=self.profile, + cause_by=BossRequirement, + sent_from=self.profile, + send_to=self.profile, + ) + self._publish_message(msg) await super()._observe() self._rc.news = [ msg for msg in self._rc.news if msg.send_to == self.profile @@ -166,7 +188,7 @@ class QaEngineer(Role): for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself - if msg.cause_by in [WriteCode, WriteCodeReview]: + if msg.cause_by in [WriteCode, WriteCodeReview, WriteCodeRefine]: # engineer wrote a code, time to write a test for it await self._write_test(msg) elif msg.cause_by in [WriteTest, DebugError]: diff --git a/startup.py b/startup.py index 7e8e0a692..8bb8517c9 100644 --- a/startup.py +++ b/startup.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import asyncio import os +import platform import fire @@ -12,29 +13,32 @@ from metagpt.roles import ( ProjectManager, QaEngineer, ) +from metagpt.schema import Message from metagpt.team import Team +from metagpt.utils.special_tokens import MSG_SEP async def startup( idea: str, difference_description: str = "", - path: str = "", + project_path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = False, run_tests: bool = False, implement: bool = True, increment: bool = False, + bug_fix: bool = False, ): """Run a startup. Be a boss.""" company = Team() - if increment: + if increment or bug_fix: # 读取文件 - prd_path = os.path.join(path, 'docs/prd.md') - design_path = os.path.join(path, 'docs/system_design.md') - api_spec_path = os.path.join(path, 'docs/api_spec_and_tasks.md') - code_path = os.path.join(path, os.path.basename(path)) + prd_path = os.path.join(project_path, 'docs/prd.md') + design_path = os.path.join(project_path, 'docs/system_design.md') + api_spec_and_tasks_path = os.path.join(project_path, 'docs/api_spec_and_tasks.md') + code_path = os.path.join(project_path, os.path.basename(project_path)) with open(prd_path, 'r', encoding='utf-8') as f: legacy_prd = f.read() @@ -42,34 +46,59 @@ async def startup( with open(design_path, 'r', encoding='utf-8') as f: legacy_design = f.read() - with open(api_spec_path, 'r', encoding='utf-8') as f: - legacy_api_spec = f.read() + with open(api_spec_and_tasks_path, 'r', encoding='utf-8') as f: + legacy_api_spec_and_tasks = f.read() # 遍历文件夹,获取所有代码文件 legacy_code = '' for root, dirs, files in os.walk(code_path): filenames = [filename for filename in files if filename.endswith('.py')] - legacy_code += f'There are {len(files)} scripts in the current folder: {", ".join(filenames)}\n\n' + legacy_code += f'There are {len(files)} scripts in the current folder: {", ".join(filenames)}\n---\n' for file in files: if file.endswith('.py'): with open(os.path.join(root, file), 'r', encoding='utf-8') as f: - legacy_code += f.read() + '\n\n' + legacy_code += f.read() + '\n---\n' - company.hire( - [ProductManager(difference_description=difference_description, legacy=legacy_prd, increment=increment), - Architect(legacy=legacy_design, increment=increment), - ProjectManager(legacy=legacy_api_spec, increment=increment)]) + if bug_fix: + boss_msg = Message( + content=f"Boss's requirement\n:{idea}\n---\nBoss's difference description:{difference_description}\n---\n", + sent_from="Boss", + ) + product_manager_msg = Message( + content=f"Product Manager's prd legacy:\n{legacy_prd}\n---\n", + sent_from="ProductManager" + ) + architect_msg = Message( + content=f"Architect's design legacy:\n{legacy_design}\n---\n", + sent_from="Architect" + ) + project_manager_msg = Message( + content=f"Project Manager's api spec and tasks legacy:\n{legacy_api_spec_and_tasks}\n---\n", + sent_from="ProjectManager" + ) + engineer_msg = Message( + content=f"Engineer's code legacy:\n{legacy_code}\n---\n", + sent_from="Engineer" + ) + bug_msgs = [boss_msg, product_manager_msg, architect_msg, project_manager_msg, engineer_msg] + else: + company.hire( + [ProductManager(difference_description=difference_description, legacy=legacy_prd, increment=increment), + Architect(legacy=legacy_design, increment=increment), + ProjectManager(legacy=legacy_api_spec_and_tasks, increment=increment)]) else: company.hire([ProductManager(), Architect(), ProjectManager()]) # if implement or code_review - if (implement or code_review) and not increment: + if bug_fix: + company.hire([Engineer(n_borg=5, bug_msgs=bug_msgs, bug_fix=bug_fix)]) + elif implement or code_review: # developing features: implement the idea company.hire([Engineer(n_borg=5, use_code_review=code_review)]) elif (implement or code_review) and increment: company.hire([Engineer(n_borg=5, use_code_review=code_review, legacy=legacy_code, increment=increment)]) - if run_tests: + if run_tests or bug_fix: # developing features: run tests on the spot and identify bugs # (bug fixing capability comes soon!) company.hire([QaEngineer()]) @@ -82,13 +111,14 @@ async def startup( def main( idea: str, difference_description: str = "", - path: str = "", + project_path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = True, run_tests: bool = False, implement: bool = True, increment: bool = False, + bug_fix: bool = False, ): """ We are a software startup comprised of AI. By investing in us, @@ -100,8 +130,10 @@ def main( :param code_review: Whether to use code review. :return: """ + + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) asyncio.run( - startup(idea, difference_description, path, investment, n_round, code_review, run_tests, implement, increment)) + startup(idea, difference_description, project_path, investment, n_round, code_review, run_tests, implement, increment, bug_fix)) if __name__ == "__main__": From a50a1f738fba5992e3ef3cfbe917a81a6675576d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 29 Nov 2023 17:31:15 +0800 Subject: [PATCH 004/101] update increment development, add write_code_guide.py file --- metagpt/actions/refine_design_api.py | 73 +++---- metagpt/actions/refine_prd.py | 192 ++----------------- metagpt/actions/refine_project_management.py | 118 +++++------- metagpt/actions/write_code_guide.py | 67 +++++++ metagpt/actions/write_code_refine.py | 61 +++--- metagpt/environment.py | 13 ++ metagpt/roles/architect.py | 5 +- metagpt/roles/engineer.py | 122 ++++++------ metagpt/roles/product_manager.py | 7 +- metagpt/roles/project_manager.py | 9 +- metagpt/team.py | 6 + startup.py | 66 +++---- 12 files changed, 290 insertions(+), 449 deletions(-) create mode 100644 metagpt/actions/write_code_guide.py diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index f1c231525..909fa6db9 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -3,7 +3,7 @@ import re import shutil from pathlib import Path -from typing import List +from typing import List, Union from metagpt.actions import Action, ActionOutput from metagpt.config import CONFIG @@ -26,34 +26,29 @@ templates = { ## 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 the provided difference descriptions. 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 +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. -## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design +## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. -## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. ## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores -## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here +## 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. -## Data structures and interface definitions: 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. -## Program call flow: 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. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. - -output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, -and only output the json inside this tag, nothing else +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] { "Difference Description": ["The ..."], - "Implementation approach": "We will ...", - "Python package name": "snake_game", - "File list": ["main.py"], + "Incremental implementation approach": "We will ...", + "Python package name": "new_name", "Data structures and interface definitions": ' classDiagram class Game{ @@ -67,8 +62,7 @@ and only output the json inside this tag, nothing else participant M as Main ... G->>M: end game - ', - "Anything UNCLEAR": "The requirement is clear to me." + ' } [/CONTENT] """, @@ -84,24 +78,20 @@ and only output the json inside this tag, nothing else ## 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 the provided difference descriptions. 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 +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. -## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design +## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. -## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. ## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores -## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here +## 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. -## Data structures and interface definitions: 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 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. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. +## 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": """ @@ -113,19 +103,12 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ] ``` -## Implementation approach +## Incremental implementation approach We will ... ## Python package name ```python -"snake_game" -``` - -## File list -```python -[ - "main.py", -] +"new_name" ``` ## Data structures and interface definitions @@ -145,22 +128,19 @@ sequenceDiagram ... G->>M: end game ``` - -## Anything UNCLEAR -The requirement is clear to me. --- """, }, } OUTPUT_MAPPING = { - "Difference Description": (List[str], ...), - "Implementation approach": (str, ...), + # "Incremental Requirements": (str, ...), + "Difference Description": (Union[List[str], str], ...), + "Incremental implementation approach": (str, ...), "Python package name": (str, ...), - "File list": (List[str], ...), + # "File list": (List[str], ...), "Data structures and interface definitions": (str, ...), - "Program call flow": (str, ...), - "Anything UNCLEAR": (str, ...), + "Program call flow": (str, ...) } @@ -201,9 +181,6 @@ class RefineDesign(Action): async def _save_prd(self, docs_path, resources_path, context): prd_file = docs_path / "prd.md" - if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: - quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] - await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") if context[-1].instruct_content: logger.info(f"Saving PRD to {prd_file}") diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 81cb02af8..0f01b9904 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from metagpt.actions import Refine, ActionOutput, SearchAndSummarize from metagpt.config import CONFIG @@ -9,96 +9,36 @@ increment_template = { "json": { "PROMPT_TEMPLATE": """ # Context -## User's New Requirements +## User's Incremental Requirements {new_requirements} -## Difference Description -{difference_description} - ## Legacy PRD {legacy} ## Search Information {search_information} -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "Campaign D": [0.78, 0.34] - "Campaign E": [0.40, 0.34] - "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6] -``` - ## 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 +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 designOnly output one json, nothing else. -## New Requirements: Provide as Plain text and place the new requirements here +## Incremental Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. -## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. ## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less - -## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. - -## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. -## Anything UNCLEAR: Provide as Plain text. Make clear here. - 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] { - "New Requirements": "", + "Incremental Requirements": "", "Difference Description": [], - "Search Information": "", - "Requirements": "", "Incremental Development Plan": [], - "Product Goals": [], - "User Stories": [], - "Competitive Analysis": [], - "Competitive Quadrant Chart": "quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - Campaign A: [0.3, 0.6] - Campaign B: [0.45, 0.23] - Campaign C: [0.57, 0.69] - Campaign D: [0.78, 0.34] - Campaign E: [0.40, 0.34] - Campaign F: [0.35, 0.78]", - "Requirement Analysis": "", - "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], - "UI Design draft": "", - "Anything UNCLEAR": "", } [/CONTENT] """, @@ -106,154 +46,58 @@ and only output the json inside this tag, nothing else "markdown": { "PROMPT_TEMPLATE": """ # Context -You need to refine the requirements based on the new requirements and the existing requirements' output. -## User's New Requirements +You need to refine the requirements based on the Incremental Requirements and the existing requirements' output. +## User's Incremental Requirements {new_requirements} -## Difference Description -{difference_description} - ## Legacy PRD {legacy} ## Search Information {search_information} -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "Campaign D": [0.78, 0.34] - "Campaign E": [0.40, 0.34] - "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6] -``` - ## 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. +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. -## New Requirements: Provide as Plain text and place the new requirements here +## Incremental Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. -## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. ## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple - -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less - -## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. - -## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. -## Anything UNCLEAR: Provide as Plain text. Make clear here. """, "FORMAT_EXAMPLE": """ --- -## New Requirements +## Incremental Requirements The boss ... ## Difference Description -```python -[ - "...", -] +... ## Incremental Development Plan [ - "It ...", + "...", ] - -## Product Goals -```python -[ - "Create a ...", -] -``` - -## User Stories -```python -[ - "As a user, ...", -] -``` - -## Competitive Analysis -```python -[ - "Python Snake Game: ...", -] -``` - -## Competitive Quadrant Chart -```mermaid -quadrantChart - title Reach and engagement of campaigns - ... - "Our Target Product": [0.6, 0.7] -``` - -## Requirement Analysis -The product should be a ... - -## Requirement Pool -```python -[ - ["End game ...", "P0"] -] -``` - -## UI Design draft -Give a basic function description, and a draft - -## Anything UNCLEAR -There are no unclear points. ---- """, }, } INCREMENT_OUTPUT_MAPPING = { - "New Requirements": (str, ...), - # "Major Enhancements": (List[str], ...), - "Difference Description": (List[str], ...), + "Incremental Requirements": (str, ...), + "Difference Description": (Union[List[str], str], ...), "Incremental Development Plan": (List[str], ...), - "Product Goals": (List[str], ...), - "User Stories": (List[str], ...), - "Competitive Analysis": (List[str], ...), - "Competitive Quadrant Chart": (str, ...), - "Requirement Analysis": (str, ...), - "Requirement Pool": (List[List[str]], ...), - "UI Design draft": (str, ...), - "Anything UNCLEAR": (str, ...), } -# 对于产品经理,增量开发的动作是:RefinePDR,输出是结合新需求和已有需求输出的新的PDR class RefinePRD(Refine): def __init__(self, name="RefinePRD", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, new_requirements, difference_description, legacy, format=CONFIG.prompt_format, *args, **kwargs): + async def run(self, new_requirements, legacy, format=CONFIG.prompt_format, *args, **kwargs): sas = SearchAndSummarize() rsp = "" info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" @@ -263,7 +107,7 @@ class RefinePRD(Refine): prompt_template, format_example = get_template(increment_template, format) prompt = prompt_template.format( - new_requirements=new_requirements, difference_description=difference_description, legacy=legacy, search_information=info, + new_requirements=new_requirements, legacy=legacy, search_information=info, format_example=format_example ) logger.debug(prompt) diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index 7d9e118d8..dfeeb2db0 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from typing import List +from typing import List, Union from metagpt.actions.action import Action from metagpt.config import CONFIG @@ -15,62 +15,50 @@ templates = { # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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. -## Required Python third-party packages: Provided in requirements.txt format +## Difference Description: Provide as a python list, the foremost differences description for project management here based on the previous. -## Required Other language third-party packages: Provided in requirements.txt format +## Incremental 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. +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. +## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. -## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first - -## 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 - -## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. +## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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": [ + "Incremental Requirements": "...", + "Difference Description": [ + "...", + ] + "Incremental Required Python third-party packages": [ "flask==1.1.2", "bcrypt==3.2.0" ], - "Required Other language third-party packages": [ - "No third-party ..." - ], "Full API spec": """ openapi: 3.0.0 ... description: A JSON object ... """, - "Difference Description": """ - The ... - """, "Logic Analysis": [ ["game.py","Contains..."] ], "Task list": [ "game.py" - ], - "Shared Knowledge": """ - 'game.py' contains ... - """, - "Anything UNCLEAR": "We need ... how to start." + ] } ''', }, @@ -79,48 +67,44 @@ and only output the json inside this tag, nothing else # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +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 in requirements.txt format +## Difference Description: Provided as a python list, the foremost differences description for project management here based on the previous. -## Required Other language third-party packages: Provided in requirements.txt format +## Incremental 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. +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - -## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first - -## 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 - -## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. +## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. +## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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 +## Incremental Requirements +... + +## Difference Description ```python -""" -flask==1.1.2 -bcrypt==3.2.0 -""" +[ + "The ...", +] ``` -## Required Other language third-party packages +## Incremental Required Python third-party packages ```python -""" -No third-party ... -""" +[ + "flask==1.1.2", + "bcrypt==3.2.0" +] ``` ## Full API spec @@ -132,13 +116,6 @@ description: A JSON object ... """ ``` -## Difference Description -```python -""" -The ... -""" -``` - ## Logic Analysis ```python [ @@ -152,29 +129,18 @@ The ... "game.py", ] ``` - -## Shared Knowledge -```python -""" -'game.py' contains ... -""" -``` - -## Anything UNCLEAR -We need ... how to start. --- ''', }, } OUTPUT_MAPPING = { - "Required Python third-party packages": (List[str], ...), - "Required Other language third-party packages": (List[str], ...), + # "Incremental Requirements": (str, ...), + # ## Incremental Requirements: Provided as a str, the foremost incremental requirements for project management here based on the previous. + "Difference Description": (Union[List[str], str], ...), + "Incremental Required Python third-party packages": (Union[List[str], str], ...), "Full API spec": (str, ...), - "Difference Description": (str, ...), "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), - "Shared Knowledge": (str, ...), - "Anything UNCLEAR": (str, ...), } @@ -192,11 +158,13 @@ class RefineTasks(Action): # 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"))) + requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Incremental 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) + 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 diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py new file mode 100644 index 000000000..391173bb3 --- /dev/null +++ b/metagpt/actions/write_code_guide.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 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). +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". + +## Regulations Review: To make the software directly operable without further coding, follow the regulations below during incremental development: +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: Proposed the Minimum essential incremental development plan, based on the following context and legacy code by thinking step by step. +... + +## Code guidelines: 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. +```python +... +''' +----- +# Context +{context} + +## Legacy Code +You are tasked with conducting incremental development in the existing code based on the provided legacy code and above information. +``` +{code} +``` +----- + +## Format example +----- +## Incremental Development Guide: +... + +## Code Guidance: +# Implementation the ... +```python +... +''' +----- +""" + + +class WriteCodeGuide(Action): + def __init__(self, name="WriteCodeGuide", context: list[Message] = None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context, code): + prompt = PROMPT_TEMPLATE.format(context=context, code=code) + logger.info(f'Write Code Guide ..') + code_guide = await self._aask(prompt) + # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) + # self._save(context, filename, code) + 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 index 527952aed..466c30679 100644 --- a/metagpt/actions/write_code_refine.py +++ b/metagpt/actions/write_code_refine.py @@ -7,58 +7,44 @@ 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, which includes reviewing existing code, providing modification suggestions, rewriting code, and optimizing the codebase. 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). -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". +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: You should modify the corresponding code based on the guidance. Then, 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. -## Code Review: Based on the following context and legacy code, and following the checklist, provide key, clear, concise, and specific code modification suggestions, up to 5. -``` -1. Check 0: Is the code implemented as per the requirements? -2. Check 1: Are there any issues with the code logic? -3. Check 2: Does the existing code follow the "Data structures and interface definitions"? -4. Check 3: Is there a function in the code that is omitted or not fully implemented that needs to be implemented? -5. Check 4: Does the code have unnecessary or lack dependencies? -``` - -## Incremental Development: {filename} Based on the findings from the "Code Review," context, and the legacy code, conduct incremental development by rewriting, optimizing, and adding new code using triple quotes. +## Code: Only Write {filename}, Write code using triple quotes, based on the following list and context. +1. Do your best to implement THIS ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT. +2. Requirement: 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: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. +4. Attention2: YOU MUST FOLLOW "Data structures and interface definitions". DONT CHANGE ANY DESIGN. +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. ----- # Context {context} -## Legacy Code -You are tasked with conducting incremental development in the existing code and creating a new code file, {filename}, based on the provided legacy code and above information. -``` -{code} -``` ----- +## Guidelines: The foremost guidelines of modification for incremental development. +{guide} +----- +## Legacy Code: The Legacy Code that needs to be modified. +{legacy} + +----- ## Format example ----- -{format_example} ------ - -""" - -FORMAT_EXAMPLE = """ - -## Code Review -1. The code ... -2. ... -3. ... -4. ... -5. ... - -## Incremental Development: {filename} +## Modified/Added Code: {filename} ```python -## {filename} - Incremental Development +# {filename} ... ``` +----- """ - class WriteCodeRefine(Action): def __init__(self, name="WriteCodeRefine", context: list[Message] = None, llm=None): super().__init__(name, context, llm) @@ -69,9 +55,8 @@ class WriteCodeRefine(Action): code = CodeParser.parse_code(block="", text=code_rsp) return code - async def run(self, context, code, filename): - format_example = FORMAT_EXAMPLE.format(filename=filename) - prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example) + async def run(self, context, code, filename, guide): + prompt = PROMPT_TEMPLATE.format(context=context, legacy=code, 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) diff --git a/metagpt/environment.py b/metagpt/environment.py index 24e6ada2f..4e409b921 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -24,6 +24,7 @@ class Environment(BaseModel): roles: dict[str, Role] = Field(default_factory=dict) memory: Memory = Field(default_factory=Memory) history: str = Field(default='') + legacy: dict = Field(default_factory=dict) class Config: arbitrary_types_allowed = True @@ -77,3 +78,15 @@ class Environment(BaseModel): get all the environment roles """ return self.roles.get(name, None) + + def set_legacy(self, legacy_dict: dict) -> None: + """设置环境的遗产 + set the environment legacy + """ + self.legacy = legacy_dict + + def get_legacy(self) -> dict: + """获得环境的遗产 + get the environment legacy + """ + return self.legacy diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index dddcd2a8b..f87f8899d 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -32,12 +32,10 @@ class Architect(Role): profile: str = "Architect", goal: str = "Design a concise, usable, complete python system", constraints: str = "Try to specify good open source tools as much as possible", - legacy: str = "", increment: bool = False, ) -> None: """Initializes the Architect with given attributes.""" super().__init__(name, profile, goal, constraints) - self.legacy = legacy self.increment = increment # Initialize actions specific to the Architect role @@ -52,7 +50,8 @@ class Architect(Role): async def _act(self) -> Message: if self.increment: logger.info(f"{self._setting}: ready to RefineDesign") - response = await self._rc.todo.run(self._rc.history, self.legacy) + legacy = self._rc.env.get_legacy()["legacy_design"] + response = await self._rc.todo.run(self._rc.history, legacy) else: logger.info(f"{self._setting}: ready to WriteDesign") diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 13034acaa..6f96aeb78 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -14,8 +14,10 @@ from typing import List from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks, BossRequirement from metagpt.actions.refine_design_api import RefineDesign +from metagpt.actions.refine_prd import RefinePRD from metagpt.actions.refine_project_management import RefineTasks from metagpt.actions.write_code_refine import WriteCodeRefine +from metagpt.actions.write_code_guide import WriteCodeGuide from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -73,31 +75,25 @@ class Engineer(Role): constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable", n_borg: int = 1, use_code_review: bool = False, - legacy: str = "", increment: bool = False, - bug_msgs: List = None, bug_fix: bool = False, ) -> None: """Initializes the Engineer role with given attributes.""" super().__init__(name, profile, goal, constraints) self._init_actions([WriteCode]) self.use_code_review = use_code_review - self.legacy = legacy self.increment = increment - self.bug_msgs = bug_msgs self.bug_fix = bug_fix - if self.use_code_review or self.increment: - self._init_actions([WriteCode, WriteCodeReview, WriteCodeRefine]) + if self.use_code_review: + self._init_actions([WriteCode, WriteCodeReview]) if self.increment: + self._init_actions([WriteCodeGuide, WriteCodeRefine, WriteCodeReview]) self._watch([RefineTasks]) - self.todos = [] - elif self.bug_fix: - self._watch([BossRequirement]) - self.todos = [] else: self._watch([WriteTasks]) - self.todos = [] + + self.todos = [] self.n_borg = n_borg @classmethod @@ -210,36 +206,50 @@ class Engineer(Role): ) return msg - async def _act_increment(self, legacy) -> Message: + async def _act_increment(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later workspace = self.get_workspace() - flag = True - # legacy_codes = legacy.split('---') + # human_str = "\n".join([msg.content for msg in self._rc.memory.get_by_role("Human")]) + human_str = str(self._rc.memory.get_by_role("Human")[0]) + code = self._rc.env.get_legacy()["legacy_code"] + + # Refine code + context = [] + msg = self._rc.memory.get_by_actions([RefinePRD, RefineDesign, RefineTasks]) + + for m in msg: + context.append(m.content) + context_str = human_str + "\n".join(context) + try: + logger.info("Write Code Guide start!") + guide = await WriteCodeGuide().run(context=context_str, code=code) + msg = Message(content=guide, role=self.profile, cause_by=WriteCodeGuide) + self._rc.memory.add(msg) + except Exception as e: + logger.error("Write Code Guide failed!", e) + pass + + # Write code or Code review for todo in self.todos: - context = [] - - msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks, WriteCodeRefine]) - for m in msg: - context.append(m.content) - context_str = "\n".join(context) - code = legacy - - # Refine code or Write code - # if self.increment and len(legacy_codes) > 0: - # code = legacy_codes.pop(0) - - # Code review + msg = self._rc.memory.get_by_actions([RefineTasks]) + context_str = human_str + "\n".join([m.content for m in msg]) + # WriteCodeRefine try: - rewrite_code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo) - code = rewrite_code + logger.info("Write Code Refine start!") + code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo, guide=guide) except Exception as e: - logger.error("code review failed!", e) + logger.error("Write Code Refine failed!", e) pass - - # code = await WriteCode().run(context=context_str, filename=todo) - + # FIXME: Code review Action + # if self.use_code_review: + # try: + # rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo) + # code = rewrite_code + # except Exception as e: + # logger.error("code review failed!", e) + # pass file_path = self.write_file(workspace, todo, code) - msg = Message(content=code, role=self.profile, cause_by=WriteCode) + msg = Message(content=code, role=self.profile, cause_by=WriteCodeRefine) self._rc.memory.add(msg) code_msg = todo + FILENAME_CODE_SEP + str(file_path) @@ -273,7 +283,7 @@ class Engineer(Role): # Code review try: - rewrite_code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo) + rewrite_code = await WriteCodeGuide().run(context=context_str, code=code, filename=todo) code = rewrite_code except Exception as e: logger.error("code review failed!", e) @@ -333,35 +343,31 @@ class Engineer(Role): ) return msg - async def _observe(self) -> int: - if self.bug_fix: - msg = Message( - content=self.bug_msgs[0].content + "\n---\n" + self.legacy, - role=self.profile, - cause_by=BossRequirement, - sent_from=self.profile, - send_to=self.profile, - ) - self._publish_message(msg) - await super()._observe() - self._rc.news = [ - msg for msg in self._rc.news if msg.send_to == self.profile - ] # only relevant msgs count as observed news - return len(self._rc.news) + # async def _observe(self) -> int: + # if self.bug_fix: + # msg = Message( + # content=self.bug_msgs[0].content + "\n---\n" + self.legacy, + # role=self.profile, + # cause_by=BossRequirement, + # sent_from=self.profile, + # send_to=self.profile, + # ) + # self._publish_message(msg) + # await super()._observe() + # self._rc.news = [ + # msg for msg in self._rc.news if msg.send_to == self.profile + # ] # only relevant msgs count as observed news + # return len(self._rc.news) async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" if self.increment: - logger.info(f"{self._setting}: ready to RefineWriteCode") - elif self.bug_fix: - logger.info(f"{self._setting}: ready to BugFix") + logger.info(f"{self._setting}: ready to WriteExtraCode and WriteCodeRefine") else: logger.info(f"{self._setting}: ready to WriteCode") - if self.use_code_review: + if self.increment: + return await self._act_increment() + elif self.use_code_review: return await self._act_sp_precision() - elif self.increment: - return await self._act_increment(self.legacy) - elif self.bug_fix: - return await self._act_bug_fix(self.bug_msgs) return await self._act_sp() diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index 0b2d83ed0..f48720430 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -29,8 +29,6 @@ class ProductManager(Role): profile: str = "Product Manager", goal: str = "Efficiently create a successful product", constraints: str = "", - difference_description: str = "", - legacy: str = "", increment: bool = False, ) -> None: """ @@ -43,8 +41,6 @@ class ProductManager(Role): constraints (str): Constraints or limitations for the product manager. """ super().__init__(name, profile, goal, constraints) - self.difference_description = difference_description - self.legacy = legacy self.increment = increment if self.increment: @@ -56,7 +52,8 @@ class ProductManager(Role): async def _act(self) -> Message: if self.increment: logger.info(f"{self._setting}: ready to RefinePRD") - response = await self._rc.todo.run(self._rc.history, self.difference_description, self.legacy) + legacy = self._rc.env.get_legacy()["legacy_prd"] + response = await self._rc.todo.run(self._rc.history, legacy) else: logger.info(f"{self._setting}: ready to WritePRD") diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 21a196d21..5d4820f4a 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -32,7 +32,6 @@ class ProjectManager(Role): goal: str = "Improve team efficiency and deliver with quality and quantity", constraints: str = "", increment: bool = False, - legacy: str = "", ) -> None: """ Initializes the ProjectManager role with given attributes. @@ -45,8 +44,6 @@ class ProjectManager(Role): """ super().__init__(name, profile, goal, constraints) self.increment = increment - self.legacy = legacy - if self.increment: self._init_actions([RefineTasks]) self._watch([RefineDesign]) @@ -57,7 +54,11 @@ class ProjectManager(Role): async def _act(self) -> Message: if self.increment: logger.info(f"{self._setting}: ready to RefineTasks") - response = await self._rc.todo.run(self._rc.history, self.legacy) + human_str = "\n".join([msg.content for msg in self._rc.memory.get_by_role("Human")]) + # legacy_project_management and legacy_code + legacy_dict = self._rc.env.get_legacy() + legacy_str = "Legacy Project Management:\n" + legacy_dict["legacy_project_management"] + "\nLegacy Code:\n" + legacy_dict["legacy_code"] + response = await self._rc.todo.run(self._rc.history, legacy=legacy_str) else: logger.info(f"{self._setting}: ready to WriteTasks") diff --git a/metagpt/team.py b/metagpt/team.py index 67d3ecec8..67f1973b5 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -59,4 +59,10 @@ class Team(BaseModel): self._check_balance() await self.environment.run() return self.environment.history + + def set_legacy(self, legacy_dict): + self.environment.legacy = legacy_dict + + def get_legacy(self): + return self.environment.legacy \ No newline at end of file diff --git a/startup.py b/startup.py index 8bb8517c9..647fc307e 100644 --- a/startup.py +++ b/startup.py @@ -28,12 +28,11 @@ async def startup( run_tests: bool = False, implement: bool = True, increment: bool = False, - bug_fix: bool = False, ): """Run a startup. Be a boss.""" company = Team() - if increment or bug_fix: + if increment: # 读取文件 prd_path = os.path.join(project_path, 'docs/prd.md') design_path = os.path.join(project_path, 'docs/system_design.md') @@ -47,64 +46,46 @@ async def startup( legacy_design = f.read() with open(api_spec_and_tasks_path, 'r', encoding='utf-8') as f: - legacy_api_spec_and_tasks = f.read() + legacy_project_management = f.read() - # 遍历文件夹,获取所有代码文件 legacy_code = '' for root, dirs, files in os.walk(code_path): - filenames = [filename for filename in files if filename.endswith('.py')] - legacy_code += f'There are {len(files)} scripts in the current folder: {", ".join(filenames)}\n---\n' for file in files: if file.endswith('.py'): with open(os.path.join(root, file), 'r', encoding='utf-8') as f: legacy_code += f.read() + '\n---\n' - if bug_fix: - boss_msg = Message( - content=f"Boss's requirement\n:{idea}\n---\nBoss's difference description:{difference_description}\n---\n", - sent_from="Boss", - ) - product_manager_msg = Message( - content=f"Product Manager's prd legacy:\n{legacy_prd}\n---\n", - sent_from="ProductManager" - ) - architect_msg = Message( - content=f"Architect's design legacy:\n{legacy_design}\n---\n", - sent_from="Architect" - ) - project_manager_msg = Message( - content=f"Project Manager's api spec and tasks legacy:\n{legacy_api_spec_and_tasks}\n---\n", - sent_from="ProjectManager" - ) - engineer_msg = Message( - content=f"Engineer's code legacy:\n{legacy_code}\n---\n", - sent_from="Engineer" - ) - bug_msgs = [boss_msg, product_manager_msg, architect_msg, project_manager_msg, engineer_msg] - else: - company.hire( - [ProductManager(difference_description=difference_description, legacy=legacy_prd, increment=increment), - Architect(legacy=legacy_design, increment=increment), - ProjectManager(legacy=legacy_api_spec_and_tasks, increment=increment)]) + legacy_dict = { + 'legacy_prd': legacy_prd, + 'legacy_design': legacy_design, + 'legacy_project_management': legacy_project_management, + 'legacy_code': legacy_code + } + company.set_legacy(legacy_dict) + company.hire( + [ + ProductManager(increment=increment), + Architect(increment=increment), + ProjectManager(increment=increment) + ] + ) else: company.hire([ProductManager(), Architect(), ProjectManager()]) # if implement or code_review - if bug_fix: - company.hire([Engineer(n_borg=5, bug_msgs=bug_msgs, bug_fix=bug_fix)]) - elif implement or code_review: + if (implement or code_review) and increment: + company.hire([Engineer(n_borg=5, use_code_review=code_review, increment=increment)]) + elif implement: # developing features: implement the idea company.hire([Engineer(n_borg=5, use_code_review=code_review)]) - elif (implement or code_review) and increment: - company.hire([Engineer(n_borg=5, use_code_review=code_review, legacy=legacy_code, increment=increment)]) - if run_tests or bug_fix: + if run_tests: # developing features: run tests on the spot and identify bugs # (bug fixing capability comes soon!) company.hire([QaEngineer()]) company.invest(investment) - company.start_project(idea) + company.start_project(idea+"\n"+difference_description) await company.run(n_round=n_round) @@ -118,7 +99,6 @@ def main( run_tests: bool = False, implement: bool = True, increment: bool = False, - bug_fix: bool = False, ): """ We are a software startup comprised of AI. By investing in us, @@ -130,10 +110,8 @@ def main( :param code_review: Whether to use code review. :return: """ - - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) asyncio.run( - startup(idea, difference_description, project_path, investment, n_round, code_review, run_tests, implement, increment, bug_fix)) + startup(idea, difference_description, project_path, investment, n_round, code_review, run_tests, implement, increment)) if __name__ == "__main__": From 452cdb7ff6d1fa6a855820530c51e71f481eea30 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 29 Nov 2023 17:56:30 +0800 Subject: [PATCH 005/101] update increment development, add write_code_guide.py file --- metagpt/roles/engineer.py | 19 +------------------ metagpt/team.py | 6 ------ startup.py | 2 +- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 6f96aeb78..66ce12f83 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -209,13 +209,12 @@ class Engineer(Role): async def _act_increment(self) -> Message: code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later workspace = self.get_workspace() - # human_str = "\n".join([msg.content for msg in self._rc.memory.get_by_role("Human")]) human_str = str(self._rc.memory.get_by_role("Human")[0]) code = self._rc.env.get_legacy()["legacy_code"] # Refine code context = [] - msg = self._rc.memory.get_by_actions([RefinePRD, RefineDesign, RefineTasks]) + msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks]) for m in msg: context.append(m.content) @@ -343,22 +342,6 @@ class Engineer(Role): ) return msg - # async def _observe(self) -> int: - # if self.bug_fix: - # msg = Message( - # content=self.bug_msgs[0].content + "\n---\n" + self.legacy, - # role=self.profile, - # cause_by=BossRequirement, - # sent_from=self.profile, - # send_to=self.profile, - # ) - # self._publish_message(msg) - # await super()._observe() - # self._rc.news = [ - # msg for msg in self._rc.news if msg.send_to == self.profile - # ] # only relevant msgs count as observed news - # return len(self._rc.news) - async def _act(self) -> Message: """Determines the mode of action based on whether code review is used.""" if self.increment: diff --git a/metagpt/team.py b/metagpt/team.py index 67f1973b5..ea5de4a12 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -60,9 +60,3 @@ class Team(BaseModel): await self.environment.run() return self.environment.history - def set_legacy(self, legacy_dict): - self.environment.legacy = legacy_dict - - def get_legacy(self): - return self.environment.legacy - \ No newline at end of file diff --git a/startup.py b/startup.py index 647fc307e..60298ed36 100644 --- a/startup.py +++ b/startup.py @@ -61,7 +61,7 @@ async def startup( 'legacy_project_management': legacy_project_management, 'legacy_code': legacy_code } - company.set_legacy(legacy_dict) + company.environment.set_legacy(legacy_dict) company.hire( [ ProductManager(increment=increment), From acb70fa30c19d068f39f3a295140436bcad7ee82 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 4 Dec 2023 17:14:52 +0800 Subject: [PATCH 006/101] update increment development --- metagpt/actions/refine_design_api.py | 31 +++----- metagpt/actions/refine_prd.py | 46 ++++------- metagpt/actions/refine_project_management.py | 58 ++++---------- metagpt/actions/write_code_guide.py | 45 ++++++----- metagpt/actions/write_code_refine.py | 22 +++--- metagpt/roles/architect.py | 6 +- metagpt/roles/engineer.py | 82 +++++++------------- metagpt/roles/product_manager.py | 4 +- metagpt/roles/project_manager.py | 5 +- metagpt/team.py | 30 +++++++ startup.py | 42 ++-------- 11 files changed, 153 insertions(+), 218 deletions(-) diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index 909fa6db9..d6a948b43 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -20,7 +20,7 @@ templates = { # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example @@ -30,9 +30,7 @@ Role: You are an architect; the goal is to perform incremental development and d 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. -## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. - -## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## 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 @@ -46,8 +44,7 @@ Output exactly as shown in the example, including single and double quotes, and "FORMAT_EXAMPLE": """ [CONTENT] { - "Difference Description": ["The ..."], - "Incremental implementation approach": "We will ...", + "Incremental implementation approach": ["We will ...",], "Python package name": "new_name", "Data structures and interface definitions": ' classDiagram @@ -72,7 +69,7 @@ Output exactly as shown in the example, including single and double quotes, and # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example @@ -83,9 +80,7 @@ Requirement: Fill in the following missing information based on the context, not 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. -## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. - -## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## 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 @@ -96,15 +91,13 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W """, "FORMAT_EXAMPLE": """ --- -## Difference Description -```python -[ - "The ...", -] -``` ## Incremental implementation approach -We will ... +```python +[ + "We will ...", +] +``` ## Python package name ```python @@ -135,8 +128,8 @@ sequenceDiagram OUTPUT_MAPPING = { # "Incremental Requirements": (str, ...), - "Difference Description": (Union[List[str], str], ...), - "Incremental implementation approach": (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, ...), diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 0f01b9904..1d8bab5f8 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -9,10 +9,9 @@ increment_template = { "json": { "PROMPT_TEMPLATE": """ # Context -## User's Incremental Requirements -{new_requirements} +{context} -## Legacy PRD +## Legacy {legacy} ## Search Information @@ -22,13 +21,9 @@ increment_template = { {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 designOnly output one json, nothing else. +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 Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. - -## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. - -## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple +## 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 @@ -36,9 +31,7 @@ and only output the json inside this tag, nothing else "FORMAT_EXAMPLE": """ [CONTENT] { - "Incremental Requirements": "", - "Difference Description": [], - "Incremental Development Plan": [], + "Incremental Development Analysis": [], } [/CONTENT] """, @@ -46,11 +39,9 @@ and only output the json inside this tag, nothing else "markdown": { "PROMPT_TEMPLATE": """ # Context -You need to refine the requirements based on the Incremental Requirements and the existing requirements' output. -## User's Incremental Requirements -{new_requirements} +{context} -## Legacy PRD +## Legacy {legacy} ## Search Information @@ -63,32 +54,21 @@ Role: You are a professional Product Manager tasked with overseeing incremental 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 Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. - -## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. - -## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple +## 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 Requirements -The boss ... -## Difference Description -... - -## Incremental Development Plan +## Incremental Development Analysis [ - "...", + "We will ...", ] """, }, } INCREMENT_OUTPUT_MAPPING = { - "Incremental Requirements": (str, ...), - "Difference Description": (Union[List[str], str], ...), - "Incremental Development Plan": (List[str], ...), + "Incremental Development Analysis": (List[str], ...), } @@ -97,7 +77,7 @@ class RefinePRD(Refine): def __init__(self, name="RefinePRD", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, new_requirements, legacy, format=CONFIG.prompt_format, *args, **kwargs): + 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}" @@ -107,7 +87,7 @@ class RefinePRD(Refine): prompt_template, format_example = get_template(increment_template, format) prompt = prompt_template.format( - new_requirements=new_requirements, legacy=legacy, search_information=info, + context=context, legacy=legacy, search_information=info, format_example=format_example ) logger.debug(prompt) diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index dfeeb2db0..2775b1cb6 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -21,30 +21,23 @@ templates = { ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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. +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. -## Difference Description: Provide as a python list, the foremost differences description for project management here based on the previous. - -## Incremental Required Python third-party packages: Provided as a python list, the requirements.txt format +## 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. -## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first 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. -## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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, +Output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else """, "FORMAT_EXAMPLE": ''' { - "Incremental Requirements": "...", - "Difference Description": [ - "...", - ] - "Incremental Required Python third-party packages": [ + "Required Python third-party packages": [ "flask==1.1.2", "bcrypt==3.2.0" ], @@ -53,9 +46,6 @@ and only output the json inside this tag, nothing else ... description: A JSON object ... """, - "Logic Analysis": [ - ["game.py","Contains..."] - ], "Task list": [ "game.py" ] @@ -77,29 +67,16 @@ Role: You are a project manager; the goal is to perform incremental development 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. -## Difference Description: Provided as a python list, the foremost differences description for project management here based on the previous. - -## Incremental Required Python third-party packages: Provided as a python list, the requirements.txt format +## 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. -## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. - -## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first +## 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": ''' --- -## Incremental Requirements -... -## Difference Description -```python -[ - "The ...", -] -``` - -## Incremental Required Python third-party packages +## Required Python third-party packages ```python [ "flask==1.1.2", @@ -116,13 +93,6 @@ description: A JSON object ... """ ``` -## Logic Analysis -```python -[ - ["game.py", "Contains ..."], -] -``` - ## Task list ```python [ @@ -136,16 +106,16 @@ description: A JSON object ... OUTPUT_MAPPING = { # "Incremental Requirements": (str, ...), # ## Incremental Requirements: Provided as a str, the foremost incremental requirements for project management here based on the previous. - "Difference Description": (Union[List[str], str], ...), - "Incremental Required Python third-party packages": (Union[List[str], str], ...), + # "Difference Analysis": (Union[List[str], str], ...), + "Required Python third-party packages": (Union[List[str], str], ...), "Full API spec": (str, ...), - "Logic Analysis": (List[List[str]], ...), + # "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), } class RefineTasks(Action): - def __init__(self, name="CreateTasks", context=None, llm=None): + def __init__(self, name="RefineTasks", context=None, llm=None): super().__init__(name, context, llm) def _save(self, context, rsp): @@ -158,7 +128,7 @@ class RefineTasks(Action): # Write requirements.txt requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" - requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Incremental Required Python third-party packages"))) + 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) diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py index 391173bb3..3e51f0d2d 100644 --- a/metagpt/actions/write_code_guide.py +++ b/metagpt/actions/write_code_guide.py @@ -10,24 +10,21 @@ 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). -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". +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. +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: Proposed the Minimum essential incremental development plan, based on the following context and legacy code by thinking step by step. -... +## 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. -## Code guidelines: 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. -```python -... -''' ----- # Context {context} @@ -35,20 +32,31 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ## Legacy Code You are tasked with conducting incremental development in the existing code based on the provided legacy code and above information. ``` -{code} +{legacy} ``` ----- ## Format example ----- -## Incremental Development Guide: -... +## Incremental Development Guide +[ + "`game.py` Contains `Game` and ...", +] -## Code Guidance: -# Implementation the ... + +## 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: + ... +``` ----- """ @@ -57,11 +65,10 @@ class WriteCodeGuide(Action): def __init__(self, name="WriteCodeGuide", context: list[Message] = None, llm=None): super().__init__(name, context, llm) - async def run(self, context, code): - prompt = PROMPT_TEMPLATE.format(context=context, code=code) + 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) - # self._save(context, filename, code) 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 index 466c30679..5a70b2c68 100644 --- a/metagpt/actions/write_code_refine.py +++ b/metagpt/actions/write_code_refine.py @@ -10,17 +10,19 @@ 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: You should modify the corresponding code based on the guidance. Then, 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. +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. -## Code: Only Write {filename}, Write code using triple quotes, based on the following list and context. -1. Do your best to implement THIS ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT. -2. Requirement: 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: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. -4. Attention2: YOU MUST FOLLOW "Data structures and interface definitions". DONT CHANGE ANY DESIGN. +## 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} @@ -30,13 +32,13 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format should be carefull {guide} ----- -## Legacy Code: The Legacy Code that needs to be modified. +## 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 ----- -## Modified/Added Code: {filename} +## Rewrite Complete Code: {filename} ```python # {filename} ... @@ -55,8 +57,8 @@ class WriteCodeRefine(Action): code = CodeParser.parse_code(block="", text=code_rsp) return code - async def run(self, context, code, filename, guide): - prompt = PROMPT_TEMPLATE.format(context=context, legacy=code, filename=filename, guide=guide) + 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) diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index f87f8899d..00ab6de35 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -50,7 +50,11 @@ class Architect(Role): async def _act(self) -> Message: if self.increment: logger.info(f"{self._setting}: ready to RefineDesign") - legacy = self._rc.env.get_legacy()["legacy_design"] + legacy_dict = self._rc.env.get_legacy() + legacy_code = "" + for code_dict in legacy_dict.get("legacy_code"): + legacy_code += code_dict.get("code") + "\n===\n" + legacy = "Legacy Design:\n" + legacy_dict.get("legacy_design") + "\nLegacy Code:\n" + legacy_code response = await self._rc.todo.run(self._rc.history, legacy) else: diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 66ce12f83..de144d4ca 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -16,6 +16,7 @@ from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks, from metagpt.actions.refine_design_api import RefineDesign from metagpt.actions.refine_prd import RefinePRD from metagpt.actions.refine_project_management import RefineTasks +from metagpt.actions.rewrite_code import RewriteCode from metagpt.actions.write_code_refine import WriteCodeRefine from metagpt.actions.write_code_guide import WriteCodeGuide from metagpt.const import WORKSPACE_ROOT @@ -88,7 +89,7 @@ class Engineer(Role): self._init_actions([WriteCode, WriteCodeReview]) if self.increment: - self._init_actions([WriteCodeGuide, WriteCodeRefine, WriteCodeReview]) + self._init_actions([WriteCodeGuide, WriteCodeRefine, RewriteCode]) self._watch([RefineTasks]) else: self._watch([WriteTasks]) @@ -210,18 +211,21 @@ class Engineer(Role): code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later workspace = self.get_workspace() human_str = str(self._rc.memory.get_by_role("Human")[0]) - code = self._rc.env.get_legacy()["legacy_code"] + legacy_code = "" + code_list = self._rc.env.get_legacy().get("legacy_code") + for code_dict in code_list: + legacy_code += code_dict.get("code") + "\n===\n" - # Refine code + # Code guide context = [] - msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks]) - - for m in msg: - context.append(m.content) + # msg = self._rc.memory.get_by_actions([RefineDesign, RefineTasks]) + msg = self._rc.memory.get_by_actions([RefineTasks]) + for tasks_msg in msg: + context.append(tasks_msg.content) context_str = human_str + "\n".join(context) try: logger.info("Write Code Guide start!") - guide = await WriteCodeGuide().run(context=context_str, code=code) + guide = await WriteCodeGuide().run(context=context_str, legacy=legacy_code) msg = Message(content=guide, role=self.profile, cause_by=WriteCodeGuide) self._rc.memory.add(msg) except Exception as e: @@ -230,19 +234,18 @@ class Engineer(Role): # Write code or Code review for todo in self.todos: - msg = self._rc.memory.get_by_actions([RefineTasks]) - context_str = human_str + "\n".join([m.content for m in msg]) # WriteCodeRefine try: logger.info("Write Code Refine start!") - code = await WriteCodeRefine().run(context=context_str, code=code, filename=todo, guide=guide) + code = await WriteCodeRefine().run(context=context_str, legacy=legacy_code, filename=todo, guide=guide) except Exception as e: logger.error("Write Code Refine failed!", e) pass - # FIXME: Code review Action + # FIXME: Code review Action : 如果有 pass 在代码里面,需要重写 # if self.use_code_review: # try: - # rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo) + # # 解析成guides context = guide + human_str + # rewrite_code = await RewriteCode().run(context=human_str, code=code, filename=todo, legacy=legacy_code) # code = rewrite_code # except Exception as e: # logger.error("code review failed!", e) @@ -251,48 +254,17 @@ class Engineer(Role): msg = Message(content=code, role=self.profile, cause_by=WriteCodeRefine) self._rc.memory.add(msg) - code_msg = todo + FILENAME_CODE_SEP + str(file_path) - code_msg_all.append(code_msg) - - logger.info(f"Done {self.get_workspace()} generating.") - msg = Message( - content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" - ) - return msg - - async def _act_bug_fix(self, bug_msgs) -> Message: - code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later - workspace = self.get_workspace() - flag = True - # legacy_codes = legacy.split('---') - for todo in self.todos: - context = [] - - for m in bug_msgs: - if m.sent_from != "Engineer": - context.append(m.content) - context.append(m.content) - context_str = "\n".join(context) - code = [m.content for m in bug_msgs if m.sent_from == "Engineer"] - code = "\n".join(code) - - # Refine code or Write code - # if self.increment and len(legacy_codes) > 0: - # code = legacy_codes.pop(0) - - # Code review - try: - rewrite_code = await WriteCodeGuide().run(context=context_str, code=code, filename=todo) - code = rewrite_code - except Exception as e: - logger.error("code review failed!", e) - pass - - # code = await WriteCode().run(context=context_str, filename=todo) - - file_path = self.write_file(workspace, todo, code) - msg = Message(content=code, role=self.profile, cause_by=WriteCode) - self._rc.memory.add(msg) + # 利用todo作为key,去更新code_list的code + legacy_code = "" + for code_dict in code_list: + if code_dict.get("filename") == todo: + code_dict["code"] = code + legacy_code += code_dict.get("code") + "\n===\n" + # 若code_list中没有todo,则新增一个code_dict + if todo not in [code_dict.get("filename") for code_dict in code_list]: + code_dict = {"filename": todo, "code": code} + code_list.append(code_dict) + print(len(code_list)) code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index f48720430..2dc306297 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -52,7 +52,9 @@ class ProductManager(Role): async def _act(self) -> Message: if self.increment: logger.info(f"{self._setting}: ready to RefinePRD") - legacy = self._rc.env.get_legacy()["legacy_prd"] + legacy_dict = self._rc.env.get_legacy() + # legacy = "Legacy PRD:\n" + legacy_dict.get("legacy_prd") + "\nLegacy Code:\n" + legacy_dict.get("legacy_code") + legacy = legacy_dict.get("legacy_prd") response = await self._rc.todo.run(self._rc.history, legacy) else: diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 5d4820f4a..8cf3e579f 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -57,7 +57,10 @@ class ProjectManager(Role): human_str = "\n".join([msg.content for msg in self._rc.memory.get_by_role("Human")]) # legacy_project_management and legacy_code legacy_dict = self._rc.env.get_legacy() - legacy_str = "Legacy Project Management:\n" + legacy_dict["legacy_project_management"] + "\nLegacy Code:\n" + legacy_dict["legacy_code"] + legacy_code = "" + for code_dict in legacy_dict.get("legacy_code"): + legacy_code += code_dict.get("code") + "\n===\n" + legacy_str = "Legacy Project Management:\n" + legacy_dict["legacy_project_management"] + "\nLegacy Code:\n" + legacy_code response = await self._rc.todo.run(self._rc.history, legacy=legacy_str) else: diff --git a/metagpt/team.py b/metagpt/team.py index ea5de4a12..d71880db9 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -5,6 +5,8 @@ @Author : alexanderwu @File : software_company.py """ +import os + from pydantic import BaseModel, Field from metagpt.actions import BossRequirement @@ -60,3 +62,31 @@ class Team(BaseModel): await self.environment.run() return self.environment.history + def save_legacy(self, project_path): + prd_path = os.path.join(project_path, 'docs/prd.md') + design_path = os.path.join(project_path, 'docs/system_design.md') + api_spec_and_tasks_path = os.path.join(project_path, 'docs/api_spec_and_tasks.md') + code_path = os.path.join(project_path, os.path.basename(project_path)) + with open(prd_path, 'r', encoding='utf-8') as f: + legacy_prd = f.read() + with open(design_path, 'r', encoding='utf-8') as f: + legacy_design = f.read() + with open(api_spec_and_tasks_path, 'r', encoding='utf-8') as f: + legacy_project_management = f.read() + legacy_codes = [] + for root, dirs, files in os.walk(code_path): + for file in files: + legacy_code = {} + if file.endswith('.py'): + with open(os.path.join(root, file), 'r', encoding='utf-8') as f: + legacy_code['filename'] = file + legacy_code['code'] = f.read() + legacy_codes.append(legacy_code) + legacy_dict = { + 'legacy_prd': legacy_prd, + 'legacy_design': legacy_design, + 'legacy_project_management': legacy_project_management, + 'legacy_code': legacy_codes + } + self.environment.set_legacy(legacy_dict) + diff --git a/startup.py b/startup.py index 60298ed36..1ecef65b0 100644 --- a/startup.py +++ b/startup.py @@ -20,48 +20,20 @@ from metagpt.utils.special_tokens import MSG_SEP async def startup( idea: str, - difference_description: str = "", - project_path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = False, run_tests: bool = False, implement: bool = True, increment: bool = False, + path: str = "", + difference: str = "", ): """Run a startup. Be a boss.""" company = Team() if increment: - # 读取文件 - prd_path = os.path.join(project_path, 'docs/prd.md') - design_path = os.path.join(project_path, 'docs/system_design.md') - api_spec_and_tasks_path = os.path.join(project_path, 'docs/api_spec_and_tasks.md') - code_path = os.path.join(project_path, os.path.basename(project_path)) - - with open(prd_path, 'r', encoding='utf-8') as f: - legacy_prd = f.read() - - with open(design_path, 'r', encoding='utf-8') as f: - legacy_design = f.read() - - with open(api_spec_and_tasks_path, 'r', encoding='utf-8') as f: - legacy_project_management = f.read() - - legacy_code = '' - for root, dirs, files in os.walk(code_path): - for file in files: - if file.endswith('.py'): - with open(os.path.join(root, file), 'r', encoding='utf-8') as f: - legacy_code += f.read() + '\n---\n' - - legacy_dict = { - 'legacy_prd': legacy_prd, - 'legacy_design': legacy_design, - 'legacy_project_management': legacy_project_management, - 'legacy_code': legacy_code - } - company.environment.set_legacy(legacy_dict) + company.save_legacy(path) company.hire( [ ProductManager(increment=increment), @@ -85,20 +57,20 @@ async def startup( company.hire([QaEngineer()]) company.invest(investment) - company.start_project(idea+"\n"+difference_description) + company.start_project(idea +"\n" + difference) await company.run(n_round=n_round) def main( idea: str, - difference_description: str = "", - project_path: str = "", investment: float = 3.0, n_round: int = 5, code_review: bool = True, run_tests: bool = False, implement: bool = True, increment: bool = False, + path: str = "", + difference: str = "", ): """ We are a software startup comprised of AI. By investing in us, @@ -111,7 +83,7 @@ def main( :return: """ asyncio.run( - startup(idea, difference_description, project_path, investment, n_round, code_review, run_tests, implement, increment)) + startup(idea, investment, n_round, code_review, run_tests, implement, increment, path, difference)) if __name__ == "__main__": From 3d3c4d56d759673a65a0273b310473c50e4e6ca9 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 21 Nov 2023 14:38:19 +0800 Subject: [PATCH 007/101] add increment development function --- metagpt/actions/__init__.py | 1 + metagpt/actions/refine.py | 10 + metagpt/actions/refine_design_api.py | 231 ++++++++++++++++ metagpt/actions/refine_prd.py | 260 +++++++++++++++++++ metagpt/actions/refine_project_management.py | 208 +++++++++++++++ 5 files changed, 710 insertions(+) create mode 100644 metagpt/actions/refine.py create mode 100644 metagpt/actions/refine_design_api.py create mode 100644 metagpt/actions/refine_prd.py create mode 100644 metagpt/actions/refine_project_management.py 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..591db175a --- /dev/null +++ b/metagpt/actions/refine_design_api.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import shutil +from pathlib import Path +from typing import List + +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 Design +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; 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 +Max Output: 8192 chars or 2048 tokens. Try to use them up. + +## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: 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 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. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +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] +{ + "Difference Description": ["The ..."], + "Implementation approach": "We will ...", + "Python package name": "snake_game", + "File list": ["main.py"], + "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 + ', + "Anything UNCLEAR": "The requirement is clear to me." +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy Design +{legacy} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; 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 +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. + +## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: 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 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. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +""", + "FORMAT_EXAMPLE": """ +--- +## Difference Description +```python +[ + "The ...", +] +``` + +## Implementation approach +We will ... + +## Python package name +```python +"snake_game" +``` + +## File list +```python +[ + "main.py", +] +``` + +## 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 +``` + +## Anything UNCLEAR +The requirement is clear to me. +--- +""", + }, +} + +OUTPUT_MAPPING = { + "Difference Description": (List[str], ...), + "Implementation approach": (str, ...), + "Python package name": (str, ...), + "File list": (List[str], ...), + "Data structures and interface definitions": (str, ...), + "Program call flow": (str, ...), + "Anything UNCLEAR": (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) + + async def _save_prd(self, docs_path, resources_path, context): + prd_file = docs_path / "prd.md" + if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: + quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] + await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") + + 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 + 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..1e2709c6b --- /dev/null +++ b/metagpt/actions/refine_prd.py @@ -0,0 +1,260 @@ +from typing import List + +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 +## User's New Requirements +{new_requirements} + +## Difference Description +{difference_description} + +## Legacy PRD +{legacy} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, efficient product based on the new requirements, the difference description and Legacy PRD. +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 + +## New Requirements: Provide as Plain text and place the new requirements here + +## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +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] +{ + "New Requirements": "", + "Difference Description": "", + "Search Information": "", + "Requirements": "", + "Product Goals": [], + "User Stories": [], + "Competitive Analysis": [], + "Competitive Quadrant Chart": "quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78]", + "Requirement Analysis": "", + "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], + "UI Design draft": "", + "Anything UNCLEAR": "", +} +[/CONTENT] +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +You need to refine the requirements based on the new requirements and the existing requirements' output. +## User's New Requirements +{new_requirements} + +## Difference Description +{difference_description} + +## Legacy PRD +{legacy} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, 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. + +## New Requirements: Provide as Plain text and place the new requirements here + +## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. +""", + "FORMAT_EXAMPLE": """ +--- +## New Requirements +The boss ... + +## Difference Description +```python +[ + "...", +] + +## Product Goals +```python +[ + "Create a ...", +] +``` + +## User Stories +```python +[ + "As a user, ...", +] +``` + +## Competitive Analysis +```python +[ + "Python Snake Game: ...", +] +``` + +## Competitive Quadrant Chart +```mermaid +quadrantChart + title Reach and engagement of campaigns + ... + "Our Target Product": [0.6, 0.7] +``` + +## Requirement Analysis +The product should be a ... + +## Requirement Pool +```python +[ + ["End game ...", "P0"] +] +``` + +## UI Design draft +Give a basic function description, and a draft + +## Anything UNCLEAR +There are no unclear points. +--- +""", + }, +} + +INCREMENT_OUTPUT_MAPPING = { + "New Requirements": (str, ...), + # "Major Enhancements": (List[str], ...), + "Difference Description": (List[str], ...), + "Product Goals": (List[str], ...), + "User Stories": (List[str], ...), + "Competitive Analysis": (List[str], ...), + "Competitive Quadrant Chart": (str, ...), + "Requirement Analysis": (str, ...), + "Requirement Pool": (List[List[str]], ...), + "UI Design draft": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +# 对于产品经理,增量开发的动作是:RefinePDR,输出是结合新需求和已有需求输出的新的PDR +class RefinePRD(Refine): + + def __init__(self, name="RefinePRD", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, new_requirements, difference_description, 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( + new_requirements=new_requirements, difference_description=difference_description, 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..354996f3e --- /dev/null +++ b/metagpt/actions/refine_project_management.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from typing import List + +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 Design +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided in requirements.txt format + +## Required Other language third-party packages: Provided in requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first + +## 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 + +## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. + +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. + +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" + ], + "Required Other language third-party packages": [ + "No third-party ..." + ], + "Full API spec": """ + openapi: 3.0.0 + ... + description: A JSON object ... + """, + "Logic Analysis": [ + ["game.py","Contains..."] + ], + "Task list": [ + "game.py" + ], + "Shared Knowledge": """ + 'game.py' contains ... + """, + "Difference Description": """ + The ... + """, + "Anything UNCLEAR": "We need ... how to start." +} +''', + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Legacy Design +{legacy} + +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Requirements: Based on the context, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided in requirements.txt format + +## Required Other language third-party packages: Provided in requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first + +## 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 + +## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. + +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. + +""", + "FORMAT_EXAMPLE": ''' +--- +## Required Python third-party packages +```python +""" +flask==1.1.2 +bcrypt==3.2.0 +""" +``` + +## Required Other language third-party packages +```python +""" +No third-party ... +""" +``` + +## Full API spec +```python +""" +openapi: 3.0.0 +... +description: A JSON object ... +""" +``` + +## Logic Analysis +```python +[ + ["game.py", "Contains ..."], +] +``` + +## Task list +```python +[ + "game.py", +] +``` + +## Shared Knowledge +```python +""" +'game.py' contains ... +""" +``` + +## Difference Description +```python +""" +The ... +""" +``` + +## Anything UNCLEAR +We need ... how to start. +--- +''', + }, +} +OUTPUT_MAPPING = { + "Required Python third-party packages": (List[str], ...), + "Required Other language third-party packages": (List[str], ...), + "Full API spec": (str, ...), + "Logic Analysis": (List[List[str]], ...), + "Task list": (List[str], ...), + "Shared Knowledge": (str, ...), + "Difference Description": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +class RefineTasks(Action): + def __init__(self, name="CreateTasks", 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 From 9aa745291767ea53c3796526375a082e47a6d141 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 24 Nov 2023 18:22:06 +0800 Subject: [PATCH 008/101] update increment development, add bug fix function --- metagpt/actions/refine_design_api.py | 25 +++++- metagpt/actions/refine_prd.py | 17 ++++- metagpt/actions/refine_project_management.py | 34 ++++----- metagpt/actions/write_code_refine.py | 80 ++++++++++++++++++++ 4 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 metagpt/actions/write_code_refine.py diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index 591db175a..f1c231525 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import re import shutil from pathlib import Path from typing import List @@ -25,7 +26,7 @@ templates = { ## Format example {format_example} ----- -Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +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 the provided difference descriptions. 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 Max Output: 8192 chars or 2048 tokens. Try to use them up. @@ -83,7 +84,7 @@ and only output the json inside this tag, nothing else ## Format example {format_example} ----- -Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +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 the provided difference descriptions. 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 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. @@ -179,6 +180,25 @@ class RefineDesign(Action): 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 and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: @@ -208,6 +228,7 @@ class RefineDesign(Action): 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" diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 1e2709c6b..81cb02af8 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -43,13 +43,15 @@ quadrantChart ## Format example {format_example} ----- -Role: You are a professional product manager; the goal is to design a concise, usable, efficient product based on the new requirements, the difference description and Legacy PRD. +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 ## New Requirements: Provide as Plain text and place the new requirements here ## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple + ## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple ## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less @@ -72,9 +74,10 @@ and only output the json inside this tag, nothing else [CONTENT] { "New Requirements": "", - "Difference Description": "", + "Difference Description": [], "Search Information": "", "Requirements": "", + "Incremental Development Plan": [], "Product Goals": [], "User Stories": [], "Competitive Analysis": [], @@ -138,7 +141,7 @@ quadrantChart ## Format example {format_example} ----- -Role: You are a professional product manager; the goal is to design a concise, usable, efficient product +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. @@ -146,6 +149,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD W ## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple + ## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple ## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less @@ -172,6 +177,11 @@ The boss ... "...", ] +## Incremental Development Plan +[ + "It ...", +] + ## Product Goals ```python [ @@ -225,6 +235,7 @@ INCREMENT_OUTPUT_MAPPING = { "New Requirements": (str, ...), # "Major Enhancements": (List[str], ...), "Difference Description": (List[str], ...), + "Incremental Development Plan": (List[str], ...), "Product Goals": (List[str], ...), "User Stories": (List[str], ...), "Competitive Analysis": (List[str], ...), diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index 354996f3e..7d9e118d8 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -21,7 +21,7 @@ templates = { ## Format example {format_example} ----- -Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. @@ -31,14 +31,14 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## 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 ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, @@ -58,6 +58,9 @@ and only output the json inside this tag, nothing else ... description: A JSON object ... """, + "Difference Description": """ + The ... + """, "Logic Analysis": [ ["game.py","Contains..."] ], @@ -67,9 +70,6 @@ and only output the json inside this tag, nothing else "Shared Knowledge": """ 'game.py' contains ... """, - "Difference Description": """ - The ... - """, "Anything UNCLEAR": "We need ... how to start." } ''', @@ -85,7 +85,7 @@ and only output the json inside this tag, nothing else ## Format example {format_example} ----- -Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. @@ -95,14 +95,14 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. +## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. + ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## 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 ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. """, @@ -132,6 +132,13 @@ description: A JSON object ... """ ``` +## Difference Description +```python +""" +The ... +""" +``` + ## Logic Analysis ```python [ @@ -153,13 +160,6 @@ description: A JSON object ... """ ``` -## Difference Description -```python -""" -The ... -""" -``` - ## Anything UNCLEAR We need ... how to start. --- @@ -170,10 +170,10 @@ OUTPUT_MAPPING = { "Required Python third-party packages": (List[str], ...), "Required Other language third-party packages": (List[str], ...), "Full API spec": (str, ...), + "Difference Description": (str, ...), "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), "Shared Knowledge": (str, ...), - "Difference Description": (str, ...), "Anything UNCLEAR": (str, ...), } diff --git a/metagpt/actions/write_code_refine.py b/metagpt/actions/write_code_refine.py new file mode 100644 index 000000000..527952aed --- /dev/null +++ b/metagpt/actions/write_code_refine.py @@ -0,0 +1,80 @@ +#!/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, which includes reviewing existing code, providing modification suggestions, rewriting code, and optimizing the codebase. 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). +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". + +## Code Review: Based on the following context and legacy code, and following the checklist, provide key, clear, concise, and specific code modification suggestions, up to 5. +``` +1. Check 0: Is the code implemented as per the requirements? +2. Check 1: Are there any issues with the code logic? +3. Check 2: Does the existing code follow the "Data structures and interface definitions"? +4. Check 3: Is there a function in the code that is omitted or not fully implemented that needs to be implemented? +5. Check 4: Does the code have unnecessary or lack dependencies? +``` + +## Incremental Development: {filename} Based on the findings from the "Code Review," context, and the legacy code, conduct incremental development by rewriting, optimizing, and adding new code using triple quotes. +----- +# Context +{context} + +## Legacy Code +You are tasked with conducting incremental development in the existing code and creating a new code file, {filename}, based on the provided legacy code and above information. +``` +{code} +``` +----- + +## Format example +----- +{format_example} +----- + +""" + +FORMAT_EXAMPLE = """ + +## Code Review +1. The code ... +2. ... +3. ... +4. ... +5. ... + +## Incremental Development: {filename} +```python +## {filename} - Incremental Development +... +``` +""" + + + +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, code, filename): + format_example = FORMAT_EXAMPLE.format(filename=filename) + prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example) + 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 From 64ebf02dcec25a13bc3dbf65bb3f957026f939ac Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 29 Nov 2023 17:31:15 +0800 Subject: [PATCH 009/101] update increment development, add write_code_guide.py file --- metagpt/actions/refine_design_api.py | 73 +++---- metagpt/actions/refine_prd.py | 192 ++----------------- metagpt/actions/refine_project_management.py | 118 +++++------- metagpt/actions/write_code_guide.py | 67 +++++++ metagpt/actions/write_code_refine.py | 61 +++--- 5 files changed, 176 insertions(+), 335 deletions(-) create mode 100644 metagpt/actions/write_code_guide.py diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index f1c231525..909fa6db9 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -3,7 +3,7 @@ import re import shutil from pathlib import Path -from typing import List +from typing import List, Union from metagpt.actions import Action, ActionOutput from metagpt.config import CONFIG @@ -26,34 +26,29 @@ templates = { ## 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 the provided difference descriptions. 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 +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. -## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design +## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. -## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. ## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores -## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here +## 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. -## Data structures and interface definitions: 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. -## Program call flow: 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. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. - -output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, -and only output the json inside this tag, nothing else +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] { "Difference Description": ["The ..."], - "Implementation approach": "We will ...", - "Python package name": "snake_game", - "File list": ["main.py"], + "Incremental implementation approach": "We will ...", + "Python package name": "new_name", "Data structures and interface definitions": ' classDiagram class Game{ @@ -67,8 +62,7 @@ and only output the json inside this tag, nothing else participant M as Main ... G->>M: end game - ', - "Anything UNCLEAR": "The requirement is clear to me." + ' } [/CONTENT] """, @@ -84,24 +78,20 @@ and only output the json inside this tag, nothing else ## 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 the provided difference descriptions. 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 +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. -## Difference Description: Provided as Python list[str], the list of differences between the new design and the legacy design +## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. -## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. ## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores -## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here +## 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. -## Data structures and interface definitions: 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 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. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. +## 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": """ @@ -113,19 +103,12 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ] ``` -## Implementation approach +## Incremental implementation approach We will ... ## Python package name ```python -"snake_game" -``` - -## File list -```python -[ - "main.py", -] +"new_name" ``` ## Data structures and interface definitions @@ -145,22 +128,19 @@ sequenceDiagram ... G->>M: end game ``` - -## Anything UNCLEAR -The requirement is clear to me. --- """, }, } OUTPUT_MAPPING = { - "Difference Description": (List[str], ...), - "Implementation approach": (str, ...), + # "Incremental Requirements": (str, ...), + "Difference Description": (Union[List[str], str], ...), + "Incremental implementation approach": (str, ...), "Python package name": (str, ...), - "File list": (List[str], ...), + # "File list": (List[str], ...), "Data structures and interface definitions": (str, ...), - "Program call flow": (str, ...), - "Anything UNCLEAR": (str, ...), + "Program call flow": (str, ...) } @@ -201,9 +181,6 @@ class RefineDesign(Action): async def _save_prd(self, docs_path, resources_path, context): prd_file = docs_path / "prd.md" - if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: - quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] - await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") if context[-1].instruct_content: logger.info(f"Saving PRD to {prd_file}") diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 81cb02af8..0f01b9904 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from metagpt.actions import Refine, ActionOutput, SearchAndSummarize from metagpt.config import CONFIG @@ -9,96 +9,36 @@ increment_template = { "json": { "PROMPT_TEMPLATE": """ # Context -## User's New Requirements +## User's Incremental Requirements {new_requirements} -## Difference Description -{difference_description} - ## Legacy PRD {legacy} ## Search Information {search_information} -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "Campaign D": [0.78, 0.34] - "Campaign E": [0.40, 0.34] - "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6] -``` - ## 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 +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 designOnly output one json, nothing else. -## New Requirements: Provide as Plain text and place the new requirements here +## Incremental Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. -## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. ## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less - -## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. - -## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. -## Anything UNCLEAR: Provide as Plain text. Make clear here. - 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] { - "New Requirements": "", + "Incremental Requirements": "", "Difference Description": [], - "Search Information": "", - "Requirements": "", "Incremental Development Plan": [], - "Product Goals": [], - "User Stories": [], - "Competitive Analysis": [], - "Competitive Quadrant Chart": "quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - Campaign A: [0.3, 0.6] - Campaign B: [0.45, 0.23] - Campaign C: [0.57, 0.69] - Campaign D: [0.78, 0.34] - Campaign E: [0.40, 0.34] - Campaign F: [0.35, 0.78]", - "Requirement Analysis": "", - "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], - "UI Design draft": "", - "Anything UNCLEAR": "", } [/CONTENT] """, @@ -106,154 +46,58 @@ and only output the json inside this tag, nothing else "markdown": { "PROMPT_TEMPLATE": """ # Context -You need to refine the requirements based on the new requirements and the existing requirements' output. -## User's New Requirements +You need to refine the requirements based on the Incremental Requirements and the existing requirements' output. +## User's Incremental Requirements {new_requirements} -## Difference Description -{difference_description} - ## Legacy PRD {legacy} ## Search Information {search_information} -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "Campaign D": [0.78, 0.34] - "Campaign E": [0.40, 0.34] - "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6] -``` - ## 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. +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. -## New Requirements: Provide as Plain text and place the new requirements here +## Incremental Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. -## Difference Description: Provide as Python list[str], up to 5 clear, difference descriptions. If the requirement itself is simple, the difference description should also be simple +## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. ## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple - -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less - -## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. - -## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. -## Anything UNCLEAR: Provide as Plain text. Make clear here. """, "FORMAT_EXAMPLE": """ --- -## New Requirements +## Incremental Requirements The boss ... ## Difference Description -```python -[ - "...", -] +... ## Incremental Development Plan [ - "It ...", + "...", ] - -## Product Goals -```python -[ - "Create a ...", -] -``` - -## User Stories -```python -[ - "As a user, ...", -] -``` - -## Competitive Analysis -```python -[ - "Python Snake Game: ...", -] -``` - -## Competitive Quadrant Chart -```mermaid -quadrantChart - title Reach and engagement of campaigns - ... - "Our Target Product": [0.6, 0.7] -``` - -## Requirement Analysis -The product should be a ... - -## Requirement Pool -```python -[ - ["End game ...", "P0"] -] -``` - -## UI Design draft -Give a basic function description, and a draft - -## Anything UNCLEAR -There are no unclear points. ---- """, }, } INCREMENT_OUTPUT_MAPPING = { - "New Requirements": (str, ...), - # "Major Enhancements": (List[str], ...), - "Difference Description": (List[str], ...), + "Incremental Requirements": (str, ...), + "Difference Description": (Union[List[str], str], ...), "Incremental Development Plan": (List[str], ...), - "Product Goals": (List[str], ...), - "User Stories": (List[str], ...), - "Competitive Analysis": (List[str], ...), - "Competitive Quadrant Chart": (str, ...), - "Requirement Analysis": (str, ...), - "Requirement Pool": (List[List[str]], ...), - "UI Design draft": (str, ...), - "Anything UNCLEAR": (str, ...), } -# 对于产品经理,增量开发的动作是:RefinePDR,输出是结合新需求和已有需求输出的新的PDR class RefinePRD(Refine): def __init__(self, name="RefinePRD", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, new_requirements, difference_description, legacy, format=CONFIG.prompt_format, *args, **kwargs): + async def run(self, new_requirements, legacy, format=CONFIG.prompt_format, *args, **kwargs): sas = SearchAndSummarize() rsp = "" info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" @@ -263,7 +107,7 @@ class RefinePRD(Refine): prompt_template, format_example = get_template(increment_template, format) prompt = prompt_template.format( - new_requirements=new_requirements, difference_description=difference_description, legacy=legacy, search_information=info, + new_requirements=new_requirements, legacy=legacy, search_information=info, format_example=format_example ) logger.debug(prompt) diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index 7d9e118d8..dfeeb2db0 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from typing import List +from typing import List, Union from metagpt.actions.action import Action from metagpt.config import CONFIG @@ -15,62 +15,50 @@ templates = { # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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. -## Required Python third-party packages: Provided in requirements.txt format +## Difference Description: Provide as a python list, the foremost differences description for project management here based on the previous. -## Required Other language third-party packages: Provided in requirements.txt format +## Incremental 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. +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. +## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. -## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first - -## 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 - -## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. +## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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": [ + "Incremental Requirements": "...", + "Difference Description": [ + "...", + ] + "Incremental Required Python third-party packages": [ "flask==1.1.2", "bcrypt==3.2.0" ], - "Required Other language third-party packages": [ - "No third-party ..." - ], "Full API spec": """ openapi: 3.0.0 ... description: A JSON object ... """, - "Difference Description": """ - The ... - """, "Logic Analysis": [ ["game.py","Contains..."] ], "Task list": [ "game.py" - ], - "Shared Knowledge": """ - 'game.py' contains ... - """, - "Anything UNCLEAR": "We need ... how to start." + ] } ''', }, @@ -79,48 +67,44 @@ and only output the json inside this tag, nothing else # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +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 in requirements.txt format +## Difference Description: Provided as a python list, the foremost differences description for project management here based on the previous. -## Required Other language third-party packages: Provided in requirements.txt format +## Incremental 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. +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend based on the previous. -## Difference Description: Please provide a detailed description of the differences between this project and its predecessors or similar projects that can include changes in technology, architecture. - -## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first - -## 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 - -## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. +## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. +## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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 +## Incremental Requirements +... + +## Difference Description ```python -""" -flask==1.1.2 -bcrypt==3.2.0 -""" +[ + "The ...", +] ``` -## Required Other language third-party packages +## Incremental Required Python third-party packages ```python -""" -No third-party ... -""" +[ + "flask==1.1.2", + "bcrypt==3.2.0" +] ``` ## Full API spec @@ -132,13 +116,6 @@ description: A JSON object ... """ ``` -## Difference Description -```python -""" -The ... -""" -``` - ## Logic Analysis ```python [ @@ -152,29 +129,18 @@ The ... "game.py", ] ``` - -## Shared Knowledge -```python -""" -'game.py' contains ... -""" -``` - -## Anything UNCLEAR -We need ... how to start. --- ''', }, } OUTPUT_MAPPING = { - "Required Python third-party packages": (List[str], ...), - "Required Other language third-party packages": (List[str], ...), + # "Incremental Requirements": (str, ...), + # ## Incremental Requirements: Provided as a str, the foremost incremental requirements for project management here based on the previous. + "Difference Description": (Union[List[str], str], ...), + "Incremental Required Python third-party packages": (Union[List[str], str], ...), "Full API spec": (str, ...), - "Difference Description": (str, ...), "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), - "Shared Knowledge": (str, ...), - "Anything UNCLEAR": (str, ...), } @@ -192,11 +158,13 @@ class RefineTasks(Action): # 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"))) + requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Incremental 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) + 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 diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py new file mode 100644 index 000000000..391173bb3 --- /dev/null +++ b/metagpt/actions/write_code_guide.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 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). +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". + +## Regulations Review: To make the software directly operable without further coding, follow the regulations below during incremental development: +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: Proposed the Minimum essential incremental development plan, based on the following context and legacy code by thinking step by step. +... + +## Code guidelines: 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. +```python +... +''' +----- +# Context +{context} + +## Legacy Code +You are tasked with conducting incremental development in the existing code based on the provided legacy code and above information. +``` +{code} +``` +----- + +## Format example +----- +## Incremental Development Guide: +... + +## Code Guidance: +# Implementation the ... +```python +... +''' +----- +""" + + +class WriteCodeGuide(Action): + def __init__(self, name="WriteCodeGuide", context: list[Message] = None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context, code): + prompt = PROMPT_TEMPLATE.format(context=context, code=code) + logger.info(f'Write Code Guide ..') + code_guide = await self._aask(prompt) + # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) + # self._save(context, filename, code) + 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 index 527952aed..466c30679 100644 --- a/metagpt/actions/write_code_refine.py +++ b/metagpt/actions/write_code_refine.py @@ -7,58 +7,44 @@ 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, which includes reviewing existing code, providing modification suggestions, rewriting code, and optimizing the codebase. 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). -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". +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: You should modify the corresponding code based on the guidance. Then, 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. -## Code Review: Based on the following context and legacy code, and following the checklist, provide key, clear, concise, and specific code modification suggestions, up to 5. -``` -1. Check 0: Is the code implemented as per the requirements? -2. Check 1: Are there any issues with the code logic? -3. Check 2: Does the existing code follow the "Data structures and interface definitions"? -4. Check 3: Is there a function in the code that is omitted or not fully implemented that needs to be implemented? -5. Check 4: Does the code have unnecessary or lack dependencies? -``` - -## Incremental Development: {filename} Based on the findings from the "Code Review," context, and the legacy code, conduct incremental development by rewriting, optimizing, and adding new code using triple quotes. +## Code: Only Write {filename}, Write code using triple quotes, based on the following list and context. +1. Do your best to implement THIS ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT. +2. Requirement: 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: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. +4. Attention2: YOU MUST FOLLOW "Data structures and interface definitions". DONT CHANGE ANY DESIGN. +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. ----- # Context {context} -## Legacy Code -You are tasked with conducting incremental development in the existing code and creating a new code file, {filename}, based on the provided legacy code and above information. -``` -{code} -``` ----- +## Guidelines: The foremost guidelines of modification for incremental development. +{guide} +----- +## Legacy Code: The Legacy Code that needs to be modified. +{legacy} + +----- ## Format example ----- -{format_example} ------ - -""" - -FORMAT_EXAMPLE = """ - -## Code Review -1. The code ... -2. ... -3. ... -4. ... -5. ... - -## Incremental Development: {filename} +## Modified/Added Code: {filename} ```python -## {filename} - Incremental Development +# {filename} ... ``` +----- """ - class WriteCodeRefine(Action): def __init__(self, name="WriteCodeRefine", context: list[Message] = None, llm=None): super().__init__(name, context, llm) @@ -69,9 +55,8 @@ class WriteCodeRefine(Action): code = CodeParser.parse_code(block="", text=code_rsp) return code - async def run(self, context, code, filename): - format_example = FORMAT_EXAMPLE.format(filename=filename) - prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example) + async def run(self, context, code, filename, guide): + prompt = PROMPT_TEMPLATE.format(context=context, legacy=code, 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) From 19b4b27f97bb3d9e0f469b465f87a98e4ff980a1 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 4 Dec 2023 17:14:52 +0800 Subject: [PATCH 010/101] update increment development --- metagpt/actions/refine_design_api.py | 31 ++++------- metagpt/actions/refine_prd.py | 46 +++++----------- metagpt/actions/refine_project_management.py | 58 +++++--------------- metagpt/actions/write_code_guide.py | 45 ++++++++------- metagpt/actions/write_code_refine.py | 22 ++++---- 5 files changed, 77 insertions(+), 125 deletions(-) diff --git a/metagpt/actions/refine_design_api.py b/metagpt/actions/refine_design_api.py index 909fa6db9..d6a948b43 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -20,7 +20,7 @@ templates = { # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example @@ -30,9 +30,7 @@ Role: You are an architect; the goal is to perform incremental development and d 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. -## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. - -## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## 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 @@ -46,8 +44,7 @@ Output exactly as shown in the example, including single and double quotes, and "FORMAT_EXAMPLE": """ [CONTENT] { - "Difference Description": ["The ..."], - "Incremental implementation approach": "We will ...", + "Incremental implementation approach": ["We will ...",], "Python package name": "new_name", "Data structures and interface definitions": ' classDiagram @@ -72,7 +69,7 @@ Output exactly as shown in the example, including single and double quotes, and # Context {context} -## Legacy Design +## Legacy {legacy} ## Format example @@ -83,9 +80,7 @@ Requirement: Fill in the following missing information based on the context, not 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. -## Difference Description: Provide as list, the foremost differences description for system design here based on the previous. - -## Incremental implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. +## 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 @@ -96,15 +91,13 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W """, "FORMAT_EXAMPLE": """ --- -## Difference Description -```python -[ - "The ...", -] -``` ## Incremental implementation approach -We will ... +```python +[ + "We will ...", +] +``` ## Python package name ```python @@ -135,8 +128,8 @@ sequenceDiagram OUTPUT_MAPPING = { # "Incremental Requirements": (str, ...), - "Difference Description": (Union[List[str], str], ...), - "Incremental implementation approach": (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, ...), diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 0f01b9904..1d8bab5f8 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -9,10 +9,9 @@ increment_template = { "json": { "PROMPT_TEMPLATE": """ # Context -## User's Incremental Requirements -{new_requirements} +{context} -## Legacy PRD +## Legacy {legacy} ## Search Information @@ -22,13 +21,9 @@ increment_template = { {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 designOnly output one json, nothing else. +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 Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. - -## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. - -## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple +## 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 @@ -36,9 +31,7 @@ and only output the json inside this tag, nothing else "FORMAT_EXAMPLE": """ [CONTENT] { - "Incremental Requirements": "", - "Difference Description": [], - "Incremental Development Plan": [], + "Incremental Development Analysis": [], } [/CONTENT] """, @@ -46,11 +39,9 @@ and only output the json inside this tag, nothing else "markdown": { "PROMPT_TEMPLATE": """ # Context -You need to refine the requirements based on the Incremental Requirements and the existing requirements' output. -## User's Incremental Requirements -{new_requirements} +{context} -## Legacy PRD +## Legacy {legacy} ## Search Information @@ -63,32 +54,21 @@ Role: You are a professional Product Manager tasked with overseeing incremental 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 Requirements: Provide as str, the foremost incremental requirements for PRD here based on the previous. - -## Difference Description: Provide as str, the foremost differences description for PRD here based on the previous. - -## Incremental Development Plan: Provide as Python list[str], up to 5 clear, incremental development plans. If the requirement itself is simple, the incremental development plan should also be simple +## 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 Requirements -The boss ... -## Difference Description -... - -## Incremental Development Plan +## Incremental Development Analysis [ - "...", + "We will ...", ] """, }, } INCREMENT_OUTPUT_MAPPING = { - "Incremental Requirements": (str, ...), - "Difference Description": (Union[List[str], str], ...), - "Incremental Development Plan": (List[str], ...), + "Incremental Development Analysis": (List[str], ...), } @@ -97,7 +77,7 @@ class RefinePRD(Refine): def __init__(self, name="RefinePRD", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, new_requirements, legacy, format=CONFIG.prompt_format, *args, **kwargs): + 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}" @@ -107,7 +87,7 @@ class RefinePRD(Refine): prompt_template, format_example = get_template(increment_template, format) prompt = prompt_template.format( - new_requirements=new_requirements, legacy=legacy, search_information=info, + context=context, legacy=legacy, search_information=info, format_example=format_example ) logger.debug(prompt) diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index dfeeb2db0..2775b1cb6 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -21,30 +21,23 @@ templates = { ## Format example {format_example} ----- -Role: You are a project manager; the goal is to perform incremental development based on the context and difference descriptions 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. +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. -## Difference Description: Provide as a python list, the foremost differences description for project management here based on the previous. - -## Incremental Required Python third-party packages: Provided as a python list, the requirements.txt format +## 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. -## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first 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. -## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. 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, +Output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else """, "FORMAT_EXAMPLE": ''' { - "Incremental Requirements": "...", - "Difference Description": [ - "...", - ] - "Incremental Required Python third-party packages": [ + "Required Python third-party packages": [ "flask==1.1.2", "bcrypt==3.2.0" ], @@ -53,9 +46,6 @@ and only output the json inside this tag, nothing else ... description: A JSON object ... """, - "Logic Analysis": [ - ["game.py","Contains..."] - ], "Task list": [ "game.py" ] @@ -77,29 +67,16 @@ Role: You are a project manager; the goal is to perform incremental development 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. -## Difference Description: Provided as a python list, the foremost differences description for project management here based on the previous. - -## Incremental Required Python third-party packages: Provided as a python list, the requirements.txt format +## 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. -## Logic Analysis: Only files need to modified, Provided as a Python list[list[str]. If the file has no changes, the file will not be output. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first based on the previous. - -## Task list: Only files need to modified, provided as Python list[str]. If the file has no changes, the file will not be output. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first +## 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": ''' --- -## Incremental Requirements -... -## Difference Description -```python -[ - "The ...", -] -``` - -## Incremental Required Python third-party packages +## Required Python third-party packages ```python [ "flask==1.1.2", @@ -116,13 +93,6 @@ description: A JSON object ... """ ``` -## Logic Analysis -```python -[ - ["game.py", "Contains ..."], -] -``` - ## Task list ```python [ @@ -136,16 +106,16 @@ description: A JSON object ... OUTPUT_MAPPING = { # "Incremental Requirements": (str, ...), # ## Incremental Requirements: Provided as a str, the foremost incremental requirements for project management here based on the previous. - "Difference Description": (Union[List[str], str], ...), - "Incremental Required Python third-party packages": (Union[List[str], str], ...), + # "Difference Analysis": (Union[List[str], str], ...), + "Required Python third-party packages": (Union[List[str], str], ...), "Full API spec": (str, ...), - "Logic Analysis": (List[List[str]], ...), + # "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), } class RefineTasks(Action): - def __init__(self, name="CreateTasks", context=None, llm=None): + def __init__(self, name="RefineTasks", context=None, llm=None): super().__init__(name, context, llm) def _save(self, context, rsp): @@ -158,7 +128,7 @@ class RefineTasks(Action): # Write requirements.txt requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" - requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Incremental Required Python third-party packages"))) + 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) diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py index 391173bb3..3e51f0d2d 100644 --- a/metagpt/actions/write_code_guide.py +++ b/metagpt/actions/write_code_guide.py @@ -10,24 +10,21 @@ 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). -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". +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. +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: Proposed the Minimum essential incremental development plan, based on the following context and legacy code by thinking step by step. -... +## 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. -## Code guidelines: 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. -```python -... -''' ----- # Context {context} @@ -35,20 +32,31 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ## Legacy Code You are tasked with conducting incremental development in the existing code based on the provided legacy code and above information. ``` -{code} +{legacy} ``` ----- ## Format example ----- -## Incremental Development Guide: -... +## Incremental Development Guide +[ + "`game.py` Contains `Game` and ...", +] -## Code Guidance: -# Implementation the ... + +## 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: + ... +``` ----- """ @@ -57,11 +65,10 @@ class WriteCodeGuide(Action): def __init__(self, name="WriteCodeGuide", context: list[Message] = None, llm=None): super().__init__(name, context, llm) - async def run(self, context, code): - prompt = PROMPT_TEMPLATE.format(context=context, code=code) + 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) - # self._save(context, filename, code) 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 index 466c30679..5a70b2c68 100644 --- a/metagpt/actions/write_code_refine.py +++ b/metagpt/actions/write_code_refine.py @@ -10,17 +10,19 @@ 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: You should modify the corresponding code based on the guidance. Then, 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. +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. -## Code: Only Write {filename}, Write code using triple quotes, based on the following list and context. -1. Do your best to implement THIS ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT. -2. Requirement: 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: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. -4. Attention2: YOU MUST FOLLOW "Data structures and interface definitions". DONT CHANGE ANY DESIGN. +## 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} @@ -30,13 +32,13 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format should be carefull {guide} ----- -## Legacy Code: The Legacy Code that needs to be modified. +## 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 ----- -## Modified/Added Code: {filename} +## Rewrite Complete Code: {filename} ```python # {filename} ... @@ -55,8 +57,8 @@ class WriteCodeRefine(Action): code = CodeParser.parse_code(block="", text=code_rsp) return code - async def run(self, context, code, filename, guide): - prompt = PROMPT_TEMPLATE.format(context=context, legacy=code, filename=filename, guide=guide) + 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) From f7205645b20451030bcfea4564dd2ac030854f3f Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Dec 2023 20:25:41 +0800 Subject: [PATCH 011/101] Update increment development for 0.5.x version: Delete files with 'refine' and write_code_guide.py. Add write_code_guide_an.py. Update write_code.py for guiding write code. --- metagpt/actions/__init__.py | 1 - metagpt/actions/prepare_documents.py | 2 +- metagpt/actions/refine.py | 10 - metagpt/actions/refine_design_api.py | 222 ------------------ metagpt/actions/refine_prd.py | 95 -------- metagpt/actions/refine_project_management.py | 146 ------------ metagpt/actions/write_code.py | 76 +++++-- metagpt/actions/write_code_guide.py | 74 ------ metagpt/actions/write_code_guide_an.py | 223 +++++++++++++++++++ metagpt/actions/write_code_refine.py | 67 ------ metagpt/provider/openai_api.py | 2 +- metagpt/roles/engineer.py | 15 +- 12 files changed, 295 insertions(+), 638 deletions(-) delete mode 100644 metagpt/actions/refine.py delete mode 100644 metagpt/actions/refine_design_api.py delete mode 100644 metagpt/actions/refine_prd.py delete mode 100644 metagpt/actions/refine_project_management.py delete mode 100644 metagpt/actions/write_code_guide.py create mode 100644 metagpt/actions/write_code_guide_an.py delete mode 100644 metagpt/actions/write_code_refine.py diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index 99a4175f6..c34c72ed2 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -14,7 +14,6 @@ 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/prepare_documents.py b/metagpt/actions/prepare_documents.py index 696dc9a89..259553644 100644 --- a/metagpt/actions/prepare_documents.py +++ b/metagpt/actions/prepare_documents.py @@ -37,7 +37,7 @@ class PrepareDocuments(Action): name = CONFIG.project_name or FileRepository.new_filename() path = Path(CONFIG.workspace_path) / name - if path.exists() and not CONFIG.inc: + if Path(path).exists() and not CONFIG.inc: shutil.rmtree(path) CONFIG.git_repo = GitRepository(local_path=path, auto_init=True) diff --git a/metagpt/actions/refine.py b/metagpt/actions/refine.py deleted file mode 100644 index beea40fc8..000000000 --- a/metagpt/actions/refine.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index d6a948b43..000000000 --- a/metagpt/actions/refine_design_api.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/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 deleted file mode 100644 index 1d8bab5f8..000000000 --- a/metagpt/actions/refine_prd.py +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 2775b1cb6..000000000 --- a/metagpt/actions/refine_project_management.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/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.py b/metagpt/actions/write_code.py index 4d0690e0f..fd6ad3eb1 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,6 +21,7 @@ from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action +from metagpt.actions.write_code_guide_an import WRITE_CODE_INCREMENT_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, @@ -114,20 +115,35 @@ class WriteCode(Action): test_detail = RunCodeResult.loads(test_doc.content) logs = test_detail.stderr + guideline = kwargs.get("guideline", "") if bug_feedback: code_context = coding_context.code_doc.content + elif guideline: + code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="guide") else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) - prompt = PROMPT_TEMPLATE.format( - design=coding_context.design_doc.content if coding_context.design_doc else "", - tasks=coding_context.task_doc.content if coding_context.task_doc else "", - code=code_context, - logs=logs, - feedback=bug_feedback.content if bug_feedback else "", - filename=self.context.filename, - summary_log=summary_doc.content if summary_doc else "", - ) + if guideline: # guide write code 也有两种方式,进行尝试 + prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( + guideline=guideline, + design=coding_context.design_doc.content if coding_context.design_doc else "", + tasks=coding_context.task_doc.content if coding_context.task_doc else "", + code=code_context, + logs=logs, + feedback=bug_feedback.content if bug_feedback else "", + filename=self.context.filename, + summary_log=summary_doc.content if summary_doc else "", + ) + else: + prompt = PROMPT_TEMPLATE.format( + design=coding_context.design_doc.content if coding_context.design_doc else "", + tasks=coding_context.task_doc.content if coding_context.task_doc else "", + code=code_context, + logs=logs, + feedback=bug_feedback.content if bug_feedback else "", + filename=self.context.filename, + summary_log=summary_doc.content if summary_doc else "", + ) logger.info(f"Writing {coding_context.filename}..") code = await self.write_code(prompt) if not coding_context.code_doc: @@ -138,7 +154,7 @@ class WriteCode(Action): return coding_context @staticmethod - async def get_codes(task_doc, exclude) -> str: + async def get_codes(task_doc, exclude, mode="normal") -> str: if not task_doc: return "" if not task_doc.content: @@ -147,11 +163,37 @@ class WriteCode(Action): code_filenames = m.get("Task list", []) codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) - for filename in code_filenames: - if filename == exclude: - continue - doc = await src_file_repo.get(filename=filename) - if not doc: - continue - codes.append(f"----- {filename}\n" + doc.content) + old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) + + src_files = src_file_repo.all_files + old_files = old_file_repo.all_files + union_files_list = list(set(src_files) | set(old_files)) + + if mode == "guide": + # 从两个repo中取code,并结合在一起 + for filename in union_files_list: + if filename == exclude: + if filename in old_files: + doc = await old_file_repo.get(filename=filename) # 使用原始代码 + else: + continue + + else: + doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 + if not doc: + if filename in old_files: + doc = await old_file_repo.get(filename=filename) # 使用原始代码 + else: + continue + codes.append(f"----- {filename}\n```{doc.content}```") + + else: + for filename in code_filenames: + if filename == exclude: + continue + doc = await src_file_repo.get(filename=filename) + if not doc: + continue + codes.append(f"----- {filename}\n```{doc.content}```") + return "\n".join(codes) diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py deleted file mode 100644 index 3e51f0d2d..000000000 --- a/metagpt/actions/write_code_guide.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/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_guide_an.py b/metagpt/actions/write_code_guide_an.py new file mode 100644 index 000000000..df273aa2b --- /dev/null +++ b/metagpt/actions/write_code_guide_an.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/12/26 +@Author : mannaandpoem +@File : write_code_guide_an.py +""" +import asyncio + +from metagpt.actions.action_node import ActionNode + +from pydantic import Field + +from metagpt.actions.action import Action +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.schema import Document + + +GUIDELINE = ActionNode( + key="Code Guideline", + expected_type=list[str], + instruction="You are a professional software engineer, and your main task is to " + "proposing incremental development plans and code guidance", + example=[ + "`calculator.py` should be extended to include methods for subtraction, multiplication, and division. Error handling should be implemented for division to prevent division by zero.", + "New endpoints for subtraction, multiplication, and division should be added to `main.py`.", + ], +) + +INCREMENTAL_CHANGE = ActionNode( + key="Incremental Change", + expected_type=str, + instruction="Write Incremental Change by making a code draft that how to implement incremental development based on the context and Code Guideline.", + example="""1. Extend `Calculator` class in `calculator.py` with new methods for subtraction, multiplication, and division. +```python +## calculator.py +class Calculator: + ... + def subtract_numbers(self, num1: int, num2: int) -> int: + return num1 - num2 + def multiply_numbers(self, num1: int, num2: int) -> int: + return num1 * num2 + def divide_numbers(self, num1: int, num2: int) -> float: + if num2 == 0: + raise ValueError('Cannot divide by zero') + return num1 / num2 +``` +2. Implement new endpoints in `main.py` for the subtraction, multiplication, and division methods. +```python +## main.py +from flask import Flask, request, jsonify +from calculator import Calculator +app = Flask(__name__) +calculator = Calculator() +... +@app.route('/subtract_numbers', methods=['POST']) +def subtract_numbers(): + data = request.get_json() + num1 = data.get('num1', 0) + num2 = data.get('num2', 0) + result = calculator.subtract_numbers(num1, num2) + return jsonify({'result': result}), 200 +@app.route('/multiply_numbers', methods=['POST']) +def multiply_numbers(): + data = request.get_json() + num1 = data.get('num1', 0) + num2 = data.get('num2', 0) + result = calculator.multiply_numbers(num1, num2) + return jsonify({'result': result}), 200 +@app.route('/divide_numbers', methods=['POST']) +def divide_numbers(): + data = request.get_json() + num1 = data.get('num1', 1) + num2 = data.get('num2', 1) + try: + result = calculator.divide_numbers(num1, num2) + except ValueError as e: + return jsonify({'error': str(e)}), 400 + return jsonify({'result': result}), 200 +if __name__ == '__main__': + app.run() +```""" +) + + +CODE_GUIDE_CONTEXT = """ +NOTICE +Role: You are a professional software engineer, and your main task is to write code Guideline and code craft with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". +1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. +2. Import all referenced classes. +3. Implement all methods. +4. Add necessary explanation to all methods. +5. Ensure there are no potential bugs. +6. Confirm that the entire project conforms to the tasks proposed by the user. +7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. +8. Attention: Legacy Code may be more or less files than Tasks List in Tasks. However, only code guidance and Incremental Change are written for the files in Tasks List. + +### Requirement +{requirement} + +### Design +{design} + +### Tasks +{tasks} + +### Legacy Code +{code} +""" + +WRITE_CODE_INCREMENT_TEMPLATE = """ +NOTICE +Role: You are a professional engineer; The main goal is to complete incremental development by combining Legacy Code and Guideline to rewrite the complete code. +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". + +# Context +## Guideline +{guideline} + +## Design +{design} + +## Tasks +{tasks} + +## Legacy Code +```Code +{code} +``` + +## Debug logs +```text +{logs} + +{summary_log} +``` + +## Bug Feedback logs +```text +{feedback} +``` + +# Format example +## Code: {filename} +```python +## {filename} +... +``` + +# Instruction: Based on the context, follow "Format example", write code. + +## Rewrite Complete Code: Only Write one file {filename}, Write code using triple quotes, based on the following attentions and context. +1. Only One file: do your best to implement THIS ONLY ONE FILE. +2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. +3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. +4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. +5. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. +6. Before using a external variable/module, make sure you import it first. +7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. +8. Attention1: Implement the functions required by the current file scope of responsibility. +9. 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. +""" + +CODE_GUIDE_CONTEXT_EXAMPLE = """ +### Legacy Code +## main.py + +from flask import Flask, request, jsonify +from calculator import Calculator + +app = Flask(__name__) +calculator = Calculator() + +@app.route('/add_numbers', methods=['POST']) +def add_numbers(): + data = request.get_json() + num1 = data.get('num1', 0) + num2 = data.get('num2', 0) + result = calculator.add_numbers(num1, num2) + return jsonify({'result': result}), 200 + +if __name__ == '__main__': + app.run() + +## calculator.py + +class Calculator: + def __init__(self, num1: int = 0, num2: int = 0): + self.num1 = num1 + self.num2 = num2 + + def add_numbers(self, num1: int, num2: int) -> int: + return num1 + num2 +""" + + +GUIDE_NODES = [ + GUIDELINE, + INCREMENTAL_CHANGE +] + +WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) + + +class WriteCodeGuide(Action): + name: str = "WriteCodeGuide" + context: Document = Field(default_factory=Document) + llm: BaseGPTAPI = Field(default_factory=LLM) + + async def run(self): + rsp = await WRITE_CODE_GUIDE_NODE.fill(context=CODE_GUIDE_CONTEXT, llm=self.llm, schema="json") + return rsp + + +def main(): + action = WriteCodeGuide() + return asyncio.run(action.run()) + + +if __name__ == "__main__": + main() diff --git a/metagpt/actions/write_code_refine.py b/metagpt/actions/write_code_refine.py deleted file mode 100644 index 5a70b2c68..000000000 --- a/metagpt/actions/write_code_refine.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/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 diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 8fd8959b2..4b3cf479a 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -237,7 +237,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): "n": 1, "stop": None, "temperature": 0.3, - "timeout": 3, + "timeout": 30, "model": self.model, } if configs: diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index e0234f378..23e4d56ae 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -20,6 +20,7 @@ from __future__ import annotations import json +import os from collections import defaultdict from pathlib import Path from typing import Set @@ -27,6 +28,7 @@ from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode +from metagpt.actions.write_code_guide_an import CODE_GUIDE_CONTEXT, WRITE_CODE_GUIDE_NODE, WriteCodeGuide from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, @@ -77,6 +79,7 @@ class Engineer(Role): ) n_borg: int = 1 use_code_review: bool = False + use_code_guide: bool = True code_todos: list = [] summarize_todos = [] @@ -84,14 +87,14 @@ class Engineer(Role): super().__init__(**kwargs) self._init_actions([WriteCode]) - self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug]) + self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug, WriteCodeGuide]) @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: m = json.loads(task_msg.content) return m.get("Task list") - async def _act_sp_with_cr(self, review=False) -> Set[str]: + async def _act_sp_with_cr(self, review=False, guideline="") -> Set[str]: changed_files = set() src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) for todo in self.code_todos: @@ -102,7 +105,7 @@ class Engineer(Role): 3. Do we need other codes (currently needed)? TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ - coding_context = await todo.run() + coding_context = await todo.run(guideline=guideline) # Code review if review: action = WriteCodeReview(context=coding_context, llm=self._llm) @@ -134,7 +137,11 @@ class Engineer(Role): return None async def _act_write_code(self): - changed_files = await self._act_sp_with_cr(review=self.use_code_review) + if self.use_code_guide: + code_guideline = await self._write_code_guideline() + changed_files = await self._act_sp_with_cr(review=self.use_code_review, guideline=code_guideline) + else: + changed_files = await self._act_sp_with_cr(review=self.use_code_review) return Message( content="\n".join(changed_files), role=self.profile, From a15e88f963bafb3de6ada2874adf6bb19d1e46c2 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Dec 2023 21:36:37 +0800 Subject: [PATCH 012/101] Update increment development for 0.5.x version: Delete files with 'refine' and write_code_guide.py. Add write_code_guide_an.py. Update write_code.py for guiding write code. --- metagpt/actions/write_code_guide_an.py | 7 +++---- metagpt/roles/engineer.py | 27 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index df273aa2b..d2739ba2e 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -209,14 +209,13 @@ class WriteCodeGuide(Action): context: Document = Field(default_factory=Document) llm: BaseGPTAPI = Field(default_factory=LLM) - async def run(self): - rsp = await WRITE_CODE_GUIDE_NODE.fill(context=CODE_GUIDE_CONTEXT, llm=self.llm, schema="json") - return rsp + async def run(self, context): + return await WRITE_CODE_GUIDE_NODE.fill(context=context, llm=self.llm, schema="json") def main(): action = WriteCodeGuide() - return asyncio.run(action.run()) + return asyncio.run(action.run(CODE_GUIDE_CONTEXT)) if __name__ == "__main__": diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 23e4d56ae..3ed5f2e94 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -87,7 +87,7 @@ class Engineer(Role): super().__init__(**kwargs) self._init_actions([WriteCode]) - self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug, WriteCodeGuide]) + self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug]) @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: @@ -220,7 +220,7 @@ class Engineer(Role): @staticmethod async def _new_coding_context( - filename, src_file_repo, task_file_repo, design_file_repo, dependency + filename, src_file_repo, task_file_repo, design_file_repo, dependency ) -> CodingContext: old_code_doc = await src_file_repo.get(filename) if not old_code_doc: @@ -308,3 +308,26 @@ class Engineer(Role): self.summarize_todos.append(SummarizeCode(context=ctx, llm=self._llm)) if self.summarize_todos: self._rc.todo = self.summarize_todos[0] + + async def _write_code_guideline(self): + logger.info("Writing code guideline..") + + requirement = str(self._rc.memory.get_by_role("Human")[0]) + task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) + design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) + tasks = await task_file_repo.get_all()[0] + design = await design_file_repo.get_all()[0] + old_codes = await self.get_old_codes() + + context = CODE_GUIDE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) + node = await WriteCodeGuide().run(context=context) + guideline = node.instruct_content.json(ensure_ascii=False) + return guideline + + @staticmethod + async def get_old_codes() -> str: + CONFIG.old_workspace = CONFIG.git_repo.workdir / os.path.basename(CONFIG.project_path) + old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) + old_codes = await old_file_repo.get_all() + codes = [f"----- \n```{code.content}```" for code in old_codes] + return "\n".join(codes) From d47cc0448f699f1edb684d110b0feb79613b8656 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 12:20:15 +0800 Subject: [PATCH 013/101] Update engineer.py and prompt in write_code_guide_an.py. --- metagpt/actions/write_code_guide_an.py | 15 ++++++++++----- metagpt/roles/engineer.py | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index d2739ba2e..adf21220b 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -85,7 +85,7 @@ if __name__ == '__main__': CODE_GUIDE_CONTEXT = """ -NOTICE +### NOTICE Role: You are a professional software engineer, and your main task is to write code Guideline and code craft with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". 1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. 2. Import all referenced classes. @@ -94,11 +94,14 @@ Role: You are a professional software engineer, and your main task is to write c 5. Ensure there are no potential bugs. 6. Confirm that the entire project conforms to the tasks proposed by the user. 7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. -8. Attention: Legacy Code may be more or less files than Tasks List in Tasks. However, only code guidance and Incremental Change are written for the files in Tasks List. +8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. ### Requirement {requirement} +### Prd +{prd} + ### Design {design} @@ -159,8 +162,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc 5. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 6. Before using a external variable/module, make sure you import it first. 7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -8. Attention1: Implement the functions required by the current file scope of responsibility. -9. 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. +8. When Task List code files do not include Legacy Code files, you need to seamlessly merge and adjust Legacy Code files that do not appear in the Task List into the code files of the task list being written based on the guideline. +9. Attention: 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. """ CODE_GUIDE_CONTEXT_EXAMPLE = """ @@ -202,7 +205,9 @@ GUIDE_NODES = [ ] WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) - +# 1. 对最后的全部代码进行review一次,或者测试 +# 2. 将user requirement也作为write code的输入 +# 3. 对前置的action进行重新设计 class WriteCodeGuide(Action): name: str = "WriteCodeGuide" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 3ed5f2e94..e2f4edba1 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -34,7 +34,7 @@ from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, - TASK_FILE_REPO, + TASK_FILE_REPO, PRDS_FILE_REPO, ) from metagpt.logs import logger from metagpt.roles import Role @@ -313,13 +313,19 @@ class Engineer(Role): logger.info("Writing code guideline..") requirement = str(self._rc.memory.get_by_role("Human")[0]) - task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) + prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) - tasks = await task_file_repo.get_all()[0] - design = await design_file_repo.get_all()[0] + task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) + prd = await prd_file_repo.get_all() + prd = "\n".join([doc.content for doc in prd]) + design = await design_file_repo.get_all() + design = "\n".join([doc.content for doc in design]) + tasks = await task_file_repo.get_all() + tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) + context = CODE_GUIDE_CONTEXT.format(requirement=requirement, prd=prd, tasks=tasks, design=design, + code=old_codes) node = await WriteCodeGuide().run(context=context) guideline = node.instruct_content.json(ensure_ascii=False) return guideline @@ -329,5 +335,5 @@ class Engineer(Role): CONFIG.old_workspace = CONFIG.git_repo.workdir / os.path.basename(CONFIG.project_path) old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) old_codes = await old_file_repo.get_all() - codes = [f"----- \n```{code.content}```" for code in old_codes] + codes = [f"----- {code.filename}\n```{code.content}```" for code in old_codes] return "\n".join(codes) From 99b1666bffb3e9c9a27b458efecc0762366527bc Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 14:29:01 +0800 Subject: [PATCH 014/101] Update prompt in write_code_guide_an.py. --- metagpt/actions/write_code_guide_an.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index adf21220b..e99e6b839 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -7,21 +7,19 @@ """ import asyncio -from metagpt.actions.action_node import ActionNode - from pydantic import Field from metagpt.actions.action import Action +from metagpt.actions.action_node import ActionNode from metagpt.llm import LLM from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.schema import Document - GUIDELINE = ActionNode( key="Code Guideline", expected_type=list[str], instruction="You are a professional software engineer, and your main task is to " - "proposing incremental development plans and code guidance", + "proposing incremental development plans and code guidance", example=[ "`calculator.py` should be extended to include methods for subtraction, multiplication, and division. Error handling should be implemented for division to prevent division by zero.", "New endpoints for subtraction, multiplication, and division should be added to `main.py`.", @@ -80,7 +78,7 @@ def divide_numbers(): return jsonify({'result': result}), 200 if __name__ == '__main__': app.run() -```""" +```""", ) @@ -199,15 +197,10 @@ class Calculator: """ -GUIDE_NODES = [ - GUIDELINE, - INCREMENTAL_CHANGE -] +GUIDE_NODES = [GUIDELINE, INCREMENTAL_CHANGE] WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) -# 1. 对最后的全部代码进行review一次,或者测试 -# 2. 将user requirement也作为write code的输入 -# 3. 对前置的action进行重新设计 + class WriteCodeGuide(Action): name: str = "WriteCodeGuide" From e65f16741c2dd843b1d2a78d7346f5e1da92ed38 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 14:29:01 +0800 Subject: [PATCH 015/101] Update prompt in write_code_guide_an.py. --- metagpt/actions/write_code_guide_an.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index adf21220b..9f1e16405 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -7,21 +7,19 @@ """ import asyncio -from metagpt.actions.action_node import ActionNode - from pydantic import Field from metagpt.actions.action import Action +from metagpt.actions.action_node import ActionNode from metagpt.llm import LLM from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.schema import Document - GUIDELINE = ActionNode( key="Code Guideline", expected_type=list[str], instruction="You are a professional software engineer, and your main task is to " - "proposing incremental development plans and code guidance", + "proposing incremental development plans and code guidance", example=[ "`calculator.py` should be extended to include methods for subtraction, multiplication, and division. Error handling should be implemented for division to prevent division by zero.", "New endpoints for subtraction, multiplication, and division should be added to `main.py`.", @@ -80,10 +78,9 @@ def divide_numbers(): return jsonify({'result': result}), 200 if __name__ == '__main__': app.run() -```""" +```""", ) - CODE_GUIDE_CONTEXT = """ ### NOTICE Role: You are a professional software engineer, and your main task is to write code Guideline and code craft with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". @@ -198,16 +195,10 @@ class Calculator: return num1 + num2 """ - -GUIDE_NODES = [ - GUIDELINE, - INCREMENTAL_CHANGE -] +GUIDE_NODES = [GUIDELINE, INCREMENTAL_CHANGE] WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) -# 1. 对最后的全部代码进行review一次,或者测试 -# 2. 将user requirement也作为write code的输入 -# 3. 对前置的action进行重新设计 + class WriteCodeGuide(Action): name: str = "WriteCodeGuide" From 31c797279a73ebf020f15a9a893831856ffabb55 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 14:41:13 +0800 Subject: [PATCH 016/101] Update prompt in write_code_guide_an.py. --- metagpt/actions/write_prd_an.py | 1 - 1 file changed, 1 deletion(-) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index d58d72f64..dc1ae1dd1 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -136,7 +136,6 @@ REASON = ActionNode( key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..." ) - NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, From 1ee35c930e4875ad212a7541c62a96994dd00ce9 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 15:08:01 +0800 Subject: [PATCH 017/101] Delete and modify some files --- metagpt/actions/__init__.py | 1 - metagpt/actions/refine_design_api.py | 7 +- metagpt/actions/refine_prd.py | 9 ++- metagpt/actions/refine_project_management.py | 4 +- metagpt/actions/write_code.py | 2 +- metagpt/actions/write_code_guide.py | 74 -------------------- metagpt/actions/write_code_refine.py | 67 ------------------ metagpt/roles/engineer.py | 12 ++-- tests/metagpt/provider/test_zhipuai_api.py | 5 +- 9 files changed, 20 insertions(+), 161 deletions(-) delete mode 100644 metagpt/actions/write_code_guide.py delete mode 100644 metagpt/actions/write_code_refine.py diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index 99a4175f6..c34c72ed2 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -14,7 +14,6 @@ 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_design_api.py b/metagpt/actions/refine_design_api.py index d6a948b43..dba783323 100644 --- a/metagpt/actions/refine_design_api.py +++ b/metagpt/actions/refine_design_api.py @@ -7,7 +7,6 @@ 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 @@ -133,7 +132,7 @@ OUTPUT_MAPPING = { "Python package name": (str, ...), # "File list": (List[str], ...), "Data structures and interface definitions": (str, ...), - "Program call flow": (str, ...) + "Program call flow": (str, ...), } @@ -158,7 +157,7 @@ class RefineDesign(Action): original_workspace = workspace index = 1 while workspace.exists(): - ws_name_match = re.match(r'^(.*)_([\d]+)$', original_workspace.name) + 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) @@ -192,7 +191,7 @@ class RefineDesign(Action): 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): + async def _save(self, context, system_design, WORKSPACE_ROOT=None): if isinstance(system_design, ActionOutput): ws_name = system_design.instruct_content.dict()["Python package name"] else: diff --git a/metagpt/actions/refine_prd.py b/metagpt/actions/refine_prd.py index 1d8bab5f8..e7fb080f8 100644 --- a/metagpt/actions/refine_prd.py +++ b/metagpt/actions/refine_prd.py @@ -1,6 +1,7 @@ -from typing import List, Union +from typing import List -from metagpt.actions import Refine, ActionOutput, SearchAndSummarize +from metagpt.actions import SearchAndSummarize +from metagpt.actions.refine import Refine from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.utils.get_template import get_template @@ -73,7 +74,6 @@ INCREMENT_OUTPUT_MAPPING = { class RefinePRD(Refine): - def __init__(self, name="RefinePRD", context=None, llm=None): super().__init__(name, context, llm) @@ -87,8 +87,7 @@ class RefinePRD(Refine): prompt_template, format_example = get_template(increment_template, format) prompt = prompt_template.format( - context=context, legacy=legacy, search_information=info, - format_example=format_example + 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) diff --git a/metagpt/actions/refine_project_management.py b/metagpt/actions/refine_project_management.py index 2775b1cb6..1cd1d1e0f 100644 --- a/metagpt/actions/refine_project_management.py +++ b/metagpt/actions/refine_project_management.py @@ -132,9 +132,7 @@ class RefineTasks(Action): 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) + 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 diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index fd6ad3eb1..9f4cba300 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -123,7 +123,7 @@ class WriteCode(Action): else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) - if guideline: # guide write code 也有两种方式,进行尝试 + if guideline: # guide write code 也有两种方式,进行尝试 prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", diff --git a/metagpt/actions/write_code_guide.py b/metagpt/actions/write_code_guide.py deleted file mode 100644 index 3e51f0d2d..000000000 --- a/metagpt/actions/write_code_guide.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/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 deleted file mode 100644 index 5a70b2c68..000000000 --- a/metagpt/actions/write_code_refine.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/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 diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index e2f4edba1..165ffadc2 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -28,13 +28,14 @@ from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode -from metagpt.actions.write_code_guide_an import CODE_GUIDE_CONTEXT, WRITE_CODE_GUIDE_NODE, WriteCodeGuide +from metagpt.actions.write_code_guide_an import CODE_GUIDE_CONTEXT, WriteCodeGuide from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, + PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, - TASK_FILE_REPO, PRDS_FILE_REPO, + TASK_FILE_REPO, ) from metagpt.logs import logger from metagpt.roles import Role @@ -220,7 +221,7 @@ class Engineer(Role): @staticmethod async def _new_coding_context( - filename, src_file_repo, task_file_repo, design_file_repo, dependency + filename, src_file_repo, task_file_repo, design_file_repo, dependency ) -> CodingContext: old_code_doc = await src_file_repo.get(filename) if not old_code_doc: @@ -324,8 +325,9 @@ class Engineer(Role): tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDE_CONTEXT.format(requirement=requirement, prd=prd, tasks=tasks, design=design, - code=old_codes) + context = CODE_GUIDE_CONTEXT.format( + requirement=requirement, prd=prd, tasks=tasks, design=design, code=old_codes + ) node = await WriteCodeGuide().run(context=context) guideline = node.instruct_content.json(ensure_ascii=False) return guideline diff --git a/tests/metagpt/provider/test_zhipuai_api.py b/tests/metagpt/provider/test_zhipuai_api.py index dc8b63cc3..8ce0f8f63 100644 --- a/tests/metagpt/provider/test_zhipuai_api.py +++ b/tests/metagpt/provider/test_zhipuai_api.py @@ -36,9 +36,12 @@ async def test_zhipuai_acompletion(mocker): assert resp["code"] == 200 assert "chatglm-turbo" in resp["data"]["choices"][0]["content"] + def test_zhipuai_proxy(mocker): import openai + from metagpt.config import CONFIG - CONFIG.openai_proxy = 'http://127.0.0.1:8080' + + CONFIG.openai_proxy = "http://127.0.0.1:8080" _ = ZhiPuAIGPTAPI() assert openai.proxy == CONFIG.openai_proxy From 8952deaa8b65c2f9be3e47bc552cfcd8478c8d34 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 15:17:21 +0800 Subject: [PATCH 018/101] Delete files with 'refine' --- metagpt/actions/refine.py | 10 - metagpt/actions/refine_design_api.py | 221 ------------------- metagpt/actions/refine_prd.py | 94 -------- metagpt/actions/refine_project_management.py | 144 ------------ 4 files changed, 469 deletions(-) delete mode 100644 metagpt/actions/refine.py delete mode 100644 metagpt/actions/refine_design_api.py delete mode 100644 metagpt/actions/refine_prd.py delete mode 100644 metagpt/actions/refine_project_management.py diff --git a/metagpt/actions/refine.py b/metagpt/actions/refine.py deleted file mode 100644 index beea40fc8..000000000 --- a/metagpt/actions/refine.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index dba783323..000000000 --- a/metagpt/actions/refine_design_api.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/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.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, WORKSPACE_ROOT=None): - 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 deleted file mode 100644 index e7fb080f8..000000000 --- a/metagpt/actions/refine_prd.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import List - -from metagpt.actions import SearchAndSummarize -from metagpt.actions.refine import Refine -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 deleted file mode 100644 index 1cd1d1e0f..000000000 --- a/metagpt/actions/refine_project_management.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/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 From 81934e2202d436c8379767282522fda810d14020 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 27 Dec 2023 15:33:33 +0800 Subject: [PATCH 019/101] update --- metagpt/actions/write_prd_an.py | 1 + metagpt/provider/openai_api.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index dc1ae1dd1..d58d72f64 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -136,6 +136,7 @@ REASON = ActionNode( key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..." ) + NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 3c8f094d3..0b6fdd869 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -237,7 +237,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): "n": 1, "stop": None, "temperature": 0.3, - "timeout": 30, + "timeout": 3, "model": self.model, } if configs: From 850c3ec0943603ddb2dda05ed73842cd089bf33a Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 28 Dec 2023 21:55:52 +0800 Subject: [PATCH 020/101] Modify prompt in write_code_guide_an.py and get_codes function in write_code.py --- metagpt/actions/write_code.py | 18 +++++++++--------- metagpt/actions/write_code_guide_an.py | 8 +++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 9f4cba300..c1d56f523 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -163,14 +163,13 @@ class WriteCode(Action): code_filenames = m.get("Task list", []) codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) - old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) - - src_files = src_file_repo.all_files - old_files = old_file_repo.all_files - union_files_list = list(set(src_files) | set(old_files)) if mode == "guide": # 从两个repo中取code,并结合在一起 + src_files = src_file_repo.all_files + old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) + old_files = old_file_repo.all_files + union_files_list = list(set(src_files) | set(old_files)) for filename in union_files_list: if filename == exclude: if filename in old_files: @@ -181,10 +180,11 @@ class WriteCode(Action): else: doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 if not doc: - if filename in old_files: - doc = await old_file_repo.get(filename=filename) # 使用原始代码 - else: - continue + # if filename in old_files: + # doc = await old_file_repo.get(filename=filename) # 使用原始代码 + # else: + # continue + continue # 跳过 codes.append(f"----- {filename}\n```{doc.content}```") else: diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index 9f1e16405..579008732 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -18,8 +18,7 @@ from metagpt.schema import Document GUIDELINE = ActionNode( key="Code Guideline", expected_type=list[str], - instruction="You are a professional software engineer, and your main task is to " - "proposing incremental development plans and code guidance", + instruction="crafting comprehensive incremental development plans and providing detailed code guidance", example=[ "`calculator.py` should be extended to include methods for subtraction, multiplication, and division. Error handling should be implemented for division to prevent division by zero.", "New endpoints for subtraction, multiplication, and division should be added to `main.py`.", @@ -83,7 +82,7 @@ if __name__ == '__main__': CODE_GUIDE_CONTEXT = """ ### NOTICE -Role: You are a professional software engineer, and your main task is to write code Guideline and code craft with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". +Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". 1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. 2. Import all referenced classes. 3. Implement all methods. @@ -159,8 +158,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc 5. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 6. Before using a external variable/module, make sure you import it first. 7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -8. When Task List code files do not include Legacy Code files, you need to seamlessly merge and adjust Legacy Code files that do not appear in the Task List into the code files of the task list being written based on the guideline. -9. Attention: 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. +8. Attention: 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. """ CODE_GUIDE_CONTEXT_EXAMPLE = """ From db22ed214fc84c9173d16a1626fff993d0c51ba6 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 29 Dec 2023 13:55:30 +0800 Subject: [PATCH 021/101] Modify prompt in write_code_guide_an.py and write_code.py --- metagpt/actions/write_code.py | 8 ++++---- metagpt/actions/write_code_guide_an.py | 15 ++++++++------- metagpt/roles/engineer.py | 3 +-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index c1d56f523..77976a696 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -123,7 +123,7 @@ class WriteCode(Action): else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) - if guideline: # guide write code 也有两种方式,进行尝试 + if guideline: prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", @@ -165,17 +165,17 @@ class WriteCode(Action): src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) if mode == "guide": - # 从两个repo中取code,并结合在一起 src_files = src_file_repo.all_files old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) old_files = old_file_repo.all_files union_files_list = list(set(src_files) | set(old_files)) for filename in union_files_list: if filename == exclude: - if filename in old_files: + if filename in old_files and filename != "main.py": doc = await old_file_repo.get(filename=filename) # 使用原始代码 else: continue + codes.append(f"----- Legacy {filename}\n```{doc.content}```") else: doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 @@ -185,7 +185,7 @@ class WriteCode(Action): # else: # continue continue # 跳过 - codes.append(f"----- {filename}\n```{doc.content}```") + codes.append(f"----- {filename}\n```{doc.content}```") else: for filename in code_filenames: diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index 579008732..8bf7da1a6 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -18,10 +18,10 @@ from metagpt.schema import Document GUIDELINE = ActionNode( key="Code Guideline", expected_type=list[str], - instruction="crafting comprehensive incremental development plans and providing detailed code guidance", + instruction="Developing comprehensive and incremental software development plans while providing detailed code guidance.", example=[ - "`calculator.py` should be extended to include methods for subtraction, multiplication, and division. Error handling should be implemented for division to prevent division by zero.", - "New endpoints for subtraction, multiplication, and division should be added to `main.py`.", + "Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero.", + "Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards.", ], ) @@ -155,10 +155,11 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. -6. Before using a external variable/module, make sure you import it first. -7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -8. Attention: 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. +5. Follow Guideline: If Legacy Code files contain {filename}, you are required to follow the Guideline to merge the Incremental Change into the Legacy {filename} file when rewriting {filename} file. +6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. +7. Before using a external variable/module, make sure you import it first. +8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. +9. Attention: Implement the functionality required within the current file's scope, reusing existing code whenever possible. For instance, main.py achieves its purpose by instantiating an already implemented class, rather than manually implementing a class in main.py. """ CODE_GUIDE_CONTEXT_EXAMPLE = """ diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 165ffadc2..2eee4c477 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -80,7 +80,6 @@ class Engineer(Role): ) n_borg: int = 1 use_code_review: bool = False - use_code_guide: bool = True code_todos: list = [] summarize_todos = [] @@ -138,7 +137,7 @@ class Engineer(Role): return None async def _act_write_code(self): - if self.use_code_guide: + if CONFIG.inc: code_guideline = await self._write_code_guideline() changed_files = await self._act_sp_with_cr(review=self.use_code_review, guideline=code_guideline) else: From 6743a4f3b1231ac4b1a8e7ebe09805a8becca791 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Sun, 31 Dec 2023 12:56:15 +0800 Subject: [PATCH 022/101] update prompt --- metagpt/actions/design_api.py | 4 +- metagpt/actions/design_api_an.py | 89 +++++++++++++++- metagpt/actions/project_management.py | 4 +- metagpt/actions/project_management_an.py | 100 +++++++++++++++++- metagpt/actions/write_code.py | 6 +- metagpt/actions/write_code_guide_an.py | 30 +++--- metagpt/actions/write_prd.py | 12 ++- metagpt/actions/write_prd_an.py | 123 ++++++++++++++++++++++- metagpt/roles/engineer.py | 11 +- metagpt/utils/mermaid.py | 38 +++++++ 10 files changed, 381 insertions(+), 36 deletions(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 055365421..0cc3395ed 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -16,7 +16,7 @@ from typing import Optional from pydantic import Field from metagpt.actions import Action, ActionOutput -from metagpt.actions.design_api_an import DESIGN_API_NODE +from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINE_DESIGN_NODES from metagpt.config import CONFIG from metagpt.const import ( DATA_API_DESIGN_FILE_REPO, @@ -87,7 +87,7 @@ class WriteDesign(Action): async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) - node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=schema) + node = await REFINE_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) system_design_doc.content = node.instruct_content.json(ensure_ascii=False) return system_design_doc diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 7d6802381..740348481 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -9,7 +9,7 @@ from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger -from metagpt.utils.mermaid import MMC1, MMC2 +from metagpt.utils.mermaid import MMC1, MMC1_INC_AND_REFINE, MMC2 IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", @@ -18,6 +18,24 @@ IMPLEMENTATION_APPROACH = ActionNode( example="We will ...", ) +INC_IMPLEMENTATION_APPROACH = ActionNode( + key="Incremental Implementation approach", + expected_type=str, + instruction="Analyze the challenging aspects of the requirements and select a suitable open-source framework. " + "Outline the incremental steps involved in the implementation process with a list of detailed strategies.", + example="we will ...", +) + +REFINE_IMPLEMENTATION_APPROACH = ActionNode( + key="Implementation Approach", + expected_type=str, + instruction="Update and extend the original implementation approach to reflect the evolving challenges and requirements " + "due to incremental development. Provide detailed strategies for incremental steps in the implementation process." + "etain any content unrelated to incremental development for coherence and clarity.", + example="We will refine ...", +) + + PROJECT_NAME = ActionNode( key="Project name", expected_type=str, instruction="The project name with underline", example="game_2048" ) @@ -29,6 +47,14 @@ FILE_LIST = ActionNode( example=["main.py", "game.py"], ) +REFINE_FILE_LIST = ActionNode( + key="File List", + expected_type=List[str], + instruction="Update and expand the original file list, including only relative paths. " + "Ensure that the refined file list reflects the evolving structure of the project due to incremental development.", + example=["main.py", "game.py", "utils.py", "new_feature.py"], +) + DATA_STRUCTURES_AND_INTERFACES = ActionNode( key="Data structures and interfaces", expected_type=str, @@ -38,6 +64,27 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode( example=MMC1, ) +INC_DATA_STRUCTURES_AND_INTERFACES = ActionNode( + key="Incremental Data structures and interfaces", + expected_type=str, + instruction="Extend the existing mermaid classDiagram code syntax to incorporate new classes, " + "methods (including __init__), and functions with precise type annotations. Clearly delineate additional " + "relationships between classes, maintaining adherence to PEP8 standards. Enhance the level of detail in data " + "structures, ensuring a comprehensive API design that seamlessly integrates with the existing structure.", + example=MMC1_INC_AND_REFINE, +) + +REFINE_DATA_STRUCTURES_AND_INTERFACES = ActionNode( + key="Data Structures and Interfaces", + expected_type=str, + instruction="Update and extend the existing mermaid classDiagram code syntax to incorporate new classes, " + "methods (including __init__), and functions with precise type annotations. Delineate additional " + "relationships between classes, ensuring clarity and adherence to PEP8 standards. Further enhance the " + "detail in data structures for a comprehensive API design that seamlessly integrates with the evolving structure." + "Retain any content unrelated to incremental development for coherence and clarity.", + example=MMC1_INC_AND_REFINE, +) + PROGRAM_CALL_FLOW = ActionNode( key="Program call flow", expected_type=str, @@ -53,6 +100,31 @@ ANYTHING_UNCLEAR = ActionNode( example="Clarification needed on third-party API integration, ...", ) +INC_DESIGN_CONTEXT = """ +### Legacy Content +{old_design} + +### New Requirements +{requirements} + +### PRD Increment Content +{prd_increment} +""" + +REFINE_DESIGN_CONTEXT = """ +Role: You are a professional Architect tasked with overseeing incremental development. +Based on new requirements, review and refine the system design. Integrate existing architecture with incremental design changes, ensuring the refined design encompasses all architectural elements, enhancements, and adjustments. Retain content unrelated to incremental development needs for coherence and clarity. + +### New Requirements +{requirements} + +### Legacy Content +{old_design} + +### Design Increment Content +{design_increment} +""" + NODES = [ IMPLEMENTATION_APPROACH, # PROJECT_NAME, @@ -62,11 +134,24 @@ NODES = [ ANYTHING_UNCLEAR, ] +INC_NODES = [INC_IMPLEMENTATION_APPROACH, INC_DATA_STRUCTURES_AND_INTERFACES] + +REFINE_NODES = [ + REFINE_IMPLEMENTATION_APPROACH, + # PROJECT_NAME, + REFINE_FILE_LIST, + REFINE_DATA_STRUCTURES_AND_INTERFACES, + PROGRAM_CALL_FLOW, + ANYTHING_UNCLEAR, +] + DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) +INC_DESIGN_NODES = ActionNode.from_children("Incremental Design API", INC_NODES) +REFINE_DESIGN_NODES = ActionNode.from_children("Refine Design API", REFINE_NODES) def main(): - prompt = DESIGN_API_NODE.compile(context="") + prompt = REFINE_DESIGN_NODES.compile(context="12313") logger.info(prompt) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 095881e60..5d2791ea8 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -17,7 +17,7 @@ from pydantic import Field from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import PM_NODE +from metagpt.actions.project_management_an import PM_NODE, REFINE_PM_NODES from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -101,7 +101,7 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) - node = await PM_NODE.fill(context, self.llm, schema) + node = await REFINE_PM_NODES.fill(context, self.llm, schema) task_doc.content = node.instruct_content.json(ensure_ascii=False) return task_doc diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 215a67202..a13ce2dcd 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -35,6 +35,31 @@ LOGIC_ANALYSIS = ActionNode( ], ) +INC_LOGIC_ANALYSIS = ActionNode( + key="Increment Logic Analysis", + expected_type=List[List[str]], + instruction="Provide a list of files with the classes/methods/functions to be implemented or modified incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports.", + example=[ + ["new_feature.py", "Introduces NewFeature class and related functions"], + ["utils.py", "Modifies existing utility functions to support incremental changes"], + ], +) + +REFINE_LOGIC_ANALYSIS = ActionNode( + key="Logic Analysis", + expected_type=List[List[str]], + instruction="Review and refine the logic analysis by merging the Legacy Content and Incremental Content. " + "Provide a comprehensive list of files with classes/methods/functions to be implemented or modified incrementally. " + "Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports." + "Retain any content unrelated to incremental development for coherence and clarity.", + example=[ + ["game.py", "Contains Game class and ... functions"], + ["main.py", "Contains main function, from game import Game"], + ["new_feature.py", "Introduces NewFeature class and related functions"], + ["utils.py", "Modifies existing utility functions to support incremental changes"], + ], +) + TASK_LIST = ActionNode( key="Task list", expected_type=List[str], @@ -42,6 +67,23 @@ TASK_LIST = ActionNode( example=["game.py", "main.py"], ) +INC_TASK_LIST = ActionNode( + key="Incremental Task list", + expected_type=List[str], + instruction="Break down the incremental development tasks into a prioritized list of filenames. " + "Organize the tasks based on dependency order, ensuring a systematic and efficient implementation.", + example=["new_feature.py", "utils.py", "main.py"], +) + +REFINE_TASK_LIST = ActionNode( + key="Task list", + expected_type=List[str], + instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content. " + "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" + " efficient development process.", + example=["game.py", "utils.py", "new_feature.py", "main.py"], +) + FULL_API_SPEC = ActionNode( key="Full API spec", expected_type=str, @@ -54,9 +96,27 @@ SHARED_KNOWLEDGE = ActionNode( key="Shared Knowledge", expected_type=str, instruction="Detail any shared knowledge, like common utility functions or configuration variables.", - example="'game.py' contains functions shared across the project.", + example="`game.py` contains functions shared across the project.", ) +INC_SHARED_KNOWLEDGE = ActionNode( + key="Increment Shared Knowledge", + expected_type=str, + instruction="Document any new shared knowledge generated during incremental development. This includes common " + "utility functions, configuration variables, or any information vital for team collaboration.", + example="`new_module.py` introduces shared utility functions for improved code reusability.", +) + +REFINE_SHARED_KNOWLEDGE = ActionNode( + key="Shared Knowledge", + expected_type=str, + instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental development. " + "This includes common utility functions, configuration variables, or any information vital for team collaboration." + "Retain any content unrelated to incremental development for coherence and clarity.", + example="`new_module.py` enhances shared utility functions for improved code reusability and collaboration.", +) + + ANYTHING_UNCLEAR_PM = ActionNode( key="Anything UNCLEAR", expected_type=str, @@ -64,6 +124,31 @@ ANYTHING_UNCLEAR_PM = ActionNode( example="Clarification needed on how to start and initialize third-party libraries.", ) +INC_PM_CONTEXT = """ +### Legacy Content +{old_tasks} + +### New Requirements +{requirements} + +### Design Increment Content +{design_increment} +""" + +REFINE_PM_CONTEXT = """ +Role: You are a professional Project Manager tasked with overseeing incremental development. +Based on New Requirements, refine the project context to account for incremental development. Ensure the context offers a comprehensive overview of the project's evolving scope, covering both legacy content and incremental content. Retain any content unrelated to incremental development. + +### New Requirements +{requirements} + +### Legacy Content +{old_tasks} + +### Increment Content +{tasks_increment} +""" + NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, @@ -74,8 +159,21 @@ NODES = [ ANYTHING_UNCLEAR_PM, ] +INC_NODES = [INC_LOGIC_ANALYSIS, INC_TASK_LIST, INC_SHARED_KNOWLEDGE] + +REFINE_NODES = [ + REQUIRED_PYTHON_PACKAGES, + REQUIRED_OTHER_LANGUAGE_PACKAGES, + REFINE_LOGIC_ANALYSIS, + REFINE_TASK_LIST, + FULL_API_SPEC, + REFINE_SHARED_KNOWLEDGE, + ANYTHING_UNCLEAR_PM, +] PM_NODE = ActionNode.from_children("PM_NODE", NODES) +INC_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) +REFINE_PM_NODES = ActionNode.from_children("Refine_PM_NODES", REFINE_NODES) def main(): diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 77976a696..ce63968b1 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -27,6 +27,7 @@ from metagpt.const import ( BUGFIX_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, + REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, ) @@ -115,6 +116,8 @@ class WriteCode(Action): test_detail = RunCodeResult.loads(test_doc.content) logs = test_detail.stderr + docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) + requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) guideline = kwargs.get("guideline", "") if bug_feedback: code_context = coding_context.code_doc.content @@ -125,6 +128,7 @@ class WriteCode(Action): if guideline: prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( + requirement=requirement_doc.content if requirement_doc else "", guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", tasks=coding_context.task_doc.content if coding_context.task_doc else "", @@ -175,7 +179,7 @@ class WriteCode(Action): doc = await old_file_repo.get(filename=filename) # 使用原始代码 else: continue - codes.append(f"----- Legacy {filename}\n```{doc.content}```") + codes.insert(0, f"-----Now, {filename} need to be rewritten\n```{doc.content}```\n=====") else: doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index 8bf7da1a6..5b0a438c3 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -28,8 +28,8 @@ GUIDELINE = ActionNode( INCREMENTAL_CHANGE = ActionNode( key="Incremental Change", expected_type=str, - instruction="Write Incremental Change by making a code draft that how to implement incremental development based on the context and Code Guideline.", - example="""1. Extend `Calculator` class in `calculator.py` with new methods for subtraction, multiplication, and division. + instruction="Write Incremental Change by making a code draft that how to implement incremental development including detailed steps based on the context.", + example="""- calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python ## calculator.py class Calculator: @@ -43,7 +43,8 @@ class Calculator: raise ValueError('Cannot divide by zero') return num1 / num2 ``` -2. Implement new endpoints in `main.py` for the subtraction, multiplication, and division methods. + +- main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards. ```python ## main.py from flask import Flask, request, jsonify @@ -95,9 +96,6 @@ Role: You are a professional software engineer, and your main task is to craft c ### Requirement {requirement} -### Prd -{prd} - ### Design {design} @@ -110,12 +108,13 @@ Role: You are a professional software engineer, and your main task is to craft c WRITE_CODE_INCREMENT_TEMPLATE = """ NOTICE -Role: You are a professional engineer; The main goal is to complete incremental development by combining Legacy Code and Guideline to rewrite the complete code. -Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". +Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Incremental Change, ensuring the integration of new features. # Context -## Guideline +## New Requirement +{requirement} + +## Incremental Change {guideline} ## Design @@ -148,18 +147,17 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ... ``` -# Instruction: Based on the context, follow "Format example", write code. - -## Rewrite Complete Code: Only Write one file {filename}, Write code using triple quotes, based on the following attentions and context. +# Instruction: Based on the context, follow "Format example", write or rewrite code. +## Write/Rewrite Code: Only write one file {filename}, write or rewrite complete code using triple quotes based on the following attentions and context. +### Important Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file and retain any content unrelated to incremental development to maintain clarity and coherence, when rewriting "{filename} to be rewritten". 1. Only One file: do your best to implement THIS ONLY ONE FILE. 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Follow Guideline: If Legacy Code files contain {filename}, you are required to follow the Guideline to merge the Incremental Change into the Legacy {filename} file when rewriting {filename} file. +5. Merge Incremental Change: If there is any Incremental Change, you must merge it into the code file. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -9. Attention: Implement the functionality required within the current file's scope, reusing existing code whenever possible. For instance, main.py achieves its purpose by instantiating an already implemented class, rather than manually implementing a class in main.py. """ CODE_GUIDE_CONTEXT_EXAMPLE = """ @@ -194,7 +192,7 @@ class Calculator: return num1 + num2 """ -GUIDE_NODES = [GUIDELINE, INCREMENTAL_CHANGE] +GUIDE_NODES = [INCREMENTAL_CHANGE] WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 1223e5486..dc1af0735 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -23,6 +23,8 @@ from metagpt.actions import Action, ActionOutput from metagpt.actions.action_node import ActionNode from metagpt.actions.fix_bug import FixBug from metagpt.actions.write_prd_an import ( + REFINE_PRD_NODE, + REFINE_PRD_SIMPLE_CONTEXT, WP_IS_RELATIVE_NODE, WP_ISSUE_TYPE_NODE, WRITE_PRD_NODE, @@ -135,8 +137,14 @@ class WritePRD(Action): async def _merge(self, new_requirement_doc, prd_doc, schema=CONFIG.prompt_schema) -> Document: if not CONFIG.project_name: CONFIG.project_name = Path(CONFIG.project_path).name - prompt = NEW_REQ_TEMPLATE.format(requirements=new_requirement_doc.content, old_prd=prd_doc.content) - node = await WRITE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema) + + project_name = CONFIG.project_name if CONFIG.project_name else "" + prompt = REFINE_PRD_SIMPLE_CONTEXT.format( + requirements=new_requirement_doc.content, + old_prd=prd_doc.content, + project_name=project_name, + ) + node = await REFINE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema) prd_doc.content = node.instruct_content.json(ensure_ascii=False) await self._rename_workspace(node) return prd_doc diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index d58d72f64..5ba694b0f 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -31,6 +31,14 @@ ORIGINAL_REQUIREMENTS = ActionNode( example="Create a 2048 game", ) +REFINE_REQUIREMENTS = ActionNode( + key="Original Requirements", + expected_type=str, + instruction="Update and expand the original user's requirements to reflect the evolving needs of the project." + "Retain any content unrelated to incremental development", + example="Create a 2048 game with a new feature that ...", +) + PROJECT_NAME = ActionNode( key="Project Name", expected_type=str, @@ -38,6 +46,13 @@ PROJECT_NAME = ActionNode( example="game_2048", ) +REFINE_PROJECT_NAME = ActionNode( + key="Project Name", + expected_type=str, + instruction="Update the project name based on the context.", + example="game_2048_new", +) + PRODUCT_GOALS = ActionNode( key="Product Goals", expected_type=List[str], @@ -45,6 +60,19 @@ PRODUCT_GOALS = ActionNode( example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"], ) +REFINE_PRODUCT_GOALS = ActionNode( + key="Product Goals", + expected_type=List[str], + instruction="Update and expand the original product goals to reflect the evolving needs due to incremental " + "development.Ensure that the refined goals align with the current project direction and contribute to its success." + "Retain any content unrelated to incremental development", + example=[ + "Enhance user engagement through new features", + "Optimize performance for scalability", + "Integrate innovative UI enhancements", + ], +) + USER_STORIES = ActionNode( key="User Stories", expected_type=List[str], @@ -58,6 +86,21 @@ USER_STORIES = ActionNode( ], ) +REFINE_USER_STORIES = ActionNode( + key="User Stories", + expected_type=List[str], + instruction="Update and expand the original scenario-based user stories to reflect the evolving needs due to " + "incremental development, no less than 7. Ensure that the refined user stories capture incremental features and " + "improvements. Retain any content unrelated to incremental development", + example=[ + "As a player, I want to choose difficulty levels to challenge my skills", + "As a player, I want a visually appealing score display after each game for a better gaming experience", + "As a player, I want a convenient restart button displayed when I lose to quickly start a new game", + "As a player, I want an enhanced and aesthetically pleasing UI to elevate the overall gaming experience", + "As a player, I want the ability to play the game seamlessly on my mobile phone for on-the-go entertainment", + ], +) + COMPETITIVE_ANALYSIS = ActionNode( key="Competitive Analysis", expected_type=List[str], @@ -104,6 +147,14 @@ REQUIREMENT_POOL = ActionNode( example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) +REFINE_REQUIREMENT_POOL = ActionNode( + key="Requirement Pool", + expected_type=List[List[str]], + instruction="List no less than 7 requirements with their priority (P0, P1, P2). " + "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", + example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], +) + UI_DESIGN_DRAFT = ActionNode( key="UI Design draft", expected_type=str, @@ -121,8 +172,10 @@ ANYTHING_UNCLEAR = ActionNode( ISSUE_TYPE = ActionNode( key="issue_type", expected_type=str, - instruction="Answer BUG/REQUIREMENT. If it is a bugfix, answer BUG, otherwise answer Requirement", - example="BUG", + instruction="Answer BUG/REFINE/OVERHAUL. If it is a bugfix, answer BUG;" + "if it is a minor improvement, answer REFINE;" + "if it is a major overhaul, answer OVERHAUL that most likely not answer in most cases.", + example="REFINE", ) IS_RELATIVE = ActionNode( @@ -137,6 +190,50 @@ REASON = ActionNode( ) +REFINE_PRD_CONTEXT = """ +Role: You are a professional Product Manager tasked with overseeing incremental development. +Based on New Requirements, output a New PRD that seamlessly integrates both the Legacy Content and the Incremental Content. Ensure the resulting document captures the complete scope of features, enhancements, and retain content unrelated to incremental development needs for coherence and clarity. + +### New Project Name +{project_name} + +### New Requirements +{requirements} + +### Legacy Content +{old_prd} + +### PRD Incremental Content +{prd_increment} + +### Search Information +- +""" + +REFINE_PRD_SIMPLE_CONTEXT = """ +You are a professional Product Manager tasked with overseeing incremental development. + +### New Project Name +{project_name} + +### New Requirements +{requirements} + +### Legacy Content +{old_prd} + +### Search Information +- +""" + +INCREMENTAL_DEVELOPMENT_ANALYSIS = ActionNode( + key="Requirement Analysis", + expected_type=List[str], + instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced features for New Requirements.", + example=["Require add/update/modify ..."], +) + + NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, @@ -152,13 +249,33 @@ NODES = [ ANYTHING_UNCLEAR, ] +REFINE_NODES = [ + LANGUAGE, + PROGRAMMING_LANGUAGE, + REFINE_REQUIREMENTS, + REFINE_PROJECT_NAME, + REFINE_PRODUCT_GOALS, + REFINE_USER_STORIES, + COMPETITIVE_ANALYSIS, + COMPETITIVE_QUADRANT_CHART, + INCREMENTAL_DEVELOPMENT_ANALYSIS, + REFINE_REQUIREMENT_POOL, + UI_DESIGN_DRAFT, + ANYTHING_UNCLEAR, +] + +INCREMENT_PRD_NODES = [INCREMENTAL_DEVELOPMENT_ANALYSIS, REQUIREMENT_POOL] + WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) +REFINE_PRD_NODE = ActionNode.from_children("RefinePRD", REFINE_NODES) +INCREMENT_NODE = ActionNode.from_children("IncrementPRD", INCREMENT_PRD_NODES) WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON]) WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON]) def main(): - prompt = WRITE_PRD_NODE.compile(context="") + # prompt = WRITE_PRD_NODE.compile(context="") + prompt = INCREMENT_NODE.compile(context=REFINE_PRD_CONTEXT) logger.info(prompt) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 2eee4c477..4aa8f209d 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -33,7 +33,6 @@ from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, - PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -313,20 +312,18 @@ class Engineer(Role): logger.info("Writing code guideline..") requirement = str(self._rc.memory.get_by_role("Human")[0]) - prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) + # prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) - prd = await prd_file_repo.get_all() - prd = "\n".join([doc.content for doc in prd]) + # prd = await prd_file_repo.get_all() + # prd = "\n".join([doc.content for doc in prd]) design = await design_file_repo.get_all() design = "\n".join([doc.content for doc in design]) tasks = await task_file_repo.get_all() tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDE_CONTEXT.format( - requirement=requirement, prd=prd, tasks=tasks, design=design, code=old_codes - ) + context = CODE_GUIDE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) node = await WriteCodeGuide().run(context=context) guideline = node.instruct_content.json(ensure_ascii=False) return guideline diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index eb85a3f90..636ec4598 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -120,6 +120,44 @@ MMC1 = """classDiagram SearchEngine --> Summary Index --> KnowledgeBase""" +MMC1_INC_AND_REFINE = """classDiagram + class Main { + -SearchEngine search_engine + +main() str + +newMethod() str # Incremental change + } + class SearchEngine { + -Index index + -Ranking ranking + -Summary summary + +search(query: str) str + +newMethod() str # Incremental change + } + class Index { + -KnowledgeBase knowledge_base + +create_index(data: dict) + +query_index(query: str) list + +newMethod() list # Incremental change + } + class Ranking { + +rank_results(results: list) list + +newMethod() list # Incremental change + } + class Summary { + +summarize_results(results: list) str + +newMethod() str # Incremental change + } + class KnowledgeBase { + +update(data: dict) + +fetch_data(query: str) dict + +newMethod() # Incremental change + } + Main --> SearchEngine + SearchEngine --> Index + SearchEngine --> Ranking + SearchEngine --> Summary + Index --> KnowledgeBase""" + MMC2 = """sequenceDiagram participant M as Main participant SE as SearchEngine From 6d9dfa73aa155f2b18a67a66e8d2fc1036081df4 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 2 Jan 2024 18:24:02 +0800 Subject: [PATCH 023/101] Fallback to a version that only uses "Refine" and update the prompt for ActionNode --- metagpt/actions/design_api_an.py | 19 ++++++++++++----- metagpt/actions/project_management_an.py | 7 ++++--- metagpt/actions/write_code_guide_an.py | 2 +- metagpt/actions/write_prd_an.py | 6 ++---- metagpt/utils/mermaid.py | 26 ++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 740348481..5f17d4656 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -9,7 +9,7 @@ from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger -from metagpt.utils.mermaid import MMC1, MMC1_INC_AND_REFINE, MMC2 +from metagpt.utils.mermaid import MMC1, MMC1_INC_AND_REFINE, MMC2, MMC2_INC IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", @@ -35,7 +35,6 @@ REFINE_IMPLEMENTATION_APPROACH = ActionNode( example="We will refine ...", ) - PROJECT_NAME = ActionNode( key="Project name", expected_type=str, instruction="The project name with underline", example="game_2048" ) @@ -51,7 +50,8 @@ REFINE_FILE_LIST = ActionNode( key="File List", expected_type=List[str], instruction="Update and expand the original file list, including only relative paths. " - "Ensure that the refined file list reflects the evolving structure of the project due to incremental development.", + "Ensure that the refined file list reflects the evolving structure of the project due to incremental development." + "Only output filename!Do not include comments in the list.", example=["main.py", "game.py", "utils.py", "new_feature.py"], ) @@ -93,6 +93,15 @@ PROGRAM_CALL_FLOW = ActionNode( example=MMC2, ) +REFINE_PROGRAM_CALL_FLOW = ActionNode( + key="Program call flow", + expected_type=str, + instruction="Extend the existing sequenceDiagram code syntax with detailed information, accurately covering the" + "CRUD and initialization of each object. Ensure correct syntax usage and reflect the incremental changes introduced" + "in the classes and API defined above.Retain content unrelated to incremental development for coherence and clarity", + example=MMC2_INC, +) + ANYTHING_UNCLEAR = ActionNode( key="Anything UNCLEAR", expected_type=str, @@ -134,14 +143,14 @@ NODES = [ ANYTHING_UNCLEAR, ] -INC_NODES = [INC_IMPLEMENTATION_APPROACH, INC_DATA_STRUCTURES_AND_INTERFACES] +INC_NODES = [INC_IMPLEMENTATION_APPROACH, INC_DATA_STRUCTURES_AND_INTERFACES, REFINE_PROGRAM_CALL_FLOW] REFINE_NODES = [ REFINE_IMPLEMENTATION_APPROACH, # PROJECT_NAME, REFINE_FILE_LIST, REFINE_DATA_STRUCTURES_AND_INTERFACES, - PROGRAM_CALL_FLOW, + REFINE_PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, ] diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index a13ce2dcd..27d238e9f 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -70,8 +70,9 @@ TASK_LIST = ActionNode( INC_TASK_LIST = ActionNode( key="Incremental Task list", expected_type=List[str], - instruction="Break down the incremental development tasks into a prioritized list of filenames. " - "Organize the tasks based on dependency order, ensuring a systematic and efficient implementation.", + instruction="Break down the incremental development tasks into a prioritized list of filenames." + "Organize the tasks based on dependency order, ensuring a systematic and efficient implementation." + "Only output filename! Do not include comments in the list ", example=["new_feature.py", "utils.py", "main.py"], ) @@ -80,7 +81,7 @@ REFINE_TASK_LIST = ActionNode( expected_type=List[str], instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content. " "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" - " efficient development process.", + " efficient development process. Only output filename! Do not include comments in the list", example=["game.py", "utils.py", "new_feature.py", "main.py"], ) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guide_an.py index 5b0a438c3..b21e66098 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guide_an.py @@ -149,7 +149,6 @@ Role: You are a professional engineer; The main goal is to complete incremental # Instruction: Based on the context, follow "Format example", write or rewrite code. ## Write/Rewrite Code: Only write one file {filename}, write or rewrite complete code using triple quotes based on the following attentions and context. -### Important Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file and retain any content unrelated to incremental development to maintain clarity and coherence, when rewriting "{filename} to be rewritten". 1. Only One file: do your best to implement THIS ONLY ONE FILE. 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. @@ -158,6 +157,7 @@ Role: You are a professional engineer; The main goal is to complete incremental 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. +9. Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file when rewriting "{filename} to be rewritten". """ CODE_GUIDE_CONTEXT_EXAMPLE = """ diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 5ba694b0f..79046bb7d 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -172,10 +172,8 @@ ANYTHING_UNCLEAR = ActionNode( ISSUE_TYPE = ActionNode( key="issue_type", expected_type=str, - instruction="Answer BUG/REFINE/OVERHAUL. If it is a bugfix, answer BUG;" - "if it is a minor improvement, answer REFINE;" - "if it is a major overhaul, answer OVERHAUL that most likely not answer in most cases.", - example="REFINE", + instruction="Answer BUG/REQUIREMENT. If it is a bugfix, answer BUG, otherwise answer Requirement", + example="BUG", ) IS_RELATIVE = ActionNode( diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index 636ec4598..d1cd1b328 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -176,6 +176,32 @@ MMC2 = """sequenceDiagram S-->>SE: return summary SE-->>M: return summary""" +MMC2_INC = """sequenceDiagram + participant M as Main + participant SE as SearchEngine + participant I as Index + participant R as Ranking + participant S as Summary + participant KB as KnowledgeBase + M->>SE: search(query) + SE->>I: query_index(query) + I->>KB: fetch_data(query) + KB-->>I: return data + I-->>SE: return results + SE->>R: rank_results(results) + R-->>SE: return ranked_results + SE->>S: summarize_results(ranked_results) + S-->>SE: return summary + SE-->>M: return summary + M->>SE: newMethod() # Incremental change + SE->>I: newMethod() # Incremental change + I->>KB: newMethod() # Incremental change + KB-->>I: newMethod() # Incremental change + SE->>R: newMethod() # Incremental change + R-->>SE: newMethod() # Incremental change + SE->>S: newMethod() # Incremental change + S-->>SE: newMethod() # Incremental change + SE-->>M: newMethod() # Incremental change""" if __name__ == "__main__": loop = asyncio.new_event_loop() From 3a819ad5762b59c969457bb3881a186e643e2216 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 2 Jan 2024 19:20:46 +0800 Subject: [PATCH 024/101] update write_code.py --- metagpt/actions/write_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index ce63968b1..33ae2e5b3 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -179,7 +179,7 @@ class WriteCode(Action): doc = await old_file_repo.get(filename=filename) # 使用原始代码 else: continue - codes.insert(0, f"-----Now, {filename} need to be rewritten\n```{doc.content}```\n=====") + codes.insert(0, f"-----Now, {filename} to be rewritten\n```{doc.content}```\n=====") else: doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 From 11faaf0651bf53ae59b19bf6e0c6672eb1f87a92 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 2 Jan 2024 22:57:42 +0800 Subject: [PATCH 025/101] Modify style and format --- metagpt/actions/design_api.py | 8 +- metagpt/actions/design_api_an.py | 69 ++++++++--------- metagpt/actions/project_management.py | 4 +- metagpt/actions/project_management_an.py | 51 +++++++------ metagpt/actions/write_code.py | 6 +- ...guide_an.py => write_code_guideline_an.py} | 43 +++++------ metagpt/actions/write_prd.py | 4 +- metagpt/actions/write_prd_an.py | 75 ++++++++----------- metagpt/roles/engineer.py | 9 ++- metagpt/utils/mermaid.py | 4 +- 10 files changed, 130 insertions(+), 143 deletions(-) rename metagpt/actions/{write_code_guide_an.py => write_code_guideline_an.py} (87%) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 0cc3395ed..082474098 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -16,7 +16,7 @@ from typing import Optional from pydantic import Field from metagpt.actions import Action, ActionOutput -from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINE_DESIGN_NODES +from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINED_DESIGN_NODES from metagpt.config import CONFIG from metagpt.const import ( DATA_API_DESIGN_FILE_REPO, @@ -87,7 +87,7 @@ class WriteDesign(Action): async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) - node = await REFINE_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) + node = await REFINED_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) system_design_doc.content = node.instruct_content.json(ensure_ascii=False) return system_design_doc @@ -114,7 +114,7 @@ class WriteDesign(Action): @staticmethod async def _save_data_api_design(design_doc): m = json.loads(design_doc.content) - data_api_design = m.get("Data structures and interfaces") + data_api_design = m.get("Data structures and interfaces") or m.get("Refined Data structures and interfaces") if not data_api_design: return pathname = CONFIG.git_repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("") @@ -124,7 +124,7 @@ class WriteDesign(Action): @staticmethod async def _save_seq_flow(design_doc): m = json.loads(design_doc.content) - seq_flow = m.get("Program call flow") + seq_flow = m.get("Program call flow") or m.get("Refined Program call flow") if not seq_flow: return pathname = CONFIG.git_repo.workdir / Path(SEQ_FLOW_FILE_REPO) / Path(design_doc.filename).with_suffix("") diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 5f17d4656..fcfaff9c3 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -9,7 +9,7 @@ from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger -from metagpt.utils.mermaid import MMC1, MMC1_INC_AND_REFINE, MMC2, MMC2_INC +from metagpt.utils.mermaid import MMC1, MMC1_REFINE, MMC2, MMC2_REFINE IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", @@ -18,7 +18,7 @@ IMPLEMENTATION_APPROACH = ActionNode( example="We will ...", ) -INC_IMPLEMENTATION_APPROACH = ActionNode( +INCREMENTAL_IMPLEMENTATION_APPROACH = ActionNode( key="Incremental Implementation approach", expected_type=str, instruction="Analyze the challenging aspects of the requirements and select a suitable open-source framework. " @@ -26,12 +26,12 @@ INC_IMPLEMENTATION_APPROACH = ActionNode( example="we will ...", ) -REFINE_IMPLEMENTATION_APPROACH = ActionNode( - key="Implementation Approach", +REFINED_IMPLEMENTATION_APPROACH = ActionNode( + key="Refined Implementation Approach", expected_type=str, - instruction="Update and extend the original implementation approach to reflect the evolving challenges and requirements " - "due to incremental development. Provide detailed strategies for incremental steps in the implementation process." - "etain any content unrelated to incremental development for coherence and clarity.", + instruction="Update and extend the original implementation approach to reflect the evolving challenges and " + "requirements due to incremental development. Provide detailed strategies for incremental steps in the " + "implementation process. Retain any content unrelated to incremental development for coherence and clarity.", example="We will refine ...", ) @@ -46,13 +46,13 @@ FILE_LIST = ActionNode( example=["main.py", "game.py"], ) -REFINE_FILE_LIST = ActionNode( - key="File List", +REFINED_FILE_LIST = ActionNode( + key="Refined File List", expected_type=List[str], instruction="Update and expand the original file list, including only relative paths. " "Ensure that the refined file list reflects the evolving structure of the project due to incremental development." - "Only output filename!Do not include comments in the list.", - example=["main.py", "game.py", "utils.py", "new_feature.py"], + "Only output filename! Do not include comments in the list.", + example=["main.py", "game.py", "new_feature.py"], ) DATA_STRUCTURES_AND_INTERFACES = ActionNode( @@ -64,25 +64,25 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode( example=MMC1, ) -INC_DATA_STRUCTURES_AND_INTERFACES = ActionNode( +INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES = ActionNode( key="Incremental Data structures and interfaces", expected_type=str, instruction="Extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Clearly delineate additional " "relationships between classes, maintaining adherence to PEP8 standards. Enhance the level of detail in data " "structures, ensuring a comprehensive API design that seamlessly integrates with the existing structure.", - example=MMC1_INC_AND_REFINE, + example=MMC1_REFINE, ) -REFINE_DATA_STRUCTURES_AND_INTERFACES = ActionNode( - key="Data Structures and Interfaces", +REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( + key="Refined Data Structures and Interfaces", expected_type=str, instruction="Update and extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Delineate additional " "relationships between classes, ensuring clarity and adherence to PEP8 standards. Further enhance the " "detail in data structures for a comprehensive API design that seamlessly integrates with the evolving structure." "Retain any content unrelated to incremental development for coherence and clarity.", - example=MMC1_INC_AND_REFINE, + example=MMC1_REFINE, ) PROGRAM_CALL_FLOW = ActionNode( @@ -93,13 +93,13 @@ PROGRAM_CALL_FLOW = ActionNode( example=MMC2, ) -REFINE_PROGRAM_CALL_FLOW = ActionNode( - key="Program call flow", +REFINED_PROGRAM_CALL_FLOW = ActionNode( + key="Refined Program call flow", expected_type=str, instruction="Extend the existing sequenceDiagram code syntax with detailed information, accurately covering the" "CRUD and initialization of each object. Ensure correct syntax usage and reflect the incremental changes introduced" "in the classes and API defined above.Retain content unrelated to incremental development for coherence and clarity", - example=MMC2_INC, + example=MMC2_REFINE, ) ANYTHING_UNCLEAR = ActionNode( @@ -110,13 +110,13 @@ ANYTHING_UNCLEAR = ActionNode( ) INC_DESIGN_CONTEXT = """ -### Legacy Content +## Legacy Content {old_design} -### New Requirements +## New Requirements {requirements} -### PRD Increment Content +## PRD Increment Content {prd_increment} """ @@ -124,43 +124,44 @@ REFINE_DESIGN_CONTEXT = """ Role: You are a professional Architect tasked with overseeing incremental development. Based on new requirements, review and refine the system design. Integrate existing architecture with incremental design changes, ensuring the refined design encompasses all architectural elements, enhancements, and adjustments. Retain content unrelated to incremental development needs for coherence and clarity. -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_design} -### Design Increment Content +## Design Increment Content {design_increment} """ NODES = [ IMPLEMENTATION_APPROACH, # PROJECT_NAME, - FILE_LIST, + REFINED_FILE_LIST, DATA_STRUCTURES_AND_INTERFACES, PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, ] -INC_NODES = [INC_IMPLEMENTATION_APPROACH, INC_DATA_STRUCTURES_AND_INTERFACES, REFINE_PROGRAM_CALL_FLOW] +INC_NODES = [INCREMENTAL_IMPLEMENTATION_APPROACH, INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES, REFINED_PROGRAM_CALL_FLOW] REFINE_NODES = [ - REFINE_IMPLEMENTATION_APPROACH, + REFINED_IMPLEMENTATION_APPROACH, # PROJECT_NAME, - REFINE_FILE_LIST, - REFINE_DATA_STRUCTURES_AND_INTERFACES, - REFINE_PROGRAM_CALL_FLOW, + FILE_LIST, + REFINED_DATA_STRUCTURES_AND_INTERFACES, + REFINED_PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) -INC_DESIGN_NODES = ActionNode.from_children("Incremental Design API", INC_NODES) -REFINE_DESIGN_NODES = ActionNode.from_children("Refine Design API", REFINE_NODES) +INCREMENTAL_DESIGN_NODES = ActionNode.from_children("Incremental_Design_API", INC_NODES) +REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NODES) def main(): - prompt = REFINE_DESIGN_NODES.compile(context="12313") + prompt = REFINED_DESIGN_NODES.compile(context="...") logger.info(prompt) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 5d2791ea8..f124ba8df 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -17,7 +17,7 @@ from pydantic import Field from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import PM_NODE, REFINE_PM_NODES +from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODES from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -101,7 +101,7 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) - node = await REFINE_PM_NODES.fill(context, self.llm, schema) + node = await REFINED_PM_NODES.fill(context, self.llm, schema) task_doc.content = node.instruct_content.json(ensure_ascii=False) return task_doc diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 27d238e9f..467d26b4e 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -35,18 +35,20 @@ LOGIC_ANALYSIS = ActionNode( ], ) -INC_LOGIC_ANALYSIS = ActionNode( - key="Increment Logic Analysis", +INCREMENTAL_LOGIC_ANALYSIS = ActionNode( + key="Incremental Logic Analysis", expected_type=List[List[str]], - instruction="Provide a list of files with the classes/methods/functions to be implemented or modified incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports.", + instruction="Provide a list of files with the classes/methods/functions to be implemented or modified " + "incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document" + " necessary imports.", example=[ ["new_feature.py", "Introduces NewFeature class and related functions"], ["utils.py", "Modifies existing utility functions to support incremental changes"], ], ) -REFINE_LOGIC_ANALYSIS = ActionNode( - key="Logic Analysis", +REFINED_LOGIC_ANALYSIS = ActionNode( + key="Refined Logic Analysis", expected_type=List[List[str]], instruction="Review and refine the logic analysis by merging the Legacy Content and Incremental Content. " "Provide a comprehensive list of files with classes/methods/functions to be implemented or modified incrementally. " @@ -67,7 +69,7 @@ TASK_LIST = ActionNode( example=["game.py", "main.py"], ) -INC_TASK_LIST = ActionNode( +INCREMENTAL_TASK_LIST = ActionNode( key="Incremental Task list", expected_type=List[str], instruction="Break down the incremental development tasks into a prioritized list of filenames." @@ -76,8 +78,8 @@ INC_TASK_LIST = ActionNode( example=["new_feature.py", "utils.py", "main.py"], ) -REFINE_TASK_LIST = ActionNode( - key="Task list", +REFINED_TASK_LIST = ActionNode( + key="Refined Task list", expected_type=List[str], instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content. " "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" @@ -100,20 +102,20 @@ SHARED_KNOWLEDGE = ActionNode( example="`game.py` contains functions shared across the project.", ) -INC_SHARED_KNOWLEDGE = ActionNode( - key="Increment Shared Knowledge", +INCREMENTAL_SHARED_KNOWLEDGE = ActionNode( + key="Incremental Shared Knowledge", expected_type=str, instruction="Document any new shared knowledge generated during incremental development. This includes common " "utility functions, configuration variables, or any information vital for team collaboration.", example="`new_module.py` introduces shared utility functions for improved code reusability.", ) -REFINE_SHARED_KNOWLEDGE = ActionNode( - key="Shared Knowledge", +REFINED_SHARED_KNOWLEDGE = ActionNode( + key="Refined Shared Knowledge", expected_type=str, - instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental development. " - "This includes common utility functions, configuration variables, or any information vital for team collaboration." - "Retain any content unrelated to incremental development for coherence and clarity.", + instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental " + "development. This includes common utility functions, configuration variables, or any information vital for team " + "collaboration. Retain any content unrelated to incremental development for coherence and clarity.", example="`new_module.py` enhances shared utility functions for improved code reusability and collaboration.", ) @@ -140,13 +142,14 @@ REFINE_PM_CONTEXT = """ Role: You are a professional Project Manager tasked with overseeing incremental development. Based on New Requirements, refine the project context to account for incremental development. Ensure the context offers a comprehensive overview of the project's evolving scope, covering both legacy content and incremental content. Retain any content unrelated to incremental development. -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_tasks} -### Increment Content +## Increment Content {tasks_increment} """ @@ -160,21 +163,21 @@ NODES = [ ANYTHING_UNCLEAR_PM, ] -INC_NODES = [INC_LOGIC_ANALYSIS, INC_TASK_LIST, INC_SHARED_KNOWLEDGE] +INC_NODES = [INCREMENTAL_LOGIC_ANALYSIS, INCREMENTAL_TASK_LIST, INCREMENTAL_SHARED_KNOWLEDGE] REFINE_NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, - REFINE_LOGIC_ANALYSIS, - REFINE_TASK_LIST, + REFINED_LOGIC_ANALYSIS, + REFINED_TASK_LIST, FULL_API_SPEC, - REFINE_SHARED_KNOWLEDGE, + REFINED_SHARED_KNOWLEDGE, ANYTHING_UNCLEAR_PM, ] PM_NODE = ActionNode.from_children("PM_NODE", NODES) -INC_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) -REFINE_PM_NODES = ActionNode.from_children("Refine_PM_NODES", REFINE_NODES) +INCREMENTAL_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) +REFINED_PM_NODES = ActionNode.from_children("Refined_PM_NODES", REFINE_NODES) def main(): diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 33ae2e5b3..4183db19d 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,7 +21,7 @@ from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action -from metagpt.actions.write_code_guide_an import WRITE_CODE_INCREMENT_TEMPLATE +from metagpt.actions.write_code_guideline_an import REFINED_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, @@ -127,7 +127,7 @@ class WriteCode(Action): code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) if guideline: - prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( + prompt = REFINED_TEMPLATE.format( requirement=requirement_doc.content if requirement_doc else "", guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", @@ -164,7 +164,7 @@ class WriteCode(Action): if not task_doc.content: task_doc.content = FileRepository.get_file(filename=task_doc.filename, relative_path=TASK_FILE_REPO) m = json.loads(task_doc.content) - code_filenames = m.get("Task list", []) + code_filenames = m.get("Task list", []) if mode == "normal" else m.get("Refined Task list", []) codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guideline_an.py similarity index 87% rename from metagpt/actions/write_code_guide_an.py rename to metagpt/actions/write_code_guideline_an.py index b21e66098..d68b02d38 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -7,18 +7,13 @@ """ import asyncio -from pydantic import Field - from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode -from metagpt.llm import LLM -from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.schema import Document GUIDELINE = ActionNode( - key="Code Guideline", + key="Guideline", expected_type=list[str], - instruction="Developing comprehensive and incremental software development plans while providing detailed code guidance.", + instruction="Developing comprehensive and incremental development plans while providing detailed code guideline.", example=[ "Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero.", "Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards.", @@ -28,7 +23,8 @@ GUIDELINE = ActionNode( INCREMENTAL_CHANGE = ActionNode( key="Incremental Change", expected_type=str, - instruction="Write Incremental Change by making a code draft that how to implement incremental development including detailed steps based on the context.", + instruction="Write Incremental Change by making a code draft that how to implement incremental development " + "including detailed steps based on the context.", example="""- calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python ## calculator.py @@ -81,8 +77,8 @@ if __name__ == '__main__': ```""", ) -CODE_GUIDE_CONTEXT = """ -### NOTICE +CODE_GUIDELINE_CONTEXT = """ +NOTICE Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". 1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. 2. Import all referenced classes. @@ -93,20 +89,21 @@ Role: You are a professional software engineer, and your main task is to craft c 7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. 8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. -### Requirement +# Context +## Requirement {requirement} -### Design +## Design {design} -### Tasks +## Tasks {tasks} -### Legacy Code +## Legacy Code {code} """ -WRITE_CODE_INCREMENT_TEMPLATE = """ +REFINED_TEMPLATE = """ NOTICE Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Incremental Change, ensuring the integration of new features. @@ -161,7 +158,7 @@ Role: You are a professional engineer; The main goal is to complete incremental """ CODE_GUIDE_CONTEXT_EXAMPLE = """ -### Legacy Code +# Legacy Code ## main.py from flask import Flask, request, jsonify @@ -194,21 +191,17 @@ class Calculator: GUIDE_NODES = [INCREMENTAL_CHANGE] -WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) +WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", GUIDE_NODES) -class WriteCodeGuide(Action): - name: str = "WriteCodeGuide" - context: Document = Field(default_factory=Document) - llm: BaseGPTAPI = Field(default_factory=LLM) - +class WriteCodeGuideline(Action): async def run(self, context): - return await WRITE_CODE_GUIDE_NODE.fill(context=context, llm=self.llm, schema="json") + return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") def main(): - action = WriteCodeGuide() - return asyncio.run(action.run(CODE_GUIDE_CONTEXT)) + action = WriteCodeGuideline() + return asyncio.run(action.run(CODE_GUIDELINE_CONTEXT)) if __name__ == "__main__": diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index dc1af0735..a070e7a96 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -24,7 +24,7 @@ from metagpt.actions.action_node import ActionNode from metagpt.actions.fix_bug import FixBug from metagpt.actions.write_prd_an import ( REFINE_PRD_NODE, - REFINE_PRD_SIMPLE_CONTEXT, + REFINE_PRD_TEMPLATE, WP_IS_RELATIVE_NODE, WP_ISSUE_TYPE_NODE, WRITE_PRD_NODE, @@ -139,7 +139,7 @@ class WritePRD(Action): CONFIG.project_name = Path(CONFIG.project_path).name project_name = CONFIG.project_name if CONFIG.project_name else "" - prompt = REFINE_PRD_SIMPLE_CONTEXT.format( + prompt = REFINE_PRD_TEMPLATE.format( requirements=new_requirement_doc.content, old_prd=prd_doc.content, project_name=project_name, diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 79046bb7d..f645225c7 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -31,8 +31,8 @@ ORIGINAL_REQUIREMENTS = ActionNode( example="Create a 2048 game", ) -REFINE_REQUIREMENTS = ActionNode( - key="Original Requirements", +REFINED_REQUIREMENTS = ActionNode( + key="Refined Requirements", expected_type=str, instruction="Update and expand the original user's requirements to reflect the evolving needs of the project." "Retain any content unrelated to incremental development", @@ -46,13 +46,6 @@ PROJECT_NAME = ActionNode( example="game_2048", ) -REFINE_PROJECT_NAME = ActionNode( - key="Project Name", - expected_type=str, - instruction="Update the project name based on the context.", - example="game_2048_new", -) - PRODUCT_GOALS = ActionNode( key="Product Goals", expected_type=List[str], @@ -60,8 +53,8 @@ PRODUCT_GOALS = ActionNode( example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"], ) -REFINE_PRODUCT_GOALS = ActionNode( - key="Product Goals", +REFINED_PRODUCT_GOALS = ActionNode( + key="Refined Product Goals", expected_type=List[str], instruction="Update and expand the original product goals to reflect the evolving needs due to incremental " "development.Ensure that the refined goals align with the current project direction and contribute to its success." @@ -86,11 +79,11 @@ USER_STORIES = ActionNode( ], ) -REFINE_USER_STORIES = ActionNode( - key="User Stories", +REFINED_USER_STORIES = ActionNode( + key="Refined User Stories", expected_type=List[str], instruction="Update and expand the original scenario-based user stories to reflect the evolving needs due to " - "incremental development, no less than 7. Ensure that the refined user stories capture incremental features and " + "incremental development, no less than 5. Ensure that the refined user stories capture incremental features and " "improvements. Retain any content unrelated to incremental development", example=[ "As a player, I want to choose difficulty levels to challenge my skills", @@ -140,6 +133,14 @@ REQUIREMENT_ANALYSIS = ActionNode( example="", ) +INCREMENTAL_REQUIREMENT_ANALYSIS = ActionNode( + key="Incremental Requirement Analysis", + expected_type=List[str], + instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced " + "features for New Requirements.", + example=["Require add/update/modify ..."], +) + REQUIREMENT_POOL = ActionNode( key="Requirement Pool", expected_type=List[List[str]], @@ -147,8 +148,8 @@ REQUIREMENT_POOL = ActionNode( example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) -REFINE_REQUIREMENT_POOL = ActionNode( - key="Requirement Pool", +REFINED_REQUIREMENT_POOL = ActionNode( + key="Refined Requirement Pool", expected_type=List[List[str]], instruction="List no less than 7 requirements with their priority (P0, P1, P2). " "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", @@ -192,25 +193,18 @@ REFINE_PRD_CONTEXT = """ Role: You are a professional Product Manager tasked with overseeing incremental development. Based on New Requirements, output a New PRD that seamlessly integrates both the Legacy Content and the Incremental Content. Ensure the resulting document captures the complete scope of features, enhancements, and retain content unrelated to incremental development needs for coherence and clarity. -### New Project Name -{project_name} - -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_prd} -### PRD Incremental Content +## PRD Incremental Content {prd_increment} - -### Search Information -- """ -REFINE_PRD_SIMPLE_CONTEXT = """ -You are a professional Product Manager tasked with overseeing incremental development. - +REFINE_PRD_TEMPLATE = """ ### New Project Name {project_name} @@ -224,13 +218,6 @@ You are a professional Product Manager tasked with overseeing incremental develo - """ -INCREMENTAL_DEVELOPMENT_ANALYSIS = ActionNode( - key="Requirement Analysis", - expected_type=List[str], - instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced features for New Requirements.", - example=["Require add/update/modify ..."], -) - NODES = [ LANGUAGE, @@ -250,30 +237,30 @@ NODES = [ REFINE_NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, - REFINE_REQUIREMENTS, - REFINE_PROJECT_NAME, - REFINE_PRODUCT_GOALS, - REFINE_USER_STORIES, + REFINED_REQUIREMENTS, + PROJECT_NAME, + REFINED_PRODUCT_GOALS, + REFINED_USER_STORIES, COMPETITIVE_ANALYSIS, COMPETITIVE_QUADRANT_CHART, - INCREMENTAL_DEVELOPMENT_ANALYSIS, - REFINE_REQUIREMENT_POOL, + INCREMENTAL_REQUIREMENT_ANALYSIS, + REFINED_REQUIREMENT_POOL, UI_DESIGN_DRAFT, ANYTHING_UNCLEAR, ] -INCREMENT_PRD_NODES = [INCREMENTAL_DEVELOPMENT_ANALYSIS, REQUIREMENT_POOL] +INCREMENT_PRD_NODES = [INCREMENTAL_REQUIREMENT_ANALYSIS, REQUIREMENT_POOL] WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) REFINE_PRD_NODE = ActionNode.from_children("RefinePRD", REFINE_NODES) -INCREMENT_NODE = ActionNode.from_children("IncrementPRD", INCREMENT_PRD_NODES) +INCREMENTAL_PRD_NODE = ActionNode.from_children("IncrementalPRD", INCREMENT_PRD_NODES) WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON]) WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON]) def main(): # prompt = WRITE_PRD_NODE.compile(context="") - prompt = INCREMENT_NODE.compile(context=REFINE_PRD_CONTEXT) + prompt = INCREMENTAL_PRD_NODE.compile(context=REFINE_PRD_CONTEXT) logger.info(prompt) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 4aa8f209d..d3584e987 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -28,7 +28,10 @@ from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode -from metagpt.actions.write_code_guide_an import CODE_GUIDE_CONTEXT, WriteCodeGuide +from metagpt.actions.write_code_guideline_an import ( + CODE_GUIDELINE_CONTEXT, + WriteCodeGuideline, +) from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, @@ -323,8 +326,8 @@ class Engineer(Role): tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) - node = await WriteCodeGuide().run(context=context) + context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) + node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.json(ensure_ascii=False) return guideline diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index d1cd1b328..76adbf5a7 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -120,7 +120,7 @@ MMC1 = """classDiagram SearchEngine --> Summary Index --> KnowledgeBase""" -MMC1_INC_AND_REFINE = """classDiagram +MMC1_REFINE = """classDiagram class Main { -SearchEngine search_engine +main() str @@ -176,7 +176,7 @@ MMC2 = """sequenceDiagram S-->>SE: return summary SE-->>M: return summary""" -MMC2_INC = """sequenceDiagram +MMC2_REFINE = """sequenceDiagram participant M as Main participant SE as SearchEngine participant I as Index From a24bfdc6c7f98c9d22530701fbb0bb04380fad00 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 2 Jan 2024 22:57:42 +0800 Subject: [PATCH 026/101] Modify style and format --- metagpt/actions/design_api.py | 8 +- metagpt/actions/design_api_an.py | 69 ++++++++--------- metagpt/actions/project_management.py | 4 +- metagpt/actions/project_management_an.py | 51 +++++++------ metagpt/actions/write_code.py | 6 +- ...guide_an.py => write_code_guideline_an.py} | 43 +++++------ metagpt/actions/write_prd.py | 4 +- metagpt/actions/write_prd_an.py | 75 ++++++++----------- metagpt/roles/engineer.py | 11 ++- metagpt/utils/mermaid.py | 4 +- 10 files changed, 131 insertions(+), 144 deletions(-) rename metagpt/actions/{write_code_guide_an.py => write_code_guideline_an.py} (87%) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 0cc3395ed..082474098 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -16,7 +16,7 @@ from typing import Optional from pydantic import Field from metagpt.actions import Action, ActionOutput -from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINE_DESIGN_NODES +from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINED_DESIGN_NODES from metagpt.config import CONFIG from metagpt.const import ( DATA_API_DESIGN_FILE_REPO, @@ -87,7 +87,7 @@ class WriteDesign(Action): async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) - node = await REFINE_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) + node = await REFINED_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) system_design_doc.content = node.instruct_content.json(ensure_ascii=False) return system_design_doc @@ -114,7 +114,7 @@ class WriteDesign(Action): @staticmethod async def _save_data_api_design(design_doc): m = json.loads(design_doc.content) - data_api_design = m.get("Data structures and interfaces") + data_api_design = m.get("Data structures and interfaces") or m.get("Refined Data structures and interfaces") if not data_api_design: return pathname = CONFIG.git_repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("") @@ -124,7 +124,7 @@ class WriteDesign(Action): @staticmethod async def _save_seq_flow(design_doc): m = json.loads(design_doc.content) - seq_flow = m.get("Program call flow") + seq_flow = m.get("Program call flow") or m.get("Refined Program call flow") if not seq_flow: return pathname = CONFIG.git_repo.workdir / Path(SEQ_FLOW_FILE_REPO) / Path(design_doc.filename).with_suffix("") diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 5f17d4656..fcfaff9c3 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -9,7 +9,7 @@ from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger -from metagpt.utils.mermaid import MMC1, MMC1_INC_AND_REFINE, MMC2, MMC2_INC +from metagpt.utils.mermaid import MMC1, MMC1_REFINE, MMC2, MMC2_REFINE IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", @@ -18,7 +18,7 @@ IMPLEMENTATION_APPROACH = ActionNode( example="We will ...", ) -INC_IMPLEMENTATION_APPROACH = ActionNode( +INCREMENTAL_IMPLEMENTATION_APPROACH = ActionNode( key="Incremental Implementation approach", expected_type=str, instruction="Analyze the challenging aspects of the requirements and select a suitable open-source framework. " @@ -26,12 +26,12 @@ INC_IMPLEMENTATION_APPROACH = ActionNode( example="we will ...", ) -REFINE_IMPLEMENTATION_APPROACH = ActionNode( - key="Implementation Approach", +REFINED_IMPLEMENTATION_APPROACH = ActionNode( + key="Refined Implementation Approach", expected_type=str, - instruction="Update and extend the original implementation approach to reflect the evolving challenges and requirements " - "due to incremental development. Provide detailed strategies for incremental steps in the implementation process." - "etain any content unrelated to incremental development for coherence and clarity.", + instruction="Update and extend the original implementation approach to reflect the evolving challenges and " + "requirements due to incremental development. Provide detailed strategies for incremental steps in the " + "implementation process. Retain any content unrelated to incremental development for coherence and clarity.", example="We will refine ...", ) @@ -46,13 +46,13 @@ FILE_LIST = ActionNode( example=["main.py", "game.py"], ) -REFINE_FILE_LIST = ActionNode( - key="File List", +REFINED_FILE_LIST = ActionNode( + key="Refined File List", expected_type=List[str], instruction="Update and expand the original file list, including only relative paths. " "Ensure that the refined file list reflects the evolving structure of the project due to incremental development." - "Only output filename!Do not include comments in the list.", - example=["main.py", "game.py", "utils.py", "new_feature.py"], + "Only output filename! Do not include comments in the list.", + example=["main.py", "game.py", "new_feature.py"], ) DATA_STRUCTURES_AND_INTERFACES = ActionNode( @@ -64,25 +64,25 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode( example=MMC1, ) -INC_DATA_STRUCTURES_AND_INTERFACES = ActionNode( +INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES = ActionNode( key="Incremental Data structures and interfaces", expected_type=str, instruction="Extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Clearly delineate additional " "relationships between classes, maintaining adherence to PEP8 standards. Enhance the level of detail in data " "structures, ensuring a comprehensive API design that seamlessly integrates with the existing structure.", - example=MMC1_INC_AND_REFINE, + example=MMC1_REFINE, ) -REFINE_DATA_STRUCTURES_AND_INTERFACES = ActionNode( - key="Data Structures and Interfaces", +REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( + key="Refined Data Structures and Interfaces", expected_type=str, instruction="Update and extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Delineate additional " "relationships between classes, ensuring clarity and adherence to PEP8 standards. Further enhance the " "detail in data structures for a comprehensive API design that seamlessly integrates with the evolving structure." "Retain any content unrelated to incremental development for coherence and clarity.", - example=MMC1_INC_AND_REFINE, + example=MMC1_REFINE, ) PROGRAM_CALL_FLOW = ActionNode( @@ -93,13 +93,13 @@ PROGRAM_CALL_FLOW = ActionNode( example=MMC2, ) -REFINE_PROGRAM_CALL_FLOW = ActionNode( - key="Program call flow", +REFINED_PROGRAM_CALL_FLOW = ActionNode( + key="Refined Program call flow", expected_type=str, instruction="Extend the existing sequenceDiagram code syntax with detailed information, accurately covering the" "CRUD and initialization of each object. Ensure correct syntax usage and reflect the incremental changes introduced" "in the classes and API defined above.Retain content unrelated to incremental development for coherence and clarity", - example=MMC2_INC, + example=MMC2_REFINE, ) ANYTHING_UNCLEAR = ActionNode( @@ -110,13 +110,13 @@ ANYTHING_UNCLEAR = ActionNode( ) INC_DESIGN_CONTEXT = """ -### Legacy Content +## Legacy Content {old_design} -### New Requirements +## New Requirements {requirements} -### PRD Increment Content +## PRD Increment Content {prd_increment} """ @@ -124,43 +124,44 @@ REFINE_DESIGN_CONTEXT = """ Role: You are a professional Architect tasked with overseeing incremental development. Based on new requirements, review and refine the system design. Integrate existing architecture with incremental design changes, ensuring the refined design encompasses all architectural elements, enhancements, and adjustments. Retain content unrelated to incremental development needs for coherence and clarity. -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_design} -### Design Increment Content +## Design Increment Content {design_increment} """ NODES = [ IMPLEMENTATION_APPROACH, # PROJECT_NAME, - FILE_LIST, + REFINED_FILE_LIST, DATA_STRUCTURES_AND_INTERFACES, PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, ] -INC_NODES = [INC_IMPLEMENTATION_APPROACH, INC_DATA_STRUCTURES_AND_INTERFACES, REFINE_PROGRAM_CALL_FLOW] +INC_NODES = [INCREMENTAL_IMPLEMENTATION_APPROACH, INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES, REFINED_PROGRAM_CALL_FLOW] REFINE_NODES = [ - REFINE_IMPLEMENTATION_APPROACH, + REFINED_IMPLEMENTATION_APPROACH, # PROJECT_NAME, - REFINE_FILE_LIST, - REFINE_DATA_STRUCTURES_AND_INTERFACES, - REFINE_PROGRAM_CALL_FLOW, + FILE_LIST, + REFINED_DATA_STRUCTURES_AND_INTERFACES, + REFINED_PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) -INC_DESIGN_NODES = ActionNode.from_children("Incremental Design API", INC_NODES) -REFINE_DESIGN_NODES = ActionNode.from_children("Refine Design API", REFINE_NODES) +INCREMENTAL_DESIGN_NODES = ActionNode.from_children("Incremental_Design_API", INC_NODES) +REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NODES) def main(): - prompt = REFINE_DESIGN_NODES.compile(context="12313") + prompt = REFINED_DESIGN_NODES.compile(context="...") logger.info(prompt) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 5d2791ea8..f124ba8df 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -17,7 +17,7 @@ from pydantic import Field from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import PM_NODE, REFINE_PM_NODES +from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODES from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -101,7 +101,7 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) - node = await REFINE_PM_NODES.fill(context, self.llm, schema) + node = await REFINED_PM_NODES.fill(context, self.llm, schema) task_doc.content = node.instruct_content.json(ensure_ascii=False) return task_doc diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 27d238e9f..467d26b4e 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -35,18 +35,20 @@ LOGIC_ANALYSIS = ActionNode( ], ) -INC_LOGIC_ANALYSIS = ActionNode( - key="Increment Logic Analysis", +INCREMENTAL_LOGIC_ANALYSIS = ActionNode( + key="Incremental Logic Analysis", expected_type=List[List[str]], - instruction="Provide a list of files with the classes/methods/functions to be implemented or modified incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports.", + instruction="Provide a list of files with the classes/methods/functions to be implemented or modified " + "incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document" + " necessary imports.", example=[ ["new_feature.py", "Introduces NewFeature class and related functions"], ["utils.py", "Modifies existing utility functions to support incremental changes"], ], ) -REFINE_LOGIC_ANALYSIS = ActionNode( - key="Logic Analysis", +REFINED_LOGIC_ANALYSIS = ActionNode( + key="Refined Logic Analysis", expected_type=List[List[str]], instruction="Review and refine the logic analysis by merging the Legacy Content and Incremental Content. " "Provide a comprehensive list of files with classes/methods/functions to be implemented or modified incrementally. " @@ -67,7 +69,7 @@ TASK_LIST = ActionNode( example=["game.py", "main.py"], ) -INC_TASK_LIST = ActionNode( +INCREMENTAL_TASK_LIST = ActionNode( key="Incremental Task list", expected_type=List[str], instruction="Break down the incremental development tasks into a prioritized list of filenames." @@ -76,8 +78,8 @@ INC_TASK_LIST = ActionNode( example=["new_feature.py", "utils.py", "main.py"], ) -REFINE_TASK_LIST = ActionNode( - key="Task list", +REFINED_TASK_LIST = ActionNode( + key="Refined Task list", expected_type=List[str], instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content. " "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" @@ -100,20 +102,20 @@ SHARED_KNOWLEDGE = ActionNode( example="`game.py` contains functions shared across the project.", ) -INC_SHARED_KNOWLEDGE = ActionNode( - key="Increment Shared Knowledge", +INCREMENTAL_SHARED_KNOWLEDGE = ActionNode( + key="Incremental Shared Knowledge", expected_type=str, instruction="Document any new shared knowledge generated during incremental development. This includes common " "utility functions, configuration variables, or any information vital for team collaboration.", example="`new_module.py` introduces shared utility functions for improved code reusability.", ) -REFINE_SHARED_KNOWLEDGE = ActionNode( - key="Shared Knowledge", +REFINED_SHARED_KNOWLEDGE = ActionNode( + key="Refined Shared Knowledge", expected_type=str, - instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental development. " - "This includes common utility functions, configuration variables, or any information vital for team collaboration." - "Retain any content unrelated to incremental development for coherence and clarity.", + instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental " + "development. This includes common utility functions, configuration variables, or any information vital for team " + "collaboration. Retain any content unrelated to incremental development for coherence and clarity.", example="`new_module.py` enhances shared utility functions for improved code reusability and collaboration.", ) @@ -140,13 +142,14 @@ REFINE_PM_CONTEXT = """ Role: You are a professional Project Manager tasked with overseeing incremental development. Based on New Requirements, refine the project context to account for incremental development. Ensure the context offers a comprehensive overview of the project's evolving scope, covering both legacy content and incremental content. Retain any content unrelated to incremental development. -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_tasks} -### Increment Content +## Increment Content {tasks_increment} """ @@ -160,21 +163,21 @@ NODES = [ ANYTHING_UNCLEAR_PM, ] -INC_NODES = [INC_LOGIC_ANALYSIS, INC_TASK_LIST, INC_SHARED_KNOWLEDGE] +INC_NODES = [INCREMENTAL_LOGIC_ANALYSIS, INCREMENTAL_TASK_LIST, INCREMENTAL_SHARED_KNOWLEDGE] REFINE_NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, - REFINE_LOGIC_ANALYSIS, - REFINE_TASK_LIST, + REFINED_LOGIC_ANALYSIS, + REFINED_TASK_LIST, FULL_API_SPEC, - REFINE_SHARED_KNOWLEDGE, + REFINED_SHARED_KNOWLEDGE, ANYTHING_UNCLEAR_PM, ] PM_NODE = ActionNode.from_children("PM_NODE", NODES) -INC_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) -REFINE_PM_NODES = ActionNode.from_children("Refine_PM_NODES", REFINE_NODES) +INCREMENTAL_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) +REFINED_PM_NODES = ActionNode.from_children("Refined_PM_NODES", REFINE_NODES) def main(): diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 33ae2e5b3..4183db19d 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,7 +21,7 @@ from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action -from metagpt.actions.write_code_guide_an import WRITE_CODE_INCREMENT_TEMPLATE +from metagpt.actions.write_code_guideline_an import REFINED_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, @@ -127,7 +127,7 @@ class WriteCode(Action): code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) if guideline: - prompt = WRITE_CODE_INCREMENT_TEMPLATE.format( + prompt = REFINED_TEMPLATE.format( requirement=requirement_doc.content if requirement_doc else "", guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", @@ -164,7 +164,7 @@ class WriteCode(Action): if not task_doc.content: task_doc.content = FileRepository.get_file(filename=task_doc.filename, relative_path=TASK_FILE_REPO) m = json.loads(task_doc.content) - code_filenames = m.get("Task list", []) + code_filenames = m.get("Task list", []) if mode == "normal" else m.get("Refined Task list", []) codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) diff --git a/metagpt/actions/write_code_guide_an.py b/metagpt/actions/write_code_guideline_an.py similarity index 87% rename from metagpt/actions/write_code_guide_an.py rename to metagpt/actions/write_code_guideline_an.py index b21e66098..d68b02d38 100644 --- a/metagpt/actions/write_code_guide_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -7,18 +7,13 @@ """ import asyncio -from pydantic import Field - from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode -from metagpt.llm import LLM -from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.schema import Document GUIDELINE = ActionNode( - key="Code Guideline", + key="Guideline", expected_type=list[str], - instruction="Developing comprehensive and incremental software development plans while providing detailed code guidance.", + instruction="Developing comprehensive and incremental development plans while providing detailed code guideline.", example=[ "Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero.", "Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards.", @@ -28,7 +23,8 @@ GUIDELINE = ActionNode( INCREMENTAL_CHANGE = ActionNode( key="Incremental Change", expected_type=str, - instruction="Write Incremental Change by making a code draft that how to implement incremental development including detailed steps based on the context.", + instruction="Write Incremental Change by making a code draft that how to implement incremental development " + "including detailed steps based on the context.", example="""- calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python ## calculator.py @@ -81,8 +77,8 @@ if __name__ == '__main__': ```""", ) -CODE_GUIDE_CONTEXT = """ -### NOTICE +CODE_GUIDELINE_CONTEXT = """ +NOTICE Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". 1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. 2. Import all referenced classes. @@ -93,20 +89,21 @@ Role: You are a professional software engineer, and your main task is to craft c 7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. 8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. -### Requirement +# Context +## Requirement {requirement} -### Design +## Design {design} -### Tasks +## Tasks {tasks} -### Legacy Code +## Legacy Code {code} """ -WRITE_CODE_INCREMENT_TEMPLATE = """ +REFINED_TEMPLATE = """ NOTICE Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Incremental Change, ensuring the integration of new features. @@ -161,7 +158,7 @@ Role: You are a professional engineer; The main goal is to complete incremental """ CODE_GUIDE_CONTEXT_EXAMPLE = """ -### Legacy Code +# Legacy Code ## main.py from flask import Flask, request, jsonify @@ -194,21 +191,17 @@ class Calculator: GUIDE_NODES = [INCREMENTAL_CHANGE] -WRITE_CODE_GUIDE_NODE = ActionNode.from_children("WriteCodeGuide", GUIDE_NODES) +WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", GUIDE_NODES) -class WriteCodeGuide(Action): - name: str = "WriteCodeGuide" - context: Document = Field(default_factory=Document) - llm: BaseGPTAPI = Field(default_factory=LLM) - +class WriteCodeGuideline(Action): async def run(self, context): - return await WRITE_CODE_GUIDE_NODE.fill(context=context, llm=self.llm, schema="json") + return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") def main(): - action = WriteCodeGuide() - return asyncio.run(action.run(CODE_GUIDE_CONTEXT)) + action = WriteCodeGuideline() + return asyncio.run(action.run(CODE_GUIDELINE_CONTEXT)) if __name__ == "__main__": diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index dc1af0735..a070e7a96 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -24,7 +24,7 @@ from metagpt.actions.action_node import ActionNode from metagpt.actions.fix_bug import FixBug from metagpt.actions.write_prd_an import ( REFINE_PRD_NODE, - REFINE_PRD_SIMPLE_CONTEXT, + REFINE_PRD_TEMPLATE, WP_IS_RELATIVE_NODE, WP_ISSUE_TYPE_NODE, WRITE_PRD_NODE, @@ -139,7 +139,7 @@ class WritePRD(Action): CONFIG.project_name = Path(CONFIG.project_path).name project_name = CONFIG.project_name if CONFIG.project_name else "" - prompt = REFINE_PRD_SIMPLE_CONTEXT.format( + prompt = REFINE_PRD_TEMPLATE.format( requirements=new_requirement_doc.content, old_prd=prd_doc.content, project_name=project_name, diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 79046bb7d..f645225c7 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -31,8 +31,8 @@ ORIGINAL_REQUIREMENTS = ActionNode( example="Create a 2048 game", ) -REFINE_REQUIREMENTS = ActionNode( - key="Original Requirements", +REFINED_REQUIREMENTS = ActionNode( + key="Refined Requirements", expected_type=str, instruction="Update and expand the original user's requirements to reflect the evolving needs of the project." "Retain any content unrelated to incremental development", @@ -46,13 +46,6 @@ PROJECT_NAME = ActionNode( example="game_2048", ) -REFINE_PROJECT_NAME = ActionNode( - key="Project Name", - expected_type=str, - instruction="Update the project name based on the context.", - example="game_2048_new", -) - PRODUCT_GOALS = ActionNode( key="Product Goals", expected_type=List[str], @@ -60,8 +53,8 @@ PRODUCT_GOALS = ActionNode( example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"], ) -REFINE_PRODUCT_GOALS = ActionNode( - key="Product Goals", +REFINED_PRODUCT_GOALS = ActionNode( + key="Refined Product Goals", expected_type=List[str], instruction="Update and expand the original product goals to reflect the evolving needs due to incremental " "development.Ensure that the refined goals align with the current project direction and contribute to its success." @@ -86,11 +79,11 @@ USER_STORIES = ActionNode( ], ) -REFINE_USER_STORIES = ActionNode( - key="User Stories", +REFINED_USER_STORIES = ActionNode( + key="Refined User Stories", expected_type=List[str], instruction="Update and expand the original scenario-based user stories to reflect the evolving needs due to " - "incremental development, no less than 7. Ensure that the refined user stories capture incremental features and " + "incremental development, no less than 5. Ensure that the refined user stories capture incremental features and " "improvements. Retain any content unrelated to incremental development", example=[ "As a player, I want to choose difficulty levels to challenge my skills", @@ -140,6 +133,14 @@ REQUIREMENT_ANALYSIS = ActionNode( example="", ) +INCREMENTAL_REQUIREMENT_ANALYSIS = ActionNode( + key="Incremental Requirement Analysis", + expected_type=List[str], + instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced " + "features for New Requirements.", + example=["Require add/update/modify ..."], +) + REQUIREMENT_POOL = ActionNode( key="Requirement Pool", expected_type=List[List[str]], @@ -147,8 +148,8 @@ REQUIREMENT_POOL = ActionNode( example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) -REFINE_REQUIREMENT_POOL = ActionNode( - key="Requirement Pool", +REFINED_REQUIREMENT_POOL = ActionNode( + key="Refined Requirement Pool", expected_type=List[List[str]], instruction="List no less than 7 requirements with their priority (P0, P1, P2). " "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", @@ -192,25 +193,18 @@ REFINE_PRD_CONTEXT = """ Role: You are a professional Product Manager tasked with overseeing incremental development. Based on New Requirements, output a New PRD that seamlessly integrates both the Legacy Content and the Incremental Content. Ensure the resulting document captures the complete scope of features, enhancements, and retain content unrelated to incremental development needs for coherence and clarity. -### New Project Name -{project_name} - -### New Requirements +# Context +## New Requirements {requirements} -### Legacy Content +## Legacy Content {old_prd} -### PRD Incremental Content +## PRD Incremental Content {prd_increment} - -### Search Information -- """ -REFINE_PRD_SIMPLE_CONTEXT = """ -You are a professional Product Manager tasked with overseeing incremental development. - +REFINE_PRD_TEMPLATE = """ ### New Project Name {project_name} @@ -224,13 +218,6 @@ You are a professional Product Manager tasked with overseeing incremental develo - """ -INCREMENTAL_DEVELOPMENT_ANALYSIS = ActionNode( - key="Requirement Analysis", - expected_type=List[str], - instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced features for New Requirements.", - example=["Require add/update/modify ..."], -) - NODES = [ LANGUAGE, @@ -250,30 +237,30 @@ NODES = [ REFINE_NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, - REFINE_REQUIREMENTS, - REFINE_PROJECT_NAME, - REFINE_PRODUCT_GOALS, - REFINE_USER_STORIES, + REFINED_REQUIREMENTS, + PROJECT_NAME, + REFINED_PRODUCT_GOALS, + REFINED_USER_STORIES, COMPETITIVE_ANALYSIS, COMPETITIVE_QUADRANT_CHART, - INCREMENTAL_DEVELOPMENT_ANALYSIS, - REFINE_REQUIREMENT_POOL, + INCREMENTAL_REQUIREMENT_ANALYSIS, + REFINED_REQUIREMENT_POOL, UI_DESIGN_DRAFT, ANYTHING_UNCLEAR, ] -INCREMENT_PRD_NODES = [INCREMENTAL_DEVELOPMENT_ANALYSIS, REQUIREMENT_POOL] +INCREMENT_PRD_NODES = [INCREMENTAL_REQUIREMENT_ANALYSIS, REQUIREMENT_POOL] WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) REFINE_PRD_NODE = ActionNode.from_children("RefinePRD", REFINE_NODES) -INCREMENT_NODE = ActionNode.from_children("IncrementPRD", INCREMENT_PRD_NODES) +INCREMENTAL_PRD_NODE = ActionNode.from_children("IncrementalPRD", INCREMENT_PRD_NODES) WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON]) WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON]) def main(): # prompt = WRITE_PRD_NODE.compile(context="") - prompt = INCREMENT_NODE.compile(context=REFINE_PRD_CONTEXT) + prompt = INCREMENTAL_PRD_NODE.compile(context=REFINE_PRD_CONTEXT) logger.info(prompt) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 4aa8f209d..fe2d369cb 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -28,7 +28,10 @@ from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode -from metagpt.actions.write_code_guide_an import CODE_GUIDE_CONTEXT, WriteCodeGuide +from metagpt.actions.write_code_guideline_an import ( + CODE_GUIDELINE_CONTEXT, + WriteCodeGuideline, +) from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, @@ -91,7 +94,7 @@ class Engineer(Role): @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: m = json.loads(task_msg.content) - return m.get("Task list") + return m.get("Task list") or m.get("Refined Task list") async def _act_sp_with_cr(self, review=False, guideline="") -> Set[str]: changed_files = set() @@ -323,8 +326,8 @@ class Engineer(Role): tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) - node = await WriteCodeGuide().run(context=context) + context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) + node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.json(ensure_ascii=False) return guideline diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index d1cd1b328..76adbf5a7 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -120,7 +120,7 @@ MMC1 = """classDiagram SearchEngine --> Summary Index --> KnowledgeBase""" -MMC1_INC_AND_REFINE = """classDiagram +MMC1_REFINE = """classDiagram class Main { -SearchEngine search_engine +main() str @@ -176,7 +176,7 @@ MMC2 = """sequenceDiagram S-->>SE: return summary SE-->>M: return summary""" -MMC2_INC = """sequenceDiagram +MMC2_REFINE = """sequenceDiagram participant M as Main participant SE as SearchEngine participant I as Index From b19995f083288863b8f02c474da4fca63e265665 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 14:19:14 +0800 Subject: [PATCH 027/101] Added some test cases --- metagpt/actions/design_api_an.py | 7 +- metagpt/actions/project_management_an.py | 4 +- metagpt/actions/write_code.py | 4 +- metagpt/actions/write_code_guideline_an.py | 328 ++++++++++++++++-- metagpt/actions/write_prd_an.py | 20 +- tests/conftest.py | 3 +- tests/metagpt/actions/test_design_api_an.py | 102 ++++++ .../actions/test_project_management_an.py | 141 ++++++++ .../actions/test_write_code_guideline_an.py | 112 ++++++ tests/metagpt/actions/test_write_prd_an.py | 85 +++++ 10 files changed, 757 insertions(+), 49 deletions(-) create mode 100644 tests/metagpt/actions/test_design_api_an.py create mode 100644 tests/metagpt/actions/test_project_management_an.py create mode 100644 tests/metagpt/actions/test_write_code_guideline_an.py create mode 100644 tests/metagpt/actions/test_write_prd_an.py diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index fcfaff9c3..bcfaf0bfb 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -120,7 +120,7 @@ INC_DESIGN_CONTEXT = """ {prd_increment} """ -REFINE_DESIGN_CONTEXT = """ +MERGE_DESIGN_CONTEXT = """ Role: You are a professional Architect tasked with overseeing incremental development. Based on new requirements, review and refine the system design. Integrate existing architecture with incremental design changes, ensuring the refined design encompasses all architectural elements, enhancements, and adjustments. Retain content unrelated to incremental development needs for coherence and clarity. @@ -148,7 +148,6 @@ INC_NODES = [INCREMENTAL_IMPLEMENTATION_APPROACH, INCREMENTAL_DATA_STRUCTURES_AN REFINE_NODES = [ REFINED_IMPLEMENTATION_APPROACH, - # PROJECT_NAME, FILE_LIST, REFINED_DATA_STRUCTURES_AND_INTERFACES, REFINED_PROGRAM_CALL_FLOW, @@ -161,7 +160,9 @@ REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NOD def main(): - prompt = REFINED_DESIGN_NODES.compile(context="...") + prompt = DESIGN_API_NODE.compile(context="") + logger.info(prompt) + prompt = REFINED_DESIGN_NODES.compile(context="") logger.info(prompt) diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 467d26b4e..a970c05a3 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -138,7 +138,7 @@ INC_PM_CONTEXT = """ {design_increment} """ -REFINE_PM_CONTEXT = """ +MERGE_PM_CONTEXT = """ Role: You are a professional Project Manager tasked with overseeing incremental development. Based on New Requirements, refine the project context to account for incremental development. Ensure the context offers a comprehensive overview of the project's evolving scope, covering both legacy content and incremental content. Retain any content unrelated to incremental development. @@ -183,6 +183,8 @@ REFINED_PM_NODES = ActionNode.from_children("Refined_PM_NODES", REFINE_NODES) def main(): prompt = PM_NODE.compile(context="") logger.info(prompt) + prompt = REFINED_PM_NODES.compile(context="") + logger.info(prompt) if __name__ == "__main__": diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 4183db19d..0ff43905c 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,7 +21,7 @@ from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action -from metagpt.actions.write_code_guideline_an import REFINED_TEMPLATE +from metagpt.actions.write_code_guideline_an import REFINED_CODE_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, @@ -127,7 +127,7 @@ class WriteCode(Action): code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) if guideline: - prompt = REFINED_TEMPLATE.format( + prompt = REFINED_CODE_TEMPLATE.format( requirement=requirement_doc.content if requirement_doc else "", guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index d68b02d38..ee6dc81b7 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -103,7 +103,291 @@ Role: You are a professional software engineer, and your main task is to craft c {code} """ -REFINED_TEMPLATE = """ +CODE_GUIDELINE_CONTEXT_EXAMPLE = """ +NOTICE +Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". +1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. +2. Import all referenced classes. +3. Implement all methods. +4. Add necessary explanation to all methods. +5. Ensure there are no potential bugs. +6. Confirm that the entire project conforms to the tasks proposed by the user. +7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. +8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. + +# Context +## Requirement +Add subtraction, multiplication and division operations to the calculator. +The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator + +## Design +{ + "Refined Implementation Approach": "To accommodate the new requirements, we will extend the existing Python-based calculator application. We will enhance the Tkinter-based UI to include buttons for subtraction, multiplication, and division, alongside the existing addition functionality. We will also implement input validation to handle edge cases such as division by zero. The architecture will be modular, with separate components for the UI, calculation logic, and error handling to maintain simplicity and facilitate future enhancements such as a history feature.", + "File list": [ + "main.py", + "calculator.py", + "interface.py", + "operations.py" + ], + "Refined Data Structures and Interfaces": "classDiagram\n class CalculatorApp {\n +main() None\n }\n class Calculator {\n -result float\n +add(number1: float, number2: float) float\n +subtract(number1: float, number2: float) float\n +multiply(number1: float, number2: float) float\n +divide(number1: float, number2: float) float\n +clear() None\n }\n class Interface {\n -calculator Calculator\n +start() None\n +display_result(result: float) None\n +get_input() float\n +show_error(message: str) None\n +update_operation(operation: str) None\n }\n class Operations {\n +perform_operation(operation: str, number1: float, number2: float) float\n }\n CalculatorApp --> Interface\n Interface --> Calculator\n Calculator --> Operations", + "Refined Program call flow": "sequenceDiagram\n participant CA as CalculatorApp\n participant I as Interface\n participant C as Calculator\n participant O as Operations\n CA->>I: start()\n I->>I: get_input()\n I->>I: update_operation(operation)\n loop For Each Operation\n I->>C: perform_operation(operation, number1, number2)\n C->>O: perform_operation(operation, number1, number2)\n O-->>C: return result\n C-->>I: return result\n I->>I: display_result(result)\n end\n I->>I: show_error(message)", + "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." +} + +## Tasks +{ + "Required Python packages": [ + "tkinter" + ], + "Required Other language third-party packages": [ + "No third-party dependencies required" + ], + "Refined Logic Analysis": [ + [ + "main.py", + "Entry point of the application, creates an instance of the Interface class and starts the application." + ], + [ + "calculator.py", + "Contains the Calculator class with add, subtract, multiply, divide and clear methods for performing arithmetic operations." + ], + [ + "interface.py", + "Contains the Interface class responsible for the GUI, interacts with Calculator for the logic and displays results or errors." + ], + [ + "operations.py", + "Contains the Operations class with perform_operation method that delegates the arithmetic operation based on the operation argument." + ] + ], + "Refined Task list": [ + "operations.py", + "calculator.py", + "interface.py", + "main.py" + ], + "Full API spec": "", + "Refined Shared Knowledge": "`interface.py` will use the Calculator class from `calculator.py` to perform operations and display results. `main.py` will be the starting point that initializes the Interface. `calculator.py` will now also interact with `operations.py` to perform the arithmetic operations.", + "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." +} + +## Legacy Code +{code} +""" + +CODE_GUIDELINE_SCRIPT_EXAMPLE = """ +----- calculator.py +```## calculator.py + +class Calculator: + def __init__(self): + self.result = 0.0 # Default value for the result + + def add(self, number1: float, number2: float) -> float: + ''' + Adds two numbers and returns the result. + + Args: + number1 (float): The first number to add. + number2 (float): The second number to add. + + Returns: + float: The sum of number1 and number2. + ''' + self.result = number1 + number2 + return self.result + + def clear(self) -> None: + ''' + Clears the result to its default value. + ''' + self.result = 0.0 +``` + +---- interface.py +```## interface.py +import tkinter as tk +from calculator import Calculator + +class Interface: + def __init__(self): + self.calculator = Calculator() + self.root = tk.Tk() + self.root.title("Calculator") + self.create_widgets() + + def create_widgets(self): + self.result_var = tk.StringVar() + self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) + self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') + + self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') + + self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') + + self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) + self.add_button.grid(row=2, column=0, sticky='nsew') + + self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) + self.clear_button.grid(row=2, column=1, sticky='nsew') + + self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) + self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') + + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def start(self): + self.root.mainloop() + + def display_result(self, result: float): + self.result_var.set(str(result)) + + def get_input(self): + try: + number1 = float(self.entry_number1.get()) + number2 = float(self.entry_number2.get()) + return number1, number2 + except ValueError: + self.show_error("Invalid input! Please enter valid numbers.") + return None, None + + def add(self): + number1, number2 = self.get_input() + if number1 is not None and number2 is not None: + result = self.calculator.add(number1, number2) + self.display_result(result) + + def clear(self): + self.entry_number1.delete(0, tk.END) + self.entry_number2.delete(0, tk.END) + self.result_var.set("") + + def show_error(self, message: str): + tk.messagebox.showerror("Error", message) + +# This code is meant to be used as a module and not as a standalone script. +# The Interface class will be instantiated and started by the main.py file. +``` + +---- main.py +```## main.py +from interface import Interface + + +class CalculatorApp: + @staticmethod + def main(): + interface = Interface() + interface.start() + + +if __name__ == "__main__": + CalculatorApp.main() +``` +""" + +REFINE_CODE_SCRIPT_EXAMPLE = """ +----- calculator.py +```## calculator.py + +class Calculator: + def __init__(self): + self.result = 0.0 # Default value for the result + + def add(self, number1: float, number2: float) -> float: + ''' + Adds two numbers and returns the result. + + Args: + number1 (float): The first number to add. + number2 (float): The second number to add. + + Returns: + float: The sum of number1 and number2. + ''' + self.result = number1 + number2 + return self.result + + def clear(self) -> None: + ''' + Clears the result to its default value. + ''' + self.result = 0.0 +``` + +---- Now, interface.py to be rewritten +```## interface.py +import tkinter as tk +from calculator import Calculator + +class Interface: + def __init__(self): + self.calculator = Calculator() + self.root = tk.Tk() + self.root.title("Calculator") + self.create_widgets() + + def create_widgets(self): + self.result_var = tk.StringVar() + self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) + self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') + + self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') + + self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') + + self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) + self.add_button.grid(row=2, column=0, sticky='nsew') + + self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) + self.clear_button.grid(row=2, column=1, sticky='nsew') + + self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) + self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') + + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def start(self): + self.root.mainloop() + + def display_result(self, result: float): + self.result_var.set(str(result)) + + def get_input(self): + try: + number1 = float(self.entry_number1.get()) + number2 = float(self.entry_number2.get()) + return number1, number2 + except ValueError: + self.show_error("Invalid input! Please enter valid numbers.") + return None, None + + def add(self): + number1, number2 = self.get_input() + if number1 is not None and number2 is not None: + result = self.calculator.add(number1, number2) + self.display_result(result) + + def clear(self): + self.entry_number1.delete(0, tk.END) + self.entry_number2.delete(0, tk.END) + self.result_var.set("") + + def show_error(self, message: str): + tk.messagebox.showerror("Error", message) + +# This code is meant to be used as a module and not as a standalone script. +# The Interface class will be instantiated and started by the main.py file. +``` +""" + +REFINED_CODE_TEMPLATE = """ NOTICE Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Incremental Change, ensuring the integration of new features. @@ -157,38 +441,6 @@ Role: You are a professional engineer; The main goal is to complete incremental 9. Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file when rewriting "{filename} to be rewritten". """ -CODE_GUIDE_CONTEXT_EXAMPLE = """ -# Legacy Code -## main.py - -from flask import Flask, request, jsonify -from calculator import Calculator - -app = Flask(__name__) -calculator = Calculator() - -@app.route('/add_numbers', methods=['POST']) -def add_numbers(): - data = request.get_json() - num1 = data.get('num1', 0) - num2 = data.get('num2', 0) - result = calculator.add_numbers(num1, num2) - return jsonify({'result': result}), 200 - -if __name__ == '__main__': - app.run() - -## calculator.py - -class Calculator: - def __init__(self, num1: int = 0, num2: int = 0): - self.num1 = num1 - self.num2 = num2 - - def add_numbers(self, num1: int, num2: int) -> int: - return num1 + num2 -""" - GUIDE_NODES = [INCREMENTAL_CHANGE] WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", GUIDE_NODES) @@ -199,10 +451,12 @@ class WriteCodeGuideline(Action): return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") -def main(): - action = WriteCodeGuideline() - return asyncio.run(action.run(CODE_GUIDELINE_CONTEXT)) +async def main(): + write_code_guideline = WriteCodeGuideline() + node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE.format(code=CODE_GUIDELINE_SCRIPT_EXAMPLE)) + guideline = node.instruct_content.json(ensure_ascii=False) + print(guideline) if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index f645225c7..33008d3fe 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -141,6 +141,15 @@ INCREMENTAL_REQUIREMENT_ANALYSIS = ActionNode( example=["Require add/update/modify ..."], ) +REFINED_REQUIREMENT_ANALYSIS = ActionNode( + key="Refined Requirement Analysis", + expected_type=List[str], + instruction="Review and refine the existing requirement analysis to align with the evolving needs of the project " + "due to incremental development. Ensure the analysis comprehensively covers the new features and enhancements " + "required for the refined project scope.", + example=["Require add/update/modify ..."], +) + REQUIREMENT_POOL = ActionNode( key="Requirement Pool", expected_type=List[List[str]], @@ -151,7 +160,7 @@ REQUIREMENT_POOL = ActionNode( REFINED_REQUIREMENT_POOL = ActionNode( key="Refined Requirement Pool", expected_type=List[List[str]], - instruction="List no less than 7 requirements with their priority (P0, P1, P2). " + instruction="List no less than 5 requirements with their priority (P0, P1, P2). " "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) @@ -189,7 +198,7 @@ REASON = ActionNode( ) -REFINE_PRD_CONTEXT = """ +INCREMENTAL_PRD_CONTEXT = """ Role: You are a professional Product Manager tasked with overseeing incremental development. Based on New Requirements, output a New PRD that seamlessly integrates both the Legacy Content and the Incremental Content. Ensure the resulting document captures the complete scope of features, enhancements, and retain content unrelated to incremental development needs for coherence and clarity. @@ -243,7 +252,7 @@ REFINE_NODES = [ REFINED_USER_STORIES, COMPETITIVE_ANALYSIS, COMPETITIVE_QUADRANT_CHART, - INCREMENTAL_REQUIREMENT_ANALYSIS, + REFINED_REQUIREMENT_ANALYSIS, REFINED_REQUIREMENT_POOL, UI_DESIGN_DRAFT, ANYTHING_UNCLEAR, @@ -259,8 +268,9 @@ WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, R def main(): - # prompt = WRITE_PRD_NODE.compile(context="") - prompt = INCREMENTAL_PRD_NODE.compile(context=REFINE_PRD_CONTEXT) + prompt = WRITE_PRD_NODE.compile(context="") + logger.info(prompt) + prompt = REFINE_PRD_NODE.compile(context="") logger.info(prompt) diff --git a/tests/conftest.py b/tests/conftest.py index b22e43e79..50fbf556c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,7 +86,8 @@ def loguru_caplog(caplog): # init & dispose git repo -@pytest.fixture(scope="session", autouse=True) +# @pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") def setup_and_teardown_git_repo(request): CONFIG.git_repo = GitRepository(local_path=DEFAULT_WORKSPACE_ROOT / "unittest") diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py new file mode 100644 index 000000000..e50288640 --- /dev/null +++ b/tests/metagpt/actions/test_design_api_an.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/03 +@Author : mannaandpoem +@File : test_design_api_an.py.py +""" +import pytest + +from metagpt.actions.design_api_an import REFINED_DESIGN_NODES +from metagpt.provider import OpenAIGPTAPI + +CONTEXT = """ +### Legacy Content +{ + "Implementation approach": "We will use Python with the Pygame library to develop the core mechanics of the 2048 game. The game will feature a simple and intuitive user interface, score tracking with high score memory, and an undo move feature. We'll ensure the game has visually appealing graphics and animations while maintaining a minimalist design. The undo feature will allow a single move to be undone without affecting the score, to keep the implementation straightforward.", + "File list": [ + "main.py", + "game.py", + "ui.py", + "constants.py" + ], + "Data structures and interfaces": "classDiagram\n class Main {\n +pygame: PygameInstance\n +game: Game\n +run() void\n }\n class Game {\n -grid: list\n -current_score: int\n -high_score: int\n -last_move: list\n +move(direction: str) bool\n +undo() bool\n +check_game_over() bool\n +reset_game() void\n }\n class UI {\n -screen: PygameSurface\n -font: PygameFont\n +draw_grid(grid: list) void\n +display_score(current_score: int, high_score: int) void\n +show_game_over() void\n +show_undo_button() void\n }\n class Constants {\n +GRID_SIZE: int\n +WINDOW_WIDTH: int\n +WINDOW_HEIGHT: int\n +BACKGROUND_COLOR: tuple\n +TILE_COLORS: dict\n }\n Main --> Game\n Main --> UI\n Game --> Constants\n UI --> Constants", + "Program call flow": "sequenceDiagram\n participant M as Main\n participant G as Game\n participant U as UI\n M->>G: create instance\n M->>U: create instance\n loop game loop\n M->>U: draw_grid(G.grid)\n M->>U: display_score(G.current_score, G.high_score)\n M->>U: show_undo_button()\n M->>G: move(direction)\n alt if move is valid\n G-->>M: return true\n else if move is invalid\n G-->>M: return false\n end\n alt if undo is triggered\n M->>G: undo()\n G-->>M: return true\n else no undo\n G-->>M: return false\n end\n alt if game over\n M->>U: show_game_over()\n M->>G: reset_game()\n end\n end", + "Anything UNCLEAR": "The specifics of the scoring system and how the high score is stored and retrieved need to be clarified. Additionally, the exact graphical assets and animations for the game are not specified." +} + +### New Requirements +{ + "Language": "en_us", + "Programming Language": "Python", + "Refined Requirements": "Update the py2048_game to have a larger 8x8 grid and a new winning score target of 4096, while maintaining the core mechanics and user-friendly interface of the original 2048 game.", + "Project Name": "py2048_game", + "Refined Product Goals": [ + "Develop an enhanced version of the 2048 game with a larger grid and higher score target to provide a new challenge to players", + "Ensure the game remains visually appealing and maintains a consistent theme with the added complexity", + "Implement smooth and responsive game controls suitable for an 8x8 grid interface" + ], + "Refined User Stories": [ + "As a player, I want to experience a clear and simple interface on an 8x8 grid so that I can focus on the gameplay", + "As a player, I want to aim for a higher score target of 4096 to challenge my skills further", + "As a player, I want to see my current and high scores to track my progress on the new larger grid", + "As a player, I want the option to undo my last move to improve my strategy on the 8x8 grid", + "As a player, I want the game to perform smoothly despite the increased complexity of the larger grid" + ], + "Competitive Analysis": [ + "2048 Original: Classic gameplay with minimalistic design, but lacks modern features", + "2048 by Gabriele Cirulli: Open-source version with clean UI, but no additional features", + "2048 Hex: Unique hexagon board, providing a different challenge", + "2048 Multiplayer: Allows playing against others, but the interface is cluttered", + "2048 with AI: Includes AI challenge mode, but the AI is often too difficult for casual players", + "2048.io: Combines 2048 gameplay with .io style, though it can be overwhelming for new players", + "2048 Animated: Features animations, but has performance issues on some devices" + ], + "Competitive Quadrant Chart": "quadrantChart\n title \"2048 Game Market Positioning\"\n x-axis \"Basic Features\" --> \"Advanced Features\"\n y-axis \"Low User Engagement\" --> \"High User Engagement\"\n quadrant-1 \"Niche Innovators\"\n quadrant-2 \"Market Leaders\"\n quadrant-3 \"Emerging Contenders\"\n quadrant-4 \"Falling Behind\"\n \"2048 Original\": [0.2, 0.7]\n \"2048 by Gabriele Cirulli\": [0.3, 0.8]\n \"2048 Hex\": [0.5, 0.4]\n \"2048 Multiplayer\": [0.6, 0.6]\n \"2048 with AI\": [0.7, 0.5]\n \"2048.io\": [0.4, 0.3]\n \"2048 Animated\": [0.3, 0.2]\n \"Our Target Product\": [0.9, 0.9]", + "Incremental Requirement Analysis": [ + "Adjust the game logic to accommodate an 8x8 grid while ensuring performance remains optimal", + "Update the UI to fit the larger grid and include visual cues for the new score target", + "Enhance the scoring system to support the new target of 4096", + "Ensure the undo feature is adapted to work with the larger grid and increased game complexity", + "Test the game thoroughly to maintain a smooth and responsive experience on the new 8x8 grid" + ], + "Refined Requirement Pool": [ + [ + "P0", + "Expand the game grid to 8x8 and adjust the core mechanics accordingly" + ], + [ + "P0", + "Increase the score target to 4096 and update the scoring system" + ], + [ + "P1", + "Redesign the user interface to accommodate the larger grid size" + ], + [ + "P1", + "Ensure the undo move feature is compatible with the new grid and score target" + ], + [ + "P2", + "Optimize game performance for the increased complexity of an 8x8 grid" + ], + [ + "P2", + "Maintain a visually appealing and consistent theme with the updated game features" + ] + ], + "UI Design draft": "The UI will be updated to feature an 8x8 grid while maintaining a minimalist design. The main game screen will display the larger game grid, current score, high score, and an undo button. The color scheme and transitions will be adapted to ensure clarity and pleasant aesthetics despite the increased grid size.", + "Anything UNCLEAR": "The specifics of how the undo feature should work with the larger grid size and whether there should be any limitations on its use need to be clarified." +} +""" + + +llm = OpenAIGPTAPI() + + +@pytest.mark.asyncio +async def test_write_design_an(): + node = await REFINED_DESIGN_NODES.fill(CONTEXT, llm) + assert node.instruct_content + assert "Refined Data Structures and Interfaces" in node.instruct_content.json(ensure_ascii=False) diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py new file mode 100644 index 000000000..86c5e3685 --- /dev/null +++ b/tests/metagpt/actions/test_project_management_an.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/03 +@Author : mannaandpoem +@File : test_project_management_an.py.py +""" +import pytest + +from metagpt.actions.project_management_an import REFINED_PM_NODES +from metagpt.provider import OpenAIGPTAPI + +CONTEXT = """ +### Legacy Content +{ + "Required Python packages": [ + "pygame==2.0.1" + ], + "Required Other language third-party packages": [ + "No third-party dependencies required" + ], + "Logic Analysis": [ + [ + "constants.py", + "Contains all the constants like GRID_SIZE, WINDOW_WIDTH, WINDOW_HEIGHT, BACKGROUND_COLOR, TILE_COLORS" + ], + [ + "game.py", + "Contains Game class with methods for game logic such as move, undo, check_game_over, and reset_game" + ], + [ + "ui.py", + "Contains UI class responsible for drawing the grid, displaying scores, showing game over, and undo button" + ], + [ + "main.py", + "Contains Main class which initializes the game loop and orchestrates the interactions between Game and UI classes" + ] + ], + "Task list": [ + "constants.py", + "game.py", + "ui.py", + "main.py" + ], + "Full API spec": "", + "Shared Knowledge": "`constants.py` contains constants shared across `game.py` and `ui.py`. The Main class in `main.py` acts as the controller orchestrating the game flow and UI updates.", + "Anything UNCLEAR": "The specifics of the scoring system and how the high score is stored and retrieved need to be clarified. Additionally, the exact graphical assets and animations for the game are not specified." +} + +### New Requirements +{ + "Refined Implementation Approach": "We will refine our implementation approach to accommodate the new 8x8 grid and the increased winning score target of 4096. This will involve optimizing the game's core logic to handle the larger grid size efficiently and updating the scoring system. We will also enhance the UI to ensure it remains user-friendly and visually appealing with the new grid. The undo feature will be adapted to work seamlessly with the increased complexity of the game.", + "File list": [ + "main.py", + "game.py", + "ui.py", + "constants.py" + ], + "Refined Data Structures and Interfaces": "classDiagram + class Main { + +pygame: PygameInstance + +game: Game + +ui: UI + +run() void + } + class Game { + -grid: list + -current_score: int + -high_score: int + -last_move: list + -target_score: int + +__init__(grid_size: int, target_score: int) + +move(direction: str) bool + +undo() bool + +check_game_over() bool + +reset_game() void + +update_score(value: int) void + } + class UI { + -screen: PygameSurface + -font: PygameFont + +__init__(screen_size: tuple, grid_size: int) + +draw_grid(grid: list) void + +display_score(current_score: int, high_score: int) void + +show_game_over() void + +show_undo_button() void + +update_ui_for_larger_grid() void + } + class Constants { + +GRID_SIZE: int = 8 + +TARGET_SCORE: int = 4096 + +WINDOW_WIDTH: int + +WINDOW_HEIGHT: int + +BACKGROUND_COLOR: tuple + +TILE_COLORS: dict + } + Main --> Game + Main --> UI + Game --> Constants + UI --> Constants", + "Refined Program call flow": "sequenceDiagram + participant M as Main + participant G as Game + participant U as UI + M->>G: create instance(grid_size: Constants.GRID_SIZE, target_score: Constants.TARGET_SCORE) + M->>U: create instance(screen_size: (Constants.WINDOW_WIDTH, Constants.WINDOW_HEIGHT), grid_size: Constants.GRID_SIZE) + loop game loop + M->>U: draw_grid(G.grid) + M->>U: display_score(G.current_score, G.high_score) + M->>U: show_undo_button() + M->>G: move(direction) + alt if move is valid + G-->>M: return true + M->>G: update_score(value) + else if move is invalid + G-->>M: return false + end + alt if undo is triggered + M->>G: undo() + G-->>M: return true + else no undo + G-->>M: return false + end + alt if game over + M->>U: show_game_over() + M->>G: reset_game() + end + end", + "Anything UNCLEAR": "It remains unclear if the undo feature should have limitations on its use, such as a maximum number of undos per game or if it should be available without restriction. Further clarification on this aspect would be beneficial." +} +""" + +llm = OpenAIGPTAPI() + + +@pytest.mark.asyncio +async def test_project_management_an(): + node = await REFINED_PM_NODES.fill(CONTEXT, llm) + assert node.instruct_content + assert "Refined Logic Analysis" in node.instruct_content.json(ensure_ascii=False) diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py new file mode 100644 index 000000000..c9fb78e0e --- /dev/null +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/03 +@Author : mannaandpoem +@File : test_write_code_guideline_an.py.py +""" +import pytest + +from metagpt.actions import WriteCode +from metagpt.actions.write_code_guideline_an import ( + CODE_GUIDELINE_CONTEXT, + CODE_GUIDELINE_SCRIPT_EXAMPLE, + REFINE_CODE_SCRIPT_EXAMPLE, + REFINED_CODE_TEMPLATE, + WriteCodeGuideline, +) +from metagpt.provider import OpenAIGPTAPI + +REQUIREMENT_EXAMPLE = """Add subtraction, multiplication and division operations to the calculator. +The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator +""" + +DESIGN_EXAMPLE = """ +{ + "Refined Implementation Approach": "To accommodate the new requirements, we will extend the existing Python-based calculator application. We will enhance the Tkinter-based UI to include buttons for subtraction, multiplication, and division, alongside the existing addition functionality. We will also implement input validation to handle edge cases such as division by zero. The architecture will be modular, with separate components for the UI, calculation logic, and error handling to maintain simplicity and facilitate future enhancements such as a history feature.", + "File list": [ + "main.py", + "calculator.py", + "interface.py", + "operations.py" + ], + "Refined Data Structures and Interfaces": "classDiagram\n class CalculatorApp {\n +main() None\n }\n class Calculator {\n -result float\n +add(number1: float, number2: float) float\n +subtract(number1: float, number2: float) float\n +multiply(number1: float, number2: float) float\n +divide(number1: float, number2: float) float\n +clear() None\n }\n class Interface {\n -calculator Calculator\n +start() None\n +display_result(result: float) None\n +get_input() float\n +show_error(message: str) None\n +update_operation(operation: str) None\n }\n class Operations {\n +perform_operation(operation: str, number1: float, number2: float) float\n }\n CalculatorApp --> Interface\n Interface --> Calculator\n Calculator --> Operations", + "Refined Program call flow": "sequenceDiagram\n participant CA as CalculatorApp\n participant I as Interface\n participant C as Calculator\n participant O as Operations\n CA->>I: start()\n I->>I: get_input()\n I->>I: update_operation(operation)\n loop For Each Operation\n I->>C: perform_operation(operation, number1, number2)\n C->>O: perform_operation(operation, number1, number2)\n O-->>C: return result\n C-->>I: return result\n I->>I: display_result(result)\n end\n I->>I: show_error(message)", + "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." +} +""" + +TASKS_EXAMPLE = """ +{ + "Required Python packages": [ + "tkinter" + ], + "Required Other language third-party packages": [ + "No third-party dependencies required" + ], + "Refined Logic Analysis": [ + [ + "main.py", + "Entry point of the application, creates an instance of the Interface class and starts the application." + ], + [ + "calculator.py", + "Contains the Calculator class with add, subtract, multiply, divide and clear methods for performing arithmetic operations." + ], + [ + "interface.py", + "Contains the Interface class responsible for the GUI, interacts with Calculator for the logic and displays results or errors." + ], + [ + "operations.py", + "Contains the Operations class with perform_operation method that delegates the arithmetic operation based on the operation argument." + ] + ], + "Refined Task list": [ + "operations.py", + "calculator.py", + "interface.py", + "main.py" + ], + "Full API spec": "", + "Refined Shared Knowledge": "`interface.py` will use the Calculator class from `calculator.py` to perform operations and display results. `main.py` will be the starting point that initializes the Interface. `calculator.py` will now also interact with `operations.py` to perform the arithmetic operations.", + "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." +} +""" + +INCREMENTAL_CHANGE_EXAMPLE = """ +{ + "Incremental Change": "- operations.py: Implement the Operations class with a method to perform the requested arithmetic operation. This class will be used by the Calculator class to execute the operations.\n```python\n## operations.py\nclass Operations:\n @staticmethod\n def perform_operation(operation: str, number1: float, number2: float) -> float:\n if operation == 'add':\n return number1 + number2\n elif operation == 'subtract':\n return number1 - number2\n elif operation == 'multiply':\n return number1 * number2\n elif operation == 'divide':\n if number2 == 0:\n raise ValueError('Cannot divide by zero')\n return number1 / number2\n else:\n raise ValueError('Invalid operation')\n```\n\n- calculator.py: Extend the Calculator class to include methods for subtraction, multiplication, and division. These methods will utilize the Operations class to perform the actual calculations.\n```python\n## calculator.py\nfrom operations import Operations\nclass Calculator:\n ...\n def subtract(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('subtract', number1, number2)\n\n def multiply(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('multiply', number1, number2)\n\n def divide(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('divide', number1, number2)\n```\n\n- interface.py: Update the Interface class to include buttons for subtraction, multiplication, and division, and link them to the corresponding methods in the Calculator class. Also, handle the display of errors such as division by zero.\n```python\n## interface.py\nimport tkinter as tk\nfrom tkinter import messagebox\nfrom calculator import Calculator\n...\nclass Interface:\n ...\n def create_widgets(self):\n ...\n self.subtract_button = tk.Button(self.root, text='-', command=self.subtract, font=('Arial', 18))\n self.subtract_button.grid(row=3, column=0, sticky='nsew')\n\n self.multiply_button = tk.Button(self.root, text='*', command=self.multiply, font=('Arial', 18))\n self.multiply_button.grid(row=3, column=1, sticky='nsew')\n\n self.divide_button = tk.Button(self.root, text='/', command=self.divide, font=('Arial', 18))\n self.divide_button.grid(row=3, column=2, sticky='nsew')\n ...\n\n def subtract(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.subtract(number1, number2)\n self.display_result(result)\n\n def multiply(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.multiply(number1, number2)\n self.display_result(result)\n\n def divide(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n try:\n result = self.calculator.divide(number1, number2)\n except ValueError as e:\n self.show_error(str(e))\n return\n self.display_result(result)\n```\n\n- main.py: No changes needed in main.py as it serves as the entry point and will run the updated Interface class.\n```python\n## main.py\nfrom interface import Interface\n...\n```\n\nNote: Ensure that the new operations buttons in the Interface class are properly arranged and that the grid layout is adjusted accordingly. Also, make sure to import the messagebox module from tkinter for error handling." +} +""" + +llm = OpenAIGPTAPI() + + +@pytest.mark.asyncio +async def test_write_code_guideline_an(): + write_code_guideline = WriteCodeGuideline() + context = CODE_GUIDELINE_CONTEXT.format( + requirement=REQUIREMENT_EXAMPLE, design=DESIGN_EXAMPLE, tasks=TASKS_EXAMPLE, code=CODE_GUIDELINE_SCRIPT_EXAMPLE + ) + node = await write_code_guideline.run(context=context) + assert node.instruct_content + assert "Incremental Change" in node.instruct_content.json(ensure_ascii=False) + + +@pytest.mark.asyncio +async def test_refine_code(): + prompt = REFINED_CODE_TEMPLATE.format( + requirement=REQUIREMENT_EXAMPLE, + guideline=INCREMENTAL_CHANGE_EXAMPLE, + design=DESIGN_EXAMPLE, + tasks=TASKS_EXAMPLE, + code=REFINE_CODE_SCRIPT_EXAMPLE, + logs="", + feedback="", + filename="interface.py", + summary_log="", + ) + code = await WriteCode().write_code(prompt=prompt) + assert code + assert "def create_widgets" in code diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py new file mode 100644 index 000000000..a7424bf49 --- /dev/null +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/03 +@Author : mannaandpoem +@File : test_write_prd_an.py.py +""" +import pytest + +from metagpt.actions.write_prd_an import REFINE_PRD_NODE +from metagpt.provider import OpenAIGPTAPI + +CONTEXT = """ +### New Project Name +py2048_game + +### New Requirements +Changed score target for 2048 game from 2048 to 4096. +Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8. + +### Legacy Content +{ + "Language": "en_us", + "Programming Language": "Python", + "Original Requirements": "make a simple 2048 game based on pygame", + "Project Name": "pygame_2048", + "Product Goals": [ + "Develop a user-friendly and intuitive 2048 game", + "Ensure the game is visually appealing and maintains a consistent theme", + "Implement smooth and responsive game controls" + ], + "User Stories": [ + "As a player, I want to experience a clear and simple interface so that I can focus on the gameplay", + "As a player, I want to see my current and high scores to track my progress", + "As a player, I want the option to undo my last move to improve my strategy" + ], + "Competitive Analysis": [ + "2048 Original: Classic gameplay with minimalistic design, but lacks modern features", + "2048 by Gabriele Cirulli: Open-source version with clean UI, but no additional features", + "2048 Hex: Unique hexagon board, providing a different challenge", + "2048 Multiplayer: Allows playing against others, but the interface is cluttered", + "2048 with AI: Includes AI challenge mode, but the AI is often too difficult for casual players", + "2048.io: Combines 2048 gameplay with .io style, though it can be overwhelming for new players", + "2048 Animated: Features animations, but has performance issues on some devices" + ], + "Competitive Quadrant Chart": "quadrantChart\n title \"2048 Game Market Positioning\"\n x-axis \"Basic Features\" --> \"Advanced Features\"\n y-axis \"Low User Engagement\" --> \"High User Engagement\"\n quadrant-1 \"Niche Innovators\"\n quadrant-2 \"Market Leaders\"\n quadrant-3 \"Emerging Contenders\"\n quadrant-4 \"Falling Behind\"\n \"2048 Original\": [0.2, 0.7]\n \"2048 by Gabriele Cirulli\": [0.3, 0.8]\n \"2048 Hex\": [0.5, 0.4]\n \"2048 Multiplayer\": [0.6, 0.6]\n \"2048 with AI\": [0.7, 0.5]\n \"2048.io\": [0.4, 0.3]\n \"2048 Animated\": [0.3, 0.2]\n \"Our Target Product\": [0.8, 0.9]", + "Requirement Analysis": "The game should be simple yet engaging, with a focus on smooth performance and an intuitive user interface. High scores and undo functionality are important to users for a competitive and strategic gameplay experience. Aesthetic appeal and a consistent theme will also contribute to the game's success.", + "Requirement Pool": [ + [ + "P0", + "Develop core 2048 game mechanics using pygame" + ], + [ + "P0", + "Design a clean and intuitive user interface" + ], + [ + "P1", + "Implement score tracking with high score memory" + ], + [ + "P1", + "Add undo move feature for enhanced gameplay strategy" + ], + [ + "P2", + "Create visually appealing graphics and animations" + ] + ], + "UI Design draft": "The UI will feature a minimalist design with a focus on ease of use. The main game screen will display the game grid, current score, high score, and an undo button. The color scheme will be consistent and pleasant to the eye, with smooth transitions for tile movements.", + "Anything UNCLEAR": "The specifics of the undo feature need to be clarified, such as how many moves can be undone and whether it affects the scoring." +} + +### Search Information +- +""" + +llm = OpenAIGPTAPI() + + +@pytest.mark.asyncio +async def test_write_prd_an(): + node = await REFINE_PRD_NODE.fill(CONTEXT, llm) + assert node.instruct_content + assert "Refined Requirement Pool" in node.instruct_content.json(ensure_ascii=False) From 0ed2c56035b4e710d971a766c44c374165e375da Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 14:41:49 +0800 Subject: [PATCH 028/101] Modify to make LLM a fixture in test cases --- tests/metagpt/actions/test_design_api_an.py | 4 +++- tests/metagpt/actions/test_project_management_an.py | 7 +++++-- tests/metagpt/actions/test_write_code_guideline_an.py | 5 +---- tests/metagpt/actions/test_write_prd_an.py | 7 +++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index e50288640..32d6c11b0 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -92,7 +92,9 @@ CONTEXT = """ """ -llm = OpenAIGPTAPI() +@pytest.fixture() +def llm(): + return OpenAIGPTAPI() @pytest.mark.asyncio diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 86c5e3685..116dffe83 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -131,11 +131,14 @@ CONTEXT = """ } """ -llm = OpenAIGPTAPI() + +@pytest.fixture() +def llm(): + return OpenAIGPTAPI() @pytest.mark.asyncio -async def test_project_management_an(): +async def test_project_management_an(llm): node = await REFINED_PM_NODES.fill(CONTEXT, llm) assert node.instruct_content assert "Refined Logic Analysis" in node.instruct_content.json(ensure_ascii=False) diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index c9fb78e0e..602145ce8 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -7,7 +7,7 @@ """ import pytest -from metagpt.actions import WriteCode +from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_guideline_an import ( CODE_GUIDELINE_CONTEXT, CODE_GUIDELINE_SCRIPT_EXAMPLE, @@ -15,7 +15,6 @@ from metagpt.actions.write_code_guideline_an import ( REFINED_CODE_TEMPLATE, WriteCodeGuideline, ) -from metagpt.provider import OpenAIGPTAPI REQUIREMENT_EXAMPLE = """Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator @@ -80,8 +79,6 @@ INCREMENTAL_CHANGE_EXAMPLE = """ } """ -llm = OpenAIGPTAPI() - @pytest.mark.asyncio async def test_write_code_guideline_an(): diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index a7424bf49..ef0dd2eff 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -75,11 +75,14 @@ Please change the game's score target from 2048 to 4096, and change the interfac - """ -llm = OpenAIGPTAPI() + +@pytest.fixture() +def llm(): + return OpenAIGPTAPI() @pytest.mark.asyncio -async def test_write_prd_an(): +async def test_write_prd_an(llm): node = await REFINE_PRD_NODE.fill(CONTEXT, llm) assert node.instruct_content assert "Refined Requirement Pool" in node.instruct_content.json(ensure_ascii=False) From 1c68d7f714199851f38599613efe6a5f4bcf9539 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 15:02:54 +0800 Subject: [PATCH 029/101] Modify filename in comment --- metagpt/actions/write_code_guideline_an.py | 2 +- tests/metagpt/actions/test_design_api_an.py | 2 +- tests/metagpt/actions/test_project_management_an.py | 2 +- tests/metagpt/actions/test_write_code_guideline_an.py | 2 +- tests/metagpt/actions/test_write_prd_an.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index ee6dc81b7..55dc14d25 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -3,7 +3,7 @@ """ @Time : 2023/12/26 @Author : mannaandpoem -@File : write_code_guide_an.py +@File : write_code_guideline_an.py """ import asyncio diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 32d6c11b0..305c708c0 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_design_api_an.py.py +@File : test_design_api_an.py """ import pytest diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 116dffe83..9ad4360cf 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_project_management_an.py.py +@File : test_project_management_an.py """ import pytest diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index 602145ce8..d68b85c6f 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_code_guideline_an.py.py +@File : test_write_code_guideline_an.py """ import pytest diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index ef0dd2eff..d520eb863 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_prd_an.py.py +@File : test_write_prd_an.py """ import pytest From 71d9fe31ab3364b3abbe145f26cf374e96b735e6 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 15:02:54 +0800 Subject: [PATCH 030/101] Modify filename in comment and prompt in write_code_guideline_an.py --- metagpt/actions/write_code_guideline_an.py | 41 +------ tests/metagpt/actions/test_design_api_an.py | 2 +- .../actions/test_project_management_an.py | 2 +- .../actions/test_write_code_guideline_an.py | 116 +++++++++++++++++- tests/metagpt/actions/test_write_prd_an.py | 2 +- 5 files changed, 123 insertions(+), 40 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index ee6dc81b7..0677f6edc 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -3,7 +3,7 @@ """ @Time : 2023/12/26 @Author : mannaandpoem -@File : write_code_guide_an.py +@File : write_code_guideline_an.py """ import asyncio @@ -78,19 +78,7 @@ if __name__ == '__main__': ) CODE_GUIDELINE_CONTEXT = """ -NOTICE -Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". -1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. -2. Import all referenced classes. -3. Implement all methods. -4. Add necessary explanation to all methods. -5. Ensure there are no potential bugs. -6. Confirm that the entire project conforms to the tasks proposed by the user. -7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. -8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. - -# Context -## Requirement +## New Requirements {requirement} ## Design @@ -104,19 +92,7 @@ Role: You are a professional software engineer, and your main task is to craft c """ CODE_GUIDELINE_CONTEXT_EXAMPLE = """ -NOTICE -Role: You are a professional software engineer, and your main task is to craft comprehensive incremental development plans and provide detailed code guidance with triple quote, based on the following attentions and context. Output format carefully referenced "Format example". -1. Determine the scope of responsibilities of each file and what classes and methods need to be implemented. -2. Import all referenced classes. -3. Implement all methods. -4. Add necessary explanation to all methods. -5. Ensure there are no potential bugs. -6. Confirm that the entire project conforms to the tasks proposed by the user. -7. Examine the code closely to find and fix errors, and confirm that the logic is sound to ensure smooth user interaction while meeting all specified requirements. -8. Attention: Code files in the task list may have a different number of files compared to legacy code files. This requires integrating legacy code files that do not appear in the task list into the code files of the task list. Therefore, when writing code guidance and incremental changes for the code files in the task list, also include how to seamlessly merge and adjust legacy code files. - -# Context -## Requirement +## New Requirements Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator @@ -172,10 +148,6 @@ The current calculator can only perform basic addition operations, and it is nec } ## Legacy Code -{code} -""" - -CODE_GUIDELINE_SCRIPT_EXAMPLE = """ ----- calculator.py ```## calculator.py @@ -381,9 +353,6 @@ class Interface: def show_error(self, message: str): tk.messagebox.showerror("Error", message) - -# This code is meant to be used as a module and not as a standalone script. -# The Interface class will be instantiated and started by the main.py file. ``` """ @@ -448,12 +417,14 @@ WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", GUIDE class WriteCodeGuideline(Action): async def run(self, context): + self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " + "meticulously craft comprehensive incremental development plans and deliver detailed Incremental Change" return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") async def main(): write_code_guideline = WriteCodeGuideline() - node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE.format(code=CODE_GUIDELINE_SCRIPT_EXAMPLE)) + node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE) guideline = node.instruct_content.json(ensure_ascii=False) print(guideline) diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 32d6c11b0..305c708c0 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_design_api_an.py.py +@File : test_design_api_an.py """ import pytest diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 116dffe83..9ad4360cf 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_project_management_an.py.py +@File : test_project_management_an.py """ import pytest diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index 602145ce8..d740a6bd8 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -3,14 +3,13 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_code_guideline_an.py.py +@File : test_write_code_guideline_an.py """ import pytest from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_guideline_an import ( CODE_GUIDELINE_CONTEXT, - CODE_GUIDELINE_SCRIPT_EXAMPLE, REFINE_CODE_SCRIPT_EXAMPLE, REFINED_CODE_TEMPLATE, WriteCodeGuideline, @@ -73,6 +72,119 @@ TASKS_EXAMPLE = """ } """ +CODE_GUIDELINE_SCRIPT_EXAMPLE = """ +----- calculator.py +```## calculator.py + +class Calculator: + def __init__(self): + self.result = 0.0 # Default value for the result + + def add(self, number1: float, number2: float) -> float: + ''' + Adds two numbers and returns the result. + + Args: + number1 (float): The first number to add. + number2 (float): The second number to add. + + Returns: + float: The sum of number1 and number2. + ''' + self.result = number1 + number2 + return self.result + + def clear(self) -> None: + ''' + Clears the result to its default value. + ''' + self.result = 0.0 +``` + +---- interface.py +```## interface.py +import tkinter as tk +from calculator import Calculator + +class Interface: + def __init__(self): + self.calculator = Calculator() + self.root = tk.Tk() + self.root.title("Calculator") + self.create_widgets() + + def create_widgets(self): + self.result_var = tk.StringVar() + self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) + self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') + + self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') + + self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) + self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') + + self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) + self.add_button.grid(row=2, column=0, sticky='nsew') + + self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) + self.clear_button.grid(row=2, column=1, sticky='nsew') + + self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) + self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') + + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def start(self): + self.root.mainloop() + + def display_result(self, result: float): + self.result_var.set(str(result)) + + def get_input(self): + try: + number1 = float(self.entry_number1.get()) + number2 = float(self.entry_number2.get()) + return number1, number2 + except ValueError: + self.show_error("Invalid input! Please enter valid numbers.") + return None, None + + def add(self): + number1, number2 = self.get_input() + if number1 is not None and number2 is not None: + result = self.calculator.add(number1, number2) + self.display_result(result) + + def clear(self): + self.entry_number1.delete(0, tk.END) + self.entry_number2.delete(0, tk.END) + self.result_var.set("") + + def show_error(self, message: str): + tk.messagebox.showerror("Error", message) + +# This code is meant to be used as a module and not as a standalone script. +# The Interface class will be instantiated and started by the main.py file. +``` + +---- main.py +```## main.py +from interface import Interface + + +class CalculatorApp: + @staticmethod + def main(): + interface = Interface() + interface.start() + + +if __name__ == "__main__": + CalculatorApp.main() +```""" + INCREMENTAL_CHANGE_EXAMPLE = """ { "Incremental Change": "- operations.py: Implement the Operations class with a method to perform the requested arithmetic operation. This class will be used by the Calculator class to execute the operations.\n```python\n## operations.py\nclass Operations:\n @staticmethod\n def perform_operation(operation: str, number1: float, number2: float) -> float:\n if operation == 'add':\n return number1 + number2\n elif operation == 'subtract':\n return number1 - number2\n elif operation == 'multiply':\n return number1 * number2\n elif operation == 'divide':\n if number2 == 0:\n raise ValueError('Cannot divide by zero')\n return number1 / number2\n else:\n raise ValueError('Invalid operation')\n```\n\n- calculator.py: Extend the Calculator class to include methods for subtraction, multiplication, and division. These methods will utilize the Operations class to perform the actual calculations.\n```python\n## calculator.py\nfrom operations import Operations\nclass Calculator:\n ...\n def subtract(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('subtract', number1, number2)\n\n def multiply(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('multiply', number1, number2)\n\n def divide(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('divide', number1, number2)\n```\n\n- interface.py: Update the Interface class to include buttons for subtraction, multiplication, and division, and link them to the corresponding methods in the Calculator class. Also, handle the display of errors such as division by zero.\n```python\n## interface.py\nimport tkinter as tk\nfrom tkinter import messagebox\nfrom calculator import Calculator\n...\nclass Interface:\n ...\n def create_widgets(self):\n ...\n self.subtract_button = tk.Button(self.root, text='-', command=self.subtract, font=('Arial', 18))\n self.subtract_button.grid(row=3, column=0, sticky='nsew')\n\n self.multiply_button = tk.Button(self.root, text='*', command=self.multiply, font=('Arial', 18))\n self.multiply_button.grid(row=3, column=1, sticky='nsew')\n\n self.divide_button = tk.Button(self.root, text='/', command=self.divide, font=('Arial', 18))\n self.divide_button.grid(row=3, column=2, sticky='nsew')\n ...\n\n def subtract(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.subtract(number1, number2)\n self.display_result(result)\n\n def multiply(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.multiply(number1, number2)\n self.display_result(result)\n\n def divide(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n try:\n result = self.calculator.divide(number1, number2)\n except ValueError as e:\n self.show_error(str(e))\n return\n self.display_result(result)\n```\n\n- main.py: No changes needed in main.py as it serves as the entry point and will run the updated Interface class.\n```python\n## main.py\nfrom interface import Interface\n...\n```\n\nNote: Ensure that the new operations buttons in the Interface class are properly arranged and that the grid layout is adjusted accordingly. Also, make sure to import the messagebox module from tkinter for error handling." diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index ef0dd2eff..d520eb863 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_prd_an.py.py +@File : test_write_prd_an.py """ import pytest From 994cfe814b3f749934e8187edf28b6567244ac2e Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 18:10:53 +0800 Subject: [PATCH 031/101] 1. Added test_increment.py 2. Modify prompt in design_api_an.py 3. Modify rename_root function using method of copy in git_repository.py --- metagpt/actions/design_api_an.py | 2 +- metagpt/utils/git_repository.py | 13 ++- tests/metagpt/test_increment.py | 158 +++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 tests/metagpt/test_increment.py diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index bcfaf0bfb..3e8265e95 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -138,7 +138,7 @@ Based on new requirements, review and refine the system design. Integrate existi NODES = [ IMPLEMENTATION_APPROACH, # PROJECT_NAME, - REFINED_FILE_LIST, + FILE_LIST, DATA_STRUCTURES_AND_INTERFACES, PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index d2bdf5d85..14df607e3 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -190,7 +190,7 @@ class GitRepository: return self._dependency def rename_root(self, new_dir_name): - """Rename the root directory of the Git repository. + """Rename/Copy the root directory of the Git repository. :param new_dir_name: The new name for the root directory. """ @@ -201,10 +201,15 @@ class GitRepository: logger.info(f"Delete directory {str(new_path)}") shutil.rmtree(new_path) try: - shutil.move(src=str(self.workdir), dst=str(new_path)) + shutil.copytree(src=str(self.workdir), dst=str(new_path)) except Exception as e: - logger.warning(f"Move {str(self.workdir)} to {str(new_path)} error: {e}") - logger.info(f"Rename directory {str(self.workdir)} to {str(new_path)}") + logger.warning(f"Copy {str(self.workdir)} to {str(new_path)} error: {e}") + logger.info(f"Copy directory {str(self.workdir)} to {str(new_path)}") + # try: + # shutil.move(src=str(self.workdir), dst=str(new_path)) + # except Exception as e: + # logger.warning(f"Move {str(self.workdir)} to {str(new_path)} error: {e}") + # logger.info(f"Rename directory {str(self.workdir)} to {str(new_path)}") self._repository = Repo(new_path) self._gitignore_rules = parse_gitignore(full_path=str(new_path / ".gitignore")) diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_increment.py new file mode 100644 index 000000000..68435e930 --- /dev/null +++ b/tests/metagpt/test_increment.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/03 +@Author : mannaandpoem +@File : test_increment.py +""" +import pytest +from typer.testing import CliRunner + +from metagpt.logs import logger +from metagpt.startup import app + +runner = CliRunner() + + +def test_refine_calculator(): + args = [ + "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\simple_add_calculator", + "--project-name", + "calculator", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_number_guessing_game(): + args = [ + "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal" + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\number_guessing_game", + "--project-name", + "number_guessing_game", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_dice_simulator_1(): + args = [ + "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores" + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + "--project-name", + "dice_simulator_b_1", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_dice_simulator_2(): + args = [ + "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + "--project-name", + "dice_simulator_2", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_dice_simulator_3(): + args = [ + "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis" + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + "--project-name", + "dice_simulator_3", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_pygame_2048_1(): + args = [ + "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8" + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + "--project-name", + "pygame_2048_1", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_pygame_2048_2(): + args = [ + "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores" + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + "--project-name", + "pygame_2048_2", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_pygame_2048_3(): + args = [ + "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game." + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + "--project-name", + "pygame_2048_3", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_word_cloud_1(): + args = [ + "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove." + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\word_cloud", + "--project-name", + "word_cloud_1", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refine_word_cloud_2(): + args = [ + "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud." + "--inc", + "--project-path", + r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\word_cloud", + "--project-name", + "word_cloud_2", + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From dcefed8a98ad6b5949be193ef27c5dce211c61c5 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 19:11:23 +0800 Subject: [PATCH 032/101] Update test_increment.py --- tests/metagpt/test_increment.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_increment.py index 68435e930..26b9951b7 100644 --- a/tests/metagpt/test_increment.py +++ b/tests/metagpt/test_increment.py @@ -8,20 +8,21 @@ import pytest from typer.testing import CliRunner +from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.logs import logger from metagpt.startup import app runner = CliRunner() -def test_refine_calculator(): +def test_refine_simple_calculator(): args = [ "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\simple_add_calculator", + f"{DEFAULT_WORKSPACE_ROOT}/simple_add_calculator", "--project-name", - "calculator", + "simple_calculator", ] result = runner.invoke(app, args) logger.info(result) @@ -33,7 +34,7 @@ def test_refine_number_guessing_game(): "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal" "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\number_guessing_game", + f"{DEFAULT_WORKSPACE_ROOT}/number_guessing_game", "--project-name", "number_guessing_game", ] @@ -47,9 +48,9 @@ def test_refine_dice_simulator_1(): "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores" "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", "--project-name", - "dice_simulator_b_1", + "dice_simulator_1", ] result = runner.invoke(app, args) logger.info(result) @@ -61,7 +62,7 @@ def test_refine_dice_simulator_2(): "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", "--project-name", "dice_simulator_2", ] @@ -75,7 +76,7 @@ def test_refine_dice_simulator_3(): "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis" "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\dice_simulator_new", + f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", "--project-name", "dice_simulator_3", ] @@ -89,7 +90,7 @@ def test_refine_pygame_2048_1(): "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8" "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", "--project-name", "pygame_2048_1", ] @@ -103,7 +104,7 @@ def test_refine_pygame_2048_2(): "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores" "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", "--project-name", "pygame_2048_2", ] @@ -117,7 +118,7 @@ def test_refine_pygame_2048_3(): "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game." "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\pygame_2048", + f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", "--project-name", "pygame_2048_3", ] @@ -131,7 +132,7 @@ def test_refine_word_cloud_1(): "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove." "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\word_cloud", + f"{DEFAULT_WORKSPACE_ROOT}/word_cloud", "--project-name", "word_cloud_1", ] @@ -145,7 +146,7 @@ def test_refine_word_cloud_2(): "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud." "--inc", "--project-path", - r"C:\Users\ASUS\PycharmProjects\MetaGPT\workspace\word_cloud", + f"{DEFAULT_WORKSPACE_ROOT}/word_cloud", "--project-name", "word_cloud_2", ] From 9435352031687a8abd5906121086dd83252409c9 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 20:04:16 +0800 Subject: [PATCH 033/101] Update design_api_an.py and test_increment.py --- metagpt/actions/design_api_an.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 0833297f1..3e8265e95 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -8,6 +8,7 @@ from typing import List from metagpt.actions.action_node import ActionNode +from metagpt.logs import logger from metagpt.utils.mermaid import MMC1, MMC1_REFINE, MMC2, MMC2_REFINE IMPLEMENTATION_APPROACH = ActionNode( @@ -156,3 +157,14 @@ REFINE_NODES = [ DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) INCREMENTAL_DESIGN_NODES = ActionNode.from_children("Incremental_Design_API", INC_NODES) REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NODES) + + +def main(): + prompt = DESIGN_API_NODE.compile(context="") + logger.info(prompt) + prompt = REFINED_DESIGN_NODES.compile(context="") + logger.info(prompt) + + +if __name__ == "__main__": + main() From 21d8b48e8e5b000c63e8c87c4e5aaf6d4d6d839d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 21:20:36 +0800 Subject: [PATCH 034/101] Update test_increment.py for "--project-path" --- tests/metagpt/test_increment.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_increment.py index 26b9951b7..a6d5e0821 100644 --- a/tests/metagpt/test_increment.py +++ b/tests/metagpt/test_increment.py @@ -8,7 +8,6 @@ import pytest from typer.testing import CliRunner -from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.logs import logger from metagpt.startup import app @@ -20,7 +19,7 @@ def test_refine_simple_calculator(): "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/simple_add_calculator", + "data/simple_add_calculator", "--project-name", "simple_calculator", ] @@ -34,7 +33,7 @@ def test_refine_number_guessing_game(): "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal" "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/number_guessing_game", + "data/number_guessing_game", "--project-name", "number_guessing_game", ] @@ -48,7 +47,7 @@ def test_refine_dice_simulator_1(): "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores" "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", + "data/dice_simulator_new", "--project-name", "dice_simulator_1", ] @@ -62,7 +61,7 @@ def test_refine_dice_simulator_2(): "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", + "data/dice_simulator_new", "--project-name", "dice_simulator_2", ] @@ -76,7 +75,7 @@ def test_refine_dice_simulator_3(): "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis" "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/dice_simulator_new", + "data/dice_simulator_new", "--project-name", "dice_simulator_3", ] @@ -90,7 +89,7 @@ def test_refine_pygame_2048_1(): "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8" "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", + "data/pygame_2048", "--project-name", "pygame_2048_1", ] @@ -104,7 +103,7 @@ def test_refine_pygame_2048_2(): "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores" "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", + "data/pygame_2048", "--project-name", "pygame_2048_2", ] @@ -118,7 +117,7 @@ def test_refine_pygame_2048_3(): "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game." "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/pygame_2048", + "data/pygame_2048", "--project-name", "pygame_2048_3", ] @@ -132,7 +131,7 @@ def test_refine_word_cloud_1(): "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove." "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/word_cloud", + "data/word_cloud", "--project-name", "word_cloud_1", ] @@ -146,7 +145,7 @@ def test_refine_word_cloud_2(): "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud." "--inc", "--project-path", - f"{DEFAULT_WORKSPACE_ROOT}/word_cloud", + "data/word_cloud", "--project-name", "word_cloud_2", ] From 0168c993235ed7bbf984679903ee3c29fde82f1d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 3 Jan 2024 23:15:43 +0800 Subject: [PATCH 035/101] Replace self._rc to self.rc in engineer.py --- metagpt/roles/engineer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index eeade5856..d7a0312be 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -332,7 +332,7 @@ class Engineer(Role): async def _write_code_guideline(self): logger.info("Writing code guideline..") - requirement = str(self._rc.memory.get_by_role("Human")[0]) + requirement = str(self.rc.memory.get_by_role("Human")[0]) # prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) From e3cd1a2cc18b1db74ca31a6a094960effd0f164c Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 4 Jan 2024 10:41:36 +0800 Subject: [PATCH 036/101] Update prompt and fix up some bug --- metagpt/actions/write_code_guideline_an.py | 2 +- metagpt/actions/write_prd_an.py | 2 +- metagpt/roles/engineer.py | 2 +- metagpt/utils/git_repository.py | 13 ++--- tests/metagpt/actions/test_design_api_an.py | 6 +- .../actions/test_project_management_an.py | 6 +- .../actions/test_write_code_guideline_an.py | 2 +- tests/metagpt/actions/test_write_prd_an.py | 6 +- tests/metagpt/test_increment.py | 56 ++++++------------- 9 files changed, 35 insertions(+), 60 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index 0677f6edc..43645e80c 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -425,7 +425,7 @@ class WriteCodeGuideline(Action): async def main(): write_code_guideline = WriteCodeGuideline() node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE) - guideline = node.instruct_content.json(ensure_ascii=False) + guideline = node.instruct_content.model_dump_json() print(guideline) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index e2fbb2599..91c1b2837 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -214,7 +214,7 @@ Based on New Requirements, output a New PRD that seamlessly integrates both the """ REFINE_PRD_TEMPLATE = """ -### New Project Name +### Project Name {project_name} ### New Requirements diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d7a0312be..d4947a8f8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -346,7 +346,7 @@ class Engineer(Role): context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) node = await WriteCodeGuideline().run(context=context) - guideline = node.instruct_content.json(ensure_ascii=False) + guideline = node.instruct_content.model_dump_json() return guideline @staticmethod diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index b42111620..e9855df05 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -189,7 +189,7 @@ class GitRepository: return self._dependency def rename_root(self, new_dir_name): - """Rename/Copy the root directory of the Git repository. + """Rename the root directory of the Git repository. :param new_dir_name: The new name for the root directory. """ @@ -200,15 +200,10 @@ class GitRepository: logger.info(f"Delete directory {str(new_path)}") shutil.rmtree(new_path) try: - shutil.copytree(src=str(self.workdir), dst=str(new_path)) + shutil.move(src=str(self.workdir), dst=str(new_path)) except Exception as e: - logger.warning(f"Copy {str(self.workdir)} to {str(new_path)} error: {e}") - logger.info(f"Copy directory {str(self.workdir)} to {str(new_path)}") - # try: - # shutil.move(src=str(self.workdir), dst=str(new_path)) - # except Exception as e: - # logger.warning(f"Move {str(self.workdir)} to {str(new_path)} error: {e}") - # logger.info(f"Rename directory {str(self.workdir)} to {str(new_path)}") + logger.warning(f"Move {str(self.workdir)} to {str(new_path)} error: {e}") + logger.info(f"Rename directory {str(self.workdir)} to {str(new_path)}") self._repository = Repo(new_path) self._gitignore_rules = parse_gitignore(full_path=str(new_path / ".gitignore")) diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 305c708c0..5b016ecce 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -8,7 +8,7 @@ import pytest from metagpt.actions.design_api_an import REFINED_DESIGN_NODES -from metagpt.provider import OpenAIGPTAPI +from metagpt.llm import LLM CONTEXT = """ ### Legacy Content @@ -94,11 +94,11 @@ CONTEXT = """ @pytest.fixture() def llm(): - return OpenAIGPTAPI() + return LLM() @pytest.mark.asyncio async def test_write_design_an(): node = await REFINED_DESIGN_NODES.fill(CONTEXT, llm) assert node.instruct_content - assert "Refined Data Structures and Interfaces" in node.instruct_content.json(ensure_ascii=False) + assert "Refined Data Structures and Interfaces" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 9ad4360cf..e0c1381ec 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -8,7 +8,7 @@ import pytest from metagpt.actions.project_management_an import REFINED_PM_NODES -from metagpt.provider import OpenAIGPTAPI +from metagpt.llm import LLM CONTEXT = """ ### Legacy Content @@ -134,11 +134,11 @@ CONTEXT = """ @pytest.fixture() def llm(): - return OpenAIGPTAPI() + return LLM() @pytest.mark.asyncio async def test_project_management_an(llm): node = await REFINED_PM_NODES.fill(CONTEXT, llm) assert node.instruct_content - assert "Refined Logic Analysis" in node.instruct_content.json(ensure_ascii=False) + assert "Refined Logic Analysis" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index d740a6bd8..f8a1ae626 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -200,7 +200,7 @@ async def test_write_code_guideline_an(): ) node = await write_code_guideline.run(context=context) assert node.instruct_content - assert "Incremental Change" in node.instruct_content.json(ensure_ascii=False) + assert "Incremental Change" in node.instruct_content.model_dump_json() @pytest.mark.asyncio diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index d520eb863..79cd913cd 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -8,7 +8,7 @@ import pytest from metagpt.actions.write_prd_an import REFINE_PRD_NODE -from metagpt.provider import OpenAIGPTAPI +from metagpt.llm import LLM CONTEXT = """ ### New Project Name @@ -78,11 +78,11 @@ Please change the game's score target from 2048 to 4096, and change the interfac @pytest.fixture() def llm(): - return OpenAIGPTAPI() + return LLM() @pytest.mark.asyncio async def test_write_prd_an(llm): node = await REFINE_PRD_NODE.fill(CONTEXT, llm) assert node.instruct_content - assert "Refined Requirement Pool" in node.instruct_content.json(ensure_ascii=False) + assert "Refined Requirement Pool" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_increment.py index a6d5e0821..25769ff6a 100644 --- a/tests/metagpt/test_increment.py +++ b/tests/metagpt/test_increment.py @@ -14,140 +14,120 @@ from metagpt.startup import app runner = CliRunner() -def test_refine_simple_calculator(): +def test_refined_simple_calculator(): args = [ "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", "--inc", "--project-path", "data/simple_add_calculator", - "--project-name", - "simple_calculator", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_number_guessing_game(): +def test_refined_number_guessing_game(): args = [ - "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal" + "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal", "--inc", "--project-path", "data/number_guessing_game", - "--project-name", - "number_guessing_game", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_dice_simulator_1(): +def test_refined_dice_simulator_1(): args = [ - "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores" + "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", "--inc", "--project-path", "data/dice_simulator_new", - "--project-name", - "dice_simulator_1", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_dice_simulator_2(): +def test_refined_dice_simulator_2(): args = [ "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "--inc", "--project-path", "data/dice_simulator_new", - "--project-name", - "dice_simulator_2", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_dice_simulator_3(): +def test_refined_dice_simulator_3(): args = [ - "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis" + "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis", "--inc", "--project-path", "data/dice_simulator_new", - "--project-name", - "dice_simulator_3", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_pygame_2048_1(): +def test_refined_pygame_2048_1(): args = [ - "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8" + "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", "--inc", "--project-path", "data/pygame_2048", - "--project-name", - "pygame_2048_1", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_pygame_2048_2(): +def test_refined_pygame_2048_2(): args = [ - "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores" + "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", "--inc", "--project-path", "data/pygame_2048", - "--project-name", - "pygame_2048_2", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_pygame_2048_3(): +def test_refined_pygame_2048_3(): args = [ - "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game." + "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", "--inc", "--project-path", "data/pygame_2048", - "--project-name", - "pygame_2048_3", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_word_cloud_1(): +def test_refined_word_cloud_1(): args = [ - "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove." + "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", "--inc", "--project-path", "data/word_cloud", - "--project-name", - "word_cloud_1", ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) -def test_refine_word_cloud_2(): +def test_refined_word_cloud_2(): args = [ - "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud." + "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud.", "--inc", "--project-path", "data/word_cloud", - "--project-name", - "word_cloud_2", ] result = runner.invoke(app, args) logger.info(result) From 97ee2a0a61c3ccf93c355c77f23cad145717e1c8 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 4 Jan 2024 17:58:55 +0800 Subject: [PATCH 037/101] 1. Added 4 compressed package of code examples 2. Update test_incremental_dev.py and engineer.py --- data/dice_simulator_new.zip | Bin 0 -> 27923 bytes data/number_guessing_game.zip | Bin 0 -> 65020 bytes data/pygame_2048.zip | Bin 0 -> 19338 bytes data/simple_add_calculator.zip | Bin 0 -> 53760 bytes data/word_cloud.zip | Bin 0 -> 54076 bytes metagpt/roles/engineer.py | 19 ++- ...t_increment.py => test_incremental_dev.py} | 112 ++++++++++++++++-- 7 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 data/dice_simulator_new.zip create mode 100644 data/number_guessing_game.zip create mode 100644 data/pygame_2048.zip create mode 100644 data/simple_add_calculator.zip create mode 100644 data/word_cloud.zip rename tests/metagpt/{test_increment.py => test_incremental_dev.py} (59%) diff --git a/data/dice_simulator_new.zip b/data/dice_simulator_new.zip new file mode 100644 index 0000000000000000000000000000000000000000..307e41541899d742a97d1209813169f672fe13cd GIT binary patch literal 27923 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 t`cn=62#v`Dtr9SA^3b literal 0 HcmV?d00001 diff --git a/data/pygame_2048.zip b/data/pygame_2048.zip new file mode 100644 index 0000000000000000000000000000000000000000..476d0b438b207f594418d2e8fb78a29371590f66 GIT binary patch literal 19338 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 z z2&f#;(IjuEgbg-vXhBw4E67rX;3x7zQ|?cZvInv==8vxddW~(60qoH{);)b^=hJLR7mRwL0MRoU>pGb6=8t72_CJ zXYq&qpvqY1`?v@518!yV%Z^L6jUhXrUQ z*x_)*l!uKJHTyp1auyD+m!Mw**jNF8FcG1-sOGB~BnY)NhIy?%{j&QzBM_g*G|`9y zkM;E$5|V|FFc3XGrT|4Ouo>x04=TNHQD$EOnHIGrV6d}gz^QbAB!?%p9Ts+MEuP6!4Lhkfa$0Si8hs%M z?kP89^mD>M%M=8%7Z!YcDD~|GaKTzcaS-&OR_K6%BLsc8M(WCdeYgh0Zqq-V+f;t2 zYwvA!;4}YjtktT0KVwNxD>8gqvS?zF9Xzd``)-vMHlbI%GQ)xz-Ys`Ge_w2PwG^6V zkr&pug~k4ud5G6dbkv4tappy^r-=D!zxML5k{c;6_>;-C=9g>XY=NR3ICRz;ej;sc zoxDkMyR%nBqxGpqlJBIsaJvbAya~^fl#|l}+|cfOz`6c1NTZS7WA@r4FYBx1N)P_K z%N24K+`HkPowqB#8q64UP=`-)BYplm4Q#L^+K$Xk5%Bvk^DK+x?c^;H@1uK1RO13? zp=eU34NfNy7jMhSF5Xzra_-6r9^7^oE>1R*C*%yJoH z)FO*GA&7UPFxO|rN}6t^?$7ei+xG6a@U7n)-oM|r-pjW>qq|>sY(M6%KKpIGp1Qr? zo<0xR`QInJKX$f$k8S;)`uzC(SnB==-uiGyc)woyyb1n%wEf(%^?vjIe6{_Y(t9Tr zq5WwJRxHlW)?w%|31CH>k_xu(akAoHN z5s18jF5jegt>(@gF@K-jPQvVP#!X|tA=#;Z&H(B&pZOIPr|UPBw4v>kn(E5k5U$~5 z6Zau|)bJ!n%48VL%$Vl`7ap9Ckv-qcRe7NF!xYRxUv*ve#jp+k&k1Y&5ioyUSG;#C zT&)(1koHWckc$hPOY3u+?2B{$wT&%<)Fqz&mZ8MZ^7jXxkG5V7bG#tR4*}Tf{O5@? zm;RoX2k&=MUss^y2>alaGbOzwL0h3MUOSyDTzu|KgA>Arq@Hi`ZoD@T5esJ}SlHvM zS-kTDK-(;6z{lGFXu+)={8P7Bv=P0y;PKT#>UNf+b0PoR;>oM?XxY2c~}c z{^`BRz5dQD+MJ$Mh&^vxKGMGJzyQKU%+;T|eOqcAL^-$7Y znl-gw7k^mctKz(Vleh`OpJ;UMuy`id;XuQ0qJ}4a1NiCr46w3Lr5xr{$Ze(6>tlrNVEDMC6|wFdCg0P?12;yhpNT zX#Ys7eb${w&6K_2Sc8bg(~3=3M>`b|S}-lXO~^=Bt}YSX6K+t{FE~qX`!a%2lDlsF zo)5k~$oG@LIsfDl*}k))(ILIr5bwmRa{m0su-X`m`mmY@R>mGRJL}L5Y8Kj7M7195 znm($$)sso1%6DK5Gcgu6uZ968BaWe=rnK$F+e;deqS|Wd{ar!Y##@yJL#vWq6nrtv zd=w`oe%WH2YGLSJlE@#%5!x}oFraFUzvm1s58$oq!ClrU6w0+}#GskyhbC|cFKOyW zkSXOs%W4vYlPMWO$!dO4*eel1%4+6)vEA>2%WCNd`^|@AW$*zMj}+bHflgE7Cq==ME!T4su7xZEv0uE_!Gmu#cBWf;R}4wT--ble8r39?IX{?wbll} z(vPW0TL}|p(8x@M#*116X=4ExGxU&W$MEC5qTHtFDFnBX0Rii|9E&NG%bgx%QF%@N z;hV$eO0^FyFzRyHW~&WgGENSygFwvzY7yQHXF>+h+*DX`Ran+T2=OH|J8GU&Y=TS1 zp+L~}j}KGK6AilCqWqHNGr@98+h9@QH4r`GfJTMP1JV1hJ8E-c!qv4KiUk+{F()G5%gGEakJLK`FPXn!D#26ravF%={ zH%km0NJlmSphoW!+XF^YN79c9Ghn0bJpl^*I)F$7>!snVDma;;&d+_KZc`aQIoHE6 zN>wrQ@DYRN_<-+bY%EL>@rbj#*Oy$zrAiRA^2h;~(hcl=)iVh6)C9-dJfiOqQ3sd@ zAy!nsSZ7=v5_0WMbwOlo zq$$r+<8rrM+`K2r;vl0-pOrKkT%7%kUl_PL#Cut>j7(f&Y41cv z195N_NcKK`gD;kuYvBp{gNhY5;Pa5|m3ujt2&KqqBfY1WF?la+mzlL6A0&lkULGv04}>9YId*y$%=idcOR;hs?qMe3`F< zzpLW2=bz|F)f}uvj?y;M?-~LUq&X;ncs7=jjk)KZS(iPsK$sQPmhVsW{H>i+4znO! z#E^^RZBFbYuetFUBuamjShgteO&lZd%@;DoROW})caqSF#Ud^xZ3m_pPbE7-W`tGk z>fjyF9(j4aPm@EsQ5H|K-s^X?JG7=?38`J4_=@aCGoNQOvJ;9C)<#*IO5ig)+Ig{xAeiKni6cuGDZ?@Oh&I}9OP*L9q6aktHHr{*jIQfB*=3*DJ&dMB5> z&kqzL1rXeG>%<#2h16TaBdm1|IzbXE7-j5_)y#*Dj#?*uoIdG`vq#`a4v`QL&m!7b zCw_$X?fuG3K~-GBE*5ZH@{5%xg{cEYOQxpdRuh2Vo0-Jg zpJ>KSzPsz`5WE1gI|qGF8i&-`;BL8(0ue-X7$*wlobz{-D8hW&n1R!4GrC2%h#SH! z(&{JFQw=`p;){Ew#_sq0{hMq{Ey#+iruq7!S=@*I60Q!$v%x>P9hXF{x5>e673qBU z_ftAMw(q)_WbC$u2f`iq=fGMo2AyEjM5unn zdBiaCMMZV6RQpY{`I?1o>#{4f_dlzT8{D+)qU2d^B&mer`+@0JYrt83_R)WRCO@LJ zNU&S(>7&OXoRB%yJ-WqzNBdbSgKH26G8uQ&C@AiROeYmmE%{TA1XZmcWP^%{zHJ!8 zu94`7I4&oYzx=R7lOd`M+B`L;)|JcKR2Ed#QCkR3QgbXhPFq1aS#FbtmSnMZfLe$M zDE3BJhKDLVfRcL-;p8F}D}oTqF7k_gvLMu${9C?XIM#E3@N_9sf3xH#Ew!+C!$(6Y z&vpQ^nX>JGIRjhy+y}9lL^S78JfXkxD*ZBKJld?T@oTXqwvF2eX3>P=GYqK!SmUo2 zit*SadqG*%jZM=0t0@tC#vPN~B|zVpWwBxoXERB2p?&7wn{ky{ce^0kNQ^P&z>PI8 z3T7MTZDTGqXh(=WPA~#hxG*WK-J@=0ckJM;F#tH%EL)tbu4VpLo2Ts(iF8dCQN5yP zNn58#aBlEu#{M5T1PF5JMF~&`6~Z!JPrC1umnfiy^HNqZ-Br8W=Pd#ipm$`p*Il}lx<*IN=Ofg`Z zpS=vt%uL3vE^j4u*Yc0GSXVIDzyW4|XLc7_eNQ`ZWn7xZUpe`_D!=N)Ox z%7}kDT$rJMie~=^Ty=MI8c$vMY5$|8elVbbnM}EHM2YqrI9DrG9VfHDH?60o$R>*) z_@(VRObNS(cwncCYc`K_Vf|xCXAnrM0{5LE$cq{zpyaO*@N{*B*3e6re~7 zopSPUQ)ic9oOA}tx9=8;MOiyQ{)90sdPUXei90}q(IkBPSQfoGk}O$1`Vp~1n0<}t zpuhK#@MY*&Gu@=fdCY#ZjWE?MrGA9hL%WoDG(y+Q>V|<;fpceumbu_s)c_7)0nNzE9jfV> z?<9E>h*mniwO^CT-BW)p(=IPCgY9uo)xnF@+rc^~u_G6djr^Td;Vgf=z+}oMST!0i zwL8Ip1TbtnaHylAyl2kz>5^HTY5p2i~a{Hf#YR0kRfWtgBAUt>UmTjT^;t7te>+~2xV_$z||1cV`*Sc6G@pI4+ zIHEk~8YEyP4c1oy6#^4}36+k$j!gqz0VkA**~PPfvz29(Sm>G*L1Hd<(F^MZ6Q$#{ zyhMHJB&sH}4<<6Bs>Qkmug@5t@$jKCB`S6JOa-s1GF>cb9ihT{nAj*cSFH91pi0AG zEXryfVAZZ#&=9Tvs4X{>)7gL=No`k~;E%G*x?7i|o3B0MGIxZ^ol1_C@;_kqoT|+N z9?7Pc2&y@Ka+AU8!@6ub?I-3q^ci>OQ0#TvARp-|nzP#AN|}7)lzhxbwtop)t1*pE za+7|8r$t9sRe~Syk666O=M?UGIf|g1+5aBxjg=+HMW(B1szeQJyCo*U56b*7M?=#S zzV1FDcS7lcRaKd_RQhl{^NN35H_~IO!B#TGOptU2=YW6u5Vr^CSR={sp@BBR#=T&< zv)Q>6&m~#r5v1%%Iwp%k(G_96uE+!hziNp$xWPOiYdIB3mqO{&mt0}3By2+?%S*dP zJF@x{U9hFA3nPQS6+)GMamh~{eP?%YaCLW7)YY@Ld2~$M zP1MzX_ml30d$r32_QdKwbmpZs3{rZJ{s&V}3rSVa-fb;(+zSdvYB!m%iPCWb6>3k! zuuFKZ=_CXa&IY-+?`cH2uTf0i`gLy7p zo%uX7BOFDw%^x}?D&4Mun>Tw{ORfhGE$s{1Cdlw@q_n|Jti9kirDMKx;<`ya?){-g z66w_}!SgpwmG^AFF7nGc2R%ugyITrN+lMY3_X=H1QEjF0UGx3dk*wW3_a7@uH`<3< zX!lw6pA!80#_e!p?jp0c-PJ$#dK{{Hil+>f8R=^t>x-E!nW%ozJ%J7WW_0^at+d9~ z*x$kk7$n#QZ6%=qN65G3wLs``-lje1gK)RQwkL>DYsn$Qggr`p9yUV|UEZ7{Jt5+s zOQFV$9(GiZ0d107R=Ry6T1rcuNl{#tCpZ>HE%#LSr013>J|RG1Wsj1|3O}0Mi~*k5 zn-I_G#bI-w&r4IC8D-Ul!47G>F+2`vaj$BmA5mrl6@eIm2_(d53ij(;Hljj@IUAr; zq!(}w#ioapt|lhzs9w{0%5JhnhuSND+$_GdsnW!iKqN0^sN@OQf`*LKJ+`#0gt5|k zrX_kQm`o^TU9!xXeJY>(NC6hPF};T6s4i0}-|&m_VTSoA29xMxT>& ztxjwj#;538T1v`H*4o%IFfX<2%BpVDI(azPX!FXWbQSGckvz4AAr|(ffO)OG{PCyM zy6+(FtH5YV4z%u_h*Hr9tdXdKid)R^T0x3eZJ^9E0A2L1T*A6(Uo!`xCg2Fyvo(=J zWkZL;WO+l!)*B~MDvJP>`L4UbLn{DkzlV1ldUUF8j*A}Tx`W|N0vj?8%^)~k98pXg zsCew#rJ}Nr?N=ghs(CX|gyAp-<+dC?L5ym{HSZ+F(_y_a!>;ESV-+vV8)Z+ z?Aic!5!ILxy9Nn9!RA?}g!agK+4*h{vTu3R&+A$eyNaLiTV%S$7+EKn zYK|epFE&g4Ih0_X2S{QAnM#B}hwSPYaBW;Eoclm30i11-I}Nj<9;wX9Zka1IgZm{b#4ZG-ZY1jYu#d0xuB zwv$3}l1*?3v8x$5LMfmm{=L!Sj1`DI4xPF2cQ*ZKRizH-ba3l08ES!k57W7&IH(qRTD_ zHZfufCQRip;OPK&T$X+9i2YwNbgYRJX5d%E3A<v)+UD{0vT8?(>TmGsJF!BumKu{3+pL~uk3>)&++0v-qxiFcPB zYZ62PIdB2cgt?^UkP<&IMCbW#-+o$OEZ?J1vkSWSVCy_ zP+Q#LgbOJGd{zT+@&w2XnL*0MqC;O|zxWHD5&H>`lKZg^Jufe02iu0Qf97dt8eMGt zqBw1DH6TC;f&hq!ofWk`WM_MTqKe$qH<@A3+;5}|`jG@3E`S*l5Pt0NH31~JMYdpy zP5ef$<+BUvZ*4eLlf0Fbz|ZNN__!?TG-~;US>+VMDUbgoe?x+qUzQ! ztcu_Q?L7UVED!+^l3Lj@MZCqtpacO4T(L`Ke+W*tp=>oSuULF5TpIWn41Ddl*anue z<=)#L6Df9FCnC{NjK=Lg95S+Dkuog12r~!MeF-zLy_4UZs&@@i=G$KzwyDk#R82s3 zC^~l_5M<7Bj!}P9b?At=->A2~$$6?eihW(08 zDJh2)d9z%Kx@%~u6V)0>Cb+PZHPv`Lo>nv4m$+i$F2$}i!&2Yd`e1G!mQ4{;6$Hw0 ztZ7n0Ywu=Nt6}rgU2=Y|L|l_%3OKfbWF>(WnJyddgl#{yVeRQhX{1BS9dJD$IB@wa zsSXF9$<+=rp)F9I8a{N&P=o?FX;(xt9Jm;zp?P9NZbk>TV!82(MwFj`s8GNVI$et? z5pE!jy?z2t3}Q$8uTgr&|B4}3E7r!T|H3o=(f%Y#@5{y)fSSq3*x1CF{R>XaZfwB8 zY4Wvc#+ccN?F+qUz`|@~%3#Xzg~uO^+xQ~XfC=lvY%R%MeYv0SkwL;TQasjf3TD z1N{FbHmGCyE3?7$e_Xw@9JDKX6p1w^1I*e6G4R<5ipAC!ma&F_v$Gl)a~Lsw@fR4eGIRb7!K@De z09gZg{qLRs4>Ht$C^Y|-RpXzSaQ-V_#hipm6|D)Ic zgIM<8P5hxF@c-fRLKLxjB;d^4m$oawAC2HYM#;j=#@4|^PtVTH$l#0WR!ikW-`~(*?ASSc z>0JNSeFy;NDq{6IoG}aEuf6vUF#m~``R`|c(ZaAWFn%$_u(N)B|03#M__fI%n;pTE ze%c%3c#xI=Qf`+d3J;3nmK1DVJ#?>ll%YgN%5a>Ny+cAZ{KMU}Wj%c{&R12wy_`8? zEtT{AXh{#iyVI&DDT~(47KFD7ujArjkN;bK@)udPg0y*Fkyi6;ncoIRElQZ=^QiX^ zgC9eQ^#Sdp4w#!hzNq&=G_rz6>kQ-#egPu`(uWNeukVw{;Mp2z9;hzx=1X(Q_xv!Jbd;4;9sG4_h zax=v5Sc}PR&}epO*R-;aRyk6bsgd->T2zK``BYoA)XjmRuPDUA1#4Pk`mEgtDW6P& zT#vDB7|nWJk#W{AwD7C+`xz*U0l8y%3==YTfez|9+1UoZbAmSunekZ-zMeCNvIVUY zC|}|l_i;V`qO}_rx6xxTi;DLKH9Y-luOpORc~ z#W>=O5Vz~CuwsQf#4yPVrINREU4XJ1e&2(zfu`Hk`GTcBfYs10M@N)qid|yJPqUlN z*ZU%lP%WRavZtda8_1ND#-2v4h`S%x)Fa-i7Rzq!^uX2y{xIn%EUUTSraU5UvAT(m zilK39h?#)MR1%m z*4)p%Adp9JQ>it}Pz}*Ai!)`h818@a>BQvmM@9ap)mQpZrZf84*nZpK3mm{W>z0)D=Or;Nx=Q}W?-rIYWhwRx^28L#X)7TuUGv&Qa+|l#XfQGSAlJT$Qa`QH zF*w^O+;U7ITU!p5n`Z=Xp@OUHS6vboRNQYS&9b1`^yIq7m{UAFp@YP?fDVcjCY5#; zE~Fk>NM_;xLD|aHqwBft`RQ|}hPz}o*tLfGBDI)*rm&=LL*;~vC0FsLhdBQf;qp|% zs|>fk6D0Lbf|2v8BN7D(?V3PG)cxr7eMOd^D3)~F}ZL?w-DPJFMTQN zsP!@Z!7;c_)YkJCgLJHPY|Sn&>KOrJH#_huA{xr$vg&$Bcn+D7OhLIrZZpgR(vyIS zt7^^kNr#>bkJ*4HPPbPH|2Mk;65g;*`k+=*86$Ne;SrFv#JT+nx9Ck4Mg&}Db z1v+DW-fu>M$=|WY+Na{r?rxSG4pN42v7s?MVRqs-uUU2a0=Vl0-L0c6I}mt4=V8Vv z>PqV!ccFUHNR)FIC)0Rzs!$4BjY(2Fs%kfQ3o{TCAVTHM+ZQ1b~t4fHyA)e z|Lq$!qHNE8&U3^^W0(S;>(*%4VO2;%t{e;kgknuHPE-9HdUZ&B6U1rrmPQWMvf+5;!FHn5z zHgIqXj*o!lIlFRKrG4jgKU#xM3xO{bMN7@8n~!v8B@gR?}896u*Ux?O};kK2e6jt zy07lAp~#Ce%<*+1D_0qZ{w%HRfSm79*r#PyD8}(=QdC9X)(#E*CU_|8r{a#Tqwia!vUue2j%TaxR4svB#ZAr zOxWi57~fd2QISwPcGYdcs~#zVwuWy}%Nds^f%v5Z&8gVzdPIX}k$-Af)*nrqCl#X6 zo=VPv(X5!QQS088Zght;;id zj<2_7SJW>&&MSt^*o(M*HyAD!8}HyvZtvAPifh)6m{y*-!Hnm@U5E1$jtJP%ontTU z>Z#2UmIF9Uz`A5ka8-i+%*@EPy`q;mr%^HWYU9u^atki&fg{8$jX1Zc-2AT%oespm zO{0S8!mRsN%=Pqoj2WY_8Glrulg<-gH3TxnYs`K*UH#^SXD0rknv@uH4rjw($={`= z1MQhkL1my5Fr8bJ_WXK@JXvhClW~qe1Lu3VWP} zhHF?s<9daAKfBdV!8>RLzOEx+BG^Om>&s1`eLxa2L%9N`7@{gH;EjntY`JbRpE_) z&W+>catHW`CdA$+rPlUnf#MKLR~tfOLGCt){dUPc5|y43K&KBed*$VQIr(vS1N^J{ zXF+zR!R+;P@z57V1zbz9x~CsLa6!*(Z;OaFtT_Ec$Lp71bZ_38s}gJ}V}&h{!)Z0+ z<{NWDk$_zMw;-Zx3GU?fJNJD3lj+BqaR~VjwC~|Vv_g6d^>N0L4z+U8d5DXwz zC}ev&uwjt#KLo&s1_R_~5Su4ymveC_!Wo5GApE{%%+ZvwFC@RVNd~Gqqs1RdtEz31 zN;1||lO30QSSkURIM?QWaZ^Y$3F8`&=Mbp!HV<6@YUDVOciuT@;|zT^nn&esIdj;; ze}f@1Zb}Nvk9fK5sLEBylQIg5Hx^H!^`Go*2<EyIs!BezYVwn_#&my zYO^w^f;U{vPwhLJ)KurIAOus&Q^E}|yMjGr-z~qZPP)&!JvdbUL0ntd67J+y!~ID6 zeGH z)t5HO(VP?Ciq3?tGe!qkDChD(mm0NpdsT{{Y0woTE z*z*4S*aII!tp4>_{q6Jb*P{P0b~^qsc>dkSzm8u7U)KKn)v$w!qph=p(SKahF#n0O z4eTuR9PLbu^bBl__5L|+zE(D0Y4f-H`T8O9|7Y-D63O}>-lP2M4U_zdD|{`*{x(+s zS07pbq=o+nr~bVOvIrWTHr`ycZjhd3bw`#CYB4|uG-hNo{%pLvN+)q`Iv&-MNZ_5Z z$`-vRd0MLwCPh~X!g3%vUZm&vV_Cr32) zfP79Rt3CCbS&o*GVt{9?TS8FKbkHfbK;->wKJpnwIc$X_M18!n6nxdE;e_QuC;g{+ z{ZKmDm>Gzh^c~|Z|7*z1liJ9yVO#v2aSu?bI^kpd>d^8jqNlzG1EHwKN%4K*X&f6({o#^eJ-AEYftqda;gCP4E!a(<-@$ z98wHw+;hm|9jAK5;xytj9cuzOK!6>p!(qSym!CPCdord4vahN`hU#TnkCxXUD?-M` z+N}Lr>!#~XTPPn_B0-_kL6%dU;iRGQFUAN$I32au(MyxZQ--Ts(b1Jl5Rn}xinfnI3JF?sW7N>HO5O8gQNz3r$x7RAW6+QVhZ$AZN~(P2v%BLim)kB~?%hSO@+Dj#C7?A(tWr43a(OVX#E&ef3spOrkD~1RSjj z$T2W}nkztl&B0-r%mdK>|(So0gIp({`*JfpjxwG_;Cg!}xVG{Fxk%o9gUS09L zkFrqnlD4yXQNNtQlsAnPSxdJi%AZ!gP#$hru+xCD*O6*lh}kj@`w1KBNal z@hwuP6BMC46hDpurQmv(_Rcuu4Ax{cQNF=jx*mS=jGM8 z7i%_2&vc-gej{!N;a8bARnSw8RD+fvS!TzavO;KxTBG?F3AXLF!q#&FIRp+DYh#2AWp7-% zo>@`eF_6s%DH@XAE{*&-)77d?64mSG?0f% zA~Y?AElTNvt*%3u&WN(J8No;O%-}okd~OQYK8g-IkNFe~&w64;Kp5Nk^6eHm{J2(3 zYdKu08Ls=r?BWK{r@&Rnz_`oG>beq@mw;$b;uIn#_nTxUqRt2&#*s<*)q=ZU9%~%? zrFD-J=rCnH3G>D?Nr-6h5k5LA+pk2YPYC!+Ym9KXEwqAxWD8>thSE6U&8)UR90AJ5 zhTsr@$z?O?RHI2izuosp5U8Nl8jIWQnT%wM&yXw2!Y99IfHO!Wp%v08@EoWVtvxUb z%XX6QJx5LC?YCg_+9ls9(7+b|x~JI9gwG?O^lOr|IzKG#wElN+d!}Q*1T=0~Bs*Hg z(>D$(OMUMz5h%PTfqCBtC?qBDIFOcQTURdJmX=I0M9p@&wE`I$nITELLC<9!?3Cmj z?rV-o|F(~oW-eQdpuE78b@dOtJ!7~b^?D3FSMW~pXnmM0c9U3VVse-39dJul{`c*N zV0;A;wr%Yd2kgm2<1aEuNRa9$4S*gdbAYX786A-RU9Lu0PbPeTW(yL9+W2H`1;pkY zU^x>q_IO$ykRabh5WogtFO-8GFX_N3RfV!1_qZSfAB8w1vhZ12bsKK{c8VGqr2)k

Ei(XwNOEtpM1u_&thFjw8IQWXi0_ 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 UqTY8I_-Dh4fe{3}4Flu<0l%(&;s5{u literal 0 HcmV?d00001 diff --git a/data/word_cloud.zip b/data/word_cloud.zip new file mode 100644 index 0000000000000000000000000000000000000000..fd395c5bd2c2770fb8a1f2482d16a2a3cd9f0686 GIT binary patch literal 54076 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 An*aa+ literal 0 HcmV?d00001 diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d4947a8f8..a1e93be40 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -117,9 +117,22 @@ class Engineer(Role): action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) coding_context = await action.run() + + # Get dependencies + if guideline: + dependencies = { + coding_context.design_doc.root_relative_path, + coding_context.task_doc.root_relative_path, + "code_guideline.json", + } + else: + dependencies = { + coding_context.design_doc.root_relative_path, + coding_context.task_doc.root_relative_path, + } await src_file_repo.save( coding_context.filename, - dependencies={coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path}, + dependencies=dependencies, content=coding_context.code_doc.content, ) msg = Message( @@ -347,6 +360,10 @@ class Engineer(Role): context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.model_dump_json() + + await CONFIG.git_repo.new_file_repository(CONFIG.git_repo.workdir).save( + filename="code_guideline.json", content=guideline + ) return guideline @staticmethod diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_incremental_dev.py similarity index 59% rename from tests/metagpt/test_increment.py rename to tests/metagpt/test_incremental_dev.py index 25769ff6a..dfb8fe039 100644 --- a/tests/metagpt/test_increment.py +++ b/tests/metagpt/test_incremental_dev.py @@ -3,11 +3,14 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_increment.py +@File : test_incremental_dev.py """ +import os + import pytest from typer.testing import CliRunner +from metagpt.const import DATA_PATH from metagpt.logs import logger from metagpt.startup import app @@ -15,11 +18,29 @@ runner = CliRunner() def test_refined_simple_calculator(): + project_path = f"{DATA_PATH}/simple_add_calculator" + check_or_create_base_tag(project_path) + args = [ "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", "--inc", "--project-path", - "data/simple_add_calculator", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + + +def test_refined_simple_calculator_2(): + project_path = f"{DATA_PATH}/simple_add_calculator" + check_or_create_base_tag(project_path) + + args = [ + "Add exponentiation operation to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce exponentiation operation into the calculator", + "--inc", + "--project-path", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -27,11 +48,14 @@ def test_refined_simple_calculator(): def test_refined_number_guessing_game(): + project_path = f"{DATA_PATH}/number_guessing_game" + check_or_create_base_tag(project_path) + args = [ "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal", "--inc", "--project-path", - "data/number_guessing_game", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -39,11 +63,14 @@ def test_refined_number_guessing_game(): def test_refined_dice_simulator_1(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -51,11 +78,14 @@ def test_refined_dice_simulator_1(): def test_refined_dice_simulator_2(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -63,11 +93,14 @@ def test_refined_dice_simulator_2(): def test_refined_dice_simulator_3(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -75,11 +108,14 @@ def test_refined_dice_simulator_3(): def test_refined_pygame_2048_1(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -87,11 +123,14 @@ def test_refined_pygame_2048_1(): def test_refined_pygame_2048_2(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -99,11 +138,14 @@ def test_refined_pygame_2048_2(): def test_refined_pygame_2048_3(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -111,11 +153,14 @@ def test_refined_pygame_2048_3(): def test_refined_word_cloud_1(): + project_path = f"{DATA_PATH}/word_cloud" + check_or_create_base_tag(project_path) + args = [ "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", "--inc", "--project-path", - "data/word_cloud", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -123,16 +168,61 @@ def test_refined_word_cloud_1(): def test_refined_word_cloud_2(): + project_path = f"{DATA_PATH}/word_cloud" + check_or_create_base_tag(project_path) + args = [ "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud.", "--inc", "--project-path", - "data/word_cloud", + project_path, ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) +def check_or_create_base_tag(project_path): + # Change the current working directory to the specified project path + os.chdir(project_path) + + # Initialize a Git repository + os.system("git init") + + # Check if the 'base' tag exists + check_base_tag_cmd = "git show-ref --verify --quiet refs/tags/base" + has_base_tag = os.system(check_base_tag_cmd) == 0 + + if has_base_tag: + logger.info("base tag exists") + # Switch to the 'base' branch if it exists + switch_to_base_branch_cmd = "git checkout base" + if os.system(switch_to_base_branch_cmd) == 0: + logger.info("switched to base branch") + else: + logger.debug("Failed to switch to base branch.") + else: + logger.info("Base tag doesn't exist.") + # Add and commit the current code if 'base' tag doesn't exist + add_cmd = "git add ." + commit_cmd = 'git commit -m "Initial commit"' + add_and_commit_success = os.system(add_cmd) == 0 & os.system(commit_cmd) == 0 + + if add_and_commit_success: + logger.info("Added and committed all files with the message 'Initial commit'.") + else: + logger.debug("Failed to add and commit all files.") + + # Add 'base' tag + add_base_tag_cmd = "git tag base" + + # Check if the 'git tag' command was successful + tag_cmd_success = os.system(add_base_tag_cmd) == 0 + if tag_cmd_success: + logger.info("Successfully added 'base' tag.") + else: + logger.debug("Failed to add 'base' tag.") + + if __name__ == "__main__": pytest.main([__file__, "-s"]) From 40db41cda5c489a7cfa7b99035d76808caa62e1d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 4 Jan 2024 17:58:55 +0800 Subject: [PATCH 038/101] 1. Added 4 compressed package of code examples 2. Update test_incremental_dev.py and engineer.py --- data/dice_simulator_new.zip | Bin 0 -> 27923 bytes data/number_guessing_game.zip | Bin 0 -> 65020 bytes data/pygame_2048.zip | Bin 0 -> 19338 bytes data/simple_add_calculator.zip | Bin 0 -> 53760 bytes data/word_cloud.zip | Bin 0 -> 54076 bytes metagpt/roles/engineer.py | 19 +++- ...t_increment.py => test_incremental_dev.py} | 97 ++++++++++++++++-- 7 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 data/dice_simulator_new.zip create mode 100644 data/number_guessing_game.zip create mode 100644 data/pygame_2048.zip create mode 100644 data/simple_add_calculator.zip create mode 100644 data/word_cloud.zip rename tests/metagpt/{test_increment.py => test_incremental_dev.py} (63%) diff --git a/data/dice_simulator_new.zip b/data/dice_simulator_new.zip new file mode 100644 index 0000000000000000000000000000000000000000..307e41541899d742a97d1209813169f672fe13cd GIT binary patch literal 27923 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 t`cn=62#v`Dtr9SA^3b literal 0 HcmV?d00001 diff --git a/data/pygame_2048.zip b/data/pygame_2048.zip new file mode 100644 index 0000000000000000000000000000000000000000..476d0b438b207f594418d2e8fb78a29371590f66 GIT binary patch literal 19338 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 z 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

Ei(XwNOEtpM1u_&thFjw8IQWXi0_ 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 UqTY8I_-Dh4fe{3}4Flu<0l%(&;s5{u literal 0 HcmV?d00001 diff --git a/data/word_cloud.zip b/data/word_cloud.zip new file mode 100644 index 0000000000000000000000000000000000000000..fd395c5bd2c2770fb8a1f2482d16a2a3cd9f0686 GIT binary patch literal 54076 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 An*aa+ literal 0 HcmV?d00001 diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d4947a8f8..a1e93be40 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -117,9 +117,22 @@ class Engineer(Role): action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) coding_context = await action.run() + + # Get dependencies + if guideline: + dependencies = { + coding_context.design_doc.root_relative_path, + coding_context.task_doc.root_relative_path, + "code_guideline.json", + } + else: + dependencies = { + coding_context.design_doc.root_relative_path, + coding_context.task_doc.root_relative_path, + } await src_file_repo.save( coding_context.filename, - dependencies={coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path}, + dependencies=dependencies, content=coding_context.code_doc.content, ) msg = Message( @@ -347,6 +360,10 @@ class Engineer(Role): context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.model_dump_json() + + await CONFIG.git_repo.new_file_repository(CONFIG.git_repo.workdir).save( + filename="code_guideline.json", content=guideline + ) return guideline @staticmethod diff --git a/tests/metagpt/test_increment.py b/tests/metagpt/test_incremental_dev.py similarity index 63% rename from tests/metagpt/test_increment.py rename to tests/metagpt/test_incremental_dev.py index 25769ff6a..a4131f1ef 100644 --- a/tests/metagpt/test_increment.py +++ b/tests/metagpt/test_incremental_dev.py @@ -3,11 +3,14 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_increment.py +@File : test_incremental_dev.py """ +import os + import pytest from typer.testing import CliRunner +from metagpt.const import DATA_PATH from metagpt.logs import logger from metagpt.startup import app @@ -15,11 +18,14 @@ runner = CliRunner() def test_refined_simple_calculator(): + project_path = f"{DATA_PATH}/simple_add_calculator" + check_or_create_base_tag(project_path) + args = [ "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", "--inc", "--project-path", - "data/simple_add_calculator", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -27,11 +33,14 @@ def test_refined_simple_calculator(): def test_refined_number_guessing_game(): + project_path = f"{DATA_PATH}/number_guessing_game" + check_or_create_base_tag(project_path) + args = [ "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal", "--inc", "--project-path", - "data/number_guessing_game", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -39,11 +48,14 @@ def test_refined_number_guessing_game(): def test_refined_dice_simulator_1(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -51,11 +63,14 @@ def test_refined_dice_simulator_1(): def test_refined_dice_simulator_2(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -63,11 +78,14 @@ def test_refined_dice_simulator_2(): def test_refined_dice_simulator_3(): + project_path = f"{DATA_PATH}/dice_simulator_new" + check_or_create_base_tag(project_path) + args = [ "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis", "--inc", "--project-path", - "data/dice_simulator_new", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -75,11 +93,14 @@ def test_refined_dice_simulator_3(): def test_refined_pygame_2048_1(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -87,11 +108,14 @@ def test_refined_pygame_2048_1(): def test_refined_pygame_2048_2(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -99,11 +123,14 @@ def test_refined_pygame_2048_2(): def test_refined_pygame_2048_3(): + project_path = f"{DATA_PATH}/pygame_2048" + check_or_create_base_tag(project_path) + args = [ "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", "--inc", "--project-path", - "data/pygame_2048", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -111,11 +138,14 @@ def test_refined_pygame_2048_3(): def test_refined_word_cloud_1(): + project_path = f"{DATA_PATH}/word_cloud" + check_or_create_base_tag(project_path) + args = [ "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", "--inc", "--project-path", - "data/word_cloud", + project_path, ] result = runner.invoke(app, args) logger.info(result) @@ -123,16 +153,61 @@ def test_refined_word_cloud_1(): def test_refined_word_cloud_2(): + project_path = f"{DATA_PATH}/word_cloud" + check_or_create_base_tag(project_path) + args = [ "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud.", "--inc", "--project-path", - "data/word_cloud", + project_path, ] result = runner.invoke(app, args) logger.info(result) logger.info(result.output) +def check_or_create_base_tag(project_path): + # Change the current working directory to the specified project path + os.chdir(project_path) + + # Initialize a Git repository + os.system("git init") + + # Check if the 'base' tag exists + check_base_tag_cmd = "git show-ref --verify --quiet refs/tags/base" + has_base_tag = os.system(check_base_tag_cmd) == 0 + + if has_base_tag: + logger.info("base tag exists") + # Switch to the 'base' branch if it exists + switch_to_base_branch_cmd = "git checkout base" + if os.system(switch_to_base_branch_cmd) == 0: + logger.info("switched to base branch") + else: + logger.debug("Failed to switch to base branch.") + else: + logger.info("Base tag doesn't exist.") + # Add and commit the current code if 'base' tag doesn't exist + add_cmd = "git add ." + commit_cmd = 'git commit -m "Initial commit"' + add_and_commit_success = os.system(add_cmd) == 0 & os.system(commit_cmd) == 0 + + if add_and_commit_success: + logger.info("Added and committed all files with the message 'Initial commit'.") + else: + logger.debug("Failed to add and commit all files.") + + # Add 'base' tag + add_base_tag_cmd = "git tag base" + + # Check if the 'git tag' command was successful + tag_cmd_success = os.system(add_base_tag_cmd) == 0 + if tag_cmd_success: + logger.info("Successfully added 'base' tag.") + else: + logger.debug("Failed to add 'base' tag.") + + if __name__ == "__main__": pytest.main([__file__, "-s"]) From 86b9d11f243a4f0d1581fec4afcdc0f6434cfc0b Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 5 Jan 2024 17:03:24 +0800 Subject: [PATCH 039/101] 1. Update test_incremental_dev.py and data files example --- data/dice_simulator_new.zip | Bin 27923 -> 56384 bytes data/number_guessing_game.zip | Bin 65020 -> 53755 bytes data/pygame_2048.zip | Bin 19338 -> 60632 bytes data/simple_add_calculator.zip | Bin 53760 -> 8105 bytes data/word_cloud.zip | Bin 54076 -> 12762 bytes tests/metagpt/test_incremental_dev.py | 44 ++++++++++++++++++-------- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/data/dice_simulator_new.zip b/data/dice_simulator_new.zip index 307e41541899d742a97d1209813169f672fe13cd..4b8d3f038addf84e74e0d1a15a5209d3a263e921 100644 GIT binary patch delta 25311 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 CD&JE8 delta 1647 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 O)^{N+tc`TY!2bbVvtEk; delta 38747 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 Z_u9A{+&;>N84D8&BL@u4GP)tk{{wf5;NAcL diff --git a/data/pygame_2048.zip b/data/pygame_2048.zip index 476d0b438b207f594418d2e8fb78a29371590f66..2f0457be626f4f60d27563c5bdc7c86940260c35 100644 GIT binary patch delta 44150 zcmagG1yI~u@-|Fxx8M@oZE$z@KyZg)aMz%NySo$I9fEs+00Dx#1b3G}kdN%$-T%FL z>#L=zfvI0JbNV^$=QI(iZx$Az;`*W50w@yE(TNj9grSL8-pfNlV?jVbz(crXk!wA; zoZK0{z6y9H+(cI4w_p~ay`7n*xni^^q8|Vmdg6q^JAmYzYLo0xBfyeE7W=WYGwfX` zeww37KUUz6_j~1TG))RO0ld}+A=R;Db)-iTQsG}P`W0>|CZi>g4+J#I&&|rS6@O+2 zkDeNi=vwv%6WL!w{BsrDk#Ho!uPcy+fPjP``n?P@7E_Qj(83&fo1&r$ ztsK)=mT|^mmhwgo8O32n724sij2bed-(=O_E6Xw|F^@3P4u6~Yph-Kd32^{1QbKMy zwFUX#&w}`u#0-|sCAhz(eI@MQA4SI^BPlB31Ty0zef{RlVgWKXb!M?Oc6I?dA^vBb z5D>pXBqaP7h~z95_V(7!e?S?m{1XB94Fts6D>42BNPz_{$beA~xEW1DJRb40+Vwa5K|(|O_+szK$m5AI7ES@ZyR^`{ay&TT z(aylUU3iT!Y8?`@t3#~oAG^@0CAG{iUP zx`XmC!Xgw4UuF(5_nfBPVCE&1Q82{Twmxe+GDLERj(zD3ZLLg8SpE88!GljF=TijM zOOlx_txxBg1i_758Fo|-3a|Lr4=Q`e@6$UEPYtL8?l}fD!~n&-pv7!V3!huA?|~Xo zUyM*cwIhl3JJH4VKX)(q2n!#cD!!EB-XQ%4@xS!*;O7YX-7BDeP!JHbe+Lz4Z)CS3Y4!6|-Xyk8 zy@ndNaeKHq-?Fsl2mfY-a%eGL?9}Y{2vUXF=E4PCLV5s-+}0MG zg(9azy{nHE#viJ#^z5zV8-GG=I(k+86uO}m8Em|+i{p1(OPQ$j3E9H}84yULh3+uGY%y4X80x&JmsTVuO_+hUdGvK=8WrcX*&{uBl&U0hrC@u*lt zC%cT9Spy!W6murbW|WBQ2h1&;Me0Qj=-A!m=JKy|15QM6?H&#@rs@|nCs_ACRW$fB z6_dy5El1tj-7vG7>Fr9bvZ3OX9VWv z^2-er+LN6XM3x@~=l3Rb91(XoYnpaRAoloCI{HHA9o!oZb!uJ7x=J(#J?IrO{_2E! zzqSA|&vY+|0Jlax|L-u2ig{*;cjo0u%(lXVop+I8lDwrs&MgcZIyxm$Az%XoU4|x> z`SZ#NYPlZ*87%N++^Gph<5BvEXR_0IY?9&=+DkKC<()vIt{#>ZYQQg88T9C|dgR(3 zmY~q!mJ~p8ODD`p(zpuT#c4%}1M_2FZ@t^QF$F3uZN!>t%iN+W)_32cz%@~wln!fh z!|>Ua(7~O42N|_n)YN-7q2P6(3$hGTF;#Ztn|u+Ja|oss$6(TF(P4F_6VC-xs_;Ps z)l}6)LctHtj}s#(6ydvi}tsR*b`f_d-E*sJD~cB zND)S~XJk0II7Mzc>4)tSRt9Q_o!CK^1GBO*b3eD75+ca!$uX{BYMJv?IC|YMYclcB zNyu*0S_RXJp~ebC^$0#%&73co1g}LeS5;xrQ;+fGtco!ZeuBsZj$K`~b52YkaK1B) z(AkTWO*9SJKcrjHp&1RD`4nzu+ZRt6jT3Cpntdj!jMEu&;Ebb_cS5bv)^^<@AkaQ6 zpJ-?Ydp?Zx-!y^ls+QP6L@MyG@wfyP$48cCga}dP18+IxN=b}eK8!#HfiC_VQ z=ie9$z5De!zsSP0Jx(v{J9D*(j|*(l*ILZa{VH43Hp|F$>D2Cc10xpF!5%lRM^w=1 z+qEAPZHRRDH2uNFNc5hSyR?;YQe~Q`YM+UxAC%f5H01Y4&iApDXmJ*hn`Yao@p8zB z(I#j~`y0e|@k-Wkyj~WhtNR9r5c9H~ta5Ldb2@(Row3jmZT$Q-d-TSoIR~w~$Dwqr zRt(;E^`u!>jeXk%@I4ftUpQdAV1L;?-B0yNj$IMP=#l}P;jX~^K|2Gx(iC_30}r)N zxvGhs^t2JTb&C;>*hDk?ZW^v}M1%YmQMz`+HdLACq=~jnW;$^Jy=5v#f+wI%Y^d z09ePUCuULv)|rAK#Y5x7;W)ivE!AK7p4PaZN5PSQ(S#o*fgHHx$JXugY_&w7?Um0XBDdXmdMN0*sscm;}UzsPZZIDPfu#{b%6j@Tk-UAFklSvPPmxnPz;?OY&>@r{8 zt;xcqw+IO3^Jav_vo){^*za%HIKR{`JitU(WgD)$lw&-F7>?c6Su602p?E<eBm5MGs<@ql33u z$X|nGVn2F56e`EQHCYd1h>YqrM-H87x%;;AULWi}wz4elPRD$g!MN|^d@i(6?FsP4 zu;1R94yA!hK1H}b5ksmMcHuNaq0b~~oe&KBXs5q#=T7bmUXjxIv89~EQ0G=Bdz%wT zt&p8w|ML<#>je2oT4k-s+>-o4ux_w5GOf<2Ht3PhUz={%yP)r0=0-COKz;WHy20=q zVG+F2`F`P3)A{GQkBJVjL)2X)uJc>`9%BzKPIm7RXn@|})a#z!y=E*eex5J0P4re6 z-m<{#cPmHt3G=@U%3mg0yY~FQ%hwmK3un}fiL51p)O^*vp|j+wu2tAUEcX||)goQfcIL->8V;m;&o0$A!ZOhM9G$n?gZ+e5 z!FnOMj^3mmmGT+Vz9BO5n4H33(T}li%AN=EM{trOA(qz;r zmxPUtldAo_)A0^lmj1L$BY6w)NL^Tx#$*~GhPH8H@uq{Q3UeW{Sf1q$K4&Bqt`9|o z33o_`76aSaGQ8%9hQ#W_hb|^B`%b z$MSKlBgIswr1LlblwqPr0TSUgzbySTX?Jh}G5ufS{@<)?Krbg}ie zinV@#>tF0S!gsUut(6LcIfY+-Bxg*K_knPI>EAuNpX92!UAFm#_D>kk8rYDdUQ={y z*gw+r-!T4_s{b28TQy_*WiIq5o2Q71x0EPGe6qr3t`PmBwGg6km2YnIm|%4Tw6myG zsp$BbBc5(M3ryYLJSbwtx}SA=xtrTp&(G-(wec{FTO5QLx-4@lG~MY&fNJ&gw=IWy zHQ<)!`QoDs=es7rFY^jT_PUvvnY?V3F4MJnH~z(hnS_3)_Fcg$dy)SK?53uVyG_;e#ur4-~9IKLJfp`eF<_5eLSi9quH z$@rdO)sS#azrD+HfAwj=*1x_wZ9n=mb8&DG)`|o(wfXhFO??}_%4&R_x+JjBVe7Lz zoSc99;{yrvbgaOhJ&br2uquQ>)iO*VxtpF|*KZFf3Ab*}8iew&d8a+UGc>yeG^5L+ zkW_-Y$PUQ?ANhMD3Qj~m!rQwrRuM<0uBp30-5z|uFgfu_pfmq5^HRM?{{H(tI7k4HEUz8%iC7N?4%P_&l+LX! zDA$Vk964o11U|{J*0w&{-==niCL^aC(^SqLhXY%?3q;iBFV6&rLyqxh5 z+G5FK!QP0P>=ARxi;3yW^9FE2kUhvWEd_&xc@R@-8{%_2@B1h1h(acizwZt}_lCnp znu;^1AJ$9xsZKHM=sWi`uh4OzZ!)D6c1_+--mga@Oimx3?MzRbP!%TUjVc#DKV9DM zCUi}AQK}n+M{#etB)>m>`Za+De$)^M+UO@{0@sBu83XfPzMnQ(%ETF^b_6=!C?<(Q_l9L$c`fgkF8 z1W5EK8Ik?MZP39$<8L0r%eIy=%A^_@AaMA$(=< z^6Jq)y?R9Ee|Ys*Q?>;;ng3zXKC#NmK>+lDvnMQNfEQ+q7kfrpC&6iPj|gF&Ennd1 z!vV3|g;nhIofmi{wD>B|*j(N24q*^wnLbHVxxc|=lK+r?})ixtpnexjE}LD3oG_)@Am5Caq`%A z<#_+i4;MC`ceXPxC7ctxxc`Lj)Lc+3yS;7e4?A~5~3={{y{FZ&>la)Bo~OS>qLc**gtef$!qy3ohuPe`u6nX#BOC( zK1GbWc;AIzpC{h0`=fd)e6VV>kt&lFl7?ti-7Y~d4tyE7oZdK3jiK~AURPgQZAk|s zYv2x)1VbC)yNC^gg}wsH8g#YO8fO(gdb_-h!EE#{kS)G+Tt&Uwo&TgU8?seUf^`N2 znGMn*A)tJcZV;k@{8+@?wZIyL9^{y5B=x~9uajO(39^-&bT6O77!Flc_BKq%zix%g zOyWBee^@q{CHZsOz}hH&bb|K*Zjo1v9W+5F$}?r!CsM0UkbjuE8TJ72Q0<_9oaJKN z8|ay^qbP;r=5N7VI7t#FH8aH3DA8@_RT4^(1&yW6P1V52EqFx6qbi~&J?Ox)gvDDU z#F{!+VD`Px4~1=Bz%GtSFNP5xc}HAhGL%jI<76DTk7~EvHf{(`VP=YND?8lwZI)@o zyANwE+ZGvO6u+u9IYUJ73Wf_+4$hY!ZCdC6%Pzx@+M&qvi^aQrS&hS+~}70{(h04r(bRkx8O$i%;fc4hQ2kc2rDVQHt3mHbpyRk2bQ@+*^=6J=Uky*T+7AJ5wpB9U&s~l8?1#Ke7?RRxJ^RDt_ybHau&3NTcK%5c!Yd`p z?+M00(5x#q*pxeTMVFc%?BMSaB~S~w{GWXG!T027p@4V&NBWyTel95T69pl02)O=I zU=fb;g0tHoo_A0RgUHI(XQC#V=o;S$KU9FEyjFA)6b?gMpMbkCX7SMQa|Z~^9d_w1 zI3nz^Jh3x1WEen}%Cf&rh-adk0(r@?S^74P9#-p)lXK<@c3tx$o}r%I3V&O@Olr9e zpc6-$pZVcn-l6YF=;90YPisL#L%i>NwHAX{YoY$r**Lg5Tl_m~2v%FSUI8Hc3cX%90508!K0lbfFEh~f^V7^58B^&(`lD$q{9^!`i%9RgCEIJI))c24RkS}(=&>n`K9|hzIh(Kqs$(D}0rK7iPPf{gY!e>lw zwWQ1B`L=zoG6azrgGi}mKAO9~iY&Jm#cm0_ZHb_I<7*l>L;Wdn(mB zXFU&JDm)x>*584Y-EFUv&Mf@exf!dHv2qwqrR{~^)UZ6MvbuERGsVmrU+85PCut&O zOaxmnT+E7R%Zlz8uw{z5z%}(DjTl~eS(|I!JRO2Lib6dcUq!EfLwqDNjZ{bG7r6yX5_2LW_7bpf{B&RF zO*MKuN-#%rBn~E%QSB!lfmPjGFI`;BSK1q<+OtW}jIE^vOcP9gYR%{{P}ABLkFKY+QOH8%8f zK8G5f9MU73Wi3=?w`yMo_J+>Dj~sd|jY_t@E-WnuI8By`Pan6WmXEQwgS7?GF2aQA zmff`r6&{{@EX~HcuYDyibHe`UQ3f4rscgRXD9PWx=F6|a*1^Nv*cN2S21pbXh6Dfm zc;R>M>;y6~cK%Nlm91rMzbu9G#P}ScE7_h>@44{s-erp~1PEt)8xK)7X@bFj8%7{k zDU0fEh9?>Qw0^ge{-eKs3WCA!v&`C^;2o>y?W#>Y4tqmPj^xC?Ub1d>^;}BGMc8g= z=z8q^8QOJhxeRtTczS{yH`u8$)mrvRbu>H$$=Eb^PHt7jN=6gr$pBv-DV-K2U)`#V znoYyP+M;aA1cr>R>!lh8h&hm8qJj?Xn-ENebWk_q?3-he+%N+v(72HBn5FaFCd>4# zSPVN+xKM*}S;K5eOOp^Qk}*+U`gEI`esm^;DoHF-Pp51Qj5Nf_lRx}rQmv>?Ct_t4 zuk3@R1qJfQo9t_9#dKo>cE(29O-^sK*0p6Slw|7kR5$s|CdNmg?^#DhqnWbcRw)CPx$k=#rdoX##cMH^vEtl&uSU=+>=o0T)3 z6Pv$w;rQj{SuZ#MmmmyiWhp^K=4Kxc`0k{dqH4=T9lr-rIT=aN=UMU z0vcM7>iwrdB8t+K0qgV%Wui*UEyb@!$9^Y0G4p=l9UT-W zm@=3VWS?OrVky3^f@>FPLa0Zq1KF>%OXU#-;4u??LZrn_)CUT6(`7=l7TzTUrMHo> zuD-B_VhDoY1tqM#mW^}4!JSXX#`YD}K)89syWWl%7kF^VDS>TOyupd>%yMcgfvCaS z+RQ9*Aw6%Bym1jrEZ;%r3xl!XI-6Lllpy7IVT6#nf^}$f8b2lxuZ9r|ZVLG__6!+5 zuwRZh;Rs9}QC8>6!8}^%{Ll{%I}M^Adz=FVz(8sIiMFL*I0{OjRcr%2>?cyQpQ$ki z6!709hj*5b#)MFXiIkq1sfTaYr8s~OYv_qb+f>BSKTrJ?(u>@9+Ud=;BIzAu zS;3Ch>|LiSMe_CN&cM;#nLbwMo6d5r{P-rcclk6kKIPa%`l0B2Yur0$4lvFr z9TM!%y7!b(cE$?HS0XnXz8I3%S!>vJE7?RH0eCF6Bnl+l>w4M_-4Wk0AV<(?2NESJ zq0R2!7wd^MDl|ngEzy$36yKG;R!i7?if>7zumRuUYFbW3Pd>M7|rY%6KY z#6CzS=T|PLr(T*t^=4EV`O>+hIDzM3Qodw!ETH#xc$la}c?Q55An`KWNjpNWxF}>h zJnIm`ebD36zw60r&Hv`NCO(q+4sC4OJoChFZhbD zJNM@wRU9S2J3$2Q#mwhxu^J`0hjY%+68x&IU-f{mlCGqI?_sS|l0OYYRRe2f@0i0) zvYwZu*6D;`=V=g{Xc+y7=7D09xe=V*Lz?`V6!9%~hCk=iSK0W&3b{y_@3EFiEr7_q659@FXQ_;*bYs_t>Xg;*?0U zA-IZAY55T87fuK#9iGiqT;Hs&=43261hCu}_O&e_81^ux@2?Y)`w@dPVXn=VFUC)| zTJEs8P-XN<{3~tQ;R4}=2&*X%KUt@@_U%Gi;^=>Tk9Z_@oPm!RSutGK+=;HMy=2KP zTa^9q-f0Vcv6caLXGMT7A_VW&S|#K5d)PCJ{zX(Ae^ykVKVppls!xTbIR2CT5|lXp z1d_3jN)RH98sIa*V-m&GFk;@ZpPc7dy5v`o^}ksJU0T8UMuXcbA+?Y;hgu z$%>MshVfb7>Kt^p1Dx?MSJFxTcAuS_aw|jz!QI#$L;jU}-dNeuKOi-Kw(^$<+L3o9 z=eBzG+R4a1;42CxQNF;U(+}C#=F#xxXvWaUB5BqM+#G9`GoR0a#WNKVR@0s~b)U%w zlEcnsZzx?o$FtQ=A=*3R^Fl!Bx^*iDZN8yGXqOoz5DqfiS1;*jAM1Piss`kwYI111 zw%=r@Vn4jm;ST(G=D%GKTl@>#NLcbG44AAWOIZqzbO@ zN0s^k=mJBAtSuNmBDy2-^fF;k*Haqgon-2EZSjh4`6E{0#2)$GMhf5^7iT@q2iceT z2&s2J@rvh1Ju;Po<&QXjF{!;wPDiFX#jetJqcrm1VIlp$af0Un@3@fSWbn zQ=@2y<^K#0N$l(8Q{g{OPPBmAkTcCJJP8gmC@97qSwWkvIHt0kpgiA7$h8t%+^`n3 zfGmV992g;VoMI6RlV>TDyuVMVsAbS~6d-X)`s7-RX? zl3yuOIhGVS_^UJ%GFqFonvndnVwr}8Ki`FhfLKTTvpw=!tuVRRGXX6?Kx=zfmw#6- z(He@*ul-Iz>2An)!Kzi(9mg1@#F(wNM4|`QDt=pK0(8BIQdz&=P}8RAm$haHiRoyw zsYE8HJLC1`W~7{sj>@Li>s1p+a=WNc(!5cE;p0niKa!_`=)Dm1RjkOG}Y1$E)>T zsf0no79dz=IMqvw$j3zGvx8CJZ=W|zdJnKFo$2U(<>-mwnH6MO(6|A1NY5x8KJAa1HSg*nCPy%};#S z%OlY&$uh~uU$`I60I_S6ObknGV192!*fx>H0*kLUb%)UCr+41a83&F*ybsw?zMxzB zj;hKI^eq^op>JiW*xMDgL!rzyK2FJ+Ry+l&U_fxXI#t-Z)WkPaK=1T1l3O31pgve| z-T3i63Eb0R0~oW;McaBRr7J|sn__--P!*(a8Ie_Jlx@LPq^i&fUF=xQi5+tkuDG^2 zz^y_pk`v(OjzL!wW`bwNqZ-`iBcrZ&uOo)8pwv2Py^nl38wc!_Fc zXTW)@b@Hnq7ot@qL$=7Qqi1>V#}g<{7aPs*!{yD=}<6vy~I2#`fdBj z95^7fbc9A%{oao&yoM(0iXuNbMj@ZB?B|qF@fsb;&u$KZ;tiK$aP!!QqjO7s!8l%f<6Y*pydqDTWFGLHYOo&Oc5Et2Ebtp9%SXevYUx z`O;)OEPLDdNYDmN`~Waom@3JKw@9yV`C3Rbk}(v5VeQ<`l^q+ zG)ZT=LU7hTBM)NCem3QEFjER|i9#9Nlq^|FNL0RMbpo7@EOimvw{v_jKG%E_j+Lk$ zfdM>kY~GKM9bVKDs$gg1(hUUMSOJ~-ZL-290DhfGmyU=;?iNR~O|Pu^F^B5?Hx^1m zetlV%I^%!AO0u<~;d?<;d_sZ*d$Txf(*0COVAx?^8MLl&1fH$j}4oIH+{1y{1u4)b(2Dr0#MqC3?kB~Zxv?MH#Zhjb&N)kp6O;?S@;k`*DVm5b)rFUn z{?(_GP+{>0Z)1$%PeX~wAm^h^mCC{)RqS>KOC$lyo-pw?A)yMG1E!=@D@kkWv4twk zruv(99cGN2kx%SkvzzANGO$>x!%+rvr?A#X^Xkx>F_ur`mT?iEen?@5$C2-FcU<4L zYu0b=$~Eu`gZmDvC;8B-6I}?0)L*J>ZNvEoBtJ0UC-0296N~{C@*W%1IAB6VQ_Dlu zT+W)`e@a0cZnoE)%Cx&+JQzF4i!KFH6_t~gy@$@R-6!Y;*S`l}eOX{Cou%Cmo8Gw2 z?M-P*9DH)Xqf~Gv;(``e(?$19N+E5K8V;s-kRp?yXO5Dhc<7JhWc{g zEP1v`zL7yi=)~h*xlX`uWGN}EJ4N&Ji=)j;^dvMKt&^r=U$iQTnvH+7<9RY}c?9*b zd3*++gG%8T7@2q*vszeb8*z1qFg*Y^S1CVNJIVLm_YG6P^-f*UGxnSYumJvXvU4Q*#a$&4H~{Oimqvc90&n;HfOJwlg0=3Bw6gYfM4iIWSzB0k;40Hh1%NC%3? z)r?P%2IpRyb%=>=T^~cOldV^s5D!o~I0hoPvyM40!Cs+jvrT-(zN3c=#Rz*R_yL`; zuHFxs8N%d zVat1e$;exn+3potnM<5TJH!3>NGYfJ9mj;bMlmUK$v&@e3wSxvCoO=06x6Vbr9Yi= zU_L)I2Y$$z>5CR+V)J9LaA?4^*T{>GxidX>hueZ-KND!F8~sUc z;q0@i`;YL))tMS(4Q(Fem)89|K-JNgab!<~bpxatu%qjiE#hO=f>8G@(lJ9ZaqIkf zQ^8GW);o`mGgAco(#C9p8YWu|tTgw-0Tu-sf zvrS)JMyle&L#nywAU~{F3t7%TfWrxRbMjkn8kF6r8`!=UZ`0r&;bIkG9TDQ=rQ_id z;Nj~KWTt22^WejYaK1cBYfy9DoR-1a4OHpnyS@ZwjC;t!V$|MCmIW83OO~~kc~E&$ zVN;BWWbCQ;&;Ct`?{?jq5(gLBi+kKE=OrNb0S&u;SC z#S)M)VUiERTgxt_V5xB)hVMNFsKPy{20USsTfRZRQFG>xpvrpbdS9nq?(DY4;EXor zJWlV79~3_ZQwNWS09(2{4zA#Fu^4o_r*dP_E*4(pIvQw8B9KyZc7Dr#CBv=R=XkNa z9}$z8O7I#@>l2?Yxw;)RU`0)x<=HPXBdpdMT4T-Z+dhshw(C0Ud(1?OD|-geu)&j3 zwVH@oqRw{Q!L14U9L=OT*@6v6BZ^F#z-rK^?EBb7ID4r$R3$h^U>Y3LL0sHI>LHrl zqAx&2{8qXan*a#4U(jyv*w)wZ-FrwYEce=Kuh!L_X}~~$#6oITu5O$TWmM@e$6`e!%e>ZDnOdx=c>J6 zz{-GuC}fiRcw4K)gBKnAwqEBAh2+Ckf9uzw+up<0kh4>`|Kv4)4FdT5HBCXrVq1N6t#~O9p-yePC#ckzA0W0UV-^J^fw+0 z+@jBzon4FqBF)0WFT$onR@!PjMs%A6aOm)=0XSkJ$c5qGHhF5!u}eR?+# zfw`9;u}LNhZL*9lVeJzn173GMD37)K?G3An^|<{=rK2 zo0aiztbff?{O1qgA%5dyr=Po-^ZM?rulUgX4WIvK_~QS-5`>YD{tGQ&EaY@v0(_I= z*8NEz)b-i(`XgK|3u2;d{#zuPP~L_c9KT*iw*-VYlkw1E>OgDO&p+5@)!l6cN#iMg zW$hs;-{dty&GZs%&EiA$80%VV&(<}H*QfjxT2&K9K4i1AwV5VgJ+(il^EkCX(6)@B8^9t{xU;hmYe`!RV(7#4Qa9He3tUy4Q|5J*;9#=B_ z>oLuL9|PF_Yb_A}g5|;Z>xS!VT=`96f5Gyfw|eE}H3I@nfk2Q6h?SL>iw(rX4d5~{ zV+WXV0N6M-AK&PISdLi&pr4dPU1sImEtkms67d&n#M00UnwgEj^O9kV z%3d0ua+w96Eg&q8$|pt9IPdCtzu$%6<~Ho|{ivkzvpod|W`(Kzzb zXocuDxV$_Os2rHN%^I9~Tf{OMVA57vM(j;j^;3)WD6^RYXLl^U2eC^gojNA|>zP7;Xm0Q>$j2%rzzdgA-VDQld{G&s8I zn;OmC@NCUBT->0DxDF02Xg{!O)sMO>*zs=54- z#GYW<*E^{N|MZC*PV)*gEwI6(56g;40kK2EcPX@3P~8?$0crE>`;bYb_UK*Ma%F$P zT*Q|+xL^z=Ew}i#Fmf!72&^ENWD`YNKD$z37|o0YEO#$|6hd%s-tF2x&YeO=m-Mr2 z6T`eJCRtfgs={!$sa{+cg-yB4fR@RA2dIlbWvKwPBELj{y3Z0uP%K*5^EeXebc<)w zWPB=t!$$&~Z(m}<{?s;akbgA(7W-VjST@ zBqioiPDiin&2Prc-Iu2j6bL38T$(b)g2F=-)gWvOFXxrrqJ0!^{8U?+PYaNIq-~K% z%re!<(LQo&f%RdNk@`Mp$X(nz-2Fn{91-;&6aW9ycJ4p4oysx8;rGPXZ~9Z)xmkGG zfyNwM#ymU#UXTe7h@B0<$;!^l$!%uJ!^X=7;4w1=ass$MzG0w~Q&b1A0s$NV06VJ+ zfDORL`uaOtBL^2d=j`a1qH(RK`&WCEWxX=(OYp+|0%dp6J76vfL;ybee+P~J@_30= z3`D;p+21Ub=Z`fv4I%#e?bv_QpKIn}F)`s`<>BTwW(R;cIe@G{5SNLu87~m{x?%v& ztFf|jadUCKt~opAci2OR5ZuFVMXHzO&cu?qrpr8GJ|U}On!H=){z!~xW@>GFAQW;j zg{E#84LgEmgRFyGAoGor?UDz7{yl8QVd3EESZMXCHjubHgr1(WAiXdF0=iyHg97@C z0v{&d^R)-R_m(TjpyaN2Y+an*&*gT?dELIgDHIJKkOi60E%wHNMi8z2d8=)n?CNrs zgg#N+h_1q{ATrgE47Jw{a;+vCNPPdXmEsQ?3n%kF*#jz0nt4p|QU3xq%#={hKpQ zG#!vC)9VrJHy3gwu)B#Nv{ZHeZmH^d(EUctyEGJ!u-LQsEtk4af;;~>v0jVwlGmIo zW1CS0ee+}TmVcDH=lFp>5C3QUD4d)-;}lGMUAV!qJcR$XXMe_+D_e=#iP!k@oBrH0HWoH^4sJ7UUREvurzr;) z(3G8%2gu3>;9>=Efj}leHd9kpHrCe|W2>fUza)Znj354TPKjzsIkPaxgI5NJNn4lU zCqX262*Y$spuuU-)}Hje{H61(s+UWS=V#CHw~jM)X3dQU(etute+itW+P-}ja67Pl z*3Ol5>9mmT{dth}*bK&Jw$(koz^@_~hF*Ce<<877e`fwDH{yjKoy0~>S8H-9pLQ$bUZpO6^biJTv4j}ig4?rzfpO z)xVCJf5d)y38) z9TZgfYr&D!gxeMBeOR0#5%6OzlJNqx*W3(RSa6$hHv;TjQV)9FGtA^orDZI+)7P1s zOtl(&efhK?`%H^7L#*^(=|5C9=xl|X4%geYjRr6%WAuPGb-%v#q!1eq939p68Gv(eR#0!{)#Q9L zCpLF%u7i-uqx*(iOyp`GBcveivQM0s1c;dVmp}S2YfemM{yJG4Xfs;UyCaH0JHFtF_S)C{O@A5^4+{sYF+0eV^VR$D z@R;(Nvhe_bY-S*14ijDgw<)^`7YB%w8^HM*3N&n$x1`Wd%_*Pz)x3KST7_g{S>mfP zVVEU$FcAXcs>?NEI;lr_*S;E%NT|Pfna{ECfA(m#4)Qxb6=1*mcFf)`cZ29l7%Th; zc5#3xLj2tR)cGXXw0OAE>j4uu;1gw(%&s{Hjr52vS9&H3{F_MrojYq4)YPQ?^ncDSAt@-snqXA)Bf7!CLnTPda`F(a1V ze#JGa@pRa1JJ8*Grq1Qp*~ZT>TA*vx)wv2@vYf6gNmsBXozKThX7e(O?Hs}|<=k1^p3$pMbb6oA-3{~Lqn+q$T{nF@=-Jp?= z#VVV8vKoGUOwldJ{ZZ&qzvxI%2h zo#^Q$$@`@%yV`hj2^6@vB&lFprRwn1hvSaGS5L%4M~*5Zn?j$>w7}vBhb( z#(P*4N-OaQOp2J399AFaNL@V}vwalXB-u#yEl}0$lvCyp8``6U71R)ik0t z^hfCD1CbZ61rGBw@>)#3Zu^fdUm)@vw<^1aq)Xau_S{s(vX8=5)Wxfbea48~=@$>l z=gadnaa|Lwhd6UTXtchVLx4{&v8|tIEqIWgq9qErjhC~jv&@Tj9U>rVMf`HTZjLJ& z`YUz@nK}x2qwYy$atD`=)X)TeZKS}GgFH+q+);9f4U$5E=S>JK*D)OT~ z%3WzIXp~=YCKVPs0Jq=K08nvZg^qdN-66$UoDmeS42CfurcU^UP=g;H>XHksVknsktHx@S21D zraxTKD=!EHWa9uC0|8vTrmV)i#;j(qg(Nos^cvN9&A7NYfm{G?UXzb+qE+gY`bCgI zQkx_KIN@$kg{awaO33)^(srBFDO2qm&N>%&S(#x$uxfU#{GE5lcgH@2x91O!PPnhv zzYM1cflMF0TS8sHAnI@I_CCKy%)_jWjVRj+ z+5sEc!V}Mw2`yM}WC~WKawKoGmIhZOFE8A*m^vKmJTjW#2VqtmOMX2%2SYYnw%CQN zs}jA28c)Mull2PN?za3wi6Mu4^#QRWr*n29T^J!fhYffdo7N2QiZ*Ps5?MVHyd}#! zwFgI1SXIL5ENa1D%h;%C5+Bem8$t?*k$`S$E!DCXgSFLL8Bv@QR^M%#h5Qg`pt`?4 z8(-9)aH!vFTde#9QAyE7IoUd0vtY?00ml&5uU@tNe;#x5>9Wye$fdHGhrN|(IcA`Md?M-0tA6$C_TpODW# z080FS+8-Pye}m-rKO)iIw;BY$!ttB_gv5k}{k8950t5j~xH(=Gn3IPS0C-hnpcyyF zjEe*C+5q7K@p5szM%34yVm~Q5gx4+h8d9&MKKFaEUs#%{Gl5HCPhcPM8YuQe&nl1= zyi2CI1Un}6DApt_EK+z7#)^^&Tifw4ne&L~z?t`5p0y2=@L-Ctez~t7%@#V;!=9^c z**q{gI&f82{=x1mUei9G(*2NnP1cQDiuU+i=p#fnfa}};keSo;Z!-VxgfXAGN4UM3 z?Qi-+=2u=5UN$Zsb^wr-%M@hH&dzIQ@~ROa6BAZ$R-g$72*Ae8Vam()@r{qFzH&b) z*3IIVmmXm1l63SzV0d){#rW(og}gUeQhmz**Vt9SMe)3ELb|&_O1h7uyF)+_Bn;y2 z=mr6aBL(S{l5PnFq!Ex5R8j#+1rY@iBoqYmv$HerJMS}- zkI+1NslT_7za5aYzGgEhG4n*?%iFrgk7n1NVU$(t<8&2uXO?T3%SOQish*1Ox~WIq zoE{=|w!y!;hSM~fMz(@OpGBL1HE}P$ z*2HZOw?>+El0EbTg;mXe+-&g4`Eu}_)1*AmxDn~Z z%?9#9Auy1I5JVIhItqdz;!udNurS!tNf3SFP4-Q$G zXTV<#Hu(pSZBM=YOy_6eR_4$A1_vdlmPGeooUKkYP-@E?BryPAL^|Ru`#<|7*h;`c z3@j`nBxWfh4!l>aBm^ZyKoSxFF-wRbR7hMzR6zJ z9u+6J@$sDZ{t(h~i+)ueJl0n{s=YUvl$n;UMwe-anz-BK`wymKCm*F-IX$U}nnk1& zwF~403kySpU_w?R5Mc?R*MNyYfNlc<6%`SZfPzKEEG;17fqg5Z;^JdxNU1@^`(Hs?ll^ro46S1xQ@GM zDA(tRk=2a=Z%!4@kvHd;4NUyx#AmW0cWM`y^dQnn&f)@MB0}OI5g1^N76t((JW*jQ zD~JRX=)J^1fEmmR3K9W~)DUmfhsGBW3qI!D1NXztHi^p&Lkl@^pQnAR@l*-4#Hi0F z62(_Yu*pp4#U*DDJJ@MqWlzj!@1v`K|ET_P%eRkhQj8k&mlhbWw)j z4c1+@Ubgm|#x9e8>&aFBxsDjrl4yyyQuca>RDJvOUgg=l ztwxRl`?U;#T8(4!kb!<%ly6f$7ccrucVc9wz_;k7fV(=_MvNw3?N z%0#_K@$`{m5mP|>H}-FfAK|Ot8Vxwe3R7T(qUg95d2)K5P0C9>3z$ZViZ^>BZizeIE&;j2XZraXS~^t`HTX+(Fa0}tbfhwg!5NoVMy zrDg;L!<2JAEHAy4w>D!zNo@Kd}x{qv@vG5 zqUV>9ebL{53g`0UDC$j0qafTejpFMy=T=9!Q+NdmCKp5fd5jCrHhw{Yn|Mj8 z82+v73%z>}T~cH%7nO zmPxZ_cPLe?#r~8@#{$~V37iJZ=KjlKE1eXI>Xrzw+i`FlbwZ&c-mbCMshuCsiG8y8+DeKP>* zN9Pd6n|;_ysSr{~&z6|%iHq(}s)p8LtGOJPHb}xu`XPj6lO)YF$H1dH$>w{mqZ>(# ztTr{fhT5Wa)0XjvN*fx`mK$LMVTsw?!%G^@u=r1WNt3Z6UlE4QZNsK_Y8DMsPvsgz7EuVs{< zsnV>z_rArFM`fP$%X!bGFZ`O?eVP-km#fYPtofi)-B4C*egLV+V>;|j@G$GlrEM)f zuK?rU!ZHrw?8H_KpCt^GO;5GB`;_b1#}x86FNM~Y$l3YW!TgWU5YJj|nBjAnZ9hcc_>Sd(f~ zMCYZ}72~e_n-9BIOZe+=$--^#xDdoNG+T`B zoWp*R(r;r`d%?`CD?%w(0{@;~_hXy;1_=~&M*Ha_F;ZO*AI;$P2GdIfH|i~HI0(5B zyN;?6Mxy5(ZZiq0GmnZyKaOtFi~gPb+iR4}d|4Vs zXW6Ncb_}`4EnIYWue8Q12o{Mra8K60XX$OG`SZ6CL$ZW$$P6Pb?8$uSC%5 z2VLEa2VenrXQPJ>F+5R3XaDu4fSq_#Jek)e!UlR=L^|2)!UQaVw}BNHWF-o*5V91O z0IW-35J*^9R7e5>yf7?8ErIti80d9lj2!J3#fjU7!VbfO4qW(rEKF`MJ`PvB_rCe< zUd;o7paW8JANr`NMBT(pyuO1knF)8MZZ-JY+t}F`rdwzGj5UMocelSb#I^gucTsE& zQ;qXJck&b{Jg*Z~q%7em;m``LyV8! zhcw5HOU}l4U95x?Z+IWQ(3bu*>rLzvJ6e0amqwatYmlm2-ScckR1T|Y1TH*>u7v`e z=|;~raE+R0abaCUUZ&D(pLT)d8r_R3je;zysg?}}@~2@X@4TngV=uBT^0CF%4d{!D zT?*d4tD_s3eIL!Mn{c1(lDFr@@aO4uT*;UQq`q|2qxhtIYe4vZPF7d>1(VDBVuw*+ zZ|C`5h?ODOo&i>{uMM%{W8Z^w-xptuUfI?a4{8%&Zr_h&^{8;^jr}qowEA1lE1rxvLDkgD*1s~fuEl5JBBPn^O7F#nf zUuF}@Dj(VMLZWmi1kC9*W%6V^j{*U*Z?g=n8(eK`PZI{fwenqZvZA-Snov~bS zu#73JX{tqrxO#AGC(PQvSJ$;Jk{?WCQ*>*)37wxn>dIfVi~n8lvy0CAlxs{?P|XoE-V>&lnW_!D9>L1wk!g zU<)y*g`hYTDrg}L5(nn4U_nb5L{L})2Kf4bS*!4``O9BCw#1V@kNteDXHY{binCTb z2jQ2VW$hE7BwQp#rfhuLV=DnY(Qh7J=+lq_d|iliqTdAaiiukZTM0{uTZ#$6EI=Yq zC@^`E5QB^_9nnn6_Y6Fshq!&EP6%&0J^FpV5su$Vbja@nsAdx3+|n$?@2oElX0FE1R^n&l z$eOSx@-G92%DFB@=*ir0FQM^*2g%{9E|S=0%3rzSRzrPbkEE)eG;Lv%WeO%!Nqg?b^GK_4VgPdH&Sx1 z^lP$btZ^?tu&gHU=;`>2y0^Ig>#^)XjJ~Rn7Y?(tW!7uwC$Joj94o`jidOy>L z)nulq&+IFuZwFn8*eTrX=v=>gj=z7Xt!iMknuGt^V|Y*zYp62QbM|QGCoKk#7%kMZ zo3*IrE`7Q79nUgQT$yG$$hKGbEc|NRA(}BhW=Z?y%NOqD-PLC4GhuPyD%jgye^*M& z9;6|wM`Y>lzmX#Hc|xNgq@;|zNPy%!mZb986t67_J3OiGG4hT*@~f;~=VR*CsP71c zSwc(0czSVn)o5B)3IoN42?MJ96+jpEiSjx`D6XV`Kzhu6*Je+?>qmST_mTlQ1VlR7 zW?=%tU|})9w+{GYC4kpHu&@B|D*?+RghWM!ghho#fWIw)H9_8}o<`@Lr^Sh95i_hH z-^V@RQNIUQm?&LpZSBuG*3Q1Y$ct-A`(#GFP*Y!LfBg%AhRHxNfgT%0c-L3&t+@*O zmg23ku_yCaTXX5)(7QT@etsT)-EV!Dmx%n@J?q`>+CC`Do}BsEHJ;(H{qnsgo#7bO z;fQS?K_)Q+XeC=CVdA`*NLJR>);vv7ZTqDrHv7`qC*Q*x4wqPlOZK@K0#fZmR9-$> zcxdS9XRZq0jJ2O$NExinbi4yI828mLW^+_537Lz@mX*yIf!BS?vcfgf8L`ooT9Yz- zQdbW()T&>|z2FiAXZ(m~-})J~tp7aD4OO0gts3@uW4iiJiB|KGeAzMm3zjb6o~Xz< zX6-#G<1-ls#*yFZjeM^gn2v-ba`1k#dXjjanVr|ph`)KLN7?)X7&m;5&= z{kzuWdR<+5tqOR%caMlcZAren&Src?L-SOk0555Rc(6gw!^sE17@s5dsOI$arZM^A z%vQ8*my9a?a(nY>cd^fkej+gJ?#7=1V*FM}0 zN*c>k`D*nh_UOyu2~3h0=-UC;w#bR=7;=l1lt0qGQ)ptUzpU1rAcilu{~gs-vjjaA z??&o0&hr5d5DS%MxgKY9xMxh_*O57|2uy|z%58&99V%P+o?yJyn}Zi+qy76=98>Uu z+nK&<9m?;bYNDoUJY9P@{8DY{qr`CK4hezH(3@R#UMA}m5{f&mhQX7vGdRy+@L`C_ zN47?{-E97~ts=T_@sAg#E}8=~Yrm}!e4953BNv)y!lisQl60F<&KmVjW}(gR(m#Ln zCi@0zLFc+9Rl(8~(+ww?!@T)zYBj!3{5iV?p>W^& zYvRz{Lew#31-rAmyb@M-@O(q^FlWim({ozrkVvFY^@t~K7mKJ^3Nhpc#;XoL@MK<% z-`#LsP&tEj`}Vi=?miFc$K!Wye|V&!*spP^pjA3mztCvnj7CF#%_Cc4uVg%C(8w^E zZX1taO~7Fi+;Yy;?8!2Xzf_ZqO!}xsJ9)%S0(xbFkoVF2($_J9>`euAhI2m|j>J|p z?#8ge4F|h=Qa9h)P(=)>zD&Nv@(%O73VDp@x02LU=tmXufeQBOw+_TL_d}mE*VqW$ z!6LHW+)qKLQM{KeAUrugRmqk$JR(wbIj*tQ%;Hw0MhJWloUnZ+ahHC26stg$Hk0Br z4I7Ow!?vPPwZttlliX3tH+X&V3GZs2%fdz*Qn^c*qYem3KS?lDGLO;U2m&CDo_ra1pK8tRkqB|dvHSz6&8`FI^CQ}r8#khvlf*SIy329U8& zQI1zml~dK#)N6$|67!v#hUv-#NU6Tt7AJxfI)){AWl4=H>G5gO(Vk9H$T%u|(ku1% zBXEm(8I~t5XuL-()lJMV)H(U|J_k7}U3x1{D04FP6#Tl!i%6t*!fD@##Yq=mDgHdg zGGHV`q!X_MAg`F1AOr#v69a<~#!O)d2qFfR09gnFOL-)$0KW)GL`+y%;OB`? z6Q9;x%-!q0GqCW~5`L2=;9Knq<+hCRn*@vAgH+qYH*sOO#7kVa*l&G0i>I$2B{H@} z&HrL(OUwWD>Ui5IONZS1YE9_<7!+lw!UbHV)h&-6sZ<~$bOkQ+QOS|{lJ?CPIOwsb^stvT2}W@qo-2`l+W+-i_0w0w zV&z1>5`RS7FR;^i56Wl`KjjCz&4- z6U-L|YHLUfkxGt;9qz z2_@b;K^7o@_$1ZwkR$s%El-YXw5)Z^KLA+~=_F^E09XWA9w`Kq5Q2)sK!Dj88083C zfGi{+Fbhk-mjH%XLZC2X?ouK>x2y9 z5c+;))8@G8+Ig|W`ZlC^d5Nk>xHZSVaH=l=-g-VDcs=~<+-tihNgs_esZoLyH3Qs) zLKZ0ozYkH)SHRk;ww@UAn{V}4=}!^A4<6X=o_X%gicW@ib6%1ox_dNsK>VS7d|M*d zWc$cUt>p8&`OiB?pXNWGdET06`7kk(^rp2t)3Z5`PZ_md4R4z_OXHWKRz6AITv|hY2-^>-&HYELdv`L)?<@u|tgGg0cq3U zTwkHrZjzw7WrnAm3yn`uH?Muom0Q|ZRqb2dlJ)U(AjMkydf9c~-ZR1EBr`8wcuIKc zR08A1f(GZf@4M+&I1R4D@4j(Rpvhd%8iFNXkuZ~d0h&q%i8ih%L1T;o0S~(ZyS+|1 z$+bocvesRj=L|_K_cH5Yl9JLsN=0^Q;{)yr<9gFi=HI8lC5X074trm`e$z6e(Wer= z=w5E;dcfy9WLD*f_a)2P>%MEo`3qkPwH2m9b+8_0wQ}hUMz7Nwz)QWcm3*h{=F3d0 z>iEh{-N>xdp}I1!Fiu`z2T`B^!ZF0l8tQJ6{7)pqy1;_ZCcGd+Qw zc!)!3LHJ3a$IGlddp6q0<6d_=dw`9Jet(gR&$9uQ`LfW6$XV6b!)D3_XijejhI`*; zr3S|~-Llg7Dt6BsW(U79AhpbfqtP@9RgrCYthj)!v*cwtruprq8mIIs!HWlu!<&so zgXBg|F9xvecr}CKY}%8)FSLZucR-o!-Rv@NcMX@aZ;TZTRyUAvvEHThz;(X5Tzxl@ zde%OfKJYn#>09(z+|SJ*5mPTz=VdR(II0hNG)AcB<6f&ZtY#0wU(m8+vgp>Wu9ynE ztAV<|a$MGX$tNZ7vRkbX{btpr5Manifq_;Z{F$RlZnG=6Dh3!b8t&(6dYhOnMW$bL zEsw$$;NFIHRe$us_)z_*BhOH%S@#OVc3>n1-|E!dH|#|3T5@Gfv^nAh;)HEqB3u;7 zdo3u@F4gWdTgFwC@CU-;%6T?8!_fkLl5762v2{LRx~MHXL>n$+;}W|EW@u|Eu2mJf zJC3kUSJVYS^}e{*Ytgah z$u*7UVHbSt;BNRywKo|WA1cEYWZs&zkWKod#P@t)xHLt+XkP|dWSf8N!Za8M^ALyo zODSh*XQvN9{CUTn8O^MQN8f8n$oTk4rsqL+G%K#k)N5KL@LrATd7?%s?d@J)^TwqcXL;6erGr5az>s=2uV7oBd>Ir#Mtss0R0{Z>XR!GdhJ4tyKY z%{XlzJG|nkD>*Dw9%ClHisiqyq*v%RMCBgotyw70o6shf)sGc9hT4hUb0)})DpBzk z5z~A52d+b}+~h{SC6|8*2A8n=65rm~Iooi-JDZzVzuZ1}*o7v%zBVi#$ffIQq7{iZM0hrW#nq*yT(C65j~Cr}48ND+?0SlXLoMNu3ezagl#V zpZvLUbA{q%`_iQcVe#UqR8M_*-b9VAMwDG#ts#4EOxKcih83=;RPK&m$1IhVNh7^D zL{4efrz4SpQbbO8HN{@V&@mm0oFem@*+?=yMo7N*ihOd&`7&%3PO z@2M`ielpvSk~Q{vXVqNKt*iLp;jJ=<$|BNhn1`fQ@S>o2t>t!h^3-c?PJ{%pKA+ew z-FCO~=-j{4Igoa@LF%nG=|+g5Nxq+nTdr1(MI1$h)waUxgHo!OSD)&bDl3u@Em_eG zYQ#ZIsn@(l_7>vt!NyS`;n`qDYB;Tj2pX@Y0D;lk6czZ(cQ0(wh1aCdEO?= ze5H*#?`O@HYWMnSWv`tr^ORSZ4bjc+YBzgM`i1ZsoVDVTzNkmUYs%8H>UkS)PEC_; z>{t~%$~E~pouQ$zGo{_3YLb#O4A;YtJOU|s79Ctz0g*c56SLWE8>2L-bMLEruf8a% zeb}5SFS#1^(R<5PVQso3;`7i(M`)>ZDGL~?A`(P=&K_dWf_NE?ptwA z{0bZRRl?6a2j3}i6*6yxmSPb~iCaVv(>uSo5krYZi(jT?;2`<1h)JpZe45n!>}Pah zrTV7wHM$kyfJyY&6i%+Lj}tttpA?AvsQE!x)Vw+QKk_?Q@iXr7ZlrXIavBOXy18^& z%QSd0t_pURk%gH^HVP4!500gTE&C2?01g2WO$mfY;I{3QaZU7BeE!Wlfavs=Ob|?{ zsRtgq-4YdC`fghfPp(;TSpA~_U9c15)GmW%jnv*}frMmq!(W{XVy~-HOZKczj_4;p zT_OMhqj^L+>V9PYw>k*O3lkC)1p=}Fd!@LggeVZbXdwy(7NCNJEg&$MAYjL|vVtL^ zNq(#js)syIil<~Ju`DCH{0|b(5&VAGk zq=vS6dssI#0ivY>u3+E$HXl!jgF$bp}2%#%lzVmxKRbzbfE!&`v{Rv z+$eyZl^_%f6@@}T!s0@}kf?yn124+G`;}2n-RHd(9aL%bj;G3J6cy z>(GiEKR;#0L@h|0qM%a7U6~P49oOACnZA}>loQ}}Za6lTQ{vjkfbXqfCjv3C`L=-O z=3tCN+XreGM2_%^J7K1eD>;QOUj_as&EB>QZ)mu-m-9{R^4@Nrw9&gq*k<5utZq$T z&ntLL)3sb8U*_{VX)6_ZL@_sJ51n?OrEb$paDBcMb=d2Z9CxmUalS4_u07t+Sjpplp_Ulj`J zG@Fjf%#V>7XLXVnnjl5iZ%fRK^_Ko93tRK($!Ur!d;1$e2Rs`p=uB|Bd z`B>8AQ&mfigY&6`?K%nUqkWa}$S5OUGNoIX<8wi@Nb27}Bf{yk_u0)q;NDxknxuDm z)x`@v07M@1NQp2{wZE?F&=^PO+e1&^kh{wKR7`zT^u=(cW*w-$zyW)q7sO|zQh;=i zg_(2jJ{yB9wn`)g!#TU;gwY$rk;}f;_*b&M@^mn3IO!HYt3fkNtD_GHzjutvEWIsx z=_1L<+xRppy?D-sro%Cj)xi%}o{r_?C@4~1@~(#oLolkOd_*#F3azy2y-z7^yrZo8 zApNH@=T%=Qh6m|GS_-CZr+8yJ+q>~K1?(cj1eFtlwYB>VEp~M1>6h(YHF3Cb#(P*T zGoH{NqFTQR%lOr(f7`dijxWbXG<%F^N-c4~LxxBvnmv#g=)1*5gaO+a*h*B$3M34L zBGyj=-K&I4CVDTNU0U;i#E#5}$RM4Az2 z%FnGTd4;j%t&Ebn8z^W>o|OL3MQDHIX}STz9`c%R4e2F8RxSIaEvdX?IZl~7mRB?h zSTAbWo>LDBU0i4wpydTgW7J%|bLX+NqyPNVRmGKZKX%q7!j%)pi6)}(gBU_xIcAiq=%+arO zbf3_UekEcQ0=%n;%a)X6NYYis=e%~!#mCA5&~ChT?SB=6jY3z%V$8PkJtqg~{5gS1 zmJCXgf+{WicxGU?RCf;xCl7aiD;FOR8)qjz5g|c-0L(G59ndKu?-Qs`>Ma{9&fB;7 zYS_!Wq!uNwr=Uc7IcoeCsalE-2?vi?uzW^nEf4uERGdQ;w(j1Jj?Rw2?lWZS>iB1` z>#E=P4n$2M)NZvlFy(fb?Yjn_hx?Ya_)ZMLM9yS4JquqjOpF(ROcZn)hrV7yJ@-14 z<6~E2iC$+4CRgd&EXB*p$PyF>_yIq34#oGX{-;-i5$Dq&JE9 zh!aRxKzU7^*QqVl-YFNx1!o7z(Bp0Ew=aKn8v3G0V8rkaHp^XmQ-voB3ujl%h_B`H z6==E9OOq9c+>`?=K0{vjey-Bj_MG@Rtl6)~o3wgHDQd4ePCIse>?m{<4>`AT`jR&|2X0S>blH52j2 zdgkgb3rDqHsqP+zUbAjqT2*yD9^-KAu?S9E%<727sIxA{4Ebk4XV@?cV(Tv7(k&uk zyYkYtB`BcDPUrZH2h@&Ro5>-V%USg&Mj+sD8(I zB`tY-0_zHMT*0_^$=Z{o3zf@S65%FW5n~Y|f!4|kpIP_aG?z`!>#E-~bO6y+PTlvP zO|ch!+0H!GHepKAW-iF}jgo}UU65PXg_2wYqP13>S=#Gv1G&Gi+ zH|^N)4M&o@zt2?@SfMT4qHn!Wc_pHK8ZYKTw|^1U0$EES4dZpOoIQe{5AlzdPSwnJ z@gfciiW#syu=wegj!>vS*~qhu&2;JeDXPrATu!4gmp0#+s)!cLWVjZ;=rGS*mKjMwF`0_>1 zA&T(})I0C*uH1zil(f0?t-SY0?3lmy{lNOm7BBO6Hgq_Bw!o`T0hr398a##Oa}VTS zKjW?#z-#5k&9|fAjhy%Hsp`cQ(xkjaJ}SvFDhUZ|EPTS|oWn9vBu&(v3LPS!|jU3Ba1;ul(v*KBuNCU<}>6kdP+1Z(<)?<(>kCU~#2DZNoim<9#z+mRE z5(1IlH{wsJ53*@Yd{AN0{dc4Yujva4Gjmw5mHTR6O*R;AA^u#v#QtvSjqj%L9hl-( z)XUar)y5Ug3UEuMl%mCyOs88rDB+HbvZg8R9N6g)HP@V0mhf#7X}$%${-7F4pJ#D6 z4?>mg6y{~mB`Z5?m}^yE%ttpf-niVs2McuQ*se{XpgePpIWPL ztw^QYpV<@=g%jTEaMOFC(95;Py)^FE$MYhEE?Oex#s$gDrI=?fkiv!}4{ntque&=k zV(L;VTv-_{>$^*lnbtuYGXpo`(q5{MpJlw2A|GTRUtD@VX`|aV5aK(^L zUywf_0zUw!@N1jyFdETIu>!}HF-reuv5%y$c3#vtvrh<@U>A7A5}@2#=C6r!7@*>U zzotu^^UX4gVJ4zYy-oY`2>*QHr|!KD_e1CXK<25qztDmszIxZ88b&Xs+OFRPbkownBdU3tzSr^Tsg7+ql0ub;w*R(@6em zx{RIW9+=?WBh#Svlq>l$W@5gHa0uXxMf*c}^RP*@t-AxV|nQ6Y|OB zdn$Ob4FgjZ&+@P=R5u;-t28eZjCLCGMU?Z&IuqJ_!J>iB;P*UNfCYtGEXdr?ad|?S zbq+RNHv*1QjW1%G)-E!8&@=wB!H*+GK_yQm+htL&bzz)bc+fqJl5yBgzM;c;gc@`I zGcL}-^&pTO$arUO)MT*bQ(XJveEY|og=T|Le&_dDFFe^AMZ9&Y&#k`b;CCE(i&D!8`P2HxkzZHuMo2q}`;PBLE`tt1hXS2%<6+YgkV80fQ zO0d1cV4K#|_~45yoHWz{G@b}P840ePSL%rMc;>Lf zz|aeaq_%^!t$OwYJFV`lp9acexhu+*Bc4qeyM&y@)WFVf=j|r(>^3Xse^rz*U(n8^ z=62@R;O&WgcbpryVuKjn<=u(xF{D29DV7t=46&8RGE37{iRB&L5F{~XM51ZrqleW=re0`=~oS{UJN!=ooAXY z9&GZL4q240X3gl#h!=j|=2+o~zzDwkmUv!z{ZkzM2;tbHOa8%-4w`1>!RvRcrd*s^ z(UNHUg(Ov+?btX_`-po;Lh`evE(%p+cSheNR-}^;hQGbXx*o5>vS8f4Z34ktWisBE z68FFOl{c#`OpcHD4K0IxJ`R5PY}`c2Em)p|=_KbYC&O$|lDr@6G2(Px zDo$$Y60kOkp+)D##K)A)-QaMOC8VAL5ta(agv6lIItpxSkmC%n2IZIgLe%yBx4*HG z3#(;JM8o4{L)GqGko&gV5ijW|aP_%3h%Sv+IFr zVOzmQ?->p2L&tXCh$au3eW2#LX5H*?b05=vm+``-T$}p*b;4L&YX88Z2Q*iDDB=k$ z*v7?JZoW0dmkPLIn9 z7-rySM!`GwsJ%9acLYDYr4GG%JBUN;Q)*%QeGIF;D05@nKF@f{iuB>)Fu%Qh^l1jI zJ!0%vz=ln!cU>K9pV_qT^hCYNGAZa!HOiJ3e@^T{(&TyjLyMk*I_YiHvmE5_jd4Rs zUr&f=ar2GLV^bEj)h`Pg06L+Dk4p|_iPxJns~t8 zcPH52xM|p=|ArUtrF#VTdh1`KluzWpkb0SwMjm*(uCO*jgOi6_*W*G-<-$y;km;0% z)^uzeUhw#HFph{C<)#Yd^BY*%y4r{Std`I2+;le|iLa8P5T;%EO0miFTpVkF{h~mu zH%41He2)C#P>YL~-D@IsYCDlxJg@JR>#f&$S~K3c_2&vJ-jpA8^_OlRIJ!BpS(`c5LX%dgh~1J;WwI0o-7$Xg)hpHWrs16o^J6Ugy&fEa-IgJhNiw8 z33`^gndJSwGB1|lF`J9e?G_4$5_)Uv24kUX8;l3F-+_%bB?(Yaz^BJ)p0+1PX`YwNoS&`VD@``!!Z00))Hlrtg&5Z4^{c9vUmRa>ji9oA82wP*DE8v5*3XsBrK2CM z4o6!*bHVet^OvK)(4OUCiW|B^!vvpi(hl~MReH4^f3VaBe@3!NB=M=FYe(erYeV8Y zE-Oj2bD}vPZAD`eDLOp|3305QA7l)^KgUJW*I1*UFZ_@i)Kxkct+@QAcien2_3UN+ zf;-PaI!)Kr&FSi(F|J*r*Fo6}Z<))B8kaJp*D|D{t-UEhltZ7SCyRW7bRWrdMeUQ% z)HXVqKP#D?iG}y*ZqXPgEL-oI+~?ddaq-ZxGlPc5`h+c#Uyec_Vi_%x(NinT^W2FL z#c$M?GL7r)2B(apoo`l4IbWO^RziRIYc?h&hOt~&xp)@V@LTMv-j@=AjrSP(eDcn+yOTh3FwC($Y=F3-2DmBLl8D2M^Pw!FDc}U@-U;r~>tl*n|;kGkU z1Cdf71JBP8F)f@uaCxX-$0In;{wQifNZDb;{}UjFY<47sx@5s2Qx>Chzv7 z)RgaF#T{NTYlXKwq-bWD3Ao=>*L2`L|FSabVIPZ4pljp9x2iiSeJ|E71$i^puf(=w z`IY#8d)qLt8$CT2L+N+Vkrk!hk;N<`MrCLeEi2EO(@N4lDg-;Yvgx5axt4r zQCuZ5Yk?{^QhQ7*LdHAF`v{}eoeP^`+O$SbEelEvNDW-!Ohz_C>5Wf!&Q-{$Kk{qN zyj3IQuOty=Aal?W{Y;e~9REu0eNdQ`2VOOenapE)wr1VW3H*YasteRIvZHbH1q-9{ z7g2|k7s0HTl8Z?bYhP>@Kz&^gM)rD(pXW3sbgYxril-X6M{@M49HcG;TTaA_6xX!)g^6IDWjH*Owp;H?Q5EQrlbD0;z3Lirjna}m-tGdEMG*= zBUn%A)P-&!c*}ldR2O{KS_dO^5 z(S{Xl$e?gYkwt=yLgNM=rEMP2xaq;0Fn`b49%~8}0(Yq>U*_4sf8LAF8$&}XqGOE{ zZ6E@klxLQSL9D=}bw#vi{OZ`1-a`XiOaIQ(Nu% z#CFjtR|dr^>-Nk(y$^VeJVh9dbYe&EFuUKKyD*y5$8F)xKlwoV)| z%{!g^>~*U7ZrlT0A(kOUX4BjWo05o#xJ>N_bfGk_4TtAASZ%&-pmq}w+$CDX&mGB& z>0rpfG$OO1yvse}jaK*+_hNt8PHI)-MZWhMYt%0&l{GG(=U((#^$w5E-zv$6hyFRMR5P?66>US zq?b?!O3E2Lr;Urci?Fz|4&i3XTyzm7zY5J5M};I!7WK<#T%|);{YJu>3pL+UaaIv9 zKYEi`JTdc0D2PJd6irX3u3fiVu$=UvU>2qjNY%|n}rYAm#!E*9QLGIVHwLl>MhJZhgZ)S zfuJD?%y)EYH@fcDE2z5M?bZ(uWTeR9sT>IjS(oru z(QYX_X4JVWfB#-C0nf^;MAaD7qC>PZ6rYxs>a;$LM7i zgoRaBkwNA}-F#UXA7kf=%OIfmZmxU#Lxs#;J-FMzOV6z30*4h2{%!5y=c6l2D-Ssq zESwtV-V6`)={m=*OAIBDQ`knpQ17>@zm!syT1Fkxtt?9}-ncxj%;8|O>CNc)O?>d> z=cwzmy0C8R-957bnfZ`KtTwF`6a`WYZj_6hygZpD3a>cQcc+KHlI^qRrRg%Cdw`*) z@PtOPMZV~}6?vas^miPOSk+GIu;3?|SFROxUKfX*0cVHa-k}aP8 z;}=OugEL2l%Pd(>I=~)@q$ES~)9i^tn#3nZ#S0iFQPIdz0M01bCTQa zHqOrW2ytEHOA6E7#i0U*L@EY^1%Ok+55VhtviBb5qZsPtuRJ`^Sx-Pr@T@j*A4p2|Or& zq5j{-xo}4p2k1%AlRJ+haDOz6E!w|=BJD(a5*2A@vY%9cJb!eo|A0NdZP`g&q}{fD z;vTiPa<)Ii{%5L(Tc;sfd4> z|95}@;spJP5t4Hg70vwTn-+0}NSgKk;r)1Otp7iFju!47P`4A8nE`>ka7@d6R<-=8&VEAcm?h){TBqDWHUNJI<%fpp3T&h}fK z@Z$v@CwfQnFGPodt%%e|!RkPTzVY>0czGRPujEIwcFt_FG-xKM}pD{1^BmSy1E} zMG^)@BHHl}q*J1yY`;|v{S#3=)xQuu(!xY0ill~#L=@0~{`0;0ol+*-Z>2K-L^ML} zZ}5mEQNM^H#(PK_l}JQ8|ABN$zmn~@0+z=MJl;n0)c->CNLKO}QACH2BrJ(Ubow7i zr$i^&eyck9C!*Dwe<6A#iHJ-TNfZ%@=;}X^P6;Nm{Z=vYPeh}%{zep`gNRHNNeK~& zXwN^8PN^ia{Z=ONc!9@neU0{Ch#tuxA`?XtLPR3^;U7q+L=xG4tCIL9qS5F7Li9-c z5Sb{F`XLh0-hUvSQbq*2r{72;{)s3==Wj$$clbz>he$-T|ABN$4w3D*dWe4_`q|)b zL=j7)k!uu5B@BtE;Kjcqol+2E`>ll7@dA&x(KKM{@W)r^k&qBFQ6woLB%-7LKsqHe z#P(aAp+6CIHTfG+gnAG%Q6&8!B%*d0TR)we;}QbIbi#(&cUCE?pXbW=#eM@GEpR1 z03@OUkiR3Hk_=$`t!BWVh(7x(JR&CJ7g5BZ1W5}3i6|EIZ$y8i69ByHeOYW9#n{7t z8)Sc+D55~5I8LMj^&S6C{a-Pj6hDGIj~96S7S_4`4gT~kM2h%ABD($$q*KvgY`+Z$ z`xDU+x4#fQ3J62Kg-9V`NJJn01L;(_7~5~-#r{O}viskNB0s%|TaOe6hD7xCKaftv zh_U@PNbFBU!5)7hdKCJFT%$e8@e-l0i|84x(aiWM@h!i1)RABl)3j9~}7{!n9vEv0Ee|o*V|8hM?5k1Ja5Gkey ziD>mdkWR(>u>CgR=TAg)ef~xi5q5)26e;iqiRjcnkWK~Vz<(Q+bG$%AoB3ZEz!%2< z9Ux4t-Z%b2{lB1_U*u0?$^1k^fIavd*ncrOzrYZ;8t?^hZT#-6-&wu?XU$~u{SE08 z=ItNW?H^l@B1P}~NBD_Peo4Y|Y-fS$K)O%AyyFO5RllSWO0pz#Rr;g=MM5;sABTC0bO?Vtq8ia^fBdwr zD1f&7<7*BvMv}Oa3|eyV(Yb+?L`msN_(vy9YtW(XB7SB~da8+=w5i01CK>`9GA9X8 zktJy<;j*0eGax&!FN7a~%Ftma#VB*4-TZMlpv->iLa31{NF@>xV#IsAg`T`}Hl!1L zk4_Lj9+ULznyBF9l1Vi4D$eH`_7!6U~3Pp@k{ z4rq~z0AxH4czTs%5|;`$+JkIF&Ln3QFj`XH(bx0LSV*;bdcDVwlKyLK#fXy#kXC;r zJqCbPj{XB!`XPiCRSM+MKr^pMidN_3Aer7h87{3a(hyaECV?Puv7sBB@FnZ#)pF9+m1N;Kc)N= AJOBUy delta 5409 zcmaJ_2|QHY`=1$P3&U&QX>5bBWGC5LN3siri4n4leM!cWwIr^xG-xw#WXYDsE<)K} zBD=EhOVMVj|IEl+dHsLK=Z@!|^K9R9&wcLsp8f%fxD@cM27oU3_z}Dh>>TK~#zK@a zftOHjXj^$%1r-^DvjZ9>V~C`nWF?N!fLg6#=je~g`iB$mo&%w(tXQ%F;*ztE^Cfo= zFEsk5HscG~gOn?`H>iedI|audNa-X)|AuX z43EX-#E^Ae!V$?>M%=7NAKPkinAukRwrEIA0L5;$AkbcPBKx9qcK61h?R}g*+zq`v z9GzX!-oF9vCIY5ooeIYiO$&&Z+8!6!of+E42j-_UTW_?FkF)zFZ$yBbYssmOPU)6T zWV@tM3$pD@nc2rSNyN!Bh*xc$^y05OA;qDlR*nX_9v5vc2Dv0A@uh@^pYe1#b;{8u zNG^tAa+)KwN0)(y!7rYP=HnMHUq&#t%RN)G;4~dNRyA9TP}+zaHx;3}`naaJ=fTBW z#~&{7YEq#mbimO9mcyoaZ++entR87RQh29Ml~I$y9A!*1i!l@M@nWs~`8enIl|-5$H-$ASYoblkA$A6SC|mKi;4s_+(I z+BEN+^Ln8)Cj2vpL&#rLBQkAU%6~4k)gUUnLgEtR0)s$&`z|#%4+mf3yGL41@d>ST z;fBztFWM^@&f)1IjL=xuF%I!tdMo>T2xE5>V z7wsRY=cV})PRwKWCdRk8E?zem4x&d@SvWTF|IlpDTN>MBoKg8+Vmy9xFj*fV<>(E? zo>b)!89A>aR_{Yqqs*_$#u2ZBvQ@0WRKO}{Q=_`3gZp3D+Ui`V8=E}etAEnm(7bGm zSzxR=AATet#ki0_cI>zx)X z$oos-ZK-0Kyh_Z?t);olmhW+24iI6p{`@eI%87z{w{`*%F9AR){`@fb+3%FSqQaJT zBXOiOyrZqN#7w^Du%YqEc4WudS8d30Gdc#(H)V#!P51Pl+_2b8| zWwHh?`JZc7GR87en?|-P633fmzjS0=S;ndManxRAqVP$0)0XfM>E6Pma_x}v=PjqJ zucY%WJzi}4eHs@&HQ~`O@~O>ad7es#!<|>u^V*?sVX?*(+;h(disoB4q;M^k6Uk>H zV!;Q6mLbu~{5ey0eh&Yq_hD z$)z|cD@Sxn^yZDU+?;2ERv+UXuJ?tjrLYc^GFn+N>$fVwOib?F$;-JNuVJC63%`Ly zY?u{%aSUNs8OY!~VaaP3ANY6dpd*a&3YVvGck6J^SwUw_PCO;LaG;C*CJBQS|dYZ;x8{JypK&w4lAPxy|u*laN&xTcGl)(O8!QeM=YI z5eV;(JR`ujHq_`%qnRrpK#+)H9y*nTMY1V*wow;NbQQ{~`z$}raKk|R-FcZ~=4VxG z6;Ex)@NRF+D2cAD^9^W0Y|$~*A+gtBfymnLAK;rCB1wn(y(cfKCG*^~n2m8Sd;GpH zW=S2>0yk*rqj@{M=GL!}^$m1$`MOx7`d!iaug|4i0y3ur)?{WGS3tCNYY(Mou|ZU@ z=a28N^qDWo)A$QVv_5j0dX=lOl`w#9a{Vzt@6S?KZzdjPU>xF@n&jh8lgrxodi_C` zjAu(x#^yC;Iib38H0u(G;Z(9|Yi6q!bGoD4$tO^o`KZs@EYfkCtrW33ojK=kM5tuf zd0AlVOEty}?z}8ulD*;b!wbvS6H4hjU z3vV~%9V}^x2w(BUOgK($hZPH}C(Y=dm#y**x9bz0Lf9&6hJ?P+TODn44`xE5bo}Vw ztwHkCmopj)Qf4fV4mC2xVehKV89e&LpIGfk4U?{y_~0pe=WM!@)`(@%srQuFd?f7q z#rny2H{2d+U!_NUSh-p}QAT+pKK?TEp`>nk?BDfMSvEiF0NSC$PbVV3is6D^l#3dS zJ+=z{CU*4Wcy!uClFEo2%^;%R_iIn{pjb8aJ8S#y2>a#nGrR#RZg-;$j=wI#OUv>U z31u;VUc4tX-UOsXEvU^+gCn@YeQLSX6odY|N_b&N}VT7#2DpQkp)1 zZ=9B*=f^nRP6xd+%eTVYHVL^_KBzt(TZHZBimHiy3^VltlbLTLsfHn+Xd(x4&9{Qtd(^-elMAB(+}z> zb?`k7gLb7{EUP;G6_?1}oUc?lyRx>;%bZTzV&xezqK-2^&7jT3ayGuJ>z7Cu7eB&1^c1YVc?^yHJ}lUQ$#~w38_`+yffPop)prV=qg*rjTK25V`4!A& z2>;gywa@J1`BKk@OYmvfOvPItwCLj*6rUsP2UXPb}gfX*B{{r7a?rUCnMR)vCWHw04L;oT>@p; zkN83uQo-_q5ihe=zSrxrw1_ixFbNpN?6oV9M z2T+2Y4$fB0T>-MZS#>CQGPlzMow@`!qxp0|M;;y7Y$)#s*{o!wunkP^$tzmg5pX4S zROB98LfTHxW`zkk7+_Qz4%m+I10QNx0YvZttRWH%VFi(R`hS@(iP!HmDDmG9;`Q4W z^k0_0Yrlu*6SaAFj6q39od`Ozx#TkA1;zFV%wPkByrcgYmEJM zUi5R<|GS_4_SwJuFcS|TcK!Tv^T)3Hm$Uz_Zf24vdeRSww5BMaKw|J;Qvb_Bkfcj) zBm;Sp0gPdH`I7?x%P)B#7_uk8eq09fXh-~eTnA#rPVx_YALk*z>%0>@kTQ0_C_)IZ znt($-l0*fdcASp*+(?z;6>*%AhinoWO#^@@#UZ-8w+DdA=)Xw1!v96Z`7^^EP!N6q z6~_x?P0|nxiy%aWbjuC6Oe;Y+gvesmG&8Zv2tuxj04Tf=U_S$gycQ#mzr9Fus_Jg>^ey>Sd(?K@g5LGyqdv3Unol!9IZoV|76X zI|PpyD-J;{8Cpon3j~`_+nf3ZzA6OFJCT5phUBb+)2cXc`E;@n3+Tq7avnetC&Pg* zb&7zQbYEVMBq*XK#kbV%>jnq1@ zdlLeJgpupeB%Ko>k$w^WL|;Gcu$UT3XK0_USj*JDuflRBBHP7s%ToE(Q>pKZ@&dbW zd1Jvt2jLdRhdign1yd9A1QDcZ24a8k2t}^tdjD8*sppN{ zH)Vq8nX(7I?a|xiO35DWa-NDu^@45Bg&ld!7bGn#2}jJux}FHlZU|jqPeGkk5DZlFR2`ycHimM^+PfIUb&tl{Qqj z&C%r!oQD5I%rloH*qfXDF8FR3b$nWpF}4(DW0}v~WE?|LE=M|+OMiSon#{A^^-Vvj z!Mff>LLPn_7BY?LD>E&ZoOj~y_q2}gQJpU8A?e=z3FWA*?X*b!i&&ZT)f%yoKWLiY zU|2ytU|A;bl~DL{T&V;E=B7vaDO}STOvbfE>y1V2EmtwGch012UUhnr?jS=e(Q9s zgEXZ&3p57c2-3qBrcpweL+9M(YLaW0LhTj*nE~iynmX$zPuH5Tcx+r>(908_(fb57LkbKcC>4zS?cNYZ|M{>Pf`2UNmGRe+3v+@C zPshA@?NtUcpJ@g^l0{ZgZWC=iIwB{c(UT8cztDRc>ty2S2eOj9zZt}yE~-5g#TUX5 z7)q=3xk62Jk%Q^nGZBV* zYGeL6FThBg6Srs&>Re^TGXUk^VcR2Logj$@aG@ zobF%2&NtoNsV@Q z!|gio>E&r@yOP2Ndt8x$K?&ygwLOr|+`8=t{bEIG z`4(|oRueGhY9HT|`9aNaDV!HyRoK2-Lf^>{^#6Ym%k?EkB{<{(rnrz4BnADu8G^1$ Huo3?O!dcjh literal 53760 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 z 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

Ei(XwNOEtpM1u_&thFjw8IQWXi0_ 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 UqTY8I_-Dh4fe{3}4Flu<0l%(&;s5{u diff --git a/data/word_cloud.zip b/data/word_cloud.zip index fd395c5bd2c2770fb8a1f2482d16a2a3cd9f0686..d15c1cf74cea5a7ac15ed29f75574351949df1cc 100644 GIT binary patch delta 732 zcmZWmUuaTM9QOY1Ty%A6xpH|mUD7sn10^tulv+;_Q3OTwvLGsAP~;v8Dvj_#tC8#r z%rK(C8Z;kbMi2}sdTIWf+Cwxcu8$4E=P0Oq&do)19`5h^e&;*i?>px%Jrh^Yrkj{u z?nf?UwqCq&>2mbkvBP3dzmBcNq6c#^zmUb#ga;=|o20)xxDs<>A}$p|@p-+dDho zlD+s<(x&d2mnkn{urf{xD+9P$3Dd9vO45Z^MH8`kXAB75vB6O_@(BK_Owd{r)i8bP zLMxV1R;TYb_~Jp7-JZ=JB)1dOWPD1tQ~U1KnUTwQa1UXw{O@nK(Td+A6QS}=BR zGgJNNyzL+7xS5|lI^B!h$_TyE=;xI|o}0aleG6Bt%^0x1K0+roIs|z=!-A^1y_Ks1 z4HLfAee|wN{rn$JHcTYgalJ9bZP_#Kw&QJMoF-WrfU~)ePCGfAY6hw3;_&|CK9rk2 YI=+)r^9Ps7rE}?88+E#`!|WQr0nI-P3;+NC literal 54076 zcmd42byVD2vIdHKu;A|Q?(S~Et!Zf7U4pv@cX!ty0fK9AcXtm20zn?RcjnBQxijaz zd;fa9Rzb7q>b>jRyQ;oj`&TORkWd(4U|?`y>22iN?;{}9W8VI1v{&NRZ=0ToBjiYoXNGAd}{Td=ng{ZkYI) z{rR}L@HLo8OXY8NfPtZeP-wqR{f`&w+q*y8VQ%Td{EttQyHaQu?iRp7zkTYr5dE7^ ziz_QC%4!%%O2}#`s!N*yoXrsa{snOVHQb}i`MuGbV&I!#{hKcU+S`FF%@t!r5&T(@ zpeE1heFBNED0fItGy^TkWig*RJHv^>aMK)B`!IsGRSqlMsGAjj1oB!Rht|ZA){~q@ zN=5Xe^(owuPsd0g9SdkyT!AXG6@O-jj9nOw>RI-M5ZK>>{bNc^dKB8+QLx0qZ&Q(d z6a0TOB~vqJpp&J8i>19C;`o^SH$_EN8ac*c<_U%o=87gw8O0F>Rhp4u22Gi_Yr8E*RMFap^6cOR?X^`z^os)xRPyBPlB3 zWCr3RdHdnaY+(j4b!N5&IJ=lR{ekI!f6<%nAFqFhsfE40weuef3HM)vhbaGq#|8%j z+jtZDH|>8N{J(Vo4h}Y+4geRR1*5IAIg>NM*1^W?L%e|!Br8(zrZYlf4IOF`X042j z11wX#U1w(5gZf!9WB8ZdCL2A&p}AMpj++uB;X{0ZBS2}bs_EEog1naYkb^fQ`@h2O)Q9yD<9J~e#xw-BTk6Ng%S{Z-7-5y~hAr4aCGWmt15Eqc`&>2EQh%005 zT}}rFJ?$#xE`=aXO<@YJj5?}gKgqZ5X;y-?a+r~ju2@LCOx&W@6EvBMB$Prrz{pMO zxP@vk-k4MGpKr{k;ZrF+07+(Ri^)u$)A6CJmlZkyvcBx{y(D-5z78#&nn4Qf(3%V; zPS^Dr5!Xs4GF?KpFmAUfMm#jUQLW4;v~h?q-}n>v z<3BQ^M&>r}v;bExR^INOU!EPFk1wolE_Sh`wJ(%0(KFB8XZL+NGZ_yP&zSLR>-EeG z*ns*2^duka!tdWp4TV!z;tY|~)bdv3e>Mzjn6x?-%rUfPBKWEjRd9)y{wvBiCuKjJ zP$!IM5E@t_%DVj8sPbK!2d~F!cS*W-4|3*dFFfA#*Cfuz^x7wG`UIV&8*>hX${M4t z-h)@~=-yujl3xop|9S6R`UrU9!-9dip@V@j|Hr)pa<;X%vvjd{Vs!uQptb#k|tJB?8@=^aG}#85HwD2>0d{NldoFgPr$L z;gY;%!OpGpTe`ZX(V>Qhdi2fAi&s^XRC3z_8O(6y+^Gr16VV0;m$I|@Y?9)WIxBNs z6`eriu5RWvDwba`GN>`(4M=s}%)wzHttl+Yt)0;4NfWBD*B6zg4opwIJq>O|;|i48 zItaBjmbt~%tVG|U{c58-DI7NBM&Pomph7zRk2C6asi+Qrglz&{kYpH3D6^x$3q;;s zfib2yhLFsPj(lJ|_gXTgj2MJhOI1t67gTqCnjCdGNRgtqXHXGhXFF*Se`d<*(ig9N z&(7+PEa{6bkXv{7ktZbA*Z0@z*S031qcw)qGpb({f4(yzy^)XAE_+v{Brmz7A*DC&w#8DcLBxHonNLAM+*DaPs*PZEWp&7+ zXFb;*6JQs4D7In;{vz)?pq59V2rb$@IucTnA~&0~ZM%YzfgEZlcAVwFqzqu{4Lv%cUDKr=3!cl10NM6_qKLr?F>K4e6jjFR zj6HV7(#=1o(rj=NES*lW;? z;UN4%4Aw=Tm?m&6mJ{9r2*6du@tSP1%laDSiNyByMpZipIsccq} z>eH#*aRx>$q(eM^xSmo%rSH{wB-#+@9cl%XAkukN9ne(8OO_Hrc{{kcJJ2Y?R+6NY`oHgDm%&HqnuZ zQXx)6BfugXN-2}u#}XidK@&OmILg!QrKU(TVsyM9A-j#TLui%ds`^$uu{his@-!+m zhJMJCcNnr$Udk#RJ0u?ntY^>{Gbz@cfhNI$@01u5$SIg?*DvL`_`zCcur`&}bd*oZ z-?*~#xp+H7+@K8gL zp!@}nnvck7O<`?~5Wp-LC+=c+cZ;2!h#ZB|8DTP)y4uL_t7&F#F9Eez`+Ol}D=!}^ zrmiBo)D>;M2z4%OM+dgr1{;y0kDLQry~f_xy&LnSjV)pc2j=qc=f##(Fb^z-X46TJ zg5OVKg~Xv^;n`*Si8f@R(_00E3V1WZKe08k3fLcQ**L$}Ej>cVRA(D)x>TS&hZ>FF z)mtm@jKA}Sf{Ngyog%yY%2n?<6Xu9Id&&i;F){MGAt4q~TzhI1jJKBIUUECY#~(QPDl-NMUoW_utl34BW@pR>j?EneHcdM7Z%L$@V$WCwg`5h_i9O+bAb)(qalI&Wrey}Ynt=_mU_=zt-hxWjyu=hdc zhgKR3)jc>=qtO-o@>-|LQeN}b=LL^M2bdwME@IckU4GB;M;9kM6?kf(k6-F-ch6x9 zhBiM>|9mr@6`GGMFq>%Y^dVvKmtn>0WLwu^z?6JL(WY=l?YPKBqM7{mJJ(;(K`p#B zlWFpctEV$*K&E(suy2ftIwK=DT=r+EpRwnG*bWKz8q#gz!`q+U1I2Zq zIM}beLjQA~%|N>Ej(p3noo_<(AM>n(y|W9WtAi=P#q4hhmT_DaJ@D;xGWD8c5@Hm< z?g7h;N|KB`swuJlsNRLhGph|SO2~d}#!LU~_kPy>JxB5nJ7#uX z?vHRjSHaRwPZbl|r;4dgNmtHJ>!a0tM>=zRk@_C{U@M9YgkST;GE4+^+}?-R;<|4?2$bFnoisCqFt;$Au$3 zL~+mpqR9;ASe+^JKrx3SZ5|wFwFEPjVn|w4`rknrjXKa7w)=M-K-DUw`Vhp2FJD@? zYIcTYlJ8kARd6F9t3nJ4v@2KOG8j_1{t)|6EHL0$%0s9rJzgxu>{3W}mj>^Pk%J8! zfkwAGH7LgAc+{Ls%+vbzLah-q!+E@`KUPQTEdDp5|IOt8hNi6sz zu7FM!r5YtIKU3uMU1y=GJNTm_Mx6U)r?&|;$|B#Vn4-n5$=5}NVtc(z^h{p1Dwo;1{2u`&_?d%WXwPjcMm%Q#vn^+8 zS?>iGhv=Fw@q>n*0;z#z>YF?sk+P*kKPvjcLxA?{p-trLu08HOC;f9y(O!L~^n3A0 zc+gP#3SEU7#H17KL*~k7w|E%p@2CQerNF5-UeHcsHs9$LH_SRG2QW*IJ!{FRPQ&;Jl83A<^|8vO2Y=U!)Ve`tOe z2%^m*msEnh&JN8v4e&t_oQ!&cvv*;rCX7nm_}~V4cRY1%a{e)qGx|oWM+;(GnTrcf zCaoC%2|aNe^#J;-gKWz^_9cZKrcd-7C*KF5rr-$#&ZhY6ktO$yj9kDgbbUuqZah?A ziw5*&Z{jk=po1?X4BXm-MIuTV@ACkQ?xIKL_YcML4^t1p0xZe$I-z-l`p~d2#<&-> zZtcOjR)kkbDRUxlNse{44KV>Wb)(c7IbYFD1HXpOep>L!38hOjuf}o(wZ@`gkfCExFLXR-nF)rTD zxX0~rq;Wn7TI`Vv$;*l9tBZ!PLJ-|Z)UAbsMfqSe8e8HE`zn3Yb_Ai5NK*#`P(2Ya zQKsVbA5I#i{MBaY_YIu8Th?efPXns;`gK}5WLk#$XFk?0s!W}PF*xx%ETL|$Gl z@F9iywxHqf&3`-lxP%;YzBHzG8!I3emZ()oF{bA*MZoH&9S<_!bxc!Q4Pi&^mNRBN_ypp=6Ly8)u;{l{j)<$2;UgK zzJ>a{w-CwrpF{qeL))4;ng5Ghe~eRB4rW0exO~P?X7NUE^=8jV>%_Y#=@!Ayx8(~O zdpss|yS9p(z4!JT4J*0H2bjyL?@;=4eJM)cGI@fyU#1R_@KL|p#l$v;&H;RB-MApk zj~m;Ov?!b3o}XZUcT__|#jH)Etg*E-Kq|2$P>l$^jc2iL6drXdKqH@t9>$>|oJ6i& zRfWfmU^c!#9|q6WU6KSG-9WH}Y0Q>cErT5s(J3Z(W3E*7mQP6|iM{w3JNU!a3176w ztzA0{c@-0sA@waZIspC%kAEt3glJ5wt+Jr{ z*mdnVum#5tGo9FQ0ZEA3!l z4cdp4pl`<~irg|>>Mf+GMb)^dbynr0v&-Ka%tq}pv&EH;uWZn`510n9Az1|{SZ9Ed z+900bvs6se4uUn3or##c7FwI320LaNOR2l%chZR|L9}s`92Rf@V3E~i@4|Hh>eskH z5>t%);n~c|pVJ05#&Ba2e2%e;y<_d5@H*eUP^9INSaq5Ogns~G4iFC24F<$pF2{pI z&4r&vE1b1_3*o{_k}#>ABdmEB(|%Pgp%hivRMyg51B}{*Ltr?qCV19|3Mx-nzB5Ls zt#<`xKL~9r?D4W3V43uw8S{~K#5W~F+B7^($M;emRM^H3!79wn@a<+t*uKv)jU-av zXx+2O5F`Ipqs192f>StBq#LUb@)i^M4>vL`Secxt{ zUzb>s`KwX(Oi$*-?ajz^>;30hS-isM*E%T!i1*hCn-JuJ&$@}3Q!;X^1R5i*PhS-h zc}j-bJ58s;%I?J^_pFlnejaG5u%GZUqDe}lbRUh7;SMmy!d!57+XW;^2(OhYtKf~B zL9wpcU{dVU7Jt`Lw}X2?kU%ct3V8l_^gxCZ#zNG0YOu5Yb4ih(AQ+KD!1b2`vv9OG ztlbvjqJvU6SXQLgCq~*CGjBOIOW&rklN!AVGR|DVu3LVDOXSNt;cx5TlUnZr#Ss_h zwjInn47~7Nd?EkYX;4rQ9y;Hg#_-K)DE~72IJi1n{Ebb7Xlz=qu^{;hy<+NKEHzK* z`*l<|g{b1tN3#u%5)E=?Nknw=xqA8mnm+T|Lnu>~TN&~sDyg|I}60vf?vQwEJ8=ig&w$3hKp zLl1Kmz!Hj%fHn6%<^&dqT`heHM=gj3@&rbr(%WQ9$KTP?*?vhUQG861lU!x4=QHy$uN5tVfA{- z>1B)Q^fEqlH~;z=kC!;(I&ml#tw##vm>?_7aXeWMbAOF1wm3<&XL~NwzGA(KSSdOg zcQ)9EkUjWPFP&L5+_@8{nz42gL#gA9+uXQ1t-8MQ!%`bP`spx6o3GxaHND4 z$Ceeu!ueTi#EJ&h1fc~ysN(>xuFDVqF4gqe49bGF@Zj^|<2LpS%A^5Bjj(Y6R_ zj(WqOA=nePdb{Y4BQl zRB_qm3kAa3-hBcqpEg0`zYE8ctCB@_2jNJ@Ja68wrEm8&%z)ARf0o&}7rbZnx?8vT zgvH(%nGHpG=jOs9EQ@WHB_icQnP+M;~M1e%n#>$L_8h(3^DqKXRTn-D^Ycw9f~ z?3-he+&Bjz(6p5Bl%@O9F3b3>L<}=gxJZ*>RTH$Lt%Z*f#gHg3eX&PHH#QeanIsma zuUkHjIK;_QFw#G*QCzPZxwei|u5M{Tj^yzp`<6;E9bm}L&_uJt=>uxpSfzZIOqHJM zCZE~N@C5X|?5J!4DGTnFapB+LeM-e`s+k|`j(DdtKPZ`ON#J3eV1OtOA&P}tWnf22 zmg)sPzr{SUYq%px0WBPT`XvhhZo zDaiEsB4rx*`9aLg5~+&v_U5!PO0WHEoEH)lUl9<80i2d3pjwj8^~1`-QE?L)NH3jT zZiXfjK&aq|_h=l!2a}aEkrP+2aqZau`l25ah>aHxw6c^SAa%3<#4_cimZE0LsUj;d zx(>|qSHyhE(GY86i5I5y0>;1dx>uekrtW{HJ#AHCx@QZQP-^%#AJXHE&C)#xZT)?7 zVe*&vBShRzb3v+Kw)!ihT#1Q#hhzZ5Pah1yI;y2DR|?EZi=O>zff&^I%%;}s79l!4$^=!G zyNbicXa48iv5WqG`?~L(pv$30kv@(n5lZoO72di?<3m1S9Lo;VtW-o6j+@}(BQEbC zKazhiUBw5r@~*%uy^o4>^@TAKgBP3?>=4u!~0bJsq(waDJs1 zc(&D_49{)nR#V&XL=87K=H>~D=y;Rl0maO5e8-)y^Z>z4HnBD-L5eA1_|W>oO(=6} ze?}4S#!(Aya`|%h3>iNE93T8q=z4;z&e!8bl(5C2ZBIK*g6%`j0X$%kH11^k$}cPh zC9`!*Lw(F=641}o*kf|IZ;~VXtEb~a$if6lFHBS;KQ^T}fR7ufiKly%gfTxa0u<7V z-FUvxfp(2IrklM(t|e^>znLoaN$tgc+rE9U*n++n@)!wfyRol5d-!3ow-5Hd?@}M1 zjpJzPb$~XjLn4U?qP%cld;Y#tjXe2wY=7YN{?Y)W6TGuRyWmqZ3Q+;|+{X$`0)sGA zJ~n+=+t*Wf#gjFzDa4q#%GV*{TBeQc%?@90G|Bb+GdBcs^tOs%!-aP{*8+x!9JwJ9 z@J+xO#m)9ZU-h306+6&jLHSU(8w;%kp>AMK)XL>Ov82Jd26tME=l zzDj6T=_?sD?kVZW#63zT7gVjLr+x=P_GDBW`_j6kI4we_^k;J{q4somny5y51;Q92 z@-o>;J3_3vC}cal=;Fhw>+>1hcV{*6xr)-5GQ^;Ho$4xFk(iG^NfzYy;iP#`n~mfg zd0mqxva$Fq`HF8k_Z6I0o+iLKfdw7LE*5CB8Yj6&a4yi`{inEKX>yUd7xvv`;7q76YWK)xttzt>4){)6JSe=Eh>`vOEzwXUR?h(L)mD z;4pWNQ7BTrx<#!hdth*#YozKy5oZQ~vD^4pKXPUQb-?w%%iKAz^A@}+8!`PpPVc|r_-)wlJvvYH9rO2S)SImx~fT}|ujO>_g2(6!O z{H20+WL?R*ZC*WgGO~}jib6>guP~@|L-uv~)Vw)bvDC7NTJ-`y&a^6+t`@{I72(&@ zo_F+KNC%R`FXw+yxOz=wYg~YR>HL%*YL>27zjoa28zzMEJ%bp`L1ypfHT}|~p}V(w zKu)SQho)-}JUbQh5nPu$$m23#uQ0CU7pAeWq{ zmTrIN39c$*N_{M-0z*cut!N)b^hQ6?$%I4R&S;W#l76sj|D^byKXM&b?1|rPv=Gj5 zdEU!>kbRX8zb1)DcSN@;g#R61cmo~WOxime&C1ap+$0n6CEdUgRom*|@+`I$WC8l* zTgArF7A+O3cRJw(KSM$jdwcj)`OlIQEnv6gOf!qlLxK$pORz`RQ06PoC@m)`u67f0 zt;CkMtOYF~ieQQcM)4hIn8m{7nad>~9ug|+*tb3?K4-A0@$WzmY9LE5hRL>P zG3OAaHhS`=vz1)ss%q|Jo*pHD5bFmP7~P+(x22!B~Lc{{N%y4W)UEzE${ z_O32}<8CpUiq3DfP(kUh5T69A*ID-+W0ewPciR&Q9$BmTZI$s*^&`t<{d>Ypn`d7) zTEHY`V?Z;Bj86A}&D9pfoQ{sF=C<2)6Gt+;=saoOXu*hy71-_MSs-dpBo7x`e7u-z z5m8yWO~11=MODvi46;ddmmZ@MnDeG(F8GyJX0>Es**(DrUbUhYjTkk-EQ9PQ18FIe z)lV8dH>%+fFop1z8BPt-BJ#1(1?+yvDtlLr(_VyCt42jjP*{9&%V6+IiMh#>wN}(I zCs(yd`ZlRza`*Z8<;{a7cU)1O*%L(hlqRTN;s;@~LcBlogwX(P9!*`y(_Kt|1;go4j=7 zf<&So9*Gu7=4n3uqN50UumhW9LKs3r^9L*Z_Q@<}7+j5+d-x`Q{j0{#_*jI8(0%1= z+Lb9}HFltH;Se=l8*}C1fvDX(id?{1O4h961yB_YjMLSr($=N+QwurNelG)=^~pK% zqXpLwf4*md2U<)PhO8^m_UyXQ2c-Xn)P&N3Oa}&{xZi`XTw+FY8LpP8bDFOwsHnh>4^G16N*hD7i467D+ zkdAIR^@9npkuBIy*5N`Vo$)J^?bhqm&%#EGc#1n``cIu6q7{uh=h(IV-)}1^eVur< zqWoHWi3!D>KLz>ZnMyzcI#Od{6*3fahIU^X%nqDztAgdh3Ezbkg?6|(K+YDIdcH(N?37fT_APsL978bEidmn-@)Z!o`GHJbQuW==^ZIy8{E<~U zj_7=7j3xBD`okUG(qfiX$><_QT$?2v1DkHm`gO1bIsuy%#5?PPuEqxq4_9Nf*!hCC zYYWKW>4r<^mQI3jRh}M5f@FXCSGh4#TaOxsuksrs2?b#eoNnuo)7pF#UX`Z8KU$|e z7IvW)t_)3A0Ug8Fk)s`I&be-yiU8^2wlHz6C^iSrGS10UW5N#$9|VUTGch*hW_}9U zn+?Y(&fdd1ua#7TrF~9&N|cP>5D8-bn9zpsIb%*HTRN1hqgYYLpWrjH+xpbfs-RR+ z3{`WqWLe=wy)x6`;)a}=`LnS&npwlVQlaKIVZnsRvn5OSNll-(`xr0DQCbTo?i%bd zt&t6>`b4Vr9dZn1rXFEg66Ujj!JDIU`iEK}-DeD~CYImdlx6%ffdK`{R9r`-Q7e=PI-)16|q*5>akBj3+2k(DO> z&43ZvyQU|+b|}I}7UQLv(gHY(^oG{qBI+5(dDqupLJ0|sBX(JNtPsIep6;9LAKj&i zJJS_HvW^&d5MqzADPBStQ+P`i%3)_@NmD|j3oL6AV0C4wirKzh;o@>FCSh5L>f;&0 z@y6wEhwk$tmr{l}1Io7GvEu}E8}>+xnptq`O}cbNByx8-l5Kiq&CfX09=A9oK3lu~sZ>+^y@|F%{8y*FVo5evzX zAuUUli2|hpuMOc>DZn3(_9yv*Il!Oo`ZTv!_lh~i3DN~unF!+340U9e+S2QJ-}>`; zn6UVx4*+c>ZzvJT%=vUjwW?@H4f6}VC8B_3cer@FkWeM`F=JAym83P*_);}`bHk4> zx=d&}qj~J0A1xu}eqyN(rx{G0!rC6@HDN!-ne!$r<0JF7r7$Dn$@aNBZtuQmHS8Y9 zHS!Ak^`6vB^P$uvy5J9ec&)azjo=@URA+if-XC+v8)sR{e`?g=fDRQ+tq9X_xolC% zOF;= zXdH=f54}cMX%At2A3r@1CReE-S0~ArXll!p<#xZm_yu!86IclMG~GEC&rNuxe{(0A ztOO#%ju@W1Kr#^20I#KY(8qV_Wx5m0I*!OzNt|B#6`6M*%OYL4Kr&D=p#iu!9b9;A z(Iq6bb$tr6PPSfmLO6cc!7&iYopr|f-8*b!zL~GYckE=T1pe?GH?R}N)#ouYLzoN! z5}$@2aXJ~t3EfhRiJ^MkqvlJ2BFE1*Q)iLzmUcR}AE@&=#TxW>LV`kenDRdT8Tm_d zdp#m+3yHHRm)IUp6mnWqSSH-Hibf-Lo7^5m8-KbyL5M?9_1 z)go!?@F2am9o@53pY~56dBJZQBG&pjy6)N{JY_8jeZ50Gqc0(BTfAy6{1KK#?9nx_9Q6%>K$*zxb8!iobVcAnu%V}#K@P`ihu5Q zF>TxJ3bW}=8ZvP;=!&fr`1+^k3w}TRtxrBTA@p_=KJvx`G5<|ivwVBz-6J^}Ugf!Cl8awB!)T}WViRc04 zEi(us0(rPT<(RAJ@u=Ewb84lI5RVnkSP9Hnb=*JtA|Bk{u%MBOLKdfKH{oH!0su5U zWZ3z8%HjBU{iz|c>7RP*e9?=63E95HgI1ZTw_YNa=vTM-*~XshMao$T1!jwUItT`dOa_=wlUw%#6xD64`ZyPHJ+v{x|?uzDI zk8IH45G&2~&ZfJ$yIbt7Pt8DQl2;OO5*ZAIS9(;#96Wcxu`-E3^!2lWzn`63mKx~F z+E;i)Ui?*}8l@~%sOd>e7pqr6jYB`$m@=RGMXSGcAV3zvk>rCqc+bv~v&Wr{H&fWS z;|`ximEtw8j6mK`KK#qWshj4XD+J#iIrKxf%TO_8(`RI8ryQ1(i!ro}4m@&mDmNW= z60HovY)c$F1zze;h;#cY^bQD(VfN5fgE274Ad*NFYZ$?`bFq_^>V&up-XFU*W%;qq zefl0zt&0eC7@<>&G4XzV0pDnV8&?gl;OpBRs2ikLxQN1F?NHc+nBG)l zxOqW1BMK1@J;MV z9|ZE^34$AqN|#X_*BUM5uKQ0`ke7MJu`_A4Po2gH9k|ZlDalQg7|5teNjzQ##BFL3 zOhIbm2{y=DAi_O#YU$H-*;OTj3mTQW*-K?h&xBCZ4#3(i;9*``n<&1F<1bnKgu;u3 z3W*?ZqJ%g-Fhu&u#e81D*%|wS&qUOH6TKZrPv<#oWjCpwRk;L$1R+m9d@aFkCElRi zcaaNQZTu`AHL;VR_H5_s*>EuuW}PJ2l1Y`lV)hG1VMc)lYnhL2bTK?Q?mf*!l0*+R zsjy4G^UoXxR{Y+lKw(E%?oKpTxrlK#oG<&alJ-ZjKX=@_UEqv{ha`fgL@#ps#k6xh zb1IM9N)YW2ebrJY5Y+hT37?)XIP_@AOqRQy;Iu+X#b&yDAf-d~f7n?02H*JT)yz2m z80D@%$MkcLIqZ~mvDRYL*(dD@rZhf5tpPg&qQP!eBiAto8oSRU$3#J3wsg1YMS zcfVpOTJ%TGhI2k{lzv%1#CbG+u7+BAx!Sa>DRh3;V+ilKx`PaTN$BPJ;X8mG>;J{K z`KDr4JxVQ(L8!)Bze7WCpf446Q>42m!*+P|s`J@HP<7x{BlgOSl}Q@-uq6*uQr zeBK)Re$9=6zHfrk;bV>NQ~6H+#HFy_r(j;3K~9=G>SLmPJKD}pHbE}0dn|7|=yXJZ z(1C9kl$U3Qf5rx=x>d0%07=H=RLSF1*50`rB94xffSmB2$D_7fVHl@7FEpRv$BD=ihPKC^TT0z8~K|-a0KTq?jI0Cs%H?pj)IukdCR&JgK@)IXa3m9cf$fC z3I47+ONOlUXb3_kxlea>N<26*A@3V>!O0~bZ~EGXhwge#+CnccVE=x?cso(NrQ$cs z{&@X6WNhrs|J?EOTSbfEV%GXiDf3Nm-{fCAe*R14ch6DGuq*-uD=PFJzk|Q#XS!B5CGe6~22_8`iD_LxO^`se{+N(ragC z1mNk7g4$keH@soXF@@;Z{U~UF8qn8OXzy~;Xm7B2$WQsau=_{Z?|&MF_n&6?U(Hbe z84QxY)cfxys=sT){TGyf#)aztgZ6*7VEz9BB%FUUU3(KNGoZ^q7XN=j`|o<({;tKs z_J=?Hx8mnQHMX3$VSbC~UkvxA$PF^%?S-WoFHCyb|4Qg zD+d<`2ag9hJ*}ML2Nn)iAPXxPL`Yykl1Zp=BEQj@;VmHfE;O(9GN`0@BbsO3#=_Vb zWKy7#q^q5wla!H}p;e%zrrLXDb5XdQ$z?ftnaeA0@BmysNGj z#Xft3qj~yhVA`3o$i3`~#r|qoY8{!=Lxt)}88NUtfHXR>W-V?Mj$O;_vpI>WTepAS zywL`>!7J+W_pR|paR0^2{t7i#mOr8PTTzBpi#zrWt>5xjsJ$t20?pV=0c@sk*l?S& zvYLX-*x5`#COjNmKz3736INbc5RjMG16)#R>@8TK-kxa%`}o(Bctmu{AYcyc>@rv` zr^vM9CoElZ2jrEe1BkwK7Uz8UwJwLu!7tW5tTO4a#Rvl{&Jmnl>TUur1>7!BRo?RK z1S!CTPj6u~$Ry}bqy`)sbhfYlD2k1XR`k})VqEc^`B`QQHX(7$xFybn)v{s}MLqBu zi18KslzD}S@8SQ568pcPl#5e;^1FrhxBMka?97~8TqZoMCM>Kx08=g=4iGzlofBkc z#?HdZ&B1EI#?8(F~NVKH`p~ zOu`-auPs>&XUM-45e1Vbfso^^my~-N3t&YkPzR0duRKp5AaEdhk zA3@^y7m!$e$v#TI-G%&?zXXYcnTG=a1n~k{L0mvn5Dzb}sR<7+3y6iAg%`*T00DSe zctAh^mj}36ysR>WC~An{FM7I<*E^`DLqYJ-j);+@ILM*m<1QmE1nu>y0T1^RGu;xu zmM%Zm8h%KsfP3ug8Hf|FQC(pXwPnut&|FIL|A-SUL1%+Z!z+NQTMCawrbK}Yd4i}Z z=M1O5&m)jP5u<6@R}n4l4-4>DoDTm9hYus_0Bh6Bp2V6hCYb0AD!FsOyZA(1FN>Eu z9}T?Of?2}Hbkyuos;q9%Naexh>(+##Qf1l~2{WjlB9_Kp3WOMCas7#M3GO{WKn79I z2xGcusY#T#ve{(PY}RtMY!_3LzkX6%8F~gCli(1SPU1Y@s%1-P*z;4FEVaCM^;Nsyj1p!%2%uGNa9#*!$$5+<3f3vW#v#PSNv9Phe zz2{=%Xkg)*9a98ctrq%rc`LdY_?$j{$CXaJeCAu2L;wqJLYMl7RsXB_2l|s$zZDaf zmgijGo^ksve`ytn82|#A0N>1DbWxR*#IL6d_>}41Tr)Rwcs{_=-P&8%_u&weeWnWm{5)g2YqwpvZ@K5a3{jXYnk zyo#&YTn09E&)Y!wkY`^#2-7iKvCEIwRH`(JF!k=}oVp(d9|E0rR|4fOt$cKjIrSoa zh|3*3>5>9RNd zs!_yB88%j?yh_&QVSeV1CasHFpK7&GegwoF*+S z8NfG9GP-tml)Zp#Z*72OLR#c&8a?t$yYN61HV3fcf&FXRY@+g;osij)}v44EiOC>GQL=S*e>={qTeSUoNaj#e|#N8 z_w6Rha}3hdcm|8NJCPOFB8kD&?|vH6Mzw307=PASFZAJFK25T+j{{~X`Sl$Rs3fk- znzs@nO|HXWo(JKa2b=!N=hf>>FRBqGDRB-ow6E>US)!05k!hwkHW zjLT$jowPXGgrkcxk-Pg8v?90bFHAJf{^)-nl=5Zd;P%JJ*?w@&H{Z^4h^_~K^p6(h zy}PLEJHwV+Y{TIYb69LO{M(_){hy`+L`NIBLj1QvYHh+oDZ8nK*M-4nwOt97lS7r* z)uq}A)*Q3g0XoNClaYn>vLKT`$q*u z3-U3dNN{{RY7aEBU7V<5wV1Ii^buKA@TE{nEGpFo&yYJj(wO|EZrGl$q8edMB-urc zgVReVVmafVu40in_#oqIU63-uKKzTMbX{*0hohTE!E9xOolFoP_p7B2kJusiJ1@h7 z%qrMlTZVk*b)-JyOu1X8OYi$dd)sVmnGEwoR6LKdji>0#n*?UQ>D`FbNv7LV7tZEs zVa=L{eY2bO9}^23%#%khyg17xD_dV-$s_A!Cv#S51t)q;H?Ld{23-@$30g;@e+}4v zX6FAI1w}MSDlX2FG1b#9c+1Gk=3NbeWz8^G`31DKN0q5en9}%m8sR;PF4312eAh9CbX!W!NOlx6u)NYN507yu~YzydE zv`#cHLtVZT$=o;zKeBLI9Ct-8ZOi<^z)ioZOs$ zw~LJ(@HfMzQ!{3Q1lVrZGA8 zlI)xX4Y!=3J|sQunZT4hrYeGOvh9e2UYjpsb8Tj66L!Fxi)fnbI2vi*rdV5~U0YND z75721eBhy2a-@6!DTe#t?lYS*rxK6DptY3v2k*duYu0er{Rc_f>k;0EVp^oiScBY4 z?3Po2>t^6WT8S&Hkpk^H99ZRri|7AwDgMkielNv%@B04G+cNx?zg&tpz5(Fn1_F54 zc>y3cPF@oe04oMci~WfX7i2w z=~Qzxy|&yet3vL4qb?U9cT4BwjML@u(^SSaO&IR$?e4DR3qlyYJxE6+Ihj&CNkIq0 z=(17rOys6`Zfaw{zDDw35g}i0^d?GeC(-%4y!tI$K&XMmWJPPdYd7ELo&M}q^}ckx zxQ`t$U_I;~0{;Q;|Ax-7|Jj3oD~_PzO=G>aLH?G%g7;04n~f95&cOlX263^nzMa|L zY5)K>kQt{L2=GSj0K9L6j*IPWr@!@W*{!l5tuGsi=7n);qZ_XRM3BKFL5vU#;w&Ub z(;sZ8sOaQ#D5&Mbo`1o3W92SC^Qw-`)9F0*= zHgDtiX9O=*)27rfYL9y(afJq=W|jXBXI}vo<+e5qqJX4yhzKG|*9^nZB}jMozzj%t zNJ%$>q;!|0q!J<_-5_0xba%u5a?W?}@$UsWDqQl!rQg(j63S^m5UDPtS$; zP-O)kcixr~GuBEuiBFfLNvzy#scnm%DvFcM=73m)bLPz5n}pf8iB zZAvMpwK>w)w69mzIlO;(&W^KC z8~@@;OoVzqbC8!ilSn?qJQ{zB%D0Nop(9N2Wl=wy95+&?zsF3<<^&OL^y;hR${bNt z$+esQkc(&Q{U4aGOf+1@HXw7`jUEl>ZytAxr`MY_bZGXg1Z6&%;mYiNUi21E>w+)D6>C+>z_<)3DNpWk+u&<4=%+_T zsi^EG78lt;(`{TdNSJnN=LXWQmN=7AeQs|I^~bfAZNym;8QyTTh}SP#iY&%-M&3DM zJ=_)-{EJ%uj=TlIe=@#q0C8em*Zvce5&-0Y1N;wg46q|$Y+PIjI0qXiI|m4a;D8u% z!k`?+TreY$kh$@T_Z>K?Cr4m zY?&>yMeS!#y_db!U|yx_$CgHUG&-I78;q)Ks(ld)ho&zGIr6w54ygq;vW1y?rdbQG zt9eE%74_;^D2yL}BKH?rc9^YvqAIJSS}TO}CPtGNNr8*{j;h$8WD~dRqBr4`2)?Q@ ziG%nVD(65uI<3o3ScPb5-x@snnoEYZBM ztBBP&XSRcImB>f_=QLfyloYmIK4t_9{U&_ zr9DY&(3=#&oW_G%)I-!{e5>#JuHgmfm5W5)XcVnk*;NPZ%MO~V8z`JzTXd}{O^Ej+ z-eFd3VuJzkVt% z??4gLEvJPvgK%`wB2w%#czh*AawYP+*X(oM>`$L~rsySK-G07wHdl+4cQ8t$^qo8R9x}&)JYsbLzAP&^`W_|Cb$P)IO$S-cZdNqVL|g*sxabq8%(;4G&-G?{&-y3=sfW<3`5O=kc@D%GhWG)weg8hxJtwpN&i!( z&Sqixvt1IrXzw^#hUi@s1d~cq9O^hH`Y`U;oHtv*9glb4)ThEh_nBThOfo!VL#;^b z>}8^V3~wlQ^zPi})0=AERuZEvdC5A>@BjI>8uHK^;4*!LBeU^>!QjU$pp=HmN6ZcqIvr2X*0#Td2OZL+tFGZAjkUR`@@Yy{&Cg$(A8wAD_q!P}nq%cV==?@De+(FK&5mUa_49byTQqNT*z zy~%pfcFcC1Kq;LYuMpKxWYoM^9IpkG)mC2v6C>m0K^o~9uW0cHMhu3$JHyj zjCAt$(kja2I~3buXumh!JPx}Jt+J z`zds__yWWntpt*F>oxV{clHIPf~MF0k^+B6wt;_D^l9hx$ZvWz-mKqB0b^EfZZO~q zH{=35qg-$(f&-9q++18>4iK2@=9eG@Hy0-ya4h~Ajsl`l|Jq&JD+^mmd^K3%INY+; zaGBS)ySe!?owcU-AyRfG!9Sb+{k?b*r{OQc&CM{?*byjpxLLp3761rh2eEO(0cS6m z9R>mdAy6{RTM)31$);1mH&%}lOk$XQ?_9uXzkeI zFkD~$=@vzwB)VYd;FJj|x0%wkjmJ)tm|$Dx)#1GUsE8_o=Dc%O+=s-NQ1j?K&Eh&f zX2lG=?B3jl9*nH})(4PC*+T3lkeeX)@~m9E+B-Y2XFM70PCiRTQyt#OXrkFm__LZ@ zu#DmY+^ecIpO z#E3-b<@0lI3+40um_FFhWu8(a9hBj(*+J)yMlkz*{H&?8NMI|1S?cnQRn!*0^IaQ8 z%HiWRoXsaT4?Y_esCOi&J=SpFN*Xj8pva5GKI2HaTPC~~W~ZXN@A6UT;kAKydZ?cF zC*>20nU4nyVqOKAm?z;sv?Luku{m8H(G)85Yc4Uhu?)<_NEGJG=|0&x+_HM0qnTQs z>HO&u!Ye3~a({QJuKm+KE-}t2A2xT$MisbLnXjUDY3UQfas4w=w>|CM_&0EaIQIcjeAww7rfq|KImRcQrXP;tKO=qbgH@kMpvej=N$8{^4R3-#{xZ14h ztV$vk=9eEtom)?_Uvl9DSOCoLhm`1sCgMy4vb zX-S(&kZDfiJuy@^AE~B3g>jqFi+3>vvz_o7H?>H9#2m*0K;XpP38v<$wWrIQ3pn!*f&B&OY%a|Ji z<1&JB7{ZMK&r7(9Y{W3zy~SjGb}8ImDb;L#8!9CnbmG>GAe5l2#V66DldU+_SvevX z&TUTLKTJ@r3 zeUHaK(-MWf5&T5qOWmEAdLQMvSw@$a0JGEQw~@^pYNQ)=fdhhZ(Ob^KC=Rf$2 zk;_dRU`qKkq@y9@Dg`8#)P)$g2WAW1+0GyeCz7)B@E4bqeuVk*h%{vdPuz<2GeWvq zOPW$yoHACN*Hp7fot9loITy#9IN;t|HbeMl;`5cMJca;r=jQmmbCioy-t`sNNpF3A zd6&V5MYrDJwXTA%5)~k~WPiLJ&yUehpmml;2`L_zN<_{gS;P?tpLi$&HCMa2LJ);g+t*`PBsuXCkO=GbT%U+FpL`tW#fWDxgcyHHXtnq2*>}a zfN)g9|Hy}F_31hJ?lbb^q_sZgB&zlMh9Ro=%B+kM_0aCb#cq{TBZ)R&HZBJD3~FX~+S{ z2SY>nP4-|RenDqU4wj}5A-jg5Fj+CC z&WfY~?~$hJwTETUElJmPx8qr7XR;o#jbr_X3GLcKGlr4_yrdw!b{`|NP9J0HJ1*4T z2T4OJl4cBd$z2_+T{WLJfGtTC?Xml=jd&i)cRE5!cyXRMJRMo_>ZN#E6DX=ol=)nZ zx`c5EV)H4XHqq=pLK8iZBAGZ87By_8NwP{B@vhG3N2s}T4sz_BtJM9jGw$uRrLlWw z&kb5-!2~q#(jOO}_w8QiSjodW;}G6uRq0InD|BN#gy2ZECm%*hS`83Tza zh7c%_oWKD#gcuvTAgA=o;@qOf5`v8-XSxv#i*(ByhN&ZOJR|V}eZO49vTFRX_;qm7 z{MM;MVjzRYEK_Vzr`DtRdbT`r3_FD6eN9J26`!{kDgCxW-3`mr54SV0OCIA*oX;IZUui&yY!&OpB#1(!8%v%vpxxQXVj0)JRDk7OdOJP6Nk zX++Y)$fbm9pD>iKBP^$GzD1vf9T$m1t}Bw*6={a9X{TA2zBnG97T89bW*dE=*1+YU z|Cxi9}>o)7NG~826=~6z(Uw7zlq3lXa~c|Wz5FG1>;~dhI4WQnTCK71SF4ea&U71o<2i%urb?BSYZz6pMGyi zo_o<>wlY>oM8YHAPwg#(q6)RDn*p7Dw7FaJd*96>2ZC!}XQ#w$g>liUgjVF~vA}@Ay<3 zk%yV=93CakXsmy=>?xA=x~H4GSAC!Qv!xb4uDeF{16rK* z)D!WQJoZg=K2&PibB~mHoqUM3`umV1Xdz*IbefZ6)gtqLbF(Ld^hYE?91p%!_HUE0 zJlB-V?Y=7;C|Wd59h|CwHOJsxaWWAGl0Fu;wk7VH2Ldx9d^G6JMEU7j2}sf(k#Dia zl2fvOIW1zms%dej#fiU%J3Kuz9Wq5b-rT+D0?}s8b?@%~Xls_Doywf&K6Z*qx-t>n zN9+12QGB(@Q?X$xdrDKf^>mGI!3*jk)y5Q*xpfh0oQ3?hGuYe`@3mm5-HN~054D4w z?Bna%EIw2lL%lt>mU1SM4_EH{t1aiO^$TK3tlOG$QpCJOncq8aM`c#D3MvIN*+fVY zFjh#!Mnd07lGr?YNshQ*_pL0v!|myTY8J{a&8-x{Zdi_w$$Lu*^J0Qa+e(E0Yilg& zLSZJd`-0HSFT28bKbz@-1;#^Cqq2&>%!SIGH&{B{efs0k2Yh-_nJyMq5{5h(@92#& zYi1imqrYhO@9=%tuYBL4##Qw{G0p%$1UC@#267UO4ULUBIbg=@AP@)!2C+kK5?bMg zATS3Iiv==U|Njn{d#0NE`|~vwg2uC3KTMYSK6%o%ry-EC2MN^v%WeIvR@~qtUEV*X z5a8T5>vyaO24e-xGA?doFc8uNw#Uw82nAv%P)<(3e1Rd1*bz`}Fc*-v`;Tpb0jBk* zV@aY@o~2i|mob=Cf{8(nd5AF|KK{yUbG=G5rN`dtPRW$*lJ|xW&!L;b*N?#oFR5rb z+dx+B`;J^p3-@H8tXH7IT+?mm{HURxlj+v_R)-)>lhLa>Cn3ho#9p3?fyjON#pO6I zQW2uKWs;oMr^`R$b6fNBmU4(`<9_%SM?k=D&(A_*I+uy#kLxHJ_%S)`k5Q05hDe$G zmni(1o8O?2=*}Ug5P-g$^;;Ag0%0d0mxG;+6ApnOpa>v4+?X2%sKdsFa4?Vv%>ig_ zfWi!9^Zw7x_+6UE+e>Tp6SPCI3w0C7%+#Uai=mR?M-P!|R|%8;%YFSCPP_8mEi(i5 zbhCcDFF32AF$}?l0FoHNfWFTTX#VU#jscLDY=q$AKyYxwxeWoy{*MgJGVniKj_7n< z{Zg_l!OoJh(oDVdgM}Tvu<=JNW_A4&!du%JUGygWdC7;{7^7YctaM%Sz?2#0*Q|2P z^qoD@tf=Pqf?hgEbA7qY+z`yVSpSNbVleXwZm@qsSC0AKdal2G`@5hy(Kn=0}J2LEcYrTa`KWd^XnY`}sTh$Jpc96Xmw81fQu8=MYk1;b(i1>D|@6 z#VHdK9>sc;=8+xNLV))e#ebO#T?|ndz@RWG-e&_%rEuOzWlCznDb61wCdvxg@x9|5 zxdO`m@fllO7{vtlz5Qs)n<$#D;8Dn@*?Wmo_1Y(n1{fIK%KD^+w|!bEqR1d?2E#V_ zUi~}!hIM+yjmb66whhvgF)Zb>QzqRU)I^V2@|=hs+@-{Xrtp;5iEpLH@*1rn5M@b3jX^DV_Z*zqM}|soQ}hlK*ZXR=KzfhCl$H$z*sjne^Rs3 zff-uA=L^lPdQI1(0NBSj+*ovEu1C+!!mWh(`*S<8d!S()_tJx4ME4*_8xs`afrI?8 z%aE*e+b^NA;==7*S?LdO`VM?k*XtH%*E;z&nY{LzS}x*4OIK0|3<_dY9$asaI+9WJ z)yIEp_4KhytXgs*_hb3a?Y<%IQ01Bj7WBKt{PIpGv62E6-NiNcN2wyWikJ!M&q=2w z*b+J6WHC z(Yu)92}3g3lke+xBFyoeLZ_VLFrS7BB-80+C2l?%<5I7oS7{q@y zccYo8u7xSpoinnH?)x=`iO@GEEbnh^@(F%+`9gU_Sq&3^$~S$t6Y0E`$|FxS>AVuf z&jTVrJnr%#vulp@)fZ70)8hB-mgBnk9U0l=yDPR8CrEtQ#U25#6Z6Bj1G(>Sntl+~ z7MAb#ZDM|D=W!pZe3DBWen*e$I?d(S6ru|BlX#e)ZMcOfbGY1JY-1*6-*H z0E7W`XaJS50i_raI0R$_Lx6646ksDDm5&`rcgSJ@UCao_Qb^q7+lf8KQ>&(z(B3=cCb> zzgA0Wsea?>>H#5nLY!vork~u}LNJqg4)Zv#re+2;=)|S`>pQHp$p_k+LE$|ZRY|^0 z)-&4lVZx5)jy68(g^lS%TCpG4(5{vz*ZD`wCr=-2!>yKW5{SgAS!~wD9Qn9ALwiU+ zXnNQ{NuGK&hL4LRc1cZwQ^*u8mdglY2NH-@eTS03tSY{*vfeqy%`y87^`~`qm>6*u zUhn1HiD4^ZHId^_cq{Y7A|%z*2lDC&J`795o3B7m|gXi)9TN-<=1<* z6xXueJ$v;P-JCrdmE>XLQGBL*8BHXL1#U;zy~S zRC9z8>H;Van{h~r@K{+Y)>0*8n#2=pJ6@waoo-sIi{d4FT^}7mb4|tN)OfLZ;f~`* z?d(?PZtp4Zc@Z4;`838{%sm_1BG#D6%KBlGhuMWVAA?K2uTI!A(_o^?qYsylozFa-|&WK|Bt-tpR_8TCL{d z)8)0{)p3Sr?jdG1+ohZJWn<;hI4|z|F_)EP8`23vn$NO$@*B_Glc5AAJNw%M)=w8U z7x?!BX>j;Lsm~oQyy;^%;Q= zaX2qi9bU(w@llKQisEw`T2sf$oTr#)2anYwGbwwP+N$JRhI~mm%aJgenxYillIRDo zq_Lo)E?5P z`iIla;{z4L$?zq)4B;~+L{3nq?=B7pO$Ls6v4EW9@{Y!jiPdf9K}NY0`*3Xv_844S zaN^GEy(Gti5AUlvWnSj$>v+gyt8fh-NDOB5g$x4|Ix}1aDQdF*%4G~}3G$W;RH3bS{%4kv-qKOj>-;sjxNfFm zJ&g;2i4~z}UlC-|bslUA-?F8KX@Z*3)5XdASYtaza?r)5bTW!wkz**ApEpwv&^=5= zR(-}Q${bVoDGv5SVPc11)qO9V-;9P3ES?P(82KPKlyY(sS>JA|JzqJ{m+V4SYyO(| zog&b2%(d@%>=v2TOh5nC61$w*3iiJ9a}F7&XVc)VhAt)56k&6#wO!8ywiX;Xsn z24KY-h|GsWpS%8;--g$4QJuk0zh(fHwUh zg}OpJ_SLph&}_Y6|M~mFa{u$)6^11D8cG34UxL2fttDdb4|wY?X$!T9`VqEDSmkOK z%Xh!;3UPn>q#DuvC09poj1_cI@mvE7sa$;0m(BKe$&?@KO5w&~tOS#J$#FLSLO^jwLQvW)_iEYv~kxcE*dt72gr{1Ob zG_}XXVWSZt=#$sW$;qw)3P>uIrbR?pZiiP)v!Z6pHin6 zf=%oN?0X^HMT4#IyLns#ItTuTvo1K2!6mq2wgP?3a_3B$GU>_>0`p)Gvm;=FP?JWs z$@Rzf8sa)dHbc@C11WM^*J*)z)_I@W)Bj7PL4HA6WqS5oTmWG=>$gaQumYJT96&V* z2OPMBH+d*fLk@1h8N|g21EP%_2r!U*4B`e9Ch4wDc_lUjB&54#$UFbtYa0LR86dYe z=cxx?@n-$*H32{{6bxiPv%{cJ5EoGE!Ug95>Z{lRUz{-)6wJoWhA;vOiviC-ctkG- zP)F~_e@+Nn{VFb|@VEf&m5?yuU`4C(SBWU*_o@%)szY2IaEL`%a$a=4Fn=wNxAo=-vuNnIT>*MItC*R)>=AWea%;v>mJh*R? zaRg@&ls*YuDvkHKethqOprNRJ?lS5r&K=F4d|o$BNdP1N{QRyi+xTZ)`38{RVOUBC z0KHki#iA)D8z+Y`Q1AjorXUbbI5$vNWC#W5Lg)qK99<^`_Iq6 zK)9)im9^bZH{Si1oAJv$ceu5Y{ZA+Q>t8BF7JO&`G|{Dcb0V8`{=k_9T<_lkcNYA}jjH<)}NkuD$RyS-<(WV6(h zKz0?KLSJY0HKgKY+tD@z9=*|A7pv}Aycy#3nzj199r5zAUTLLj0L!U%A^(_#kyjH* zDNS@{UnDRe*D&W%@p!rV8I>acr4vd&>LmQTwULt@y$LNTR~wX3 zN!)v9_Y%X+=Lb=bhKJ0n{U@Tz@0i}#et`V=A2t6Cf7bj@{896dEx=qgXe)jH0izaU zsqX!w1-v3Hv2V+jRx(nFADA3V7UMY>0}l#$vZYm1Yoe^2-ByJOr*v+q8sB&5uzE6K zk}Z_>Xc|gebI*zoO--P5Tz(}6f-&$%#s7nwihp69sPoTT0v~O3)(04}8bLFA#IHZa z-5!#quD)~OT&j7?z*NI?$ug*fxLt#tY}u+aR>k@_B=Fh&yW?%o*3f}s|884B{}}Fh z9s{@@2Z?m-+>jkFF{+On9d3+i_`I#6FE7;A+2@_@MppwV^iH7uyxXG^SU_{%Dwi6b zYN~6BdgblFkL6Lh+7)vu7Ex`qxZO$FK%GA>5!GDHA9emhO3m5Fx^MgXSDhQ)QPq3r zK1-TEFQL6<=|ayiPZi|zN0om)lP#`9xi%rp+<+;SCvuRJCtrfWp0eM5^$9T-*0e z{03+W#Hxjo?5;r$&n5>~5SgCRXB|R2y3j@~RTsQD$sG4qm$T7V60h{Nn+4zcgqv4p zuAzB3D6^8+`n@RHxwR;m>tmDR$mXo<7`UfvVy*P9LGZOH&m~bBiKy(BJWu=Pyvq^< zjLLo%+~FkXEGJ4i{FLqVro7(=DDU?)*e8VOzrA$;$M{}{dRKtW6*Wsbbx$injsMmj z#qz@uHFC7G-N^PJTaN*|v5DkQl>K_LD=tUfy$1VPuh)w8Lk>febXz}ZyZVwC?pX9w zRSCbk(0Q%@y4`pBeAwiCB`fB9ALH|d6)9^jyzUYU{ zd6G5C^=1qyS<3v|axa6^6%_A+BfcHp@)gnWw-hvZ@>XAV!9BZ{Q?dI8w@%)S?w_^% zf#&eT>1AiXK5KG9aaU6(p)G%VRLpsEC)=>-Zz=cCQoBc&*n1zC+O3k<3|K4OMKWC~ zepy!LI0>P7F~G=J8v7>UJRX$_^8vXp%S1%&{;Tq13sQ|Y2~dvx6?0$D?-OQ_49)tM z+*D4n;A@z}@jn07qc`z!V^>QoDMiQAWXrsXcq!5}*Mp>KUBfs@#?xn_mcuLJsrdN$ z+mwrK001p2~pIl`g<-&{5|4F$DgUa2-jDqAZlnaWZbz58HQkhq7CSwqYUnL(>YDzaZ zsG{9kQRyo_qlz4K2;-UVedcnGkgVr^D3W)>X(UV0*RvR_&fv(ecva$#(4(rw-_jW5 zZVvJkeKw`@@1pfF;>!1bpY8Pd2_4pznF8XuC;r}xPB~v)q2hIM43t`N&Wc+0iAb#> zpa@tvi7&dgGc98P)hT~~&7f~X$}a`aQ8FRYpbYXt-ogQ|&jakY!#!>Pp~f77&9rK456H0U?;|%hpvsbO9L@ilC3CYt)%HftL-aQ6%d=a^W{F436wRBf_ zsy-w20_77%k|=SNMj5hhQR_v!z7ih8Wgtk5+H{HT`Gq|>vp<1BiYdn!+gBt;B-UpE zaqUOh9P~xHQS;R*@)#!sOl5}mc1E=>miM1%R(*ugfq0J6t)G;Q#ng-nE?9`}KoN4) zao^wKfUN@>*&h=b)c`A%g$bFv#medHQjmoAN9-C3aIQc%k<46W$~= zJH4dwbG_UMcXM$BTzC~6{AJ+Tyuty4?+2s(MT|caYuC6q~gm!DdxGGF$vDH z!DLIu>BF4)8O{5f>PRPs5_H)Dew$^M>N^&~Z)3B<%rYp7F0gmL&OO`>!F20{ZqR)@&Tfh>ReUjwZngP%pWiXJUgOJ6at<9SU%<*DPe7bAv=# z)Enj4PyQnJA>D;iyU>|w*RAW#9V^zy?>XEG_OS1o*ja8S2&e)BQcArbG6pHhF zkMEf8x6`<5qG@rzB{-~6H2_0xCA|wvj(RC9{Gofsj)<*ZaYY-C?x6Y8xvJ~;dU=)w z_Y=}0Nm4>^ySCF1_3{_x{3k|X|9ejHi{@vCu(x)!Gy1KvNcX>A`7aQdjj6u94Z=tt zW(C*(b>qLjG56ni$}gy}g#U$zU(bNy-#r!>@oa-|Fm(Xt%>eHQvv_H5`q#Oj|AEk+ zB-<${G7^$JzzO;O)^mP+K}$FyvRkfMh%n&z$)}c1i)jrXFCCq?Lg6)Dq&_g11_e*M zlVO7Q$C#r{OG`{56YlHdSN2OA5A4E+&hX6KULv2qZ*IH2(K0XI+HC9<)k2fum}o%l zBN4@({>dN*j3%h|x`jY;=tV4~C?=aSw;3rZ9Zz@$g2lH@IsW!39m@B-^YR$goC2QD zyQlB=nVR!$HHnYD9?W8wadr9s*`m!s&cUk~T_#(n$9qWks=z)Q(?Xb%mX!B`bp<&I zhI;?j``$T;{H+Q@+(vV@Vj;_k2kzC{E#@z3tj36TL&POrK0k~6Xtq%dN4qnn&t+j` zDgVwB&pBx~G=*0?py3h4$TxgZ{X)^j&8ywlPdx*kn&+VhIzpBAlFI2b;%qMD?!Lw- zQ}?ycg9YD-94gm; z`iTE~o{kDlW-V-34J2o{0#7V1y(yeDVFwE1h>aJ1#kD@;eHJf`!miv>KHTH#b%;t$ zosF)X$)_}&CU}eFRENj?4tF-Pa_LrPs>?YH!dzYFsO<;bYS%W7^k$E&u%|oBCIBBKvn31FXYebO^y?BS#9rIE({~!+%SKH(k+l#o3RLkc-Bj@h&>8?kh4XHz_!06z75~7VbV3dawj;q%)jK*KN!~gA zmuzj-kqWUBxA}KGefd_D{hl6k_xaQPS;bGllUH@i(?OzW(*}=hF&-Ejrog7)(E+AV z=Wzgf!X<3Epgf=R+f=1?qD4B|)~S<^g}PK6^Z->hz#tMe#kAlLjq?5~AjIS!R}R-pwGwNF$a@_lOyK59H4Y zCxK+|+@%^TLg-FM)-!YOG*YmK8vSSK{}p8Ds=T&W-@dKRkD4MJf`w{niT zHJ0U9aT|&#VUiW z#6i335t{v4Hg#|%F|S@JS9uPFFK-a7>dkBK7kps$lL#DMO9v_ahd0*Y?#9^E?Dyxo zF_!uvo1s(um7nxX{Z zZ`vu}UwJ4q4=u`Xpwgc^N}SSugQmP{&&-I%vFC=G|Di`~^)1@yi;PLILVC@0N(34v zbNTR=o(%__vJKE%GZ1?NJu3uY= zGBu97dJ3~)B4}t}h;}}6E)ySph?!@9rj7XHEafeH%y49=CPEG*Qp zr#|o91hJSpnWi}o=j(humnQ6P48->G)GCDkE11J|H}w%8USjpww_Byj7N^Z*%b>%c zp`4f3SsG~=v$S!&N62g=eBo9(y^=*;5t| z_gmJDdFL&RbG{KbKtv>Tn10_kw0C!^B+n!6$e^R6N((U_Mbc$L8t-J1nQI=dobw~+ z5@Nxo!b@@@5#^obFhiv!z2G^!ui>QN*ID9=yGn7uQ}e=nbvh}+gk*bZY)|v*>Ziux zzq00EY`nkJFnffpzOjY1^Dq241SP_H7GURnfQG~N@7Vf3VSi=PmT=3+MnD4IYdaR2 z70$wxrTi=!hwg*sO%kn0arA}1tpA-u=D{W-C+dW3m1pC2#Fh*C3E3OBk=t2;r!ESs z+&d!t+y;u`!V0KC`Z*m|BcZ*R@H3%H* z$Y=Q3V24mnIMZ+;E%oD1VdBc!Hm(Tch_kHd$V%8gB5d!)}lIhiHC z95~FBB!BMDzlceEhD#iE$K4M8o=4Nk;bCtL>i2wt&bul=+EM7i;6AmtY@NMKh*> z+e97Y3W(Jrsr_#TP!|zs)TqLpp@V=jZvW{-dCDDip@mhi^wlr)&bMTW1y8;1+{XOK zksyw~ftq8N+Cw>hn>SF95xg3l79;R-FZzYL!eseSmpAjEOhd8GWX|Y8$c}p@i*!|S zxyiUDnj3h9l-&e}yLq;NBTvQDMuX?TgvW`bpO)P41d9@?V=?vh_G@O*e%fZn1g|Z! z7&Hu_RBzMCCTfWVgh~42Oawnhrkb3a_ydr%GOn)3{JE%eY4_(-VgD6!CTH z1HM2yd+fv@#wyn${4Vxb zT4_4V5Q=p546}#~^P+@#@3daGc>SV5sllkplPkbCr>OdB)nwi0oF257qn4LV<2H7g zvcF~9`eC`TJ#vRhM ztPknyB1f7d#OAe(I1Db;_9fA79=VmH!a!ia>?}f6FZex2{VGvz4)zf*P`m70PyRUK z`w{i!s^NRx-fynLT30=8?^E|Tm)#7~_g6CGlel$c3+OASrx63T67bS=LBBEMPw=dDxC>2tKs#$K=fdTUb=s7aiOG*T@oCy&>Zt% z2&ctKi%0bR>N6li^(-if=yE&jN5luyI#o1n{DSsl38{T-=tM1Bt_<_JrAw)Cr9## zshP(!Y%Tkvl$g_58i#R7Q|ooNrE(aJ)dw|uuoMeyVDl7xTKb&Xk~Xt=?HrFCg0S1!w_e48ny%T>?stt@ zw{sFV&1`)@b~jLE-a*$UxG!HA@h~d&kQ1Frf`LS0v8;~vp6{LHR~Yn{!l8bKljnH- z6BbT7x6J}X78KMBAVoFq5gb&$&rzRXem76ajpCr=cZ@kj#-Lp5IrxTNDD@(S1#8vL zA*|a3Z%8lVp)`K$yZ1iUbu5{x{lRxwDnB+sFvWO$ObeL589qZNmt$ilFrFu&l!+A1 z*TKbyqOPaj4SeE3qp0>!ocn;rppYEFXCqHFqj;t#WyhZ-EL`$&4FSj zusx$yen5der`*y!uYy4DL0)?v$?if@-93o+c9gR3JnfnFIOYSpNyzuPYsy7bf)(p)(KP8-sgX$j8QFE?aO@*tZyr;aV7@=fFf8IDHYZOk7=zNx8|J_G^ zNquN6ca6xbpBFX*Kc)_D6gMsRV6R?MH9t$#SI8oo4Z{(XI2OpKM3uE$qg5AC?ioBd zUv956sGG2Qrv^ofQ0aVGhs?tX`TTKY$llXt9MYbR{&ZS1Kts-8FfOM)QvM?)$yT9i z4C5V&xK!zqt~;_#Fa|@~*tZLJUCVPY>SmX#h{V`+mGD=Do=sApwM5%!_zupOlESE( z%c!lOCS7v*OdVvi!pw}U;lxL%v4g2AacB67aG zt+Ve7x!k6&_&kFBYCNL$GOXV%ndn@&O0a2Lu)V(g!LwAONm16%_YEiKB+ZzvHWbzI zb>pTUXTLBHhwrwat-^Ys(i)KT@cyRqQGbvuvAz1f&*%^)gFt}1tnFaH95?k#3Aj$yoBVB5-Rl1}PJco>Y$!ES;k8mPyhFsq$mzD#%=?iMrnJB{3p7ok}ld*B*a;U_J=im|J3As-@z(iiDGqKw3eBhLmM zi;JGUg&YN3N1;uvpRYy24ox#6?dhcHptvw7tJ0HQapZhwm=O-9**#FK-(O>Y{$#1B z4jogcg)dL-x6@cnf$>`V>J}z7ohh%pMMwBSj-Sx6Vs&}c&8+9 z_^nm7c19E}2+i*im!tN>;0F^pm4PSQTt-g|79QU@8+cN^oYW6mNU)WclW1B9!>Bc8 z=yGm<&exs8(aL8Jd;3VGWnp(vse3}HhHXEha_M!m%Z@~lN*A&o=V(QrpUSs8xw=1_uYfb+x9ivt zwA1tseSKS$U=Tu1+$dEQ7Og_OgZ%`hsCeK6|GRKjYoeboFJPq27(Yk^b>h>;lTV zw)TE9ZvC_s*Z9XKyuB@6wo*>5_I}WWhV)*;GXA`&*_6AgvZ&dsqC?T+A>EZm>uKCD zXvhdm`Jg@^c-V;Q-9hHj6OujLl0C*$ICq=2czost_jyw`x_5Yb>w>46=km_G9y=tD zHE!9jeszDY?Lx-F7b6R!6? zs_x>^%Oo>3BA>7M;ImfGODE=8o{Wf|Wl-g{HtzXullel;49zbW?_BHBvy&a)DRo7o zZG1p;Y5fM#l>pC2i&uS%P&gG8R_3|pWPib;72ivy86SwgU_R(tO8wY9rz<*W@CHpQ z`}?9?%jxLX_Jvy(Z+E!jcc~@TcV6u#yR!0$V&`JV)YAU@%w6q-Urs)dE49x%xOv@F zqql_{*YJ5aSD83ysmzbj%qSR8u;@jD#elkiPzMXog39SH_VN@bnP+v2x->oLqk)fh zbc>VnJ$un(8__*Sk=})SuH~XJL682lO=vy)vVKzjoeDh*!M8Dqg5d)nud~_qCTH>w z|Lo!EHf15_H_fZ8yJ7gy^2o`R@-HL$csc7@*7+nfTpe{| z{!YJ1*F^l-QCX#}t<(1mJzv%|W4g{6o!Ac%qN9zY#d4>Tyf-bhc2g2o)}e@k$ELZt-TI$fzMO}l ztJ2`(BR1#qN0)YrhK{gldg+5#ywe`K>WFsmY%`nYK9Tjlqs~`6RI7WvVxWBSoX6t| z1ZK-;j8uzQCtrEjSJz`)!rfX?T~#&j=*V$i(I$E~U9|@u>$l^$gYu3;rpdFa4h)(T z(L5=!@K1}(xV^1C3eS(Kt5ER|QZ;$1P#yQcXhzYBbC)WIDhao|zZ<&Y;BUc-<-vzv z1&@dwBT)HjSn@5vr6_Ay(Hm{Enwq4YiOY2wLP|D1Et_RLGt9h&KjdUiN&ZPYErorb z1xhCicf^EWp6Ot^ak$Q3jTUeJR*5W&ZKxEzdnR1u`~13G#OZBQURY$$(eUtmqLWwZ z{-Zj{_{Zg}iEk#aye?XDI_&7_w@pjGES)GW+&H;P<5vH1S;y-eCm-g!1aix1Q{^!0>1C6`t8Ur?uIMV9!{Yd!upG}?#N(sNKpI%3={Rw}Ln&j)JIe_o|i{QL$ z#ClFfr)SUk!eC*Tw>Sh|>qZD1fft?HS;Y_iR@H3iSbg-<^r!7_gTB^}Nf|$2j61)F(CX=#7_$$t-DZgj1{g(bGf)_6zWi9_!=l&vBa6f4 z)^>D#P^H%Db7_;sCpq!KJwrP+&f4PKas8Tmi|h^d8(VDtP`cxD$DH`$-B#bPzW=si zQJ!_YZ}$Bgm%}!ul#bAitiEe3M4o5wj-_6O*!mlc|r*7@7 zanm|ZT~VH%le$#F?oi3|?KA&((?dB+*r8=$Zn3rM95X}vkmt9%BqnLxyOX3_21NoKkUE;H%iOrbuP&jVWeEcS>whjoezb ziW{TkvliGaPb>*ExBgV+`^dv&#Gr(-P6sTm>sQSP%Za!8ekFB6ZI<%cnXbGOKi3}) zY)SI79a@$0)v8Z=e1_`T>uY-_oy}CZ8~G}4o08S|u?=yAV3 zwZXn%CYxkZ8Il51d=r?HD&f>Qs)36TUKx0yv29`ur&!Xrh7%Tdy~PriJ^0z$x>_?3T?Sle1~G*DffDX`coZCX6f6o56GR*1 z#>QwX@_3P8^q4S2-N6qh*`o8pMIpff;EZV$1(CtUEerA1uFWNDfaqE9g0~!Cqf&9H z8=H7&NVwRDQE?v6!>tkr`OY8@`=t`RNrhE4OlX9X%!1okIHK!Xkla8JIWxhp!Nhvm zxd;WqRFYXlt;c_y`xC^y1JJ_nBiN`Moajjs6$!)U|3dn&o|ePkf-!X;9Ldam2diGQ z@`5`%?ZSkCp{lC<^5!-aG#1*+$c06+@B>CD;sA2{t}EeeGoV^zZT%Q zXO|2E1}5P`7SWW_mNYw%TV24)9Ah5fCmBi9R|qCZu&Cbaq}22V997Sh@x4|*fK?Sl zZX^yd^7R+`27>!`EYg)tt)0?<^dvA?;g3exr~%OjJWG}h%z8Y)^EqDaoRZW9A0LgS z1tqo5(IF*0)gDfhp&C5z`B})4kj&L*o}KV|worl!E1gZEt-3882F62mPQ5c>mRL%| zv=xv=Fi?VCDiuE0hmC^CXOqCc4sa4U<(_IVEiPbw1-`2!ByB4T+Hs_dn(DTxw0*4* zb=s@IwyVjm0}BPI;I45{qt+mhat?1pQw>HMJxV&#_C*^cmG*CPD$c_M6-g=A_7G(k zbERb(&HPi?t!=?$wJ&} zebBkd`R*lP=av-jVCEYy-+{Ot$wJ(UdCh?{jhy*qNgugqoV zmRPGJZYYGCt0V58WFc-V;OX4td_M#?q1i4!B@XC_8w%kb=!jd*izvj+5uH1?vNxeS zXk5$W#*N4kH=iuT&6b?r9eJFb^JC|hSTQ4RD1@6aBkpFh5I1XP?%bP&1a9VW3Au5% zWyD=U7UJf;%$@tuJOVd!oIq~e;uvvvpHCFxW_Zk<`zWb9>FZeB!WePak%hP!8q>MS z=aT||_B2Ushtc?kLb&lT;;ttPakC-j&OK@Yft&gJ5_02Ky@-1uS%{l)FL&-9A_6z_ z`WtfNMy!b2o-D-8mX$kqY$!Xo#MY8IPHw9rD_PzX2CLfp^DLfkC1=-uJ= zRP9qisOKb0;@G*{EV2+eS?Ux)s?9QsU5feK?UM|tUq0w!4x~Ygr30fmW6?7NcL;8( z1$PMbjLVBGB>i|xmUkAlf8pb;GB&w3CoPmBSzcNQi?@U&zJHyy=po_`0Vv(NKvF2J z)o!4LaPVayFc_-AzcQE-5?2&D0KRp<_KhWzU9QO3TFPLH9R)=LQ93MTWCposAs*S5 zTM&ub=ay^|*g`UBz=c$$9Vk$gxPzbb1IYFYH9g)3kwu3ZqftYJE*1$J{GfT`~nRh`RFVGWlNc| zw7t>h?Sc647@33aJ1&I0KG-D&0|KhSJvuyzktVe0RHpX_QpF<#PmtT1F8GZ652{!y zp%)1129F=i9cgP2zMQQaFjq;IH%Pm#Y;AQb@`@i%p8LTnZP&XZIt)#vC?56g*?D|8#o=Fwc|6TMhp0Fyir6?ScRMKTt@^ An*aa+ diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index cdee82d2a..c69515b86 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -28,6 +28,7 @@ def test_refined_simple_calculator(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine") logger.info(result) logger.info(result.output) @@ -43,6 +44,7 @@ def test_refined_number_guessing_game(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine") logger.info(result) logger.info(result.output) @@ -56,8 +58,10 @@ def test_refined_dice_simulator_1(): "--inc", "--project-path", project_path, + "--no-code-review", ] result = runner.invoke(app, args) + os.system("git tag refine_1") logger.info(result) logger.info(result.output) @@ -71,8 +75,10 @@ def test_refined_dice_simulator_2(): "--inc", "--project-path", project_path, + "--no-code-review", ] result = runner.invoke(app, args) + os.system("git tag refine_2") logger.info(result) logger.info(result.output) @@ -86,8 +92,10 @@ def test_refined_dice_simulator_3(): "--inc", "--project-path", project_path, + "--no-code-review", ] result = runner.invoke(app, args) + os.system("git tag refine_3") logger.info(result) logger.info(result.output) @@ -103,6 +111,7 @@ def test_refined_pygame_2048_1(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine_1") logger.info(result) logger.info(result.output) @@ -118,6 +127,7 @@ def test_refined_pygame_2048_2(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine_2") logger.info(result) logger.info(result.output) @@ -133,6 +143,7 @@ def test_refined_pygame_2048_3(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine_3") logger.info(result) logger.info(result.output) @@ -148,6 +159,7 @@ def test_refined_word_cloud_1(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine_1") logger.info(result) logger.info(result.output) @@ -163,6 +175,7 @@ def test_refined_word_cloud_2(): project_path, ] result = runner.invoke(app, args) + os.system("git tag refine_2") logger.info(result) logger.info(result.output) @@ -182,31 +195,36 @@ def check_or_create_base_tag(project_path): logger.info("Base tag exists") # Switch to the 'base' branch if it exists switch_to_base_branch_cmd = "git checkout base" - if os.system(switch_to_base_branch_cmd) == 0: + try: + os.system(switch_to_base_branch_cmd) logger.info("Switched to base branch") - else: - logger.debug("Failed to switch to base branch.") + except Exception as e: + logger.info("Failed to switch to base branch") + raise e + else: logger.info("Base tag doesn't exist.") # Add and commit the current code if 'base' tag doesn't exist add_cmd = "git add ." commit_cmd = 'git commit -m "Initial commit"' - add_and_commit_success = os.system(add_cmd) == 0 & os.system(commit_cmd) == 0 - - if add_and_commit_success: + try: + os.system(add_cmd) + os.system(commit_cmd) logger.info("Added and committed all files with the message 'Initial commit'.") - else: - logger.debug("Failed to add and commit all files.") + except Exception as e: + logger.info("Failed to add and commit all files.") + raise e # Add 'base' tag add_base_tag_cmd = "git tag base" # Check if the 'git tag' command was successful - tag_cmd_success = os.system(add_base_tag_cmd) == 0 - if tag_cmd_success: - logger.info("Successfully added 'base' tag.") - else: - logger.debug("Failed to add 'base' tag.") + try: + os.system(add_base_tag_cmd) + logger.info("Added 'base' tag.") + except Exception as e: + logger.info("Failed to add 'base' tag.") + raise e if __name__ == "__main__": From 8851147df74f2111b392fb88802d19b46145b4bd Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 5 Jan 2024 17:20:15 +0800 Subject: [PATCH 040/101] Update test_incremental_dev.py --- tests/metagpt/test_incremental_dev.py | 65 +++++++++++++++++++++------ 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index c69515b86..216b54952 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -28,9 +28,13 @@ def test_refined_simple_calculator(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine") + assert True def test_refined_number_guessing_game(): @@ -44,9 +48,13 @@ def test_refined_number_guessing_game(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine") + assert True def test_refined_dice_simulator_1(): @@ -58,12 +66,15 @@ def test_refined_dice_simulator_1(): "--inc", "--project-path", project_path, - "--no-code-review", ] result = runner.invoke(app, args) - os.system("git tag refine_1") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_1") + assert True def test_refined_dice_simulator_2(): @@ -75,12 +86,15 @@ def test_refined_dice_simulator_2(): "--inc", "--project-path", project_path, - "--no-code-review", ] result = runner.invoke(app, args) - os.system("git tag refine_2") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_2") + assert True def test_refined_dice_simulator_3(): @@ -92,12 +106,15 @@ def test_refined_dice_simulator_3(): "--inc", "--project-path", project_path, - "--no-code-review", ] result = runner.invoke(app, args) - os.system("git tag refine_3") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_3") + assert True def test_refined_pygame_2048_1(): @@ -111,9 +128,13 @@ def test_refined_pygame_2048_1(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine_1") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_1") + assert True def test_refined_pygame_2048_2(): @@ -127,9 +148,13 @@ def test_refined_pygame_2048_2(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine_2") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_2") + assert True def test_refined_pygame_2048_3(): @@ -143,9 +168,13 @@ def test_refined_pygame_2048_3(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine_3") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_3") + assert True def test_refined_word_cloud_1(): @@ -159,9 +188,13 @@ def test_refined_word_cloud_1(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine_1") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_1") + assert True def test_refined_word_cloud_2(): @@ -175,9 +208,13 @@ def test_refined_word_cloud_2(): project_path, ] result = runner.invoke(app, args) - os.system("git tag refine_2") logger.info(result) logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + os.system("git tag refine_2") + assert True def check_or_create_base_tag(project_path): @@ -194,8 +231,10 @@ def check_or_create_base_tag(project_path): if has_base_tag: logger.info("Base tag exists") # Switch to the 'base' branch if it exists + stash_cmd = "git stash" switch_to_base_branch_cmd = "git checkout base" try: + os.system(stash_cmd) os.system(switch_to_base_branch_cmd) logger.info("Switched to base branch") except Exception as e: From 7a5485bc8bbe1e8c3044311042e8f2748c2b6bb6 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 9 Jan 2024 10:49:10 +0800 Subject: [PATCH 041/101] Revert changes to file conftest.py --- tests/metagpt/test_incremental_dev.py | 191 ++++++++++++++++---------- 1 file changed, 119 insertions(+), 72 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 216b54952..e86e6d5ff 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -6,6 +6,7 @@ @File : test_incremental_dev.py """ import os +import subprocess import pytest from typer.testing import CliRunner @@ -33,8 +34,15 @@ def test_refined_simple_calculator(): if "Aborting" in result.output: assert False else: - os.system("git tag refine") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_number_guessing_game(): @@ -53,8 +61,42 @@ def test_refined_number_guessing_game(): if "Aborting" in result.output: assert False else: - os.system("git tag refine") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine"], check=True) + except subprocess.CalledProcessError as e: + raise e + + +def test_refined_word_cloud(): + project_path = f"{DATA_PATH}/word_cloud" + check_or_create_base_tag(project_path) + + args = [ + "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", + "--inc", + "--project-path", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_dice_simulator_1(): @@ -73,8 +115,15 @@ def test_refined_dice_simulator_1(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_1") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_1"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_dice_simulator_2(): @@ -93,8 +142,15 @@ def test_refined_dice_simulator_2(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_2") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_2"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_dice_simulator_3(): @@ -113,8 +169,15 @@ def test_refined_dice_simulator_3(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_3") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_3"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_pygame_2048_1(): @@ -133,8 +196,15 @@ def test_refined_pygame_2048_1(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_1") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_1"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_pygame_2048_2(): @@ -153,8 +223,15 @@ def test_refined_pygame_2048_2(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_2") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_2"], check=True) + except subprocess.CalledProcessError as e: + raise e def test_refined_pygame_2048_3(): @@ -173,48 +250,15 @@ def test_refined_pygame_2048_3(): if "Aborting" in result.output: assert False else: - os.system("git tag refine_3") - assert True - - -def test_refined_word_cloud_1(): - project_path = f"{DATA_PATH}/word_cloud" - check_or_create_base_tag(project_path) - - args = [ - "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - os.system("git tag refine_1") - assert True - - -def test_refined_word_cloud_2(): - project_path = f"{DATA_PATH}/word_cloud" - check_or_create_base_tag(project_path) - - args = [ - "Add a feature to customize the resolution of the word cloud.The new version allows users to customize the size and resolution of the generated word cloud after uploading a text file, and then generate the word cloud.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - os.system("git tag refine_2") - assert True + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_3"], check=True) + except subprocess.CalledProcessError as e: + raise e def check_or_create_base_tag(project_path): @@ -222,47 +266,50 @@ def check_or_create_base_tag(project_path): os.chdir(project_path) # Initialize a Git repository - os.system("git init") + subprocess.run(["git", "init"], check=True) # Check if the 'base' tag exists - check_base_tag_cmd = "git show-ref --verify --quiet refs/tags/base" - has_base_tag = os.system(check_base_tag_cmd) == 0 + check_base_tag_cmd = ["git", "show-ref", "--verify", "--quiet", "refs/tags/base"] + if subprocess.run(check_base_tag_cmd).returncode == 0: + has_base_tag = True + else: + has_base_tag = False if has_base_tag: logger.info("Base tag exists") # Switch to the 'base' branch if it exists - stash_cmd = "git stash" - switch_to_base_branch_cmd = "git checkout base" + stash_cmd = ["git", "stash"] + switch_to_base_branch_cmd = ["git", "checkout", "-f", "base"] try: - os.system(stash_cmd) - os.system(switch_to_base_branch_cmd) + subprocess.run(stash_cmd, check=True) + subprocess.run(switch_to_base_branch_cmd, check=True) logger.info("Switched to base branch") except Exception as e: - logger.info("Failed to switch to base branch") + logger.error("Failed to switch to base branch") raise e else: logger.info("Base tag doesn't exist.") # Add and commit the current code if 'base' tag doesn't exist - add_cmd = "git add ." - commit_cmd = 'git commit -m "Initial commit"' + add_cmd = ["git", "add", "."] + commit_cmd = ["git", "commit", "-m", "Initial commit"] try: - os.system(add_cmd) - os.system(commit_cmd) + subprocess.run(add_cmd, check=True) + subprocess.run(commit_cmd, check=True) logger.info("Added and committed all files with the message 'Initial commit'.") except Exception as e: - logger.info("Failed to add and commit all files.") + logger.error("Failed to add and commit all files.") raise e # Add 'base' tag - add_base_tag_cmd = "git tag base" + add_base_tag_cmd = ["git", "tag", "base"] # Check if the 'git tag' command was successful try: - os.system(add_base_tag_cmd) + subprocess.run(add_base_tag_cmd, check=True) logger.info("Added 'base' tag.") except Exception as e: - logger.info("Failed to add 'base' tag.") + logger.error("Failed to add 'base' tag.") raise e From a9e0b67b7187474efe1272b3c294b6c1a3b75110 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 10 Jan 2024 14:13:48 +0800 Subject: [PATCH 042/101] update test_incremental_dev.py --- tests/metagpt/test_incremental_dev.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index e86e6d5ff..2ff33dff3 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -278,11 +278,11 @@ def check_or_create_base_tag(project_path): if has_base_tag: logger.info("Base tag exists") # Switch to the 'base' branch if it exists - stash_cmd = ["git", "stash"] - switch_to_base_branch_cmd = ["git", "checkout", "-f", "base"] try: - subprocess.run(stash_cmd, check=True) - subprocess.run(switch_to_base_branch_cmd, check=True) + status = subprocess.run(["git", "status", "-s"], capture_output=True, text=True).stdout.strip() + if status: + subprocess.run(["git", "clean", "-df"]) + subprocess.run(["git", "checkout", "-f", "base"], check=True) logger.info("Switched to base branch") except Exception as e: logger.error("Failed to switch to base branch") From 3848596b0fa8990ab74002e44e426283df41c190 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 11 Jan 2024 14:04:44 +0800 Subject: [PATCH 043/101] update prompt --- metagpt/actions/write_code_guideline_an.py | 154 ++++++++++++--------- metagpt/roles/engineer.py | 11 +- 2 files changed, 98 insertions(+), 67 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index 43645e80c..c08340cb7 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -10,70 +10,97 @@ import asyncio from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode -GUIDELINE = ActionNode( - key="Guideline", - expected_type=list[str], - instruction="Developing comprehensive and incremental development plans while providing detailed code guideline.", - example=[ - "Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero.", - "Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards.", - ], -) - -INCREMENTAL_CHANGE = ActionNode( - key="Incremental Change", +GUIDELINES_AND_INCREMENTAL_CHANGE = ActionNode( + key="Guidelines and Incremental Change", expected_type=str, - instruction="Write Incremental Change by making a code draft that how to implement incremental development " - "including detailed steps based on the context.", - example="""- calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Implement robust error handling for the division operation to mitigate potential issues related to division by zero. + instruction="Developing comprehensive and step-by-step incremental development guideline, and Write Incremental " + "Change by making a code draft that how to implement incremental development including detailed steps based on the " + "context. Note: Track incremental changes using mark of '+' or '-' for add/modify/delete code", + example=""" +1. Guideline for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python -## calculator.py class Calculator: - ... - def subtract_numbers(self, num1: int, num2: int) -> int: - return num1 - num2 - def multiply_numbers(self, num1: int, num2: int) -> int: - return num1 * num2 - def divide_numbers(self, num1: int, num2: int) -> float: - if num2 == 0: - raise ValueError('Cannot divide by zero') - return num1 / num2 + self.result = number1 + number2 + return self.result + +- def sub(self, number1, number2) -> float: ++ def subtract(self, number1: float, number2: float) -> float: ++ ''' ++ Subtracts the second number from the first and returns the result. ++ ++ Args: ++ number1 (float): The number to be subtracted from. ++ number2 (float): The number to subtract. ++ ++ Returns: ++ float: The difference of number1 and number2. ++ ''' ++ self.result = number1 - number2 ++ return self.result ++ + def multiply(self, number1: float, number2: float) -> float: +- pass ++ ''' ++ Multiplies two numbers and returns the result. ++ ++ Args: ++ number1 (float): The first number to multiply. ++ number2 (float): The second number to multiply. ++ ++ Returns: ++ float: The product of number1 and number2. ++ ''' ++ self.result = number1 * number2 ++ return self.result ++ + def divide(self, number1: float, number2: float) -> float: +- pass ++ ''' ++ ValueError: If the second number is zero. ++ ''' ++ if number2 == 0: ++ raise ValueError('Cannot divide by zero') ++ self.result = number1 / number2 ++ return self.result ++ +- def reset_result(self): ++ def clear(self): ++ if self.result != 0.0: ++ print("Result is not zero, clearing...") ++ else: ++ print("Result is already zero, no need to clear.") ++ + self.result = 0.0 ``` -- main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Ensure seamless integration with the overall application architecture and maintain consistency with coding standards. +2. Guideline for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards. ```python -## main.py -from flask import Flask, request, jsonify -from calculator import Calculator -app = Flask(__name__) -calculator = Calculator() -... -@app.route('/subtract_numbers', methods=['POST']) -def subtract_numbers(): - data = request.get_json() - num1 = data.get('num1', 0) - num2 = data.get('num2', 0) - result = calculator.subtract_numbers(num1, num2) - return jsonify({'result': result}), 200 -@app.route('/multiply_numbers', methods=['POST']) -def multiply_numbers(): - data = request.get_json() - num1 = data.get('num1', 0) - num2 = data.get('num2', 0) - result = calculator.multiply_numbers(num1, num2) - return jsonify({'result': result}), 200 -@app.route('/divide_numbers', methods=['POST']) -def divide_numbers(): - data = request.get_json() - num1 = data.get('num1', 1) - num2 = data.get('num2', 1) - try: - result = calculator.divide_numbers(num1, num2) - except ValueError as e: - return jsonify({'error': str(e)}), 400 - return jsonify({'result': result}), 200 -if __name__ == '__main__': - app.run() +def add_numbers(): + result = calculator.add_numbers(num1, num2) + return jsonify({'result': result}), 200 + +-# TODO: Implement subtraction, multiplication, and division operations ++@app.route('/subtract_numbers', methods=['POST']) ++def subtract_numbers(): ++ data = request.get_json() ++ num1 = data.get('num1', 0) ++ num2 = data.get('num2', 0) ++ result = calculator.subtract_numbers(num1, num2) ++ return jsonify({'result': result}), 200 ++ ++@app.route('/multiply_numbers', methods=['POST']) ++def multiply_numbers(): ++ data = request.get_json() ++ num1 = data.get('num1', 0) ++ num2 = data.get('num2', 0) ++ try: ++ result = calculator.divide_numbers(num1, num2) ++ except ValueError as e: ++ return jsonify({'error': str(e)}), 400 ++ return jsonify({'result': result}), 200 ++ + if __name__ == '__main__': + app.run() ```""", ) @@ -81,6 +108,9 @@ CODE_GUIDELINE_CONTEXT = """ ## New Requirements {requirement} +## PRD +{prd} + ## Design {design} @@ -403,22 +433,20 @@ Role: You are a professional engineer; The main goal is to complete incremental 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Merge Incremental Change: If there is any Incremental Change, you must merge it into the code file. +5. Follow Guidelines and Incremental Change: If there is any Incremental Change, you must merge it into the code file according to the guidelines. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. 9. Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file when rewriting "{filename} to be rewritten". """ -GUIDE_NODES = [INCREMENTAL_CHANGE] - -WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", GUIDE_NODES) +WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", [GUIDELINES_AND_INCREMENTAL_CHANGE]) class WriteCodeGuideline(Action): async def run(self, context): self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " - "meticulously craft comprehensive incremental development plans and deliver detailed Incremental Change" + "meticulously craft comprehensive incremental development guidelines and deliver detailed Incremental Change" return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index a1e93be40..e0b22ea5b 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -36,6 +36,7 @@ from metagpt.config import CONFIG from metagpt.const import ( CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, + PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -346,18 +347,20 @@ class Engineer(Role): logger.info("Writing code guideline..") requirement = str(self.rc.memory.get_by_role("Human")[0]) - # prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) + prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) - # prd = await prd_file_repo.get_all() - # prd = "\n".join([doc.content for doc in prd]) + prd = await prd_file_repo.get_all() + prd = "\n".join([doc.content for doc in prd]) design = await design_file_repo.get_all() design = "\n".join([doc.content for doc in design]) tasks = await task_file_repo.get_all() tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDELINE_CONTEXT.format(requirement=requirement, tasks=tasks, design=design, code=old_codes) + context = CODE_GUIDELINE_CONTEXT.format( + requirement=requirement, prd=prd, tasks=tasks, design=design, code=old_codes + ) node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.model_dump_json() From a363691c38345c7e9e0bdc4d7a83e077f37a2882 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 12 Jan 2024 17:33:15 +0800 Subject: [PATCH 044/101] 1. add guideline in context of write_code_review.py 2. update prompt --- metagpt/actions/design_api_an.py | 7 ++-- metagpt/actions/project_management_an.py | 8 ++--- metagpt/actions/write_code.py | 9 ++++- metagpt/actions/write_code_guideline_an.py | 22 +++++++----- metagpt/actions/write_code_review.py | 42 +++++++++++++++++----- metagpt/actions/write_prd_an.py | 2 +- metagpt/roles/engineer.py | 14 +++++--- 7 files changed, 73 insertions(+), 31 deletions(-) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 3e8265e95..02f20a133 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -49,9 +49,8 @@ FILE_LIST = ActionNode( REFINED_FILE_LIST = ActionNode( key="Refined File List", expected_type=List[str], - instruction="Update and expand the original file list, including only relative paths. " - "Ensure that the refined file list reflects the evolving structure of the project due to incremental development." - "Only output filename! Do not include comments in the list.", + instruction="Update and expand the original file list including only relative paths, up to 2 files can be added." + "Ensure that the refined file list reflects the evolving structure of the project due to incremental development.", example=["main.py", "game.py", "new_feature.py"], ) @@ -148,7 +147,7 @@ INC_NODES = [INCREMENTAL_IMPLEMENTATION_APPROACH, INCREMENTAL_DATA_STRUCTURES_AN REFINE_NODES = [ REFINED_IMPLEMENTATION_APPROACH, - FILE_LIST, + REFINED_FILE_LIST, REFINED_DATA_STRUCTURES_AND_INTERFACES, REFINED_PROGRAM_CALL_FLOW, ANYTHING_UNCLEAR, diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index a970c05a3..9b54698a1 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -75,16 +75,16 @@ INCREMENTAL_TASK_LIST = ActionNode( instruction="Break down the incremental development tasks into a prioritized list of filenames." "Organize the tasks based on dependency order, ensuring a systematic and efficient implementation." "Only output filename! Do not include comments in the list ", - example=["new_feature.py", "utils.py", "main.py"], + example=["new_feature.py", "main.py"], ) REFINED_TASK_LIST = ActionNode( key="Refined Task list", expected_type=List[str], - instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content. " + instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content, and consistent with Refined File List." "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" - " efficient development process. Only output filename! Do not include comments in the list", - example=["game.py", "utils.py", "new_feature.py", "main.py"], + " efficient development process. ", + example=["new_feature.py", "utils", "game.py", "main.py"], ) FULL_API_SPEC = ActionNode( diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 7555ce101..acc25627e 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -27,6 +27,7 @@ from metagpt.const import ( BUGFIX_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, + PRDS_FILE_REPO, REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, @@ -115,6 +116,11 @@ class WriteCode(Action): docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) + + prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) + prd = await prd_file_repo.get_all() + prd_json = json.loads("\n".join([doc.content for doc in prd])) + product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) guideline = kwargs.get("guideline", "") if bug_feedback: code_context = coding_context.code_doc.content @@ -125,7 +131,8 @@ class WriteCode(Action): if guideline: prompt = REFINED_CODE_TEMPLATE.format( - requirement=requirement_doc.content if requirement_doc else "", + user_requirement=requirement_doc.content if requirement_doc else "", + product_requirement_pool=str(product_requirement_pool), guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", tasks=coding_context.task_doc.content if coding_context.task_doc else "", diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index c08340cb7..9521e51e6 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -15,7 +15,8 @@ GUIDELINES_AND_INCREMENTAL_CHANGE = ActionNode( expected_type=str, instruction="Developing comprehensive and step-by-step incremental development guideline, and Write Incremental " "Change by making a code draft that how to implement incremental development including detailed steps based on the " - "context. Note: Track incremental changes using mark of '+' or '-' for add/modify/delete code", + "context. Note: Track incremental changes using mark of '+' or '-' for add/modify/delete code, and conforms to the " + "output format of git diff", example=""" 1. Guideline for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python @@ -105,11 +106,11 @@ def add_numbers(): ) CODE_GUIDELINE_CONTEXT = """ -## New Requirements -{requirement} +## User New Requirements +{user_requirement} -## PRD -{prd} +## Product Requirement Pool +{product_requirement_pool} ## Design {design} @@ -388,13 +389,16 @@ class Interface: REFINED_CODE_TEMPLATE = """ NOTICE -Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Incremental Change, ensuring the integration of new features. +Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Guidelines and Incremental Change, ensuring the integration of new features. # Context -## New Requirement -{requirement} +## User New Requirements +{user_requirement} -## Incremental Change +## Product Requirement Pool +{product_requirement_pool} + +## Guidelines and Incremental Change {guideline} ## Design diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index a8c913573..e3c5fd2ac 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -7,6 +7,7 @@ @Modified By: mashenquan, 2023/11/27. Following the think-act principle, solidify the task parameters when creating the WriteCode object, rather than passing them in when calling the run function. """ +import json from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential @@ -14,6 +15,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions import WriteCode from metagpt.actions.action import Action from metagpt.config import CONFIG +from metagpt.const import DOCS_FILE_REPO, PRDS_FILE_REPO, REQUIREMENT_FILENAME from metagpt.logs import logger from metagpt.schema import CodingContext from metagpt.utils.common import CodeParser @@ -138,17 +140,41 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.context.code_doc.content k = CONFIG.code_review_k_times or 1 + guideline = kwargs.get("guideline") + mode = "guide" if guideline else "normal" for i in range(k): format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename) task_content = self.context.task_doc.content if self.context.task_doc else "" - code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename) - context = "\n".join( - [ - "## System Design\n" + str(self.context.design_doc) + "\n", - "## Tasks\n" + task_content + "\n", - "## Code Files\n" + code_context + "\n", - ] - ) + code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename, mode=mode) + + if not guideline: + context = "\n".join( + [ + "## System Design\n" + str(self.context.design_doc) + "\n", + "## Tasks\n" + task_content + "\n", + "## Code Files\n" + code_context + "\n", + ] + ) + else: + docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) + requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) + user_requirement = requirement_doc.content if requirement_doc else "" + prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) + prd = await prd_file_repo.get_all() + prd_json = json.loads("\n".join([doc.content for doc in prd])) + product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) + + context = "\n".join( + [ + "## User New Requirements\n" + str(user_requirement) + "\n", + "## Product Requirement Pool\n" + str(product_requirement_pool) + "\n", + "## Guidelines and Incremental Change\n" + guideline + "\n", + "## System Design\n" + str(self.context.design_doc) + "\n", + "## Tasks\n" + task_content + "\n", + "## Code Files\n" + code_context + "\n", + ] + ) + context_prompt = PROMPT_TEMPLATE.format( context=context, code=iterative_code, diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 91c1b2837..2eaed7114 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -160,7 +160,7 @@ REQUIREMENT_POOL = ActionNode( REFINED_REQUIREMENT_POOL = ActionNode( key="Refined Requirement Pool", expected_type=List[List[str]], - instruction="List no less than 5 requirements with their priority (P0, P1, P2). " + instruction="List no less than 5 requirements with their priority (P0, P1, P2) from high to low. " "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index e0b22ea5b..e67963de5 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -117,7 +117,7 @@ class Engineer(Role): if review: action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) - coding_context = await action.run() + coding_context = await action.run(guideline=guideline) # Get dependencies if guideline: @@ -346,12 +346,14 @@ class Engineer(Role): async def _write_code_guideline(self): logger.info("Writing code guideline..") - requirement = str(self.rc.memory.get_by_role("Human")[0]) + user_requirement = str(self.rc.memory.get_by_role("Human")[0]) prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) prd = await prd_file_repo.get_all() - prd = "\n".join([doc.content for doc in prd]) + prd_json = json.loads("\n".join([doc.content for doc in prd])) + product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) + design = await design_file_repo.get_all() design = "\n".join([doc.content for doc in design]) tasks = await task_file_repo.get_all() @@ -359,7 +361,11 @@ class Engineer(Role): old_codes = await self.get_old_codes() context = CODE_GUIDELINE_CONTEXT.format( - requirement=requirement, prd=prd, tasks=tasks, design=design, code=old_codes + user_requirement=user_requirement, + product_requirement_pool=str(product_requirement_pool), + tasks=tasks, + design=design, + code=old_codes, ) node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.model_dump_json() From 684d10bb247dc06355b4f8fd51510b64449be9ca Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 12 Jan 2024 20:38:23 +0800 Subject: [PATCH 045/101] 1. Remove PRD when write code --- metagpt/actions/write_code.py | 12 +----------- metagpt/actions/write_code_guideline_an.py | 3 --- metagpt/actions/write_code_review.py | 2 +- metagpt/roles/engineer.py | 6 +++--- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index acc25627e..25e65ddc7 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -27,7 +27,6 @@ from metagpt.const import ( BUGFIX_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, - PRDS_FILE_REPO, REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, @@ -117,10 +116,6 @@ class WriteCode(Action): docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) - prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) - prd = await prd_file_repo.get_all() - prd_json = json.loads("\n".join([doc.content for doc in prd])) - product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) guideline = kwargs.get("guideline", "") if bug_feedback: code_context = coding_context.code_doc.content @@ -132,7 +127,6 @@ class WriteCode(Action): if guideline: prompt = REFINED_CODE_TEMPLATE.format( user_requirement=requirement_doc.content if requirement_doc else "", - product_requirement_pool=str(product_requirement_pool), guideline=guideline, design=coding_context.design_doc.content if coding_context.design_doc else "", tasks=coding_context.task_doc.content if coding_context.task_doc else "", @@ -188,11 +182,7 @@ class WriteCode(Action): else: doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 if not doc: - # if filename in old_files: - # doc = await old_file_repo.get(filename=filename) # 使用原始代码 - # else: - # continue - continue # 跳过 + continue codes.append(f"----- {filename}\n```{doc.content}```") else: diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index 9521e51e6..7b27fbe4c 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -395,9 +395,6 @@ Role: You are a professional engineer; The main goal is to complete incremental ## User New Requirements {user_requirement} -## Product Requirement Pool -{product_requirement_pool} - ## Guidelines and Incremental Change {guideline} diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index e3c5fd2ac..7fca9748c 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -161,7 +161,7 @@ class WriteCodeReview(Action): user_requirement = requirement_doc.content if requirement_doc else "" prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) prd = await prd_file_repo.get_all() - prd_json = json.loads("\n".join([doc.content for doc in prd])) + prd_json = json.loads(prd[0].content) product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) context = "\n".join( diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index e67963de5..1d1fed2b8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -350,10 +350,10 @@ class Engineer(Role): prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) - prd = await prd_file_repo.get_all() - prd_json = json.loads("\n".join([doc.content for doc in prd])) - product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) + prd = await prd_file_repo.get_all() + prd_json = json.loads(prd[0].content) + product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) design = await design_file_repo.get_all() design = "\n".join([doc.content for doc in design]) tasks = await task_file_repo.get_all() From b7adb1dc7d1353435cd2939bd4ae5435844b6ea8 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 12 Jan 2024 22:12:16 +0800 Subject: [PATCH 046/101] Update prompt and code comment in ActionNode --- metagpt/actions/design_api_an.py | 18 ++--- metagpt/actions/project_management_an.py | 15 ++-- metagpt/actions/write_code.py | 6 +- metagpt/actions/write_code_guideline_an.py | 4 +- metagpt/actions/write_prd_an.py | 13 ++-- tests/metagpt/test_incremental_dev.py | 82 ++++++++++++++++++++++ 6 files changed, 109 insertions(+), 29 deletions(-) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 02f20a133..ee1941350 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -22,7 +22,7 @@ INCREMENTAL_IMPLEMENTATION_APPROACH = ActionNode( key="Incremental Implementation approach", expected_type=str, instruction="Analyze the challenging aspects of the requirements and select a suitable open-source framework. " - "Outline the incremental steps involved in the implementation process with a list of detailed strategies.", + "Outline the incremental steps involved in the implementation process with the detailed strategies.", example="we will ...", ) @@ -30,8 +30,8 @@ REFINED_IMPLEMENTATION_APPROACH = ActionNode( key="Refined Implementation Approach", expected_type=str, instruction="Update and extend the original implementation approach to reflect the evolving challenges and " - "requirements due to incremental development. Provide detailed strategies for incremental steps in the " - "implementation process. Retain any content unrelated to incremental development for coherence and clarity.", + "requirements due to incremental development. Outline the steps involved in the implementation process with the " + "detailed strategies.", example="We will refine ...", ) @@ -49,8 +49,8 @@ FILE_LIST = ActionNode( REFINED_FILE_LIST = ActionNode( key="Refined File List", expected_type=List[str], - instruction="Update and expand the original file list including only relative paths, up to 2 files can be added." - "Ensure that the refined file list reflects the evolving structure of the project due to incremental development.", + instruction="Update and expand the original file list including only relative paths. Up to 2 files can be added." + "Ensure that the refined file list reflects the evolving structure of the project.", example=["main.py", "game.py", "new_feature.py"], ) @@ -78,9 +78,8 @@ REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( expected_type=str, instruction="Update and extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Delineate additional " - "relationships between classes, ensuring clarity and adherence to PEP8 standards. Further enhance the " - "detail in data structures for a comprehensive API design that seamlessly integrates with the evolving structure." - "Retain any content unrelated to incremental development for coherence and clarity.", + "relationships between classes, ensuring clarity and adherence to PEP8 standards." + "Retain content that is not related to incremental development but important for consistency and clarity.", example=MMC1_REFINE, ) @@ -97,7 +96,8 @@ REFINED_PROGRAM_CALL_FLOW = ActionNode( expected_type=str, instruction="Extend the existing sequenceDiagram code syntax with detailed information, accurately covering the" "CRUD and initialization of each object. Ensure correct syntax usage and reflect the incremental changes introduced" - "in the classes and API defined above.Retain content unrelated to incremental development for coherence and clarity", + "in the classes and API defined above. " + "Retain content that is not related to incremental development but important for consistency and clarity.", example=MMC2_REFINE, ) diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 9b54698a1..559c5ef8e 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -52,8 +52,7 @@ REFINED_LOGIC_ANALYSIS = ActionNode( expected_type=List[List[str]], instruction="Review and refine the logic analysis by merging the Legacy Content and Incremental Content. " "Provide a comprehensive list of files with classes/methods/functions to be implemented or modified incrementally. " - "Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports." - "Retain any content unrelated to incremental development for coherence and clarity.", + "Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports.", example=[ ["game.py", "Contains Game class and ... functions"], ["main.py", "Contains main function, from game import Game"], @@ -81,9 +80,9 @@ INCREMENTAL_TASK_LIST = ActionNode( REFINED_TASK_LIST = ActionNode( key="Refined Task list", expected_type=List[str], - instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content, and consistent with Refined File List." - "Ensure that tasks are organized in a logical and prioritized order, considering dependencies for a streamlined and" - " efficient development process. ", + instruction="Review and refine the combined task list after the merger of Legacy Content and Incremental Content, " + "and consistent with Refined File List. Ensure that tasks are organized in a logical and prioritized order, " + "considering dependencies for a streamlined and efficient development process. ", example=["new_feature.py", "utils", "game.py", "main.py"], ) @@ -113,9 +112,9 @@ INCREMENTAL_SHARED_KNOWLEDGE = ActionNode( REFINED_SHARED_KNOWLEDGE = ActionNode( key="Refined Shared Knowledge", expected_type=str, - instruction="Update and expand shared knowledge to reflect any new elements introduced during incremental " - "development. This includes common utility functions, configuration variables, or any information vital for team " - "collaboration. Retain any content unrelated to incremental development for coherence and clarity.", + instruction="Update and expand shared knowledge to reflect any new elements introduced. This includes common " + "utility functions, configuration variables for team collaboration. Retain content that is not related to " + "incremental development but important for consistency and clarity.", example="`new_module.py` enhances shared utility functions for improved code reusability and collaboration.", ) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 25e65ddc7..f92b72f7f 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -174,13 +174,15 @@ class WriteCode(Action): for filename in union_files_list: if filename == exclude: if filename in old_files and filename != "main.py": - doc = await old_file_repo.get(filename=filename) # 使用原始代码 + # Use legacy code + doc = await old_file_repo.get(filename=filename) else: continue codes.insert(0, f"-----Now, {filename} to be rewritten\n```{doc.content}```\n=====") else: - doc = await src_file_repo.get(filename=filename) # 使用先前生成的代码 + # Use new code + doc = await src_file_repo.get(filename=filename) if not doc: continue codes.append(f"----- {filename}\n```{doc.content}```") diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index 7b27fbe4c..cc532ed0f 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -434,11 +434,11 @@ Role: You are a professional engineer; The main goal is to complete incremental 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Follow Guidelines and Incremental Change: If there is any Incremental Change, you must merge it into the code file according to the guidelines. +5. Follow Guidelines and Incremental Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the guidelines. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -9. Attention: If Legacy Code files contain "{filename} to be rewritten", you are required to merge the Incremental Change into the {filename} file when rewriting "{filename} to be rewritten". +9. Attention: Retain content that is not related to incremental development but important for consistency and clarity.". """ WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", [GUIDELINES_AND_INCREMENTAL_CHANGE]) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 2eaed7114..7004bfffc 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -34,8 +34,7 @@ ORIGINAL_REQUIREMENTS = ActionNode( REFINED_REQUIREMENTS = ActionNode( key="Refined Requirements", expected_type=str, - instruction="Update and expand the original user's requirements to reflect the evolving needs of the project." - "Retain any content unrelated to incremental development", + instruction="Place the New user's requirements here.", example="Create a 2048 game with a new feature that ...", ) @@ -57,8 +56,7 @@ REFINED_PRODUCT_GOALS = ActionNode( key="Refined Product Goals", expected_type=List[str], instruction="Update and expand the original product goals to reflect the evolving needs due to incremental " - "development.Ensure that the refined goals align with the current project direction and contribute to its success." - "Retain any content unrelated to incremental development", + "development.Ensure that the refined goals align with the current project direction and contribute to its success.", example=[ "Enhance user engagement through new features", "Optimize performance for scalability", @@ -83,8 +81,7 @@ REFINED_USER_STORIES = ActionNode( key="Refined User Stories", expected_type=List[str], instruction="Update and expand the original scenario-based user stories to reflect the evolving needs due to " - "incremental development, no less than 5. Ensure that the refined user stories capture incremental features and " - "improvements. Retain any content unrelated to incremental development", + "incremental development. Ensure that the refined user stories capture incremental features and improvements. ", example=[ "As a player, I want to choose difficulty levels to challenge my skills", "As a player, I want a visually appealing score display after each game for a better gaming experience", @@ -160,8 +157,8 @@ REQUIREMENT_POOL = ActionNode( REFINED_REQUIREMENT_POOL = ActionNode( key="Refined Requirement Pool", expected_type=List[List[str]], - instruction="List no less than 5 requirements with their priority (P0, P1, P2) from high to low. " - "Cover both legacy content and incremental content. Retain any content unrelated to incremental development", + instruction="List down the top 5 to 7 requirements with their priority (P0, P1, P2). " + "Cover both legacy content and incremental content. Retain content unrelated to incremental development", example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 2ff33dff3..223b0fb10 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -35,6 +35,7 @@ def test_refined_simple_calculator(): assert False else: tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + # After running, there will be new commit if tag == "base": assert False else: @@ -261,6 +262,87 @@ def test_refined_pygame_2048_3(): raise e +def test_refined_snake_game_1(): + project_path = f"{DATA_PATH}/snake_game" + check_or_create_base_tag(project_path) + + args = [ + "Incremental Idea Gradually increase the speed of the snake as the game progresses. In the current version of the game, the snake’s speed remains constant throughout the gameplay. Implement a feature where the snake’s speed gradually increases over time, making the game more challenging and intense as the player progresses.", + "--inc", + "--project-path", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_1"], check=True) + except subprocess.CalledProcessError as e: + raise e + + +def test_refined_snake_game_2(): + project_path = f"{DATA_PATH}/snake_game" + check_or_create_base_tag(project_path) + + args = [ + "Introduce power-ups and obstacles to the game. The current version of the game only involves eating food and growing the snake. Add new elements such as power-ups that can enhance the snake’s speed or make it invincible for a short duration. At the same time, introduce obstacles like walls or enemies that the snake must avoid or overcome to continue growing.", + "--inc", + "--project-path", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine_2"], check=True) + except subprocess.CalledProcessError as e: + raise e + + +def test_refined_gomoku(): + project_path = f"{DATA_PATH}/Gomoku" + check_or_create_base_tag(project_path) + + args = [ + "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", + "--inc", + "--project-path", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine"], check=True) + except subprocess.CalledProcessError as e: + raise e + + def check_or_create_base_tag(project_path): # Change the current working directory to the specified project path os.chdir(project_path) From d07760572073981fcbaefae190015dac2c4403ea Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Sat, 13 Jan 2024 08:51:35 +0800 Subject: [PATCH 047/101] Update product_requirement_pool to product_requirement_pools --- metagpt/actions/write_code_guideline_an.py | 2 +- metagpt/actions/write_code_review.py | 16 +++++++++++----- metagpt/roles/engineer.py | 22 +++++++++++++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index cc532ed0f..528b4e8f3 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -110,7 +110,7 @@ CODE_GUIDELINE_CONTEXT = """ {user_requirement} ## Product Requirement Pool -{product_requirement_pool} +{product_requirement_pools} ## Design {design} diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 7fca9748c..444af51d9 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -159,15 +159,21 @@ class WriteCodeReview(Action): docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) user_requirement = requirement_doc.content if requirement_doc else "" - prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) - prd = await prd_file_repo.get_all() - prd_json = json.loads(prd[0].content) - product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) + prd = await CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO).get_all() + + contents = [] + for doc in prd: + prd_json = json.loads(doc.content) + product_requirement_pool = prd_json.get( + "Requirement Pool", prd_json.get("Refined Requirement Pool") + ) + contents.append(str(product_requirement_pool)) + product_requirement_pools = "\n".join(contents) context = "\n".join( [ "## User New Requirements\n" + str(user_requirement) + "\n", - "## Product Requirement Pool\n" + str(product_requirement_pool) + "\n", + "## Product Requirement Pool\n" + product_requirement_pools + "\n", "## Guidelines and Incremental Change\n" + guideline + "\n", "## System Design\n" + str(self.context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 1d1fed2b8..772cf0944 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -347,22 +347,26 @@ class Engineer(Role): logger.info("Writing code guideline..") user_requirement = str(self.rc.memory.get_by_role("Human")[0]) - prd_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) - design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) - task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) + contents = [] + prd = await CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO).get_all() + for doc in prd: + prd_json = json.loads(doc.content) + product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) + contents.append(str(product_requirement_pool)) - prd = await prd_file_repo.get_all() - prd_json = json.loads(prd[0].content) - product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) - design = await design_file_repo.get_all() + product_requirement_pools = "\n".join(contents) + + design = await CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO).get_all() design = "\n".join([doc.content for doc in design]) - tasks = await task_file_repo.get_all() + + tasks = await CONFIG.git_repo.new_file_repository(TASK_FILE_REPO).get_all() tasks = "\n".join([doc.content for doc in tasks]) + old_codes = await self.get_old_codes() context = CODE_GUIDELINE_CONTEXT.format( user_requirement=user_requirement, - product_requirement_pool=str(product_requirement_pool), + product_requirement_pools=product_requirement_pools, tasks=tasks, design=design, code=old_codes, From 28c700341620eb16179201d455f1802028bac1f2 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Sat, 13 Jan 2024 09:40:02 +0800 Subject: [PATCH 048/101] Add 2 code example to data --- data/Gomoku.zip | Bin 0 -> 94089 bytes data/snake_game.zip | Bin 0 -> 119511 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/Gomoku.zip create mode 100644 data/snake_game.zip diff --git a/data/Gomoku.zip b/data/Gomoku.zip new file mode 100644 index 0000000000000000000000000000000000000000..d6c6b8d16148047dadbc180ad3974b612c48dac5 GIT binary patch literal 94089 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 F{|Do68TkMJ literal 0 HcmV?d00001 diff --git a/data/snake_game.zip b/data/snake_game.zip new file mode 100644 index 0000000000000000000000000000000000000000..d737ec0d3d862fb7211da4c0a523312fd8b91873 GIT binary patch literal 119511 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�v;=JNQFLwp)B2zTelPChT8Mx8E=s$EA5%7w&h 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¥3C)2XL(kJ+9LpzVY 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 z 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 literal 0 HcmV?d00001 From 1b29175116f7c21fde080f7a5f3a10af18ea7586 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Sat, 13 Jan 2024 10:02:54 +0800 Subject: [PATCH 049/101] Add 2 code example and modify 1 code example to data --- data/Gomoku.zip | Bin 0 -> 94089 bytes data/simple_add_calculator.zip | Bin 8105 -> 56538 bytes data/snake_game.zip | Bin 0 -> 119511 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/Gomoku.zip create mode 100644 data/snake_game.zip diff --git a/data/Gomoku.zip b/data/Gomoku.zip new file mode 100644 index 0000000000000000000000000000000000000000..d6c6b8d16148047dadbc180ad3974b612c48dac5 GIT binary patch literal 94089 zcmbTe1z22LvNnu+umHg=xHj$(+})j~ad&rj2=4Cg4nYD02=4Cg!3l()%$=Eg=FFV$ zKL7Od6pie~UiGe3Rd3Z^)e6#(P#9ofU~phx&FD0$HEBt`-hRq~00YB$dl$F0wzYI- zWH2(eGqy1@wgH(NJ2F@}+S*j8Ne#<0#Dr5<(=kYnMaxUlkIPI%hBGk(n3(`j5G)^s zK_9y+x1V{*X@kv5w*&GM4Nol$oiTDPq|hT9?5TvXMS`k`->+ReUN=aC3>|*Vuq50f z&`7fQV^fl>#K&vIrzUD-N;yVOzjrjZl{BXqtNJGU{d++)c@vnz-=_@rw<*8P@}DoH zx9|QjWm9t}#(!K12ZmTotMTM?cCY_-MZlY&|LIDQt&NGfscf_$f*$}0YT}I6D}d;d ze4FH0Ex`PvB<6EhR~TU^PO80PKStn|!a=zUWs}Tp0GH)aNOdep9q~zoSol}8ewn+E zQ_&(wN4#p~mnP*|vJY9oqv!e~+UEVi__jCyocRAtha(b}NZ@T6l5g_iPt!3nb_6+? z+c}xr+8~aNN`IG?Riu)l|He2@H_TYxs3svhOs7aS{EbdcV)VPDs=^!mattGMRKwpV zl+~$*)xnOyMoLJ{r*|O!dC&g=Ytcq-k^1dkzXkPA_mmJ76afL)jF^mASlC(FS-H5_ z*-SW@*_qflnL!|c2^)(c2OBG=G02$N1mPdZf`O^NZJW3M^v~Cyku|fmwRHT`N(9S4 zz<&S-16zL+%D0h!T(keV9%yH0#QdiSZIw52$)M!N#@_TkjRD71)7u8%Qpw)UY? zN@|l-1kd?+B)HNTD{;c;<)K^*DmTL^a>RPGSS%Rkv9*F+O*D(|j!5!_p;GR&y#cU;2X zlobz<(z=e%^(X=!SqIdF0L5I!OIhe<-goSifof4-4Uls?5QX|3sAKzoeOdGt5I8=U zeJ%ZPi};VV_@8Zc^e}>Y|Awm{BpBGo|A;Nf*4oV^ z>AA@WVwt&?!c|g0q8WTU(rb}b7bm%Z{S1< z5`Qwu06hn_{U)mJSVMN5U!DQCidTjB03@lkH74W7>`vA0J|<{?$hy*t_oCqbc$(BS zO1jC^L#q;)*xgsBgzU>1$Taa;0ysT_7;(_>`ZW@t(Z(P`ed3Ohock1>8c`eM8mic# z*GJ|vmXo`pRhRL%ht1G!2hBUnSX(s z_A}BaJ9#gRKr@tc5E@i0$h7p@pzuSS6Svo5XHmRn7jpV}H!RNR=LEKQTFo;DZM^2< zwJ9q?MYVo+-~Ov-RNqfs(J%QM|GIB3ym;O5V8Os#-VB)bKkge7M{8Rfb0=E|de`5E zX$`dbyIoePuh`&mp?fEH=TD;%Q^&PuosJ4cbg@X7m^5IMi7{kAZ%2J{Rz}~!TB2A| zgNof>X(|6UKj455+u?3EYovNLdxr7&ps2=^A)7Q#V?OHA;ewvoLURxL)rGD`%jddv zGWQrcbtp@I+hE`Eg6%U;{3XIzRZ0Wf5uTx?{Kw`ysTdmnCDRVlPZ>nc%bb)Z&Bc&g*;{n`VBJkmTx0$dt({3oI5Wb;fA?oG=R z8LR~cyY3^yM7c_X99wBOwX{m2LiF^sX`2`qE-NP}q_%j|8R5z}QsNE9qjV82BxmxN zMTI9cmuI`nyFdorJ&daqfS)iDsL^5dNVPqTL7~B|$$+HRF6gtwaYfjx^NJEXhUdQC zdKbbm8FCFxgqmveoT4fw!tYVOHBnt;cI#5Za9Nd5!Cihw>9so)6bHAV8z3hn3HoC4 ztVr7LCZm2jP`cloIgxlpUWZMx6GO#b|Bm6!=+~kL!hB z7_z%{glpcjF!>>i`k?dX)E;z@)NVTzrW+2 z(bxSaR{S_TtJp=Yd*1uzJ42Fd=@^aDca?I|qKhhG+G8%8fFcz_jx)79GBTpZii!~p z1dB_H1Aw;WY5F4SROgjd7 zAVWWglpKPw=Zk$@!}JQ-%&sHGyh1#OLmG5Pb!TE;@bl<#`vpG z4aFFD_L>kaUM^S$XzcpBgKc60o{dmHLhB$>GQlY1@R)j4i*ht*HaFbFy6-btG*+-) zTh@i3JXTlCkt3E?-Wi2jd;3i*FK@@Nbb`JO%;n5GUxyNFb|V{MaB>V-Ei~bp{)qBV zvh!Q{8N}!F9MU$CTv}v`ff#Un4i?SWpB$1mi5Px~xnLV9KQof3EDlS7PDV?a%3n?| z;BA9E294?V!_I|Zopgw(0!Cxl;LSjAJey;o_dlESi_DBV;&d`68LADvonR8b)uK1| zEACKOtsvEI*19EF;cFkL`xhh9cvS9FRmO>xsUxd26U;ox zb%3c!9}rz0V#rZpEh06|bx>eulMtXxP!abx2<>B+tYdk;E{a$84GtmXWjR>n+%jZ$ zJ{(*yQsQqu{G2-hcWTK-`O;%oI#w$L=d*U!qOHQR>jan##o-YM7%wo9-&ZpsbmWS!wiPMVXVryIjf8MtsJA!?M)?OK75nWk2-;Frqfq|(5nT)hjMkA$LTW$f2y#*ZX3Wk<*7y>Crw{ z0NKjLjf$xyi!OFao%e|{2ez~GgVH)Pp{$pb9dn(^?w8$b)5P^nA`v^rvL9zf<`giG z06pWW#3#NV$1(iEP%-c<5?=||C85(=dHM6X(!)M8H!$(q9&TDWzSb^2K}T0*>2Eld zqrHUakKNT-%5aXo^Mry5=cb+{z5Bvm=RO^3k2-V04yQ6c{P44cDw+djPA68J2N8w0 zasHdGjlh$y;N8%G?45wZvo1%(896bSwTXbKo>rtgZ*XgNQ0Qoo&7UzN(<>I<~qZ z>`KjWpH6q^?Rd$*S?vMvLbKi7n+c_aO*)6aITJ#x7jR-Tct?{#)HcBv=4PXNXyZ!i z24+h3D< z->abSQQ}rT6+m$h4%MK43BR=3rLdUWblE)bmS6`nMA1#;ys*RLKKA6~V50y}3G(ty zx#{UWXu;6n;ru$+L}P*GB?-zRTs?V=U-+q4{yNdtec(SSU0=8%kX|$PX+6PMdh4C@ zPw2oFuIh=@y!V9d;+<}XeJJjz?*!JS7pfNY9Yxo)Ey9jsIll6(6=`=cJU>#hB0hR_ zE4Aa58E?!}d#OCxPKXt36#CZD7}g_`{erMScX{iwI(M^`s(%L%a+9Of~k)yR#z zH??aL+lgXlyZj3MuQ4_q>Aol8Ew*;O3E6*)v39nOPV~-pMnEUyzXwhR!vywjSoKlO)!w3%?EHVjc?5+w*t6PQrEZ6~P6 zP>3XyXTFES7D@iW`(uP5M@XjzE%U_+ocf5G$l8-~HzMbZ22ejf>!}Ght=aeejO%;W zq+1(C7A_8NIIqhfafj#fag7t%6oXE z;YXzPc~gFB7i~9lZ(Ex(Z2wZv366`oPpw!O^f}zhGbvrNv^SXZYybYq;}m<%-HO$B zlz&C@qJbGH>W#5m!u%C$|BdGVF!#SBX{`dZU13Lkv3iN9cu)4ufLl_)#2KuAv=&Sd zwi5g!q(tH8(={7Dug*7c&x)78|rdSPC7sGXB`-0Ub!-)V(S zrs-Zg!njs9f7g7dR}FS~f%|iG;X?Ps>w-*?t#$@_1{ZUs(@bsNt$#6I#$X5anRWTF z`!sN-S9*P16Nl;LvjbC8$(+gVQZSvV`!q{401c$aXEH@#C6vuWRq|*I9=s zkA5TC-8dv%Xeb?-?gAAel5v(HQ~A>yTny!R6afZe;9r)n)kF;z0crya%?>!d0NE5LzYkFy5puG&I_N2Ov?{4O6C|cTd~* znhSeFb2}gt>dcR#a*$V9A=xMXUI=^>k>~IpPMR?EX30tW9&|mB%TkbzxklA2*Ma{BttMWGnjU%u%#$^pJx~`|^0AHc& zIsVmtCQet=5wjHf7K)F)r-Zc$6m;1&IFc1;KrEa8 z2_`Gr$)(b(MC1O5lDIslTK@XN6q6dkxwP3I?4{TaxQ+R$)p?}H-KV$-F za(@7-HykF?NSIdjxL(XpX_|IV*RiK%m6{cGn?AX)d+KrOaU&9bYUcQ2Z)V1jyf7(m zRKD=n%a6zX`0kl*GF83sD2^ScB!%;rpA#s)&uY9uoBagzb)m~ZP~Pk0d6T(BoI!HV zz&W{C)oeaDQm9W08s6^Q_fzjh%G3Y_uMzv_`A~b@8yl(#%yiuVEQMM_Kua84+5YLT)be&13b}NJz3IIyKv8o zdp_aiS#t-DJ{=LbTv^1<+DFh7_= zX9GK0*Ut&^Vn??{%}VFC=Ehmx9ad9OFltcAt88u$kcceuRv|)f;hL@Khee+7Qb}i^ zhq9^&B!1MWti~39CPj+ zGk9z5fG61N(yoz-yn@F^a$?TKP_Xez8j)Dmel?fHXaTY~9Es2!AhTo^qAO*`h`l(t zyFPoKI`vsSJp|u!V&){Yo_#H0o7n&GuPC0qX9pa<)gfo0|0*GUL(vZSPq+Aof<~~) zl+p?S)yt-P+m1OX?i<6IcA8U&k`#-wRA`|sPJm$!2vggE90#F)=$?$rJU5L!0apC( z{OrtTbxk^1h@$w=iAR_7v)>mt)fBj3r4|DvdJ9A~!K%7_++HlWvT_<=o(gU0BzAXS zYHdj;9aG>QqzG+09%00$-eO+?Sq-Ynd5xnYH;ql+=3o|Tx3M*jcw9xj%Dw*-kQvD$ zDBdz1jKm7@7!OcBMLh`CKzjPg)VaXY7&XW~!$3^gC9jJ{NDiWngZLny6$p#0BzYI6 z1?3mQvXnyNvOJ8%eeCsp__I#7XS z@k@6G2sL%ipsYv!Ety>|z&@5?FPZ^2X=hwx5~NlA^Hf|P`F^=|+z_nH>@@dIR=D;1 zOrr=w<@MHGvvi@4KdaT*LOx*^3>PXMU9LP^wNe9CoQ9t@Ly;DiidT3?spwK8lDdH27j|d{jh3x(>-iME*D4_tt{uABpt%pTf9{eCgR$k|y zGK>OIp0GBX1PgX@VPKhAy7Ux86W!yR;m0x%WH+)7d;(!88xycsKt^{pKUaW&)N!}= zqCNZp;|mLYL%JSBsU*w$_|Npz)5e}s%;r9gqsP_SA4cItZIIr%{TtHGe4AUt-x8I0bW!I1o=+p%+YH2XV)2v*s!Tm>Nc@V{be zoi8>`>iBk6H3lnU(?&55jt~yAXNrV(al4rC$nx9`$=5q}tze&SacU;>dq0txK5n;8 z-*HlrBwsE=@Q?Ta;6~a!;hGkNgAO;ryKHg{l$ZmMiD@|~e1ic|g1`pw*5pA0$hr3z zSus#U9MIp`^I-`Dhrycqp0We-g)SF6!cg<0K%4;)sI*pD;&FG>G}ax7N+ioTbV+UI z)ES)LcP|x(z!G8*$TUnxa}L*#qz z@a4keF-P4!2+949I`NFcZ(ZB5is`Gz(d3$*I86;JQ;KWLx8BnXOrHxqP2xli#en!Q z1;fQm*w##__5nLa=!@*r%Bci!@++F`8>VSs3{f9d!;L+gnzC%ZWxF3lKK5{Qlm&GL z3buudv)1Va4#6HXR)~&VEEWu@thOS1APKsJXUF?eD(UHu zMs53LT*poS_3#~zX)tQ1a7slWgKZU!?k&NI#0+8`iC^Rn3{lLPgwSiiA@Jp~(2IQZ zZWMQ(@q&Su+!)AA%hkQ!FK4lt#mo z5rIZI^HOVy782^vFM2rAh-p;s@>MO$D45mEEX~TM4WUV>yI-rZKS&dXAr7%|<_~|JQYos_idbF4E>ku)`-tRrn{`7Wn+DWlp=+etX7e&>TVEl6 zmqd}4;v${VME4Bxx#+BDG?C}qDP_mI!~L9s(^x$>*c1Lvb8b*H$sFI!AYKl8a0ikA<+d1#iFzl zOVeV05;CxAdc_re(esg`&5UJh%CDsE8in|bmO$Sa*>|Z6y?X0>ye^}H= zYNDOSA~j7F0VI&I$9*yg=Z4A59?y==U%#^d`ua;JIN$?r7|6n01fRsk_A_A8K`B|u zh)qF~cVrEe>nDr(oUJ0%28a_N_W;Gc^SGBEFQWYVLVePzz;MqTCL&k=eJ;4y^8=t~ z5Zdy`#{9%j&nJl3?WX(`-z?=(F^UG)9G-MDYM+KCYGVRuOOplN#M8n6hVU2el6xD|>gZLjylLsu* zD&+Ai&39zK8Jzl^^~5ar`R-}GbAT>`9zpURmLm}3?k>1-632sl#yFDvMzvfXQ7~qR zgNL}Zjr{ac)o2CJq?KzKUhaKltg{b{z7Ra$B%g?-=L#^#H@NHN6lhyf4T4?Jzwhmg zaf0(LImflG`mA?mJ-d?9hAXJIzCJrgP)Ng-C=D!PjO9M+dZh*OZ7>V9iSdz53c!cd z6>LD6Qu@(<@@yC}tB5$?O_@ZQSF7(cQAzT7Vcis9Q0F;>viI%>knE;&K; z!@0jqT9FH92aU;&!TMB_XYiG%Rl#>7xqh+TnD1LRk7k?D_xx_dfo<2eHK&iaX1jY} z@B1%w@R(T-7heacGdo2R2_eb~_B7`1yOcgA-Hh%HoZMgNVswFbm22dGZbBiFoEn%uQKqVc1m_wfKL9J#kz>v+v#qo z808TFqld`FU?Xl1vFap~W%o-94^~-+Tlc;vvyt0bkjjWI8qMQGOXiZ;bnIC)Kd&D< z)s514IQ!7!iX?#<;I-%@yy4iNe^PN059a_Dco4IYufb%H=o-#8PlfxlwtmeWu1dU; z60V1-PEPtf3|U1ZYtIyRis`Z>rA{jZGfxfAP|aW~niG;);+Ajj0CDOeG2(mdEKhds zH_6Y16;hGVld&mNn}M(|+E7Fp{$MOtepOGbJ&tnjcAq8CiL+P^G^NY1 zMMY}jkOt-sSf-ug0g!t&!)lO5+;rnU9$LzrXfx+x%>2%Vhnja z;YIeYFc-#okzR%r#+g6_dMgGYK;L=tU6u|T{TQS5TetuI&G-0KzMBP80CjWMQqa^h z+=OT1Zqh0H2d1!=<5WdSV#D~HPjxn`%MsT2*K6@af175V4)2>@RYWE4{NBNOX1ZF3(d;OYr z;a1<%S2Z9dR+CNDy$ha|g82ll#S!Rs;lEoDTl^E#KtS|?6Tvs;(`*}KZvD_K!x#Z< zT?@edL~kMWQ5n>S<8f?isjxlN{gbcQ214I9Sh4HZ%b!YY@n0X3CEqe~;x~%?7sc1N zT+-p~e1o~mKRAN3!l+z70F`%0pQ#ni`;+#_XBvqx$eU?3(k>EJoA%GL?|CBDV1=G} zTt*7u?3d;|Ob1z3xbdnJ3AKi`Dua36afj8@z)h#Vvr?-V>BUJj6kgN{7*@2d3M$KF zUPk7nO}de77->;gpm?Vlmj4hOlF--7t;lnllwbzCDP@#VcorO_S5W+6WEEwu;*{Kc zg6wi9KF306Y15L=45ARGa9{+_ewtAzOq#Jw^zkviqLyV-RrV#FS&3&Ga!>_Xd_J$H zcPCV`J(Dq;Fr~qrJ54s_zF~lM@4NOSozgI7B_bS=u@~{XJueR+Dvnt9kN?e}VYSSGC5pXCEV%5VO;sfd9l)#bYgxi>eb*D(Tl7 zYSc9Iy50gNG81hwoj~t!58PO3LCo&#tZZt#Su?aJwTa3V=ZfMBA76&uN}2(o_C|2B zGsndVITsR^mRWsu6ep|fory*^jOx~=mjiR$FwX(MRL`gq4Jf_GSLISFY*C3;;>*;{ ziqsVsBVPHe(tE8K1_4t5Z=UW@FaAk7CMuuB7g=HVvSG@DpmIgOa1jcNTWSdmUM?Xg zX`;r0GWz(k21zG70=YDAgD4f=WY7Q(Mx@MjE^cXKf6*03P;=&3)`{87tTst*6t@q2 z=DlUeuGcB-4&*R}<+TkXu7&YwfAA$5fLVDi9C$3*vl!>BFZ?$%~N<7P7Ahgl|%qTplAAMf!98f$q0j^GJOx<=%;hp&=nVh z@EEcue?`4KiLAr|@+laiq-kTUIM^4oc}JE5JWbA=kv#_~qJgnFJ5*RZ)qHOG2({No zM{0R|hWup4e(T5m!uv>#382fo6m0LQ6t560Z;JWZNnVh)V?a`&R<;9Mk)lY=f3;^e zFLcUUxa!<)2fGHjM2h<%XAG(uFJpE*s=;L;GU{gkCSvFsQYD!;AJ&RGifvARm+k|h zAv)cP87`!~3wGULJZwbEhiA($e&VjU<%xF7HOdzOeR^Ek?NgoSE;qsQhV8QtHD7<+ zRFL~PaH&W7w)PPb2s?fb^vX37fdqD@#K6j=%VrPl{HizJcfhF(k_IPu7g`w7>0$>t zQ&i&q8r8@`i}ha4WiRj`VEpTMf?voEiAAa3vsT71_%bXQby&?`fq`tQq{^a-@3vpo z##-VIEz++P9~hv*2k z83RZ4hf($6b+NhxinZ;J81f9g0+Pgxr~ZT2tBC^xpRl%7mL$C3Pc`!zHHwt%SkXOJ zyBe1(7YQ_;-wC{zo(r3^81{+CCgR+V0kcTV+mXw!PZ_p!XRqMBo)cG8^#HY2@Xu?= zqGb)R(6NcP&M1~?c5do6K0EL37kctJOEjWqTO~)TO>8bHt5&d#43*lm5eWd1U$mzT zmAG{)(C%ATbEyY*9}9o9C9ED=tvXF0nXR16T9R-_?cxF~MOet9ene~hh}Vd)w7-9m zzxS=Y@a^uGyCzV9kk&2d-ch-!ur$3cgL0`695s&$jHcCu0 zqu+Oo%=-p}mX1(rt3LX%hu2VMUVqF_ijm2uE_;~fFJ7l6dicW1TfFIX>f18^gVpwE z5C49Cuv@a3k;r+%HA;4cZ+K+$72;o=<~Ku z^RI{s!>>)iVadD3XWVuuf+v8%;&e$qoLO3Z>$gJ6Y5O_n*Ps0H@$|zsnYm05K@{$; z8*ARK;zV6(GQpXLbesq=hgoF5g6WgFN@U7lrzJ^}L!$D{tK(s{Bq@rRzhC0uurDNH zSqSRj>cMfv=52-SaUqwG2Ri~wH{n0T@@m!Zk`y)paOw=ZwLXdD?64+T^-7wavMN1( zXC%|-0k|ThFJWlNP&7iahJ+>9@s!n?M)b`&z(@d3zuF|q4B$t8^RBIp=Y|0^*>L(n z=JQmTmaB}Xk&Q(cI}geZM(V0BkWQG73?KCt#{r@%)yFfI$DgS@WYW zejx!1NslfyQ;~rTr5v{n;b#fZ510DkskNW%UP&^@RJu1Z8&!*0m<0$WLvSaa7YQWgVr38*Ssf8xSgNB0{VzPF~vgEl45MJ z3cacRwnK{nEqf%F#pJdnxXf25#qK1Xp-Vu+&9pl7c8oE1+&nHKcS{U2JdSjaqx0sj zL%n`yU#fviz_;(XdWsvRI>8BVNcFYK+B%$PKvbFGF==nq6?Y7FDfT4Q-I30KE&;(-|I=tMf*& z=*2eaW;!{Z1E*`{1}=|*xu}5lH08rrd#l&zDJWPf2X)!LXeA;QEB|Qw%cKwG5frDU zpVPVR6br|Y2zJq{1>|-S*7oqy0$_6F@^ds3eF!HvjQ}@$bw$50=hZ+3aL-d+qj4Mr zmpa#Xf=O~Fq#we+&7LFa@~ME=(Aw$XIrTBz@ns%`XDK91E&hziy^jHi7t9k66pyO_ z&rb&DUt6>Y2&|o-LoJgm*BlUz-gUALL~vxDviF7_EcUMz+`IKv6(f^qhG z%19R=MS#Sk;z68B!gfG67h<5RT63%J$d_e(XftyB6xPyC!+eW6mtCYnYs1gSZ-XiA z^))?jadx-&)9QS}49dj^w`Vdb^+_y4jvCp-jAh%r!X40$i9T^&c*LNFeGJ{1dZHBC;W*S5oZK-J0DaU>7;4L!sfUwh{rYlP>_MgA{$h^Ms01Z@kKO$E21 znS}107e??nrHxs*HT2eK7^yg8FZ!ozVvY_10jtk^;;)`@7W8Xgq)G9w!A2SA`Hl44 znXPzdF6UF$Jow4otlQtLA>|})m zt~%2(PZ*M}#Myw^&cxZy-S4x8e>;Poirm_EonjSN6|U4{!w@)W;i?LmkjD3n{th@A z!Kfa|NjN8Cv^gz3K{qTjG0G?<$}WFa@CyEXzPsk-CHb1Ksy#vAM)}Y>uwH5^81-%9 z5lQI$vF&lG=!rUO2+lG>>R`9dN8e7s_iyvRbUL!sW(*Zz-7VeS^d)q8Xc773)Y%$6`aTCJ zqC%p~c23Kr(p~?)5;#M!$Fg9R3k79-5yckUC}>C;N7c zf1Z&_ih7>ok7xHqlt{cCFl@DL14#qqWo=i~Wdl8h5u=b) zThTLwYRxU{E%R_rGMCTZKX?Ll#S(S~hsPapc6;s;$*@d!kcdV2Jx4|t3-tZgykJ61 z9f`))ovzXwTwC_rf0W{dX5T=`8=-zeNH>&N^!u*-B{}X>PZx3w-Ue+`aL|Q?9SB+@5*iQyxsf6Su3i6A$jMfJw zjPdegvnutgBaj|rQ-Xfm!J-(9aFF-#V;giCd5=;79j2MFp`5WjMRD{e+O4}WeXRk^ zx&uU)pQ&T2Q1_X44vSaPO5Liu$M0=pW_U`VUVEB1Wgk3j z&NN$EzJ~6T!C{PdH$9AZAkJX#qTPYBz|Vt`jeG6Kp`6otR_e`4YuB70dA;{|{aSOT z28*MDw*R37Yp`T*H)ZDs++sFM1TEZd3QkAN!b)bXW1|kYCONg+0ohOK3lVp0*3YNC zDdv7I!BB1O%mBbS5YVUq*sq^fJ7DoDpFA0HFgQU=Sh#3G^hJl-gNJ#aTR8GKKcrW; zLa!V}^wVf%C}=PR_S%009Yz=$vRYVDno(y=z|rNR|Av;|)K>SoqwWEQ6DPn}^Mw)b z)GV@3$60_mg4p}8c8bW)s^`J9jfUok@+Te8#B^+W{NP>;5ah=(}vl< zI>u0Z_a(LKfjd9MwR0F&?JZGO3y@3@OmsZC)pe+|cr!ZeW^Oe{UF$^?33(o{4~1&C zu4B^GXj6Qi9?MPJXLt(k`i;5L`D-P>XgAM0sP8g7?Jpw)Rx_H4r!0JlEWUqo3b0a7;VatbRII)nTAk+wzy~pqRuzh zpg^$9mA9Sw6peh!U~TR^XnD@wSz*V~jm=Cgh=B>U^ew2pJP#tz^x&JcmGt!>ID+FQ z$1TK=;wi+AJs)OP?vmE{VC=WdQ@;;Mci(u4&SWWB-pb?wrP2T&zh?H-cgm0B?*K|V z1xjLgXI*)_><#fNsZ5`_wWj4XnxV*NafPAGR7}%X2O-0fNoz?p|HZ;*JDm+GUKcSknFsB?BeJMukFR{IIMPvW7StWsgr! z;E#3J)Eh==)u|!bKoXVR=ojBb$%j>+pWsp*dr#2jw#K_U9jB9c`2DKM0)LGY)_+pm z!Pw+~!TNV>@&9s+nK97le@DWBd6m*?U@w<;Wxb7HdV`tt{}^cvbaXOyh>np(fM7xr zTNtNqSAmi&k@Xc3Ng*sKhR3oR`rLc1or?Iq1wo&17;+Iy)KAkCl^^5GVd2J+2r)t^E<1 zJ9&ICOvo(x(VA!+WYwiAd9?Z&^#_;_hfzhIBrZGex7=8R(kG^YeI33z6cJ|XW!kF= zt8x&Yf+VrhQbY^nP^9^by739$a%qOm$cL$hxwMDGBuIR~F-kqwi=mCjhr6F6)Sp*X zL(gUlHs&5hIN7;CKtom# zrx82A5X1>$=j3n$r=^yXRRu7a0$2e67A8f&A6eet*m1~zBx2x|c<6=yg*NPe(xzSe zkXQLlli%`}+ORXS8MB$Zb&fJKu`rpiajqyiRZRDF+#@`3({>EM3zqj3c=)s z4!N#Pw7&O#dkQ%B?Zdi303>BnKD$aXk;q59u>V3u&OfO5+i4DC5K#V(O~2(YRpex3 zHsN|x$_T(@!U_bkF|n}&*+7Oy#z13cLjaSpkuet+GY63M9}C9JYV`LNllVSMTWWOZ zac03lcKrP4Pk&H?cY6E%eV}o*5N07+G8qLqR!Kl2b^G({r5tmiuktK2cpka@gel?D z6ryT!b}FKp(y<aI$oA04BL{$*P->W3JTSx^4vrjn)@`&g_?tWp{&(&qo&{ zJ-fCX(F)J)Qkd-fC+e*4-ri{%2vSl8uAgjAQ(gaOOd)XeG+dB2vckXkrzNB|g zu!&w`yYO)Y>vWi1xL{w~F(2xB{uovg?^OcCpnrwzsk7aK0C~@xXZovt5+I{gsEL5l zrRwlkPVdJ+CPXZ>yCF4T$lwh;_ZcQwB=ASVy$GF7(=dG ze;gECnY zLyBETFhmBT1Jzc6CppNx2dQSITk@%}^p6XgN`89|&tAc)P-h>3}f!vp|eVgWKS z138QV>>vQEks&kJ8&~3DdGr2%k7aMa{SUEhbrmCo17%;D;8I$i|DdO>S>~lBpDR+A z7A!6W^}q4+hJR4zcVN~D(6RrAB7fi6%jkuWDO$-5sKo&L@Ag2ikU}(r}$ZlxN z$;<*Y1TZtR16kgP-T$e~f2;w3r{=!@!@T~ST>cfyVEQvlElir@|G!wq+aO~T;M>Gm zK`d|F)XdnOf%j-iA`v1Srm{|S*!SAIJC^i12`BqT;Eq}E%Z-Y2FfdF>E+ea9%a~QKRb8vDp z8v!_&jNY~wE4v8?kdwoZ)d*N~>DgcUJ@QNZJi@v!Zdtvkd} zWPu9PixfhiX(X2bSU)%J-x-T0X7%r*M+zfvcwYBXN6{V*<@i1xg)$}>uVGg&iD5Q1 z6s8aIsGd_s4hzVLTO&33AI(so1EXVT_-pOo4e-qrRIEu$^Qc}Ys;Ign3$EzaC+Ugn z(g=&vBGM@B*v}4ry7=WL9<4}u=ij_O*E*<`nW5}!Y|w8kuOi1tB8 z$AYv!WFxFD6E)2{+RYZstxu7oD0GuuUiBO0@dr0oUK*$g_B^_BM{dPso+PDK!cgVY zrKp@_DQtnhn7d-ldABm)t155xAi;<(H)8zTlb8HC89O2SkbuB4e z=M23C4~hvaapy8T<;wqI_1^-upA0Z;M4^Fo^h0NrcELRF~RbL{dDO5x+#%HwWf}M`}W~4yc(XJ zDE9_#N$$$LwC?I7=ShJm_XVRhT>(ADqB^auejAuqO&`Oh^}il8&%KPU*7ZUrG0ok* ztKKN$TX7`veIX&O-qZ99h=SyWkEh~b=^{jZPu(qdID=Dz1Zh=Hbqx?6l;5t zW&UNteJI{dRMjCHUw@K5oDhASbfk%P7RM(fo1<{~YnbM(svD1rtL+kSq#cY3ZO${~*igl|6xIq#gb&^1hD>Lk6N4AoVa;Xc{r(yGSI2D~^;sPSCdkg?+;T73X za3gFRpT%0c1aw1@mMXK$sdHONfu*ddMMqL^)d#qSIWyd0@z|OTusBc9_y5At{W+aN z{#+6NyE6D&{?gI0GqQ5Aniv|gaDZ4$SO6e)b|C9pIAVGWsmz9NnHG@Akkbfg2>OTH zW^FO}N3J1BCn+OECpY(&ad37Q`c9Qs9UynU_gXcyooyf%+dc3qt-txIQyVYJMOble z+XQsSWB$3C^&`a=87e0+gl2eHenJKY$MJBUC8z4l2G6&6MDqNLX@B~`M}1h;gpAZQ zSfzU5Ivb1Irj+-;F0%1`XSZYSWcwEb708Ryh@pWw*7A(JOBE%zTlY9W#gbo#7_p(j z!LDTFWBv>9{>VmtM+w|t3$FiX%mXsA8yN!GfgH?0mbbY5mV0t?a4@q0xi|rD*&4Gk z`x`lV`xW~?!25q>)-eC^i3nIh!h6*JLK~Am#Jt~WrwqfD)5Tl7`z?RD6-*d8IarwA zqP7Wu(-36%CQNKxM(peWBjdLOiIa)zjd8st*l!WMN?q0#kK=dDnhE_fQD*) z`x=6LlvgFMnpfmPoi-MsL)X^)|8e#eKyhwc*0{Sngy7n^6WrZBG|;$1aM$4O65N8j z2X_eW5*!lTE%0~#shM}{)!dnRuZmp-6sL-FzW%nYz1B%7cSM@DgURHA~rrq$zbVV zh?Lm0XWoBb*?W2E5%>|4VBHmlF2{QJiwML0h<}XG3R&2N~cfZRp{vZx_ z4f{I`j&@5oJDECcDI$#x9S(B3W;nbw-vcu2;|!nq;1Q*kfFk?o z_14m4gY(JVTbYOPR%XPSCR3xKv71l&-2Od|UbN)u7y9mVIJn^ih!0hMNPUO&qEXFO zI>b~EG82^+gM6AR_{Nbe=82|7Qs;v0ryeuq$|M7ID+-ApIn4%ycMX}eMz{m8hcm4Y zdtFf0DxKo+HCnA-AIUn27GR!W-kS{&phJJ(YBIX9pw#O!r-qnU5tb!W@2qEho~*-Z zXpbad6%ZwXbDB1th{tyONqdvhe~`0K)=f^r?-Z~7$ZT+sbJRDxTwpD4kM9RF&^-OV zs>5P^teK_l>jX@~%NUTW$4FC@C-tpyW?R)p{{EC~Y8h^ar9J}*AW*hGg<~5{t)W#9 zllyMu?nxHE%6Ota_%~ZbEFSgFQt{YA;LJp@`W3B@t~bIeh5y#ok8$r;+VR<`s6IOA zlo!JBxFgMW#5Q2?M13FK^_Ij?EkLvnqsJp6eu~m!?(LDt$zm5UiH!vGRFvXn8;><} z*rDh}Fm&k4>|o%(q!!Fg^Ww6w)S-Uv3YdzUA)r%|C+cJ5S}5|n1N^F#`W8He- z5xUB&?b|IR6p$lU`Ad$}ReWj5HP-tF!GthY`^t7Esu;jL8BtO#gwKVun@RG$qX6nn z%nQjwmaC-YJnC)MZod7l?N0n%&> zSWMwF%yfN8>`#JxU){&Mn|9|1?Iv$?2uks_Zl3coMl-(aOx2pgF!&NSx*nCvUnH-` zefnfi__YHx`zE>!9~jSA$<-f%$}_1WfUTHL=pqC~QEVnx(=vRs%4iC*c;Vb^@Vp0o z!xE``t%ol5QcN2vaOfVa+X{)E?dl~ncj1l4P8$tg*d`J@xrf?f=NTWDx?WgwKv7;B zI;9?JP19NT4jX=Z_mG707?3U`?f2uiLnMJ8C0(}mTO?aHCBvS7wQhRN901zFjq(fT zVZ*mKNRN8Woqyf^va|h@`;|WRt8oLn-aqzt?iaj>lhfSX8~_5b@qx9|zzr!bAds64 z04{GiL0qQh;D#-y2`?|WPdNUakjIu=CIir$g8m}m|XeA z?)&$t?+^UgZBUQWeYoD+xm}wR8uQc)m8=n(ZYU-l;(+DLP${I z;S0)qWLv#B_vBTi2q z9!y(?KH@PmNIuxJ=d@=!0Extt8&t^OD`U|-rNf4(CY*H}eQ%-3*;-v>Lzb~*%I%Ep-2$mivebwv+;3XLKeLOi= znk@XF%W=a5(^fZ_j&MD)3f$ zfC?2m3?O+vi>rqJ;N4F=+!6j|-P_Ur_o!nugTs2=*4nq!sNoGEcFu0n7_`rll%BCV zsUiBQ{XYtKb@7|;vT?X47k>;c+iY>(|18J;#ic#@w0e0*Rb5R5(`F!wkkYp+^chP= zxzHKoHC4K&*dB?wxurNBMl$P+*v~qme%H81QkYZQhDw-lh61zxsGgPJ`!Vkm=1L_; znpz{d?F`M=bhC#DSewg;6lfZ3tSlPbmRv;+TU4P+@7w9%AXcv%61#YHjc`Ds7<8Uh zqE0k!6Pg0LDPd(Qh=dxAE;HE}2zwJx6@kh(?o6Yr=c-ATl`v;KC>2ZJAIZm=jP>k=DM|D5^_Zr z`tu#G*sdgv5@p{qlS1YVx6iZCv@o{~R*kg$V!wFR84(Uae(rCeMy1!$x#+65g56bP z7f()Y%*60inFv!PQoJUj1_3z}YuhJ_pF&N(07e{9N1+j;Laj8X4u9hbD{cszWP@4Ri3T1W>3ZtneL z12aT!JYA{akF9DSKM}8TVL`Q&@W0wke5@HgV)92@s0m~>N9-RY@n0>s9v~ylS<=%J zm*I&d^eW52Ir?4t(9~k=d<;>7`yBKAo3E=-+Ts>t?O3(r?z*mEGqIT^YWBf_Y`ak$ zDUn2fEGfWS^UcTa1v;|b@$ujAf}DTxf`3wKY1`&%K`_wy$Nts}a08r9wM& zU}VqL^Tl~&ttdxG!Dp`7fv#E*#y#d39ng%Lit$?O2*Av#(k1aKbgy)pn(%rsPU+xD zL~8O8R_oW5nRG@$hSiHU>4EnZuSrx?W%r_EK;io$`??_+5hC%?Rw?HLq$*9tHMm_a z!7xlA1t@#P75}KvT!s!DA`zU97Pe0duwNW=iwjE^3P*QLc%S~1(%3_?0w4#Hd z(jq*Il0!C`{j)I!y-xk&wk|qrxO<_ly-++5#wY5C!IeRj%Gb1*+oy^HcDuI(PwVjr zn|0dQCTNJ(P2uQ&Z2bS)3uWi|%ZK<+B}IFF6d(hJnf}<{o)!-)55Sa-1B}`8ahRC# za{meHCTtugKobzSoMJaMGv_k}0YKm`wXddO+?*Ur*LTKK|Jng@Ry7_y5wXsj{2lp) zeJH(D+7J!*A*-+)g}wbI$s~3SnrETNHQUBpWoku;T^?43gD0S zJqv3q2H;JSlF=I)85kLecDuY&U$@Q0Ro>6;_DVY*HNS54&@&iU{0h`=LarWi$eJwN z(F6bGgv+;aTrl7iOL`b)&}Lbq7v1(&zPV<8IjbiBAAl3)F`wr#)UMwgNlu`h$QCV6 z-)m#VOlZNWwvCREgV=$3(A$4NPNVxsds+VEV)g;O7U7$|__i?8m58-=T^#9#*hn7v zB3J;?oZZgBpADRER>h@~DRCWjQY#WiSoc*psg{N3*R!jo2KUELP)P_ANq%Zz_EHd64@fH^UfXy|sY1 z4WkuVJpsr?!biYC2FRJQQ5jR*TdDSkt*$}!8WOJM3(RF;RYBj)?yZjaWR)~~f$x#3 zQ(7s;To*PSZGgVR_uVGUSM_didCt~t7V};JrPSH^>!z zDMx5~`QxEQY*3)*QL>_PXKOw?9mrhfmplv+dW@_+k{ zzU7`3&2@daQSsl6%~D&Y zAg`JFx_+$Eswr_@Tl#x4tw7@ax}&`d(aZY)hppXuni-86V}eS!Cy?>M+`!;tzb~&^o|on`ayDn1QdM3LM@`}! zOu1S~ny&C5p4QJ>(PTU>DRO-8u>MOBi9S}_NHC}7XJgjGHLoROq%A~XID5Hxb+Kc_ zLrOx=rkwK^(obU6%9baruD&6lWE;y~i8KM*+MBRDBp1Of!?|{1Y*!+GQm?2Fp@k|n zN532w3%y(?IKnsPzZsBc@BbEEuE~_M`_gZHdG#eVZOyV{K3zHGtKN@eFN*E{f`~G{ zj5O!Qyc?l7HQdP<+{+^spb*!t?&HR4bw9lAWL33CGVszcj41^oe?{s`-JI z4gY6`S;a(#!PX+7?>n=%Kv9F;-v#hJydlGypuHh)hj?;Gi4AKKoc3m!+@ZSYYhfO! z@SZ21sSkh|ovn$QGm(JL#f_XlN+*JszP)<9&0W2Bn?b7DeI}dswiflS?eA5jPQMOp zLW^oDOoVUNK8Yzy7v8C-d$q5FNFT+kGL@d_Zt0X14N&Fv*&w%qF7|qw|CTziBxjF8DIYcgRu63XpTXBF$f_DQl>f z1N*u?Lpv}EurQt{X>N6{Pgh&vUw0=Eg@6U5Q+c8VR#EoIkYt>~0iYCxei)mvbPazG zdE8s`zh9#n|JVI6$6t$tKl#-c4}JGAWdG_)#hMjOb(x z$lK4}&y7Afz4Xogy8nykY^v>Y>&whyDYED)I_(p;ah%;@;gvhK z{LB4b9~J#bHDq~?C^Z-LYq`i6|H(aJ z>(8CS)t00Ee!#Ns>brQAV~bflj3x{RVWwa#@~@ve=U@Age@Z^)1}mpO*LM7|zdLvE zA`>={IXgQqHy0-tj|mVwg3Js4aGC)C-0XZD?CfAD2*}CtAMB53xI z(lW#pr-ShCeS0i3@vNMYkV0s*v4D{pG7Ldax1_U)KzvExS_vz6H*s(>!G;oESS z5Fzjv*$NKv>4GP%4~a5{!^SA%AgTVKmNnWQEe+wr(1PCVDiQ}=&Zr=ZUcTK*OcXV9 zbxsT~>{FP3qEv`Ta4c$Z`jP`3|E!iYbM$M@GGL(qgk@;ah@&c?&*CRKyIZj2FD`6L z(=+Dz6WVN_MMLZ=@h%!VUmX#%ExC`~>oX<$ZB8Hc#Dn@g;xTK)IOcJ-UE)p>)Z?f@ z{wG!i)@f+hX<8sIjy|$iJupIcVf+Q-d{8R{x+$BW;5vO=!CFcf*yQktS(UntfLq`5 zmaL5u*SKnKBwufDJ?mHLGWuSRR`Y|BYn)f^h}cwjd5qHV<(Jmczfe#d)W;a5oAn3Zv$qJogXz(VY<}O?$z7%1)eA?^73`n*&ShPqZo+2~?>VFW%F)~R z+R9fOCo5h4cC#>(u)Jlq_JWf$o!Hi?Owe*<&SQS$yIIj5bgiXlj@c zTZ_BXqR>kSXD_1JfB2l8c*cfVKLLQ-dxsKx**=RAa%~Hzq?5Y)<0V$@XA#|43GQg+ zvo2<)kIqZtq8!i)`(1DZEo5Xd+UU7w;XT%3mhC9EQ3x437u37~3}bV8ZC@XnCr$9L z7)n)5rhEtMX^6y!?ginhs#+L>!WN?kOy)P}@Y%tC=XLq37ylEJVs6RRO~8U-f9!8x z7j9PYbUp_U8>g8WFAxL-Pv@JP^O=DFreNYX-zsV40PK{J+EgC0qw;RohZS5*l<0I#Afx3zlmkhjS!+ug@Z`pht zZ^Cb`cSpsoFsWd!$~OF!EvpJ5Rd3b+jh4jc_4&-2Pa$|yTAw!vY(EHMn95U6ap!JKKgN?4DP*`{z=q8%xf1p6?sb5- zXo|*%H>KW$>L4Qdfri3|b_UIn+ZmGzGNhzEeLqrk)yqMDkd&HmGY^mQ zMKBFnEp(P90Qk*kIY0^FV)^TjB=;Wg8^BM9Ee`#v*4X82o==XqbWJ5x;`szU3`D$Z ztQtRuTcC;63&*OK%3sTOzbE8zJoPWfApWkLQ8GdyusAMGV zek_6k4w3dJ=0B`~cEV+TS?H2{tAKNkg>dpjKCZR~A~ye6LR4OLKStOv)81YJ`$L!V z^Bzr_7URvPo1Y=oP2cqO-QA_spDOAaFO%A?T6q=#KZGuHzod3kDpS02@H?OwB0R>io_JuiqQnfnJG_Mx`4y1oyoGI8-DRVduvG*TCvyH> zYF~-q;8d+mk2}DqFYPAq0$wHDr4UEs;-Jvk-C(Zi))JaAS(slizUv-4bVjmpD{we^ zN_|k8Gvh_7(5gFi=iv9fhh-L;;VLSun9tT^z3WvlORBg0rQ@%QJ?a?YAc5&TI)($P z!_iXvVZ@kff~CT-WtEjo@-q*~LgL&Dy%bc48^_yZsgD^iv)t4k$9}NOMa*{2MK;nF zH>ZP_qnKU1!oLNc(o??-7xUELR2UMuDjE1OkJqg2lkN?p*0@cT){jgm*T!}zxszDd z5iR+pHYMF5iLU;b$U5`hJ<@6uLn0j%S-*|Se#PwwlCi$+6M5@NcvrX)B&e;VO^{_g zq(qH4UWmo;y%!NmAMh1A8_*FtNEs_YkI6SsC=_ZUse=ML$hN@(L{t2{%S9(OQ=mbd zk5Z>E120KftNYUyQ7^Fs*t%%=ms(7PmUkFLJuua`Wr`ITm^bbE_Y<&I@@ zD$>ADpspns6ki|o(9~Tp8>sG!Lp*2g6P5rVE3k=A{8J_XAd#DqOPuc-ik;XY$|hU} z$*@4rdC5Z0!`&>J^&WGFJlP#}GKx1mSY~-XNDKs582U*BI;3X0#1Yxv7b|6HF|}SW z-(mL`{_cCCoL|q5{le?HYSrk0lp|P{vD0Wd8%}YVyul_+aaNL^-#1hQu4#Af zCd@#=JQHNCL7>LEk;FnWW#OP3%%js)#hN5E8xg`);y9KqVM-Xz3b@&A+XINFK$-8J zRUGfoeqcUExahDTI0h|x;y0oJd0G1MI_*5E<#tgAe3$&FtWm#9Ip!Yq7;~1F&}C5K z$}Euh#Fvuo5ry<*|3Ym*%rq241k9rh#~vf0A|4ZwrSr!9a%3=LH}|G6FC4%PUfvjZ z0jd{Biy%`c3W8#gS@RG+v&pmu*4hj-_H?`zM|Am zx8w#D62UNAtrgKK-CQLM>&Rg83U3i0=Kd~rXW)e9=?A}e}Vq{S#%t@+o0efWSVok=L`G7+n*MQ{P%}~9_SDe zqu8$h#%BJSzWmAdSuj{jaKUZOKlXR&3l}#lxc)RVH39F4kJpSH#15`P&A~t%hdD1B zcp}W)l*bIf!OjIPsVB8m|A@wHHuw)tvL-@`S~i>x1^`?cr|&?dwRi+SV4=XhO`C`t z8Ga?ay>Ittbh;WOtjFU3A*x0X_m{Jd^H{wWbJP!=?N7%73#T!+KiuWKtwebZrb<|^ zmH277o$c(c{9m|iDvyod^CNZ*3mAMi*JR8N{!X6C(Nmg=np`|PmInH1wq|xx55sN* zLtLNr{m@qV7rJGY#Mjq^A1zqoqMtoA{E(q4ZHrRc`waWxa2V3}8r0+WjdU`IB=AV1Sc+9YL61psvHXhQtGH_rJIu@A$CZkF~JpeB;PjAw7s=T7*B3 zaU(EpdYZIEjHy$hl&BSRpt>HgIJ9|hYsg<@;`E#SYY)WCR1rCjv^U`#d^n{sJttHK zRy!P33``K{2xg~WuW{()T7AeOIVB~fZ#*&KgDfg+0Y&R5P=^wmwcr72=OZ6K^bU4_ zBpZgDpq#2*exltorwn`D!2`-lAwU*?Pd(!<%V&hO2Z{eN)P_t>{^w}Vxc)~f*|6;< z*4e!n$(h~nOWIK=&1!bd-K}GQ-c^Sv@&@dEoX0-(jOzXh+^VBRE;sP)2IeZb2!4o6 z>+Hr1U$rzl=64QEa1JrJmiLIi{=|8IqpIM2#2L!({$sj#ylQ&aE7W``-NOTa!P8l= z4Wto6U5-|Vp>N1Pw;h4Vi<1;Wo?xe-7^ zqAQ^ni7ytKJze&G9>K&<|2^)ro2#PuBZo#-i9lW~9jhw)oN_GX$bts_`KcVQ;CJCH zQC8#U0xf;r)3I7iE)w0{t~RpP`(=|s>^ zUG8i<*>UNd0UGRmVO0bzeqG^y!PxQ()le=B(NR|_x^V31ARm3vAit6?tm57MvJa1L z8FPFo_K`o{M31!i98NaA`SuN1pG{g}OeYW4XsW*dnpr$`EA+{*^4lirFcIqF9&66t z$LQH(>v_GSNO&~;IUKojr=SmJj&bSKMzyIq_iu<1_e2)6xo20;)>L2?%Ck0+)#%lrwPdUI| zm%|iHTLp0e0DQb$U~Coq4Zs}C&fsP@XXo&MJk?aRSrfrH<%@p<48Y6B*q$V-0G!@& zHm1#rWr_C^G^8%xnOJF`RGTDz+{!SlqF#BV;Ox@G_dT8J^gN$DVb_*#ec*k)s9p*- z0ILKuohyS2uGwrt=&hsiy&IT|u?M6O-Ss*Hvna8R%aIG86p`}iTCq{Y<%$VUDEUEG z?mz8hqWOAQ2`l&i5}2K$#-N5<+g%F&U6fhOI~(*y2t80=n4Jg%0E`Mp%Sz@ zVAN8E^E}Jjwr7#r+&mGrw@!FWu_AF!IK!dT$m3_5{Tvs|rqYlsxR-$B$L_TAvoYo(mf@-umB|TF0bRn5A;Y zKy0Phy3Ag}R;P{ek9Iv~8s(||^2Wh9J>?8<()GK9mnO6kzgmO2~ zPncDwS=N*FGU1hm-)=JGjQJXUn7XYS_)B~fI-7aFPLa{SLS^$~s#Vm^T1-}<_jt3q zN`9AP-vh_Mce=(IVN-EJW?GG*t3(kinx$;(b6!qi5t$A>almJ&!|wJKOSE&tkpX$R zpJD#H7d6O1Vg-Hc z629e??b#f_^?%TIy@)Q@-tw1>%MlUwR-2iUd4XoLhbsQp-)f$}^3(t1QQEI&U#Y-X z4E(Xbd#k~VIR8w|o11a*aj}8B_h23$I|m;d8xJoJCl9wN5G-u~o-pACQxKZ8LxrmS-z4M-eINK)FaWN}J+hVul zUq;x-NPr9yi8zEy`jwKh&JYsFl@Jff#C%j{wjv8h+{V2V36m?niAbdoI zlQm3lY*u1KJziE!83uqhZt$Vc5+Ll&5kl5? zZ{mp>9K;d`D2n}c@+m3x-F(=bG$(rmS|Ym2F#zLM4$jTOcx z+Gw+Q+b6j?uT_QD zJF*$9FmkJ&!(vy0EKWVt~`8?$R_5r z+oItxbepmdiea+nZHM)-T~G5NysX5s4U$@dE+L=QSBW0`*ZH?qBcF7zB!HS$y@W=O zeK1spjq;2n+i>`;1v`qS?%WErU7rz~9ZuPpz0yyR0}T*|s&xk>ws7v)>KSg8!gu|} zZBlwmOhwrH14frq@{SRtA**{A(AK*>s^!J-0|G8Yg8aU|j~^VZv5PT6b4S0w@=VtL zo~4hh$oyB)1k_tj4>+&{mSx_9J2LPY$@JE>+Q|3x|a_8e|5y?6w@ zSXphRd{e*{OUH$U85BmMlJS&}<;Bxz{&m%r1HmVL_i)H`KGHMJk@NzR(uZo!-F}id zwsBD!P^Uj+@$8mtac?NgX#4h#peptUf{j+!z}y>Q_~J`EM4!DmKEwwSIvM~23c^z= zJEA4L%qx8So2SLU?z4IS85|15B54u?*S>%3?|e3RkttY*!W6{G3+4owad4Xez=|RO zUM@CHu=oat2|KtD27$@5|DZ1aZS6Z8bDpv)*5}SuQ)@IHa`nAr=SJ_S$cfs1$m_zMg)@_~Oe0dj)Nd*1&8 znhd8j)X$=~rYAhe`+JnX`0Is!sh<}vM1nu2`M@a?aTzwfqo0m^5NUZB?8f~U#Lcr#QMYXPhpyyB$yyW zYQZLQl}+8Xpl=pc!Lz%>zNS=TC_F+U6|dHGa_~!GX9&5-?r_vtHojMFb!UQ{Ee6Tg z*#b+lNlR?m@RFp!#yP6gsSl-Yy}n7@M4HJzI|hK#cVou#$uvrMn9Y~woE+C3j=0S| zI$f`C`od$!hKv6=#y=-gPyFq}0JyF3$Nq;C0hw@efuR|8HZD#u$q@wLFaz+Jvzze( zc)$_@;8cT)gU!^8%L6h;dn*deQYHhk19T@XIsXIJ=~!`h=zj{wZQ3!eW&^0uR5H%`HeFYOVM~&b0$i z6BSJxI*{gQf%aV|?23-ewiVaJ0WLZ^x*z?w&S^3FXG)EGW6HH1NpzW>FBTqM;6Ct69?~!P;mMOVg(oT-eDAsWr9Z;VH0IEi>-9^MGuP^3keF+$x~Sk#+1- z^Xld-KP!l%KrQA71s_WU+UUGCxmL1#keJI;--%w8v^N2qF)ua91**_Gao0-!XfkCo ztfAMuiA)Vcl>{p4BToq%;|fbMxqLB3($%8q9Yb2*B3H-fXMlPxwDW29lY$=Dp0bnu zAY-3fWGI5qPOe6gj6M#Sb|VGyB^68z4;@J9_OR;6u1W_uiS}c9D-xxFH8j*)cVcw? zf}OY{tU%5fTn9=9OC4c))IV{+_nO^9p7Q6%>v?Ii&oLozqZdul#d938Dt0hY2}Bnzpx5&;ko|u$$5y)=4N>}J>y*l)6D#q z_^SaWb0#zmQkG>}3OTENjD?r9dKtbL>5;nDNq|@6THpZLPj)MJrI;iBPc)JO?$i|? z*_d59a1bSflegyN+`fgYP5qSDdjb-Gu3KY5eX+$e!Su&dg?(3($L72>F)6)?X8hkI zP|J@~1F@NT0$U0_m&%RUB!@)oyMa~>fqkwBcg~9{-BwqMLCn@a{qw^(%6UyLL0D!z zW=h4Q!z-$nTk$^;)>^7E>ly3>b~~GTYvKLTZe7W+S9sgx zMGSg|H}zMEgJIQJS6q%8BqJZ{r@Wu`2F8&t(pIP-Tw428&p~NZQ(tcP*E58Qw3$r1 zg9IjFsK|V^l+oYhiA9Lk;BRmb%)wG6X58Q?WB#W;;|8#S6*Bltz<~$cNd|$yP8Qtz z{siU{0+H2wN$vp&GSyR(eV7 z(ngzh9T;=_6rq^j=epT8|4NnSPTayCAAi%z+C#t*VqGCtP` z^qecfr}I1<>IB35T)_8?PBHz+lg_1}m3407XLh0>_O2}i_$hD}4g<~y8B zmMi1HqX$$wP>$*^H8PkNEmG{gJJM*#^~VQzEBWiSv}jO*P_3T*1+si9EM%BEj&uTbirt{GIt@2fHK1@kvx#ly zOEPg)t8C6Lq2e+ejrHB2@-D7@D2*eDhdspM%8B6LRW8QDRK!TElQ62$JLIdXuE3!y z%VK^QHV8|T{eiDw?t|sJdzU1_c!*>k$2O+5F4;y9gcKMZ3nFq+4mDWo6Nm}=2$id0;Wy zO)bSljv*Bjpov(sXI~nE97Lk7ha_*0`Uoc-=H*U9j(aks!&xm~6;;a&Ev7r~nbQ*; zP&Bv6a=y&nThNxud%0chNF&vJ@Xb^%uWw3pEJP?q>pTkn{|%+@(vHDS>zD{*mWW2K%ET}tY#x| z6!i3!!tASQ@j*$|yp4>M$Rc=99!uUY)AI4%cX{_7Ms*j8u;(H6N$hJ--`|&n=goc= zii2(*R}ZRJIkjQ7Q6M=(40R0Rpn!odAS*vdtcs#kP2UH4a?Xp#9KyOTx8j{QRGK)C z#15~Tt{9sCBJtA0<1JIQYQqpb)}pp??Nx&ze9%o(I+9m2vU3%zO%;?wHLsQ7HRppf zXDtwuTQbHgWYBOE)WeiZY-pUVRk$ib^Q6vz@;J@juG)Lvd0H`%y+-!^BKR6TS+f{F ztVtSn`y$Q#Ok$3I?u;6k&WVH!bLeNFln6z_m|{E@B~pVgO=_f2Wk0k4TGv{3{5GiY z01;I?%s3v4kCT^%HXI77^%|?QujDig6houL<&Q@cOWh0ztV|h?fMZhU=fVo;DV*02 zt(p}RAx}TO@5WSuKr|84=&$_(*{>xrDW4X zaf*fg;&v3)xp~sWCeAGa@eEjsCLenAZxvh%yF1B55f6y%m z*7_W=<}7!!hxT0!vrbF~o?j|1MbtBy;h5$YM1`UTa)uO*f<^un^ogmDdPz-GrVUvn z;Y6B0_`B%hvMwewyLc4bVQr(!*5wrJ6}bw-3$YNYB2WO%2IWrb02@|;vIs}59!(5I z7q7p&yZPY_GVAH-@pS+BcG9-Fmm5pQEtkje#x8ummA{{bpYn;~68;xjt|O+Z>zNgk zBeh$r?2S|}%Xh&-t&FSRl?^JD$%u)`aO=#Ua!o_~;Lxd59}<~rd!Z5=O zXQ|WtV=p@#d$n9d7WAKKkyxtyYr>!4K#X z{YeOa>$c!}TMg53XWxj(KUKk`a{;*?_0zpLVvZ-7z=M672e>=E*T zoXH*V?9l|CKy(bDaNp7f$*(sf{5<-)VDx^H*A)7UHpykIu>;bmFn zXDGt4h?4BSc6A3eR@y`^A1dDo{9|*KF``Zx7DY;9kDZ6@bgJJM{tPd}wS% z0upIc1#G_??@?4yS0(xIx}9+oR*-~L00nPbU@nNZR4FTuZDwmw)I!t_NRP`U_fp-2 zlFDN~NG!+!SupRYm^@oZ>}DcOF$NYywZ89{tI83G8C@>kMF5f>CNc~Bro`+ox41kwEz$YinJ@fI!HgVg8))Y(^aEy=A%sxe1qScc zne`NhmxtLI;jz}K55oy5qSk#!*I9Mf^5z(Wo@hA#ksG?Hn@XI zJ>01}p1t&X%JfciY&0*VOvNE>Go>U98!Q+@xkNIda0L%55S#S%4M;Fq?<0E@wW|-P z;Ep6>c*e;KB(g17eX;0N0$)_PmuMuFQbLn|YP}-II3DlUO;IOw@e{WFtUQ*d z#65OlOJnja0moMIHaj)jt<3+^1s9)^`W58Y-x9^{nK6{vNJ;Ovc5mZ2T9d#coeQD( z!Mm`Y74#@VGh{Cx@LtBqH;w|GLy1KJ-7x3n<9*1zzP{uIXISm~-warezhr5-dAuQd zK|!!gzQSIbKmPhReCA(yxqkuxkA$k*|JJnx^00F7foF5sIk?z>AP%r%Bv@S$Je+IJ z1q7qX?Cczz?ChMJV2Z(ib^87`02mJcvQt{-(__i0OvLGQN&H>xULXkJHG$#vZ|vo- zBEf(50vkuaW(Kal{@CC8e_pT-G80W<`b5s9rTMN(>R(ZDy~t%Sr% zD$AL02|=>+?&q`aaWEG#Cztfg%Z-B@y~09-ocXvy<~RN?lMOZ&jI^0j8Id%@OV29$ z!PktX44{w_;z#AuY}+D4vU~9=!R>E(=*@NLq?!c@IM$eDc={3NS=E{9Uk8o&m17*y z>+^DuxC~WUqU#{PWfbCw&Fn+QhJ^h(UunC{Nzy>tFMwV>Dc6W2F)5=KP(p7~vgaeo zG@Kktld~Ycvo%ILJS@SZNt^dPmJx2Ar62t^EEDn$Bvu-<^H~vGg#ACxz5=SMZEc$d zX(XgUx+FIZigc%dbnU$f=?>|T?(UWrkWfOpJEWykxTHI{g&-yerp%(by23P3(M_ryRXlk-5_DP(7e* zYZ0-Db;rfTB}f)r*81qfCyRFit1^XjBK;042@u5|1_Y*fWJPE&d@)VWhh(=>ZoObq z$lOGM#jEJ9*osdjx~vrGT?#WZb+tigLx*NDFS3Qvx$B~bN2j^*$4f&6zaydVra@&W z_$lGLWh?~9UNOB$P(`Tmi5jMI%CIb7t7#~SrOdYAF{I!k`XCS2V#u@>mrc@`hM=vb z!|qm-Shz|qYly;xz$wrQ+9dbYO=;H(3}~SNnHU8uak2gL9#&31YE@X->xpJ;(#i0% zvi878hWSUKZS`qb#F49$fPk?W`SLo9n`Cgga`6C5-e;8}MiEp7hmPS7>~9kt$B2Hk z!kb;R4by3DwbnSa3hv>FkbGpl%~3tqf?Q>t?nf!&MS2|TJ=1O77Nh9KI=&PX zyF#|0s%XsQVT@N2I)T&E!CKTNmvYgn_RPKY33UcPTR+UdK(jb!v#G(aAnow@2Xr(FwKvb5HP#WFNkwcMPcE$k3|% z1z*Eas?nRz?1gr#W2Hh%uj$^OxQf@SbSe1m!cIsk7k5SWAm3~hi^@zn*Y>}WXE=Bo zV%zl95FME+%lsUtJe2Bj=2x(n?W7vaG+I$z-LnIFobLQSL2~~Q3HQtW2^vNtZu9-(y!{!o=?Sksp}zBr!xkigkL;oy!`uSG(j$$KuoIRX%(2*gv(aV@K7WK?!?f zZoPro=UcYP7kKY%r(-zj9(#eT-cBJpRcgmF7!eohDkowymO{J3k4vaMCpos1RN~WE z^CT8iq1mpqxSv0oMRoi@94}61j2y)LVK?$-CUsHDicbDK7WsBx0K#t&-_p9o+(gJN_l z+4wB+#$gK?N+-pnli#b-ABhAJ?H+_fSeIqGRYXlw&L}0_>FbHJJoQa-Bx-um$#%-S zo{v~{lMsFSf~1{zzozHHEIB~|;Xe5!RPYI439&`$)}QLL3!hMPlgxAsh% zzoGMf1x#ioS0Q>}EPRmP>O5m0Q-l+M8Q}u)^YfXoLpgvdPyidBhnEY?#cK@29yx$) zTz(#)6!4#wk|5LID7GSbMfqXrh^VdcarkOj^Ei}0-M0SIE1L12gQO3Er%=zMK1=|| z=0SefU7!&k025%s4aAwaI35yN__;XvP541v{9t~-W6ICX!wm)8RsSwi~+C&7Lyb!8V3nS9WDz z8@#46B#sMDscbtkGBm;pSsu+Abbq<{rr1nla8Wagr}tZ0@fQROd3!Hei?{CPP+1E_ zs6JDt#ZU+)YA69-%Q58rf-knaGCsH#vsrL$_d1Lxs5r2ur-ef9{^zkFku)O@k4k( zz$F^O&&$KX$@Ku{@IUC=|5}{?O^X2_iGQfS7^*q?V$+v<`t@q@dt9}Ynq@p}NDz+d z-x$lU#-OouMU4os%O2#nI}Kor05Yfe0pA9g2Ox(4j6!2$elBh(;O_uXDL9S6z|P|a zn*bk%o=TMkz*4-k8)BYU0e=@ zn*~4Fw(bs`PC)(pqHQ+AR6#>eIj_Vstpc?2U-b~ul?A+USi)cLk-rGq&(|n%fodHw zf^ASw7N79Q-+|rG#UznD293|_`vO4_4CkuFS({!=2QfDOzM}*z%;6qaT`2Lz5E^ z>t|e?R)^kZ(9^AfS<~iS0_li`=6El>SupxCEAiC~ELG>KsdJb(_~-5sn11U0kmm9y z==^GuZRX+pWU?FU=q2&BhVu-Q*63UqSNT|7QH>(<*9NAi@BKlKKGPYc52QX4<;Ua` z8UK{(Cark~3s+mx|2Nk2&q4eL<34{BWc3Pg>^#VC*Asxb2SU?8nkyd{1jGScTDXCV zTM!R$_89}zA6!r%K_9{appQJjkIOy4j~{5Y82bqCA$gJ3iD2;~NT*sGwwl*}<8}uz zm|NoJNGIf55=#8^XvxW}b9k*sW7oUFyCmeWX8+@MU@Ob}$C0mwy45FR67|X9*pfGg zuk3oOTT&F~2rb|yrX7St2+0!Og4ZlUkqe(_ey$I?C|Tt%u0A96tGl6LZi;MB4myz} zc$Gi5Rutj;{Qfw`B5!yPejgwTi>T$R&%f_)4GK#>d+QC<^3vxye7H?<9(SSoX zjA{U@z9Q7{v5{-{MM|R3%J#U10C5?%9XXc=>~Rmh!JWyJVh{2nGf{++_6O|o1pJPB z&fW-1v9%O9OSR8D0+|z-hxVi&4GlqJDa(&NMW^atObc)(FqP=~s&XJ)x;|xD;WxXs zstqix$cUPp8CTheL@$TCx8tU7rXJ9(CQL1w;l>G$rA1`G_PEqvVZHs7x=;0~`-y)e zxu$T+0E-yR+c6~Y@(GKMazTyWlXqHRfgU$EoqV10)*{UU!bLjWI|9e{?Dyq>9Lx4de_>^nf-zQfJF8d1Mmqg0EA zB5r!f$kSUb*q^}#pPAxX|BWw3UO_Tk37vrQk_HscRP)ttew*a20>+bL9y>>B7AI^k z_p5UJkl07(eS==6QlqaX@V2y7&EcF1J1&aCS-pdUgnP*QJKs|mPo&X|s{ROX8uSzR zGIH6(%ypOwO=qgl(_@6~gt|dh6XV%QYiP zqtXm(9@5aFeC-?^{)C$d|8t`9nQJO5HXOkOClu2v0)A{WB!JP{(ev*_0mhLslYWX?VUO%1g z=GP3g>ioRL4jCPSHsj|U1+w!@_@alKk}MVSB7@fHYT@zW=#*9II;g^I_tVp3@J1gE#>)As8#u;bwCzI;ojC+h|ySfD! zau4#`GtPvKkKYJ@a{w5excC9caR6C~6U@tRWW;OC&(6cm$IETP107xVURUScA2cv{(LquqA5I)u}D-)!8$N@=G#l~{-0EvT(+0r@NR~h`CPjw4ix4u zxV5a5`#F7>idk4?h61%}7Jvdewc;Lr%dn;WsOs7Lz5LZtG?)_-u;zb1?IwTb>3EoS zCJ^(9FEHf~^1EpV8gYX;06H)p5D#EQKUkmu#t1hbClD}z0_ueqfS!ezKsW)vQAUIe zK-Y&2BfN$0`6OR(hXtNW-%~Pnr_Dzu{ptOZ&x<2%W%bX~+DOp`ifh=gxK88bUD2;2 zj38m@9J7N#w0(&}DGIp7S41OTA`#_XGgB@}sG8NVv;2p9o=a?>r{`Iy*%nE}`AkG! z#SO1Td+VN?LE^9c48%QQbGmu@{>Fs-8c6Z98RY!G(*I0=Knf>-YRC@e=7E}+n1BJ~ zc8&+8JrM94VEEvGf{g)G7Vv+P{uhLAM@4ZjrGR$f@w&Px`!r{J3k!M*TdIz0@yBQ$ zR!S`v35y-5ba{~9t`sL5*a)yiO^ktW0mNg>0R>iz0{{m$hJqnL#D|*);9B8;@&cHP z`w0qfNI0;AJT5*EuKMpoP4+}&IcCMo&DaV8>)!csNG_7iw>OScnVSIBE=1-pOx|OF zdJfvAsCF{O1f!t5DT}S=LpLrqL1MjUigXjJM(om@M$g`^Tz0{1%h;ifR~$;vO*mB? z+F5eUODdZ&XG}8rMG(-H-%n{gHx_D12^eCrwX<6ZbiK8U>VsV;K&Zrf=_IEhOe?c*Jwb=xs|l zBs715ggA>WYFi}gdCS!2>Ze=A#k+$29Sv%M`Jf<#Hh~rFvpR*_reqgW*cUsldulew z;eF;R&`5=prB)4+t1<^qhMnv^vKMIn6`)WWd6|PiA7OgxD;iF@@%0>j*|7ah>!%hv zl-9#b2#+`nk?J`{1wLWa9`)VGUJ;#~Id<#RB4-uC8VpwZcH^PFvfIV)-_`1^bS*jRee9?*zD1wLX{yki@OgYOXS9m;Kr6Z^J4lA z1jJ+hl6741D^rc9#m`}-PxQiS@KGg$yz6)z^42=?vh+J%o9(1544v_}4ykv0c0V-b z`wmNx*2X^IU{B=P zC3O(DH6rn3n7q(v@tVk0^%po*qz=|Ly55V4lI^%mtLFMEF*pYyoV-S@8!9(ArF&_z( z)b4+5F4q1kDDN#kpFR);O=r*S<_&ZPdXaa zE4piCWiXwKOSixtQB%`q@$Kbze44>Z{D}oN=jO~_)ME7Q7FDAwb(?Z95>zwz_EAYe z$Y{5}txkT^beLKp_ksq>v~Vl^%9~)S7&Kgoip`xle=t7&pk>8N5=7o*p+5Qh&k;N* zv|opnG={Sm6zI5eWa^#{UhPLo0W7uH&oh+Cd@mvV{`o~;&9^WA7+2noaHk?LO zV|kPD2*>W3DyC_D9LbsEx7?!9Hzau7YR%lpvm38B`J*DD`L*RC!9deYk^b5xzFCavB0V{*jdmnLb>(dy z&(bW52wOL>iXhT0?WP1Z+Y4n`CPU(@=UzGD)(cMF$?)}boRRgi#|W{{ScSRw>fG8H zz+Mt-h@_|<5DBnw*@63<7&>5 zk1RJ%(Kl4jx9q_W_aYkpHVwba-L{`GdlK@p-n(qHfhbOkPQE^vI56`m=cstfD2d^y zpJz?VB-3Fca<%6&8j6*im%-O}lG5(8&^zsrsod{$8BlDePItO^6(*-y`W9_UQ}`nj zfl6u>*es4{BB9XJReyil0S#r#N%;Hnx{3s@qjT~=;Bz3Qdhxm!3 z3nk_k*XQ}MwCA;5sID+eKT#ZMwGRfS^Gb);Omtr8jnfC)3kmc$61cm|wuh#bw^I?p zT`&d>ZC$`=ZD27;ly=n=w#gX?@L3u~)?{Ye;N8L7iOF%z9kDRWQ;iXhv)U&5h ze!$IpjAVNI@(I&~dK&M04|N&gbx7I=HdVxk6aua074PKe+g7RaC3tI3xgfP0q|dTMJWJO;n;z~ zvfJ9SBodQot|-)y6R}_g&MtETiKa$;H0PYrgp_mMLk}hQ_&w!)%}dcds;6ezivf}( z_B^O<%wNjCA3HZK9-YV%hEWSD*}6=u4e)MOLvbhIYhPq5jl%6R9|yqB(*#KEzM94O z$b|Md@f9&q;2emD->NBWQ`n7ldek3*^Ybl%D36Q%XANXqjwF)%#vjKBPP0@W{`(mN z|7t2LJh55e1UzaF^1B%W8u5TQ0RSfe_P_)<#i2lO7$`Yr=Q4tF7y)+_pwO7l1VF$B zylRk`mrCs-z>M86(|LS-obR*Uh7hs*G9E;X71YrhS!v$rKmSejzS+t}g~}uSusNlk zYal#{J=kXW3z@9{+;hv(8Hb`QbslPLwPAiI_O5ipAUaw!2iAD?vIPAw4Q+|n#c-X% z1Qv+6qPyt$aJk!We7=cLyC^oZjoOKR zSxKXOJ!LC1j{F1vOr%L!c%E}o$W-np0!MHF6%&VpE8Gp`bCghwMtL}OoynY^M$EWk zG}bF0=Xdl(>n99qj^Ub*z%$W&(E;r6Evib$7-R?nDv&!|43tNQLQzrQRtO9%4a*9Q zXq$_?h2B<#)_~*O9lvGkZ~xTJI_c#QiqG)n1?_Iu1QqCLu@Q4Gj;$iU3(JuIV+9|! zF9y;ig9?DH^&tPB#ekZCK}KL6US0sDg4cxCm>UEI83T&}=v6*$ZV0fuc{wtdj$ zo)Rf9-oDc3xg^hCKa{8D{bB`HaA0*&hNrRxK2#%v-}I29nrls+wbbtpg{L#wUYk+p zw9b6_La~+UkxrB}Ha1X?{NhenK+{>J6fr7N%CcqJ2t=gkUVl`=QmZ#=cx|uqETiiO z{z&bHU&Yl2Z4cI24cH#jjsDIT(pmtvR=LKoaRg6;_PFlSnsm;VL&kLBTntR;y>E;JQ<>7PHST~G5 z6zQ6du++AeI0{~xKRi6rkyR+?a=SOV-lMl)@d#AqbgMfQ#HsYxtv2g2WeV^*my zDcO2L)4+G-uA!%zl8_*^l5l{J`8uJ4)%sZ_%?^}}JNO!%UuUB>j%vgbA3iC~rL$3XQuDKRs^sY*X!oalzc5X7PgXn#$L^f)o`$P7s3uGAEovWQF(Ezn~zLk&;rD zsJNPDXlPzOb}R5Cm?7ZGsjn#D{p_dw<17==a}{`m^KhrS7YhWJ6;3{G{sxWWJxyTD zZnYCWd=}-;5!SMg-Lr*V5DmxRc;M}Yq_%wHMcQaPs#)#gFdu2F9&IP%9rJfcJNvr^ zN%;Si&4oGdVL|F5n-L4TTc7o-QOXS3IT!IE5k=Q_17y8Svm>B;Ic} zO2k&TkPs$oL+W1kRP*Y2h7OWW98OtT?rB9FJkoZ}lM*5>gCkeo7Senv_&O#N-r8qF z$X+~6E+8JgC7N6U?>!UpWSwJ$oSvPXd=|n_xX&;()ILw2^$AOExWxF&YF5^tK_n58 zo25bT(&Fw+eo$g+ozZ=V`7w^Myj8{&EfK8PLwDI^uisws1WhuYdSNMgiTydk;pW@XKKTY3Dx_$BSq@v5ymteT_($d3R~ueB;$P{o*$S^ zU^HZ?ULJqjjle>AMvGt}qvzMxvjNTWhzoP^Y{o*Wnpui=qXNp~+B=<6!|ld}Drz*v zaNJ48x433^$%=2}OhxzCxOmCMpHb&r^t#l-HVB{;QJtl0Y%lMs=W@#YGy!7drR`{0 zO*%{lzL#yZ$6~iP-H-2K=Z|&^eHH9C@=jcI#|qG3zTmEYAu9NG>49qUXxx!K^@O}nh1y33H3Y@rT$ z&t}cL7o^4`h#eSsBs#L1$HSSLGY(xIO&Wzr(auv*mG=pZEP7V0a+l1MMc*b1*eR5(2h z$4$mt;a=yU%~$cc2-r;Xog^k`_2q)OdR!--Ol?-H=ou}NVPF<0#=hf78Rf$hwPWS? zIdpbT8=|8`K`*}apbpp#kbEcPn3N`R^$H;)9QV=gR!-$n#Evno+mb8Nc^bBEH5nF# z>ci=Ts*wIBoK|!uiDJNz z*^i?y>jZNJr(LQnV0W+T(ycqm5zD~ZWG^-zP~n)m!uySiA4_;d$jUxu@dp%sGX>(HoS{8ngqEJMyk-ljS%^8*&R-tUYV^w*HbX_)KuD1XYne+XUAg zhpxloJ2{U^J=^LcbK=)vrwO7_60}dHM5^b*EVG(wkT8EblBBwp#-i~Ud@(#5!Ur7DD zMME#D7yd(6|EJew@UQXMh_bLfF~HJ#kl!kJE;fKc(Fh>72fSPlNo?%=?Ce083t$`J z<%WRxp->LMRm=(D1*|QVMvG2%Y}~QQYUm*=Yn3nQ1y|UvSeE*o z@`i}?#c!1fD7QF1LhNx*up98dR~qcO+(hIxm6S~B+__QM8o{_Lng;s{iEaBL)^kLz zcXC~A=6;Huwms?uOa17U_(*d{MJ}>fciI~rlXA;wKuVy9rVXxdY5m!>8UBa{t8h^u zWKA%5PV3_{^7Vph7f#IbTq)>u|BSP{C*-bM1F59{&2-CnP<~j7j97vMh7x>(QnnM} zENv)y{L>+F0oiR=p^jlF3P(^lnXM1C>{lzbnUJWWh~wC9LNlGB@G&P6H7m5@74)HnYMMxre3vm;6=2Js1dM8uI`^ zVkRIS9xfg}6EF`H!fVV9MA!K_OrY$5M-w2( zfXe_o5TFGCr2|kSE+e390Lba!Gv+o167GjI{~43z7rBSuC1rsx8O`76^3ewKwp#mG z)z3)jP+}lx=UPt1ZpXeNN%6#-UnZarJJn&G6`W8LntsA9xjXG=R}7r{C9t z_>mcQDw(S>!MTfXA{S2^#GlUeSj0q;rj2kE!E2MG;-RU`$pk5w5WBofAmVHu<$VGme6Ov3B`<-)r!gb}2DDowbd(MucI(IEz1D z%I1(Nw&D77EryD)K^Ur!jU#d2eOpnn@5^aCve4Y$_^^wo#cojr^)y+avG6AbtQs{j zZqI+kUlZt`9G&1^^q*me$vBBv?7=a{jvBv$wUawK{y+>+d7NxNz$cW001}Dt&XxU@tAMQkbhII2KD~(1 zs0g|ILKu=+xzE=(uwum;m#d>^PkAYP?v7v-QIAKXvPRPx;O-qA!rDa@Tw(B@tGTgD zLw00+sL>mYo^hgciTW_^YbA4P__15@cC~ej`j%S~6QY|N@*0G^&G_tJiKRjcXNyej zw?=f$lt26G?R2T&ib|CPcQ1TgpJB=Cw()ocw4wS5r4*^06+A+kkemQzbdg(WTLdu} zNy14o>!;}sXC8|dpQ2eDRGg;Kd=EU#3{P7!P-iavW5)*1gxCB#8j8N7nv=WJ!_rm!e9B znTlRH46}Ks7XkW1(|f|-JfBZdbl}6-?`FV6xU`3B$xBcYs0Vvhy>H<;%Eof!x9ubucrnbOw?m5%g=l|2zjjaVVW?;vP$;n?OJ;kaU>~*mv$(IK-X%0bPeZw4$UXxK}OtaEbaj+ zKk+_FeG5k1BGZYQhFCRV7l!e`O-aFrLe_A z?;s_!^5|5*7{XefMe6W`k&a?gtur?_uNJ{ahJzlm;8u3v*ZR$q@bW~R2^#5qOK!!d z@*gG|AJKY@C}{a!$__hoNssFUS;C(;Fs=K(-^p>LR3m?jt9y8+mVtkm_(Q97o79%l zQ7(&NoUIKR>S6;WZp#`X?d-&kf#f3YVshX4={WgtfoE62X4oJKiOQ!J#eSN6%aoDW zqS6N#1|lF2q=q4fRbdaDr0=Bfw#^uu(}!X)-X+*|gz358?{k@B?{i&5K2dcNOvzb| ziXg_KjGe<@x!(9L1Wj52iI+yhr8w~}jfwa7kpyH2$M(?&-(_|%H5ZA0H?fuIp)Lc- zIA4c9b_+==smPkvKu37tIQzXpCrhu(;8Vu)qt)-D)C;qI!9SYcDuEc6ktQsjC_}ax zVeX7k1RP(cjHZ1}>>z1&XpVYZway#uYAxfQa)z;Ek~={2A>PjB$4x#}5(wYdJCA5( zh_5V;RyDq<<;lT!fzgVUqH~0?$)7LLg6Nc#?qMCKsKI|j@%$N|zq1|f`4psSZ;<5F9J{OU`ZHoNZ3=A)q*&xUgg>Dxr3+n`UW4o# zGvU4On5@~gKWXB8YD~rj_6B1JZOAbtnyP9T*TgBIzPx>fop4oO1?BKrZz0`EM|ZWx zJS7uas4AhCxB`dgU01=|xLt$9VSim-C5EAG|35fO< z575<2|5M8s@@w|qab9iD zzf7dxE-)V(KPLbk59A^OR3DsN5CBFN!UgbK0GWxL99$;AtOEgiBjZ1SWPfia;kOCD zZzgrVjg7ZaPx{fBwstnm2n*xBj9j8-aOwIC%k1TMjOOL!1l32cWG$ViX>nra{7KR}>Fk(*S}Om~>_cSh)da zLWG#`g6)O-m3R+!hD-2C3E%X=L7>?5hw@9vDS*XJm+;r`r?x5;_bQ zzvI|Mzy&e#Cs(U0R%y7?`sql%Wp01*+D9a`2m#Y4D;3|Dff<&m3+Kk>N^h=ey)ckW`Mph{+q=D5GlA}@c*Dalgtx{4d+pn|AIwf zBAQM!2fG}kAC^EX6>G9E?s6Ej_svyqN5OADg==XreKx|5#NEZ+>*Bkn-GEZYMcFnq zxc1{D>StUX%&smm9G5xCi-8R{Hw~81Be3K8<-mryo5q)CUM%{tEnTSq_eVWhEaTK% zX`IFtAL%3g#yq7^ox0HSNzz^=N}0=ktJ?eod?<=Qju2kNXq_dpp{@YQG4?j|eJqFy z;k#2AV_=h)*jP-RfQ8Z7NI-iHK1Q6GPF_Lq-0Wj+@=(Q}1uiX_lT2I1Xl4xc29NsC zh_&F2#APsx{Bpfojw?`P7&Ewcl>2Pr#586w2XG-aY6 zru-D9SgNx2saiT+s&H0X6L7^G$#)4uE*c#2?S{DugN)$t?ce+?E+?!rs~J*8tF|T{ z`P5&v7;dJ2(t#jA;?yoqy#_>4$=`X0v=h2Kuzt|YI!uk(RVA$)x-_|9(L8tR9{mng z$bo3-;`!X@hdII;lLrB>xOeNn|4N|0Mu*4+j;ob{L+(L-_mu#RK)gmk83LCPgpU`>VGMBNK==Rz zO9%i~35J>gk$=E-0enP2j;^Y(%|DoG4cd}rhhmHSqn}oTwW_5?wk<0TiU}E(UtM1! zdh@(2@AvEUxLj-$BrYboa2I;MPwItPq&KsE7hwf zkD_}nN!{h6IjIT`iWIW<9#^3YftCyVzuP{a?h1*a8bO|W5hdY-lX;5k@H~HAuQCO; zxR%Ju-=Bw?mnUw7$scDqhL^Z3nVG7o_x#AWXbr9Vt`CalQ~BVKd2rl ztTf^@mKedCeyg_m?)84h>zTn%%vcoWC=C;`^#DI5h+~}hiPg^7-&o&YNyaFeFlRFW zlj}i#yS@MuF_@PF3Ig)1x%h!X6O#wWg)xYSmz^I-yyE0E1~AsR`9S}!-uN@+EmGVQ zj1MnJzc#8J&Mr}mwBJ|u9*f8QHRG+=^j9(xSIzP}$0iXEPu+=rwV&|_c)wEKqBI%Q zjr45K9Hb&1EcV&x)uoVY1eGw$une<8&2&hpy*l^v9Q{%;PGJ+{=07Eo0vK@#xxy=d zW3qn5aNTHC{_zt?R)3J+P8K(it7Z&T9subr06;Srpfxy6fOJAGFvJK@9vom3ZoqIg zc|c~40Pui-g!O&EInn{+F@q54Fe`kG-D*BJDKgGl_vUQ7fTkZR7Q@6ys}gp9`Q<5X zSak-KK8vVB(CIOh@9l8S(Duapku2}4Bi}Fmszh3&B_*|G`R3wzWjPQ%LFSWh*R$8N zymI{@`(1Hr8Id|Fo#`o&6vV{T44Uhbw|Rq!2Ud_Q?YU4{{jsq8Ck5OxA93lDUdGYp z?`v!XeEbr~I9)j=y4l}x-Sk+g(owD82^E|AQNIBtUBM%Suh19?DF|T-=`8r$U=vv`tZ@6!EwE zRz#&wW(rlCjkSuZU%8d0Mb^AcoHKxZYIYVQ#)yzWO`3qFpw-wFWLD&?%p37Os>L&3 zJ4{hPso*1i842#V>1BBmYxbcm>}`4D#BVpqFWKoI{@_LRO|7* zpszhb=}B#(qD=foYH7Isl@=^Tguuxqzq)A3Zo?YwbeTb2i9x{Qi#Oubgd_K*!2)gD zqlPZZIsc-@WG^4pc*-$CJ~Cqetf@r}u3RG{3+K7fJ93;ycepODF5NkLy5k?!B^K^@ z)myeOuJbgv2YiuGZlXibyN;#&AR6DbVZ z&T*6QfI<>7MouyYvv_ph>`-Q0NxCc9tL11vsXgB&eM2x=^C>$iUaJ)MNjanCq|jrx zS<8UAa0NRK)+Y;UHmmje3ZrWX1fAJ*k#={f3Z;>_VdbA6MN`P3Z4eziJx=z9;ftW7 z1QlJ_Qp|@6dquC&ni8y~pLL26^2w+T#xDt>m4{x!HY28b*2Y@PqVgVh4-H>F?XuhK zI^8MJKZjoiVa|mbbNOK((o=V4Y5jvCGlJQdBXRxti|v5?3PM#v)9oK912og4ej%(b zGiEzv`}haPZ@t8J?^ClUu6Ad1noxCe^v9;XY(<(+)|0%ISY(8$LmQ3eGE@yr8vk zz1AFnx8XFl&{ch4$mI8+S?Q<=VO7Nu4YWiNN?7=4DE|~+xRODJHsRa1YTHybrmc5_ zhsgbFx0}%BRgd|lM#2^Mz&j7?bh}CMkh(fdmA01hHFB3hPa89LC8^J&7+V{cuz{ zm2XFgqv0y$Enj0KhMf{dTwRvUv63!6XbkGJ<)MrMiumIKu;?;D2-t#Ui z(ZJ0k@cvkgdOK2EnP1)^TH}#y^ZSo$!TbE1nMec~IQXZsqy$}A&kAAG7IK~TLd>rd zw-}Q-O5T0=b`J)BNI`fL*(89DT}>-fdEa}9(n)bUP+U!tB2)yv&A=W+c1GtMtgu%J zLl-A|MOXt0au|%-i6417{zf}tlK!jCau$u4Xgc~wWXU0;cxL)AW+t6?rl&+%)+iP#|?UBT^2togE!f>k@C4 z`stPleImABUVdbrbId&+--``Y%cVhSrKzb+>+}t|7q5oQa2ZkPf)``gTNkTsk90p# zaXy|xn8MI`?zkff6ZULTExYyBKaz!x9uEJGkb?%_$*K47dHFkYH!XYp{yGb;%ZUIH zyhm9#UU9X;;Stz;bX)$PMd1jC&)?3RXF0o)aSl=3!BK>Y1pJ}69tK@_AG7x`*ni1i zMH_&b0+0|uTmU086a)ew#er}V04mN7;^PNyGDcv26FyE(K#~8OqyD$>K1lUI&~S-l zH}Tumi_5lIj}Ku75`@oSVWK|arvA~d|I{Y{{q+u}X%xEkuYvq_1;K2crpr1cJ~QQBBpIYNkrB&Nx0K&Gmg) zaImC?aUQ|?_{U%#?uNkp{`-#z`1MzlY;Fy;`K8$7-)(>&ziVb|Wb)=u&3>IaoL}B% z4}m)Te;$AGSC5-PjKF_x^ssG^{3N&;frlT2?DvhVj2s*x_RxO>cK+{g2KA3w`*j=y zfAtPWBhx?k>tUv6tQBYe)i2`TwlOwx0J4Vvvuce$b?VoFlmGSwz%-j%Lkx`n#>jpB z09r)Yv5CjPfBqo9tf0B6wT(T*z`)kc#K^=9_<@1S#0vc5kE!c4WBFej4*ic_f^AIx zJYkQ1*_tH3JZWMBh8Q?FSy>s`n?wHG@Yh%TsgM77g{}RcHVg{P!{Y;G1PrJE>WVA| zz*+Fid;ak_Cp!@L;{g7608bVttA&G&b%KV1%>obh9W%V%Mf5j`)wiIdoK~JH+&slS zzDKH*JgBS^GG&bRVycb$cz1`MVzSJ$)|eP?dqX=`2Ge*?HxE~JXBu#8nIh33KlemA z*jSrOo6U5r-lUQm9GI-J46`*=)ZzzKOhzesd}ShmM>o!cUr`zroNkzpv>0OwjR>9$ zE^ohd(i|~2>D0d#DL<+&$LV5l)UaNYuEequII%La>d-T}(!PA(-4%$p&A;t%B)OAESceA(( zQ2@S2UZH-$vPy?Y8>HuL=6Cex1-V=ejO;;4Iqqw75uu+SFjXiV{QyiA#wKzgscWA_ zz4x_0?n{TS-T%Z?iB(0|Po7{z^+oODqZzcLOxU}6#AV|o`Cc?v-a}xzJhDWn5UqmA zlqA>X(<74lS-ye5O_%WKdNVA!a^{5kv-5CSe8(X&Cq&nqLjJ-_0NZjQbZ%HYV=PZL zB$xoDPb;@VljH&0vH~BaXr6nf=T$3nT&ofh6@YEI;T$LOW=ob1z_y&CCiIi_4L=xb zMo#e6ogD{|Enk>|)QrSu9Jn74Rm3_amvZMVccO1J3UKl|_NT7rO|-{&BjWU4mr1mi zPK?D6+Xuu6pN3=WadCtvPKKJa7y) znSu}s)=sv$q{RecU0)RA7ML=%m7oA{1<-3L}4Doo!IWZT#QR8Fg=O6l?Y1XBuuZP{NtbU$XA*P^zcQ3f$^BZD4X!tN=UgF4m*BCMoItYq4Kw@at(Sq`ENFh^kC_ zF--&q<|@Xj+X7a}RZ-~qJ`Qy%W7W9Jlvc|Jm~y8?MY^LdyN4`?&!g|ckULecaa<|q zZ_Rp4dB)U&)3*d@ZX~+8X_=n{O_&hrjBn*n%x!-%-EoU1K4eFd4d7|@*;iMQ-qC)5 zO;PU=<|{Io^M0-FR{d_n|A(G{X|~)_*uZ;pf~C8Wz>TN5aVZE{Bg>~&NKvL~D#`lk zXP-J~V5EX$>zmlm+BwH|{Rixt`7Qqel~OnmK{A#4jE$pcHm?AQHEO0Hp^*7&5o!*+ zx41WKz6pPZ64*F1(-P$0G4x4eM&Q{pk>`cZO3qsbQXw(Nj>_g7C<%^}tcd%&3&Lr( zXvNcmcIP~=g(G5TNt-wxTuBZ5o2fLG7lKxU(T{9l+*;C)FX=znz zN?Rk*;JL`o*2EpxcEXfnKXzG(pWfmB1|N(eCNMlF25~EY{OAL$XuFU)oj)??AmJ#{ zfUct(uHWnTo@kxDhtj4;{JoO3cWT?c%cK|A@-MnI_ti5wyG6`qqI_9Oy-2VKg`^qt z!DKY}CC^TnTB>jMws(7Yx9CfmecTcy6ZeFYU?XN{(T6LG$iKU>9eNBXCvo6?_J4VV zO0r1fnzx)ppRN-2_I;Km!FT*0Lu$mfHREm;38-fEO-^k|66Wl44)(?QQ~+gheY!OC z%aH|P`$qNEmchTY+dp*)B|C1R1fWBt0r^#D*+cA{%N*QJu`Evd}xD>!3Pz{>OU@4L=?@=Lv_q_VytDT3r2XEE% z2D@83Jx!H@vvP%r?!{78ZNy&RcCF^U2+5Yi-n+n7*tv{~6g>4H{G<6)+ibBTGBih} zom)wDp@NZ~4!`7ZkMXa`7d^=hPy9+)#j{JoQGLbiXW6f&Us6?LWSF8Z7f#J>yGjc- z>8vFO!qF4=BP6ZIC41jhO~w>!sFdSR1)!|D;;^jDT3ink(ur&DSmvL5{Jqmr$7YdA%Ex-Ne4<(YEbbz-!gKi>y3ETKv%14Fa z#^v;5T9o{j*YNk#D^9L*QUBafB5kNHXKKP?(?=JWGZw;Gnx`#=9tQ}>uz&vI^?Go-mek{Er5Al})(=LV@Q%1x zn%Aq-GW(5rc}CZnd(Axql8wXyT5yAD8=8AMiq%{rpL+Io?Ch-1n+yu6i>6e&K;d(&*&gOu@#{p$rr#o>77{_*54 z7S8d@%vujpB08CP5}u_#FFNI)+L&amy1My?%y|gt<<2G?YrQ77M;rAUhrHgo69g)~ z6sJ(kqm0z73!QLyT_6#{$EUV~Vce1KdpcNXsP3`Et5n=0AF)?tr}x}JD5iv75Hn7p zKGVW8vW0QntW6Bd4*aci$+a#ectZUtNu6PTkp;n@=@lP^ii%!a#o#=R)wfh7)DU7t z-~XIBuAsMz-b>mYA~KYKDS+i7j2Mn$F2rb;LufdgJ6#?o(nZi_K_pn$FgL#FR7h7l z+q*o477zkvkBHCf8TkqG`xEKm%J4te%YU3mjz$h|{xIGDc@ovgqYXF!VM-(*DUbFq zpZ|*!C{RuB&4LK}EpxllGhKP}r}3@Aq&vt`6QKM)H3so|=K0mB#^D2jcRD|oO7(77 zof*$IFhAsvnSp+&FN~{=WP%pZtLz8_UU`%;;yuBVSoj#RKD~Rn;a0`e9=9TWIJH~9 z;dWhkRprijPe5{FI6BpgCuTXb`|YN+qP|M$Jw!M+qRP(+qSJ8 zCp&sMRk!M%cW-_5y`QVKh1S-bWAuU9`+)PoLHoI4hd93&zhr`{48U(lx=F3x7!WPW z3?i2dFB)kxVTXW~IxY4J@mtkb5EBLv4hLI;vQ|&neZ46JD z(i`NzemTYSe_6x^&dj6@icxV?lz8w892?ZH;>MU~9lJOPx`j9&<>9pK83<7Y#u12* z#*@9<8na2)Qjb#omrIN9xUDXQT<99v-J86TGgaujR9|3!Y{> zmg|0=pM;s!HihVp%Y*T>@-|41Azil)Xkm}%4h%F=QA%hbeM_1^UTO!5G5(aR+J}L( zi)#s)9?n3zHo-OsA4Fu@qKmz5H%ce(NQ9;^)QkW>d5)7R^)Il}l0oAg-#c?TBDHBc zj*pACEd^wugd4Mb4H!XW+n6JA){_)sgo0v44@T9rEIASzNygE!XE9KXk0AGlONwoO zI#Ze>(;487)kFFE0RR8q<#7Mpwg1@c|Gf8KO9y|$=zrY%x9s_E7@hK6)~LcMPYn6a zQe(b9K=W_k`1^B)mUPAr9xfL4cC_CG!M`%_Wc4lkMKOdO-8XOi_Rb&yLP^UbUxc;< zJ;aZ^==rcDDU|S%GXDxLB+cn+dd8Q_2~YaCM0qPws@;6}cTijWSNlJgD^khs$K^hj z=F2@Z-Vok*St}RmYp+xGW>bYj-6hIS*$&YcZ$THa8ngq<)v7wlUK!|5AWDJLoxS+*TL8%_AjBiWd| z=InwTMPi)gjCzNYb67IzGm8%{NqfURa4`)rfW4PA>7+a-hd8k2=90oE9(q5?@4SIh zlp@x~qtQ_&he5B|u8zd0Pf7*VvYR_BYOXMn`$0<gxAWP&b7@&GtC40jrHO{+3ki_Qgd4doZu^5+5QQ^f z?MCE-w3bsT*ipy5Hahu+hXdq}Pl_m3y3d=+1PM@v+>z6zh;=rrKU@+a8@VoKuAQS1 zb2VTj#KA=Sg_zJd$pVj>^c@+bH0ui3tA{Ms0^Q;b#cH4O2vc%b990o^4C{~aT^dk) zBTuXTrIqo$h*d(LGRz1s(~rmaQZlfa-vvcd)Y;aD?%}C|#LQ;u;M$4aP-Zac;TlEa z=@OAq98_sn)H+dZ`*Mp8?FW?)FqSNZ(9p(K;&4WQdAMJR@b#WLP=LI>TX3WIvX_v6lP zRIi7tDsx=WSDOq1Rwb&Rhz=zcstJNJLi6atcKZS};k(9v8oF|cfRTj2onR#x!VRJU zbjIH(*<|x7@fdT%WRA~en0sXe(?ttB_mDv$K2qLX41X(}w!x%Uc~UN4#GFJkchwyN z8a(E$Y2}DFo*vMg=>YjY3pX+JpC_RI%64eyn#Jf)A+AEO;$O$t$qaaZ;DHOkBw%n zobHn_!m4{|ZMY;sX&S#UwK<+Q%Mh0u9N?jr1vG#VZG53p^P^EvXp&HO=%EdYdC{W) zHR|8_Xd&}=a1fiVL!D#HTB~+yEVpD)R-6z*qN>!-EQySVYa-dtqY$ZgA47UUhGE)| zmTnox7AGI-;SMA?QXvTuFYrnc)#u?-_+*GmrPX-5fpf~bB$nxuM8FD+0eqXTC{$+AM?kDvwYIn8VaY7x#1h0*A@Kdnj`orXuh{W-ry{}85z)a?_y?MI z>GZ)`(97jwZS8@(fs<~GqjcqB^1M-&>ZOEu>H}0$sfcm}ZfKg=PZh3;)G;Mnu?RAm zG!5_uu@41)R=C0gk5?Njyp?n7L|QO!j89 zO0}p-vA>d-HjgnHMM|GSTT<8Ys6-s_c^?#wG>>`i4rnuV!E2ls6WxaR@Hxi#TEEJYCs|2KKAikP zND@EDBDLh3AjMrZvASQI_@k4883pId#rm0^p-@{;xL%2x5v1}<-5I}Cj+#z`VPpeO z51rP!)YfY(O5iSnDS{FOqx}U16pWUSoEQ?j4)_A2n_~N96ovNW?AsCt?h(Qf!2Khpdz=CCYV}habn^h>owr`EtIgLr1yw z*1s<(tvxu&SK?LD+56dHd33J{S!4h8M*n#qK;lCys{{4U7o7XSjFC1dp>4HG(<&)$ zH;P-Fk8qYVh*H9rn)au7QvtJFa%hpALYHzpr^9$XCbi<(%XT|UB{ydA3l)rx6(SLf zN!yS~U8|v%1r$n|=0nG-NF&6Op>z#rqCd;B65MAa=EHK#J# z!!7+J!2Y+IX1p57Y{zKIu67P>BQ-(yubKw@KJRedK;v4^b=(2Q7?@1IAFr$D8h+Oo z0fUp-GFxC?tp!P4xF^$iIeE%w9m?!n;27cImQ z?3TEL+GcOGe~9*^6cI5n={K4>bhlk(Dlcf$Z^IuYKnASu-6r%uq>#djZ>Y4{;o)uQ zE(esZ^4!|^Epjjl8tJQ)XHD?0okrSP_4!3`L*)%IDOO_PZeFKjN?v5T{Y%fd?5Zy? zT^O#Dri&zF+ER&2ZN;cFOo-^#DCmH|o{`z1Z{ry77?87Wq_>O#@x#y4Wl8dG*lDxG3W!LV}?ct7%hATBh1LrF;i_CyCMQgQOaiaJF>LGjobEQFNt2$BanZ7KYdzEzd zq^q@7xo{VFrPYmQt&5NRYr@2(Q;B|uTK&w|)vHy-N8(GtS3(;6vHJ?jVv|<=99vnb zpDS|6A#=QMditTFWXU&FwY!CJH^9zEucGFw0D~S!TV3X`i;AJIiv?gTWYzoyy=(0U zM^_yeE;Ce@Mob|e)CUmEjkKYg*%FQB>}F-*rHqjc%_s?`$+rebJfDRN!RYUzE|{?< zG{w`>+xQ7KE0!x{D-z}7t4lNuD!TmkbOok72BcLELp{8ZSn*?8q(|8VZy{Lalp)c* zk6EVpSOJ-UySS!{Cx{N)iQ`0(-)@zM1is~D5s}u+m%lFGQSl&@Sz!&wksg~nc^XWT zj&qh9Z5GHsT>y$o5*X-7N!uQ87}|bpJ&SF6O4XfbQwtvj2k4CFF%3YVx>K|!r8>2V z$uc;cv+%yKGSrwQ`PAMln%(XFZeld z1Q9)?66a9K@y?N=nNafUVye$fXT;V{(on8E0}MefF`%kPPoX4Y$_VCCV^JFV`a$#u zjk+o&;PDAVTkGZ$C4=#rVt>Pd@Usry_%m{GFIYs@gQb}Rv;mB6edncx3?3C?RIH&b zR(@3$^-NNc&TlZmYzthKXMRT>H=01%)P|HgE_DC4^Gr^Ex)RkAE%+QKX&^B7ZM|bryCD z30@kQ8CB&sw*gn1bzTu$1KIw1b$q;I%CAZ1nAj^(Tg-}1K*{*eSz)9IloklEJfA~NK)8?P=^(3ggq-bIhw zi7p)25%;_bO|%gS&lfhz)_E?eFH z<u4u=wKs7-nFw)}1O@u#OB;Q*=t55|)V^wG_${l3{|x);cO(!EiIY2qjqJNG)2JhV zGNVWikPT_vBj}UrJ~@YX*ns%Ka@0ERq3h+UjYtw1Y8YGXQIoWmnQCWG73~q)JE}s! zyBNb2jOT8HKqu1JjMAlLD-g3Cam;&(LL#(@;wS(A4epdvjHExm!Cm~D<3jhpg1fn) z?f(Gm+V!aD*CuS$ z0mI%z5XmZ7===cFuB*%;Bb=7RD1!1)`u z_a=gfH7_`naU1pV+Gr<67QC=8A4O0*o||66dQJ8Iv>sg7(KHBd=+o+ismkW8y}dRg z?eManO-Tk`R)^!*h5Oo{7`xHLF*8KyJ27o3CFLUHR*WmTqnLaun%^=&Fiq%6IyD{0 zo$B6!mzhnx$K6cbr~_*;nWtTr_8jZW!@amhUk8W9)HKaTc%l3JGMp7 zu2~P~#}m+kmtlg>*4X^=3Kw)v&#E0IfYJp*2;lHKceRo9!Ui2k)g1ry2RI@@L*m09 zc*feDe0^%0Hh)0drVYZN_ANV}umm}TVtDNPIcV9STF8A}TZi!0%@<|$DJrZegr}mm z7hJaY-R#Jd_sp)O!A?nx8@pxdV{!(@9~9Ug)yxcHBbuG~pd;&te0IgzW`ZT(M7XNB zG~jo3^!M1Sd93y68o(Or)B(Q~+fwF(183xcQ7 zd7EDS9-BK$72RFH3Wl0r*MYD+?_iC$Ov8{4V^S4M(l2q1BVE|n2=h~%kZ{r#B|E4d z7IRQa&f&?pNU#C2y)ts1Ve1$7P@l@I${Ne?#HG||xWUD$1~9V?%HB|QbNKO%K`ntN zDoYxIhF&ipqDI4BDE~h6aVZC~vR{n$M%5d1ZhP=CTSjjFE2nUa8xbx>pwzlfft%O( zF}&lfICCyv23N|PGrogNgkgtmr-(xC%8SEmLTS6T3me02^BGZu?>JOM$#hZ@6udY` z?I+YTa^3|Wct(Sdb&&1bWWdAPfI2e?&-Ow#FeVaxbc!L=3m4RZMy9A4njMNajptGK zqx~`|gX71mC+^9QL_wHYX@~4Wh9xKDmu`XC`BQ{ z5SgiaVUz3KAThzxP1SC*H)DffV*S7e9c=nX*52RH*3Lop{6`YY2+&#ZCTqyxGg9{P z`WF)^DMcQ_IIy9ekaQF+6k%x{kYi+AzbaRw%#T6seUYr)w0&L@0~6j~tX+;CBw~vT z&2j6qcO$47GczE3v6beRgLrGlkf@k4sx4&2nKc6wCy>(V{ReXS{X#^@i+ z{tZU%;-Jve;x%faUumR!7rO$&zKnTH;U^LPuh7ggpM)rs=M`k~#y|A=5-`;Q?{Xw# z*i6X7&j|#5RPxk|UAWMv!rEs2wI~R~77jxS;5ks?)EE z$lEpU=>n<48%fS~wGh=ML&iv`IDL9|_NY*RTSDL|Phm~|GzLNG_{lDq3sH|!VmmH0 zCkP}GaRBzw6QfGVdBnnzC9`E9LKzOsFSw9*C1BhMX znuJlbEzeh+mPmW&IGIMLnJ%ZJqCYUQ8Ks*Wb zvZXJ;9b_SGN2zx#*#0<#cHQTQca4uC{lj>srUI8D!ft2COw-(746Jkn z_0+FI&lb`A1~#|2RN3^uXMexqLNg7Lx2MByRXGP98FYsn8Ahp+C;8Z(DhEPrEV;cc z5rmQPs$rNP1HY5t>bvl5G*z=O2QdbYOR~YIruT@U@nL+%L)q{Q`zaI8Y+W#ZYp?qL z-z}W~Ow>6^81ucpo5PZj008v=E0){Xn_C+HYnEQEs%^a~hLSU9s!m;tGXl9t=Yx?E zpl*PsWdU7+f}PnhyU6MaWZEb?nz7d0pk>L++)zI!MCfr2PjRK39~a6O@`>G@!uLY| zK+4PPRlh6;HI`KxuL`ItjI8cv z{tIf8b$EHTq^*ud0oLId^%$Ty#Ghdt@8@RD_)6tnKShnPkD^ZfqWD_MBMAbD0a2Qp2v)YrZ(Fy4 zQ$Shrppcbw?j<5?uPx~~XDBHmQ&N;kx3!<1v4T11yL|GTc%5QxWvE1CgI*fBQt-ur zyd?dB2|u=3U>8=vjjNWu$R5W8Qr)<7NdVuy1KyMDooX6H8!XE^585TxPV&BJ_=X=* zubNf>DQ^u%KdNn8wF}u`Jquw>SH4Nb+jHht^|f(d+vzn9FW)(yo(MT1KHl9WWn|H* z?}SO4-%FXaFs9p#0rnsV$F*EmI&3(Rlb4I4XFUYVq=3H~I`k6fl&!ai{6cqk-GS7A z6-j=ar>^J8cNtbw%9V+@v;XCY@`Cjs7f2LZSOW#W4`Q-)q#1}B4ZYcwr3T$~$jn#& z>@6(AZ?|5V!|QZJe+1SlS~t8*RbvJw?lL+Rs?$Qj)$NSW6Q1VRct$mh@g|PfB3(w} z%0TPV#h$zpaG?wC;kL?W9nmv@G%}6*?D@|F$vveDXBM|+w{iz)=%SB;n(aI*@zaOC zOvY+j1Fg>JGjpHkNbNM=@=<#u&s!}SdUOU@`5kq{2kU%m6{~){#3=X!7cs?}S4<&V z3FYB2M~XDx_lRz)){OqIx2K&~;cVsetBNrkTFj87?zdL`=2n5niZK>^JYkGJiZakh zCroPrZvu&NjNsjI8C?Cg^XiJIIB%yF7Srlzd^QN2ofo?Sb;&DJ52$7{J=0Oa6t#ZG zU%2kUJxADGaEa7aYGCTUDzs@+>a{28wY!y)$E#FsJ7-;SvPC6U zT);S~tqV!I47v{H(}aTqX2a)8XyOe|>_~O$*yTx+1R*Ka{2aDL782Occuu8SXG|C% z`*>B=_yVWzXf82iFAZ3^LV{DL;BVR7>PGu1GuAM==lV^Hd*n0)aaRG%A!-x(SKhnN9&ylN_jf}22OK+$dQCoSkk-|eX zW4MPkJhq;4#?U3Fy?`ODX|^!rf$G57J{I$%Ki+#N7Dr8Y)2HV=#4)bqS}^IV&7aym z6M7pupFJKJftS^!o(rj=2|Xo=?{u}%U&MJ;j2seBM%(z4r|({=7UIqIgN zH_*pmSYeaf&)b3GsS`@{v*W*~opHV1jxgdLxrj?*_pfa|x&2}dL_$BqULJZ?qX zvtY`AZ8r2UF~uz@puTsMCK4PAcW|?;yS8dK{ec3wA&dGE#LP2@Y4r&c z>(V!2SU3nW(hZu4@w1j2bQ=l;^8G2Q7VUn31L}t$9V#d+IfEKmI}Z33Em~C`a(q@< z;yW4EK~Vu&cz#IhWdG<(WfjA^Bt3N%E}rcT04*F*$`uNmLMe0#P~|xhCq)%Q_}dnq zlO&T^0=_)!TAk)4X9+~G`&Ih^x>TUh|aUei1O)!eSk&YQIBDHEvC-V7q?A}VSGPN1J0f^ zp6xj7oxe;(G@2o$L4F6PhvW+80o*FEfqxUqs~{y1h9wX&-qcp{7GuC~ED1YEACE&P z(J-917T-WuNOCSEe1}BoB`H2oFliTSw)ZD%1lTTO8}eZ~a*v}~M);*K2_R3wH4XSd zeVDv+T8j0vgb_IE>kkJ5p&$wDr0Z)_)w_Hlv2XdM0G=MD?r<8xmTO^4q;8yW8@T^| z5EQTf#sN5~%ED8%2?a9!k*vVQcKb)?2A%uPhP++tFh_6g=;8(BA@$8wsgo>wZ^-=u ze%pnzwXfc3D37j}IocAP{DMKIJ!x$h)}?#zj517O1gUDfCVr{JqISppj88dppE5-F z!n$S<(83Ff@+T-XERM|EhBusxbB-)}Lwo#*R+a(`1;{)4f*_$fog#!uyJ<?z*$*pff6jrfj~cNoy~gCB7oDO~l!P!+;=#31$Kel5Cxm7qGX^X>luu1u%WR ziPq1X&rPI+0lrB*Pf$6Bym6Fme*FNeq5n6{(b|Q+E>5$=H{tWox}*Qr;g|7%H9!D^Hxz+aZ)EKZ@Sq z{Axo^qHg~95dd-~5r9(|LRb|zO&C=83}icD0LO|f2}Df$V3`JocY&L^76&5a2gYuu z<^k(=SKv`JPzDuMQBcqW=m*i4?`D&=N@4hg_Mn#E$~Vi!%H~{!j|mJ+!kR>)^9J%q zPSxkQh%t}6u~fu&0;|k}?Q5bKy~0>g{l;ycHh6DnXkIIV9^@Y|arkw_|O6{Uz3z^{qnH)##{g*;8C z)(VSPTpWC%dJ9;8V7Y3Am5NwdLyy({iBq?Rm$e$*Nv{l*j?q=|k+_y)>-C;LK70`> z@%(d?bo%zW(9&$txmLp_hfPeVT}bGManB-$?i`$vvGH!OvT=Qj%P>P(%9Ne6UNe|C z$^@#JZblKSr3}qG?>Z`6*!6vBV*+bjD5)s>Wzp2!+Q6SR^Wjt5ZG779%K~fiw#L); zMqwJ{PMZID!0*@BEF3_Fv}=|EqQE;4!7PW52+F;G5g@wWorY zAe9ev%Lb{?3R-rmWx?E3UO`0PUV|oyWAmes;Pc1y>k^CX#4K6RQfLdG-;bTG6K;Ml zFv5k)5X#Hqi-W=gV!TozD}^1Rn%kw@IiU!Q2d<+rS`;tgnkL|Fs_5A)2%-{{EOZ9) z5pe+@gw`2sdm*_lykOizQLbPiI zh}4E!hkXeWK!B%2g~*8G4UBWu-{0qRw`hsk%59h#$ni7k?+`Yt#tN59GAda%Du#v= z5P-sIHcEgID}0)obyjpTlM!#BS&)4uO8Uh3g=@5Tp8*!6oO#uy2>mI)hqvoNZ&RC+TCUxN7V*UC-p1(4H3tYZf0EJ-XbcAJ zMkz?}$WuB{m4+zf1&CM>$#=fZ%RD48v=$Q<>h&nf8-2>d_ZrvDTWF^=PhH#WKk=e> zeV)w*_ibiS8<7yR2bAl^ax$pPN_G8G@LTcEv|x>VUPb)R zNZ$s=ogL*#tf-RfLFN_rE=v+qyKya-J!wDpGr2zBi=VG7IEW3vZt1mak*_5^jj5`l zouQ)2oomHI2jmkU^E@yT-jC~iV*=qI&=V{2zS4WInU{{N8){9)K|HxP%6D-NZsxL| zb{mye;hv<-tjhcg1ZeFQs@i8-N7YO|pZE<~9m3C(fo6xl?U;JXkeG6^7Lqu-OXAH~ ze@i8UY;!&SVYmLg@f1%v;#Ym#J9_(XeWU+0XTG*u9ggo}Q~9?!qyAg5=|7r94j$F& zJCWaN62AJYUxpoTyCB{n)DNU}!A|0_LSAnU>KqJx6fFs1&7{PX*z>(T%n7vA5#2w6 z&kcWEz4Gp`8($p|<7kXTemeR6@M~SE0QNcy=J|Q*Sxoy*88x+cQEu@@gI6$+(I3T_@vX^eONXrbyXE;0cfxBz@f>$9WVOVKp>fSkHSej->Mf|yk zjWFMj-1TM3Tejfg?w*GCDIndar2^)4IL-V*U~U+Dn3Jt_;REzaLlHgaxxhTtQ5mK` z2mJwRb;DaB*PYcRRUZd(q{|T++h zTwcrBva-h%y@vfKBZJTrEwFo#{jYWAP7LpfCe}Li_0>VZJ>C4xJAA?b#b59%|L)`mi34j~=?52aYB>;ry3IDUuN&`-sjRD2yn~6q0U-@AQ06Ua@ z!!Kk5#1o8t4~)o;BN_Vry`iN%87t++3*uV+ipIDE>m57yXX{lIJCKw=U;Z+JZIlab zG%!YcxVo`r$4^DY_%igNsEBylDZJ*^cXjII;R2L*E@-29Gg5b?-U|9uUQ-8>QX}Fn za%6zlDB7;)$$v06Qf{{GC#Gh!GjilNx%T7eIJWdK1-sMUVWvwxDBw1SacRKTgMWHR zeNcmNJ@sd=*YS>oVYlH;R12Gxs&s;k>YqX~I(SfX$^y8`XaJp}qT4dJ3Y6yc`WM&L z&7WrpSFf|GZ7%4HF{q;uGWSscSpjM1zWjpColVE2Y3?(7pVkuLF!%tQJR^8Vk?rR> z+-4_w^_L`)L~k}39kvC&1Puo_cN8A3!_zXPT49Et)o_40rERk`6UL8sYc(lgUP~?O zq*eUE?eyTOIJp{){qvDGb&4u4WoE5*g&D6uh2b)JW;vZByi?RX?TqojJ)h*Zu2nCt zDQtRjzVK$HnMmBl+-8p6)=~&X-9hud#T6SfQ;@zOSk68>O^ub!YZx;--DFceDeX~{ zFuTdT*+1pRlE+qTBi6+ZmyQH?f72Hxr%_-)<@5&X(5Z{XyU$C>Cx|lyMp&?s1JZ0h zJd|8dwk>I?DWj*;?^Ep2#Akkx-W&E94Fojl%Tn}xHI+aY8f8Qqo&hiZt?odlBkCY1 zl@D9A_D|Z*!mer=W))d*Fr)ZB8J8ge_O{bR<*s#e2@+pNt7PuS623m`obJc?#n%|q zYW*M5TYf)%NBkwjy#z!JRytij;z%rv#HC)}L2h6yw@o}$y^S~mJff;LLV1i43wWyP=9L6k8oQO=irLfpMQJ-{j*2?7g_4{;@;=- zn`)r`O?vtphx;Fq!++MY9Xzg7WUV&@5W2zd@Izh*D&wPdM~6g0=V$e#pl#tnjVHCR zHX@6^&pt#*`udow;itUEha_xwpj)yw+MDfoW%L0sb;pmjxp(w*^ho{3cs2UQ3+lY& zeXJr~z6hC;2_63ZS)-PiyjeA_a#av&S{W_=6g56$o^I%ig)TcgJGjhPZ5cO$6@s6A z5Li4cFh>|8h?m71QlNH`s01cVsnY=ploq1!zq%ac4WO_38Cy3AW}pm3wx3u7;djT0 zgfXzXV+rOoA^tI5?ZDar-rp8JT7K6XIqLD#U#tEag@`gbpH_n$CqNJ1v-jsQXGWjA z)IsYuA_me-+ zH>L^DDL=XCRV{e!s0CDysS6?rC=D1Xvmd6?vO$B>kizBe`}Fepy;Ab5NkjT-0Vud% z(1NO#v{EB*W;4)wtX${XlWLnT>O3KCE4n8jbWMFa%(r?~r|lWxJVeQI$_ColoSbbK z?PF*?vsuozk7_SSzNe^}Z73zbIw1A2L>X{I8z}K-epq190f5yliSb$gHAId0dx)B^ zbn&XikbS@;vsKHMQk6tRIk|M!5-_G&^^V_E-v6oL{PiRjVW?y_Y*?ybGCX*WbR;+k8O)V zRW^>s>`|#ZbouWP;KqFIy%W1vcDEHerc^|w6H}j>}`T}&?+ibDq!1DztNlC2W zDqi?nE#F{lN1CrlU(O$SnV)XHnBBXnbAG+=+4`Qw*X;kdeXs=n@L~McLSy{4k$;C` zTSH5`Z)$;Se7#)|15C&dlI=b8RtIQ!1;}!x&;{7+^~^$+AR}B73P!HE(pqGT+Ui9b zj^WRbll#4&l8M&G%{1cyC2Qbd`eXI5v)@OGP6Ko65T1Hd?{hp?55Yb0!jVu5_{=39C&Al zG3TCZUd5?an%DGwK;Yb=)Eb-m*ET*u$nmo4+YsT$vh~DtP zsx^OKhDpXMrG4m5XUawRjeR9sN?;vzu6Y_}kDB{~C`G|7>j-XC7DBQxQxMU>;euMb z>nK{U^m%Nr7<3lW)Q2j`kx>V?h!(2|m8&!_$W2(XeWr)vjqnfw?>vD#h3$m+D$qsB z|KI<(<@~o*<#+Z%_N|2QU6%eYdelFB(03jBue?R^KY|_jL;lluBkZVr@m!!19WRF; z^xZ^t7f5f<=L+Gfu}TC-8u z-a55|mn%)6#xHQ!i@=ys_7|$fsuONqpv#@!{U?uB-BeiL%HdC@je3kaq$)Hs!6!m}Oa(2?WwOsB;2C9m^{9GX$;S zv>n^}?ShX5v7{tsR!nc9`6KKv7N@r}kaKHMMsmC5ZeJF<8!|Wdvmt=ys#HGdI#@Za zXjT^WA#7aTNC8zQnQ`u$WK7_S?8M67psbkM#l^ooaaH<5RQx>XMV`LXn&%E|a9}Fq zP#QKJi{nKCw`;+XA8BBb!qq#Zqi1n|ZWnHmK^!XJXyQhkn!FX=?d6mXozV13huK8Z z{1usjI?Iw;mk_TuqRx>GfRP2Y)HM~J5_%kbU+ERovk=b!4bKKW0CFImDY^$9t=hMe zyFCI~DR^m>*)7lTeG+ADX3XOyShcKW2+9oB{zl3s4<=DoBcAYGR-Xc%iXKbxk~lik z(HqjEW1%35K?GlnRhOD_IbcNPM4P2|t6+=v_}VWs*&D169{OQ54r+_?`m6wk&o}@U z=CFeR=2}oI1BrI0FhsNR+R##@&Dc&sKo5kJ2Zq@Q3vT>ja-ok%ex^Tpnt8VX>qJIM zz&QX@6uAAiJS5BY2L(EIS}N;^UFu&A&}pa)BC2^l^Z8vW`q3c3t{A4pip^_CnR>EM=X3m;FKF ztm7Y4$?M5Yo=lJBb+)xH4o*m36}5|*9f_JtRQf}AQ5idV8VTa}5EIj+r|&}U&`AC< zi)PW4-~P>ERCOTR<52Kk<{BtxHy)H!)Ij+}Wg=mI#~MzhD(Z8At!MjWmqeWF#l@g@ zr~R8vE$PdGg@CZV(CU zrH5Slg|5nn;tvl=th*VL1sSM~ava^P$#W@t2*&=+ha@nwHmsVDU5S^rd4eO6z#4uz zNlg{t{aBwo#dUm`dx!l>G5iyY#IzoBe*ft1<}Xbp@enlimQ_b?vvb0cX5QBIXYTMi zw5XP$51aGCe^U-3b8&7FYDmAlCoH#{aoI5dpZDv(`%M4<3;!Zt@;C9Czp=ob|Du8a zmqP#m_*Zt=-=s^NOr7mros9oirIWu&m;Cc~LkCNJX9rVbeM37F{r`z@sRw|l=lkv9 z1-=QFtpD^t|LYt6g@S2o68s$`eUGC3D@X!!a)6#z(t_xIF_?#!`HO;CjzT2e*coVQ zNo;A{@|})pd1!_T(T_=Gc-(JKV(aVt;^lirnf{*Hxbz**Eo*L(Nh!nDP zZtO~W@?t}|7rJ!r)xpUKJYAU%ZqwFI{US9#f`y*t~0LC%p++tqO&{*NCHIJoz)wNM?Nf^oS z-O-RMi60=V>u>Jeh~5SUzn0iXKh_c_;H4ctO*)jPFzXW=a>qZau7pxSqoXY)V+MNt z8x8SPHvNz4=BULT?|7-HMwnn;Zg_Xmo}p0$h@}QJ7)_1ErkmT5fY&X+*nt1`$bZ^?NMRqChZ8U1tWl_j zDs4UhP2uC+*4y#c+nIPIJ96@Y(3yjq<+fD>H4DJ>sPH8~lX_pr4)U=_c2$GLQFM-w zNid&^9AC)8qKLlLZb@hzZHvFq>cVlRa^I!&7dC0F8?;@BWK3p^Zz$oO| zJ!_)xdHH9B^`fQKEZ}`VyI_kZXJ2y@ELD{47xX_Z=wCGu$o{7dZESDrVCrJ&V(Di3 zZ54(#9?q8k%0jM0sIn*g4t=P;4TIzV`5pgi7PcmTiCqtTM@Kgj@8Hzxrz}rHgMgL< zY;Z6oP=kP12560suA$Ra22!Wagrx)#c7n1l{%)dwTJeNJ$sPv1+?`2& zE7(cGsM;lZvs_F;Lhb1`I=FyZ#elz%Fs!My5q#{N`g@jGb(xn1MSV!d@|C3#Q87&2 zCcz1GBe%hxl!H;VT5KR*9moVrZ2b~%UFZz5Gq2<6M+c0yTC`V`NfX)ygtz9gxm`qA z3Z@|#?1C5iqS%r7H1ZE$>K6mIc4+$5-&SNG3DjAmS!+tK8pxkMC(n!SE~>eLPg`}L z48PpgGCX1b&!ziU=STQI{r10@K>B~Vw0~2vxcru;LWC+GC;MJC_V0j-`Tynde_ya@ zr9P`c0)%e*Lwug=gkx~0Q_Fmmb>kT(#W6T*mNH3AsjqG;EH1JwCHlVS;jymg5-teX z?Oa4paA8-2Ta%z}Agrf)g0n?okQj0{?Ih?wk`uWG3Qb*SG#qP8(R>IhGSigEFJ*fr zIuqUwL087e*T5jnrgZhuuC2DcTxgxY7CMcZCc`6<)IVc}DbUvn3{{*}z3UpeVwQ~s zthLZe6f2Q0s+NkJnC9t{lc)xJdC}(=IBea5ngj#=<=gO`u}o1W8}+@^3gq>wnNsB4 zPVCNF)t?(Z>v&{|qVN1-9;mABumfc~IQ+PG?sgW-2{VxXhw>;sW2=c9=^*1i_Up6`% z3hxT>&1LrbuJZklHu~2i|GGLR|Ipm(Mj!RVsDk$zy(V(x!+KFL$Q%q;BzMaOp+o=y zRWBe}Mk$Y5ta;vfhLx;ta?{QWfp6i!neOsnwwIGTRr*%-X!d?sD9kN-ZG3(o8H3d2 z*5;mT8DA786<2V@0x*rg>gej0^0He^dHCb?d;SlIQ*610)>ddAQ$8N{&oKaoxvdkd3(R>yKEXVZSjp%E}l9 zU&Pti#$o!5ai3tVh7TC0*f53ppx#DHB4M%(Y`}H*_^2sg&cGw)huP({Lhv9e#y+aL z0}a)-X$Mec0fL?;nJ0{Bo|Y)a{IP-9Ol*4C1vD2rC{#c0gQ2$MP6@*Q~HKAFx-JEYplZ4*{jsAS|U90wm-He5-K5C4cZ#yF1jhwlZ z%^aeV+th%tO|X=$Ip$n+zy2s2K^L26Z$Kn7*9~m9G5h@wH$KTOfSPXLW5ZH!3}pI` z)z0F(nwx%*QSimLZuDYfqh7lpXq(@}eq;C${B@!Zjae#D$G|fK0xQb{u}90Z zZNN9?y#cYH+2#*sO*)@KjjruS`zZ&{E|$6Uy6d`Fr1tR9Y4$zpuJ^kXx_Cf(e8p?v zc#Q;78=NTRUG|Prjj>2Gh2snsw(h$Jm{WF#N_N55LjwyZEN>BeE30~`JTwBCHcXi= zSC>&eyqei`r(@@@&xHpgXQ!n2yx1{h*@-dwzi%V=Ucz3-cGu=ULYw5&W@3Rb+A^kn zj~f^EqSD#g@9%zh|9H@B%Ye>)pw;X)s#M=$&L$i8WEtZ#R&kRPa*N{eFMO27M(%9>EWSIcqVxzDj(tKN{)S+ zh=`U(qn61J9d9m>SIkRRGn_GUrf1bNVrc>|8ljmIqKN2kNm~#IuE_DSMd)e+tWUT@ zd9L)M*l%v;lALROeN21};B=G3P)`5#QWw0)qBqlDS>CMWn{nza6pFKhqD)f zx6mK=XaeHtd+4_53|9&3HBCL^GEqO^+jwLP#>%s82WKZ=sI7e3lT=9Zf|rH#iWD~L z)RAT}NW`~60T!|0Ok5v{CE$9=P9G84P>}ZswpZzIVpFTgSY|+~4_~?|$by z-}%nD=gaCXmfrR5Lxbbe`qU{d59)CLs%{M`Z<(&veJJtWyQRO|&OE*Tx$%uoE!a9_ z%*BMdxmPRy^nJf6DXoTOwVCtmymm7W?x}V$d-1ni+Nqk|?`m{;ZqB5f_>Et#Kl#D8 z53B#3Kli`mx&JHs{HK#2dGBP6`|f-%_l=2rR()Et@w2a9-D@9c5KQ=U*q%?fZk2bm z?v!`D%JSpfFFRK6ZoPZV;dXbIJ=nAHw`;o4zlgBMD|KjsInS)PGT$1!NU zZ4`CtDei&)ZiT4|J@d*Pelo0K1MTFrQNA(g;NU%PHHWv*1KvW&c^VP!^AAKR=Cp3R zQeR+VH?`l{Dz8+x3wNl;#?6eaSY7Q`?T39&ZW(!P(as}>PRu$#capZ+h3)O6KgG@#wwd$~@B|K4DzxAG?j+ zwy)l}M!na+FmJ>0<0Ds|Z&J6-oQGcd{oSXIf4Z>$ybl-d?$>Hao92zaS8}_|8nR;2 z>~G^ci-TqzsejV`E$5zR^V%NXRjh9PvVF4Z9Db$p;i~TWu~q!~!>?|u^~v=nNEPwd)&U2UVvAq>I(E%34d(sZd`P1*$^8z; zR;b#geM*_Lr!IBbbN{e*wFdps|MAqEw=?*Y{bPiN#zz;fElO!yzFpepvxz^bZ;k!z zJ*9f-OUv(>5J>788(2B6+?r!oCs*J2Qs#3F>V5a?&{1>i#vHxaxAU(DE+1%FDVQ7E zsBu!;Quk-=sCw6DrB7E#8Qd~Hn5?gyHvNyKzx^_2dLRAQ(vQsjwc^;*7d~ipuz8(^ z6~64br{*2CTP?h+)43k=TdiKZN1Aq_Lf@6u5BxK^cF7x?=8ro(KB33<*0YcIuhwYE z#I_YnOdWNm#N{RB4=q{{bLg4T9S&Vs(IDfir01R<_rXuGHI6O(?47Q6rzCzm@Ac}v z*Cmfw_voe(f7N{a%3~kjyXKMgW16VT_cW-N7+kgQT7P}y;t~mcjvOg=ZC|-fSw`Nh zQF)gp)xCVRRQq?HJbZ0kkAd9KNmnxen%L_?xgA!oj}Le6D{nYkr1EJYW+l2Id)p56`#wE zV?G^m^~RPiV#@^&Ke6?EO#SJtK6tmI^-aH*v-b{|QBN-SzeDnw^(o8upPKMvl~Fsh z#@$#mxPG~fD;CFp-%Xy-bZxKL4O>zZD$gqQdS1sJStSlV+G+3ngoJHxC!gE8b?C{L zUplpaf9{ZSug^dIWqh=)=66c6Hz8eY3Qzxf`^S z1CH07a0RP1#TCl7`u&n*>_Jcp^HDiC@aru#rKb#NLU6RuVi1*Ll3%+buey%{OSXFG@(t~(7XLXfPy9j0t82f zR6{8YPD{%_KhAvyqzRL{N$+kz*Bo-#QU}M^yJa$TBb2LE+r$eX@-`Po@}|K z9J!pz+ZlTafaBmS)b)UVfxihSg&0#xQf485XU4P`T@5ctk38GYW|algoL612NeS2To7R$c!72j>w8dLEQcMW5w@6 z+n0w4vD_M8;U4^SD>Tu%6Mc@N2nCQwyiD^ySR}9suCl z9=@B=@gf4prH{xMOlkha)4iYg9r{!UjrGcN0}h#s3^&}$KUF8P#%FEIo6`ZZ41%Lq zkAAL02Q(~1RVftr$CNsI0{+?&_-o!Ll!yu2~XBBYA`wVykXPVu;`+fjutaW7u}1 z-psF1`A>NGB%DVsN1pQ{-F`b0PIlHf7ZF1imY71Nn9ty5;)eoRgtwx40Yv^V@D&xK zAR9yhQmFt*i*Wx>%Qr6o0@=#|)y==01_dM<8wbfr5mDGfCPTk-k=Fg;YDgxMV3@XGnJh1Ig2WWoVlUq=Dupk}Csyzn0{O1R-5C$mei-aXarxH9w~2HR z!8VZym9rEJS@%qa(~%iPQVmTP48@cf-B1ikK|sK*%I`vW`POJo;Y)J#x*)`l*x2u6 zi|$}YikELUI*4eAHpenZ6T#Y|EUG%sh%Be8hO97xpzuh0uP8_e4zRfY5I z*@D7X4dZ(_Z$%#w9sns`zEkKR3~TU$z#%QJBG??V<#M(mX@hYYb>Y979$~XkBNNF=#MHN zg+835Ri%6Q68(K@!c)TvxVrKs{jEkD3SYgMVwox{**c?ZyvS>cZ7|3{txLM4aQPXC zI7#E9un%h~Rk7jrZ0T#?KOAl!g5din9mKLVa1#t3R!rAr1GY^Tbcwf2f&38h4?M>f zT_0YEk1QIN50h`J<<|8+a1}`L%6B~-1Sz!{iBT*=k}Z*wkyzJcWY&Z#2$C&pimVH! zW)_`qa(fEjaC=s6*2*m6T#(}ByO<6lsDiEQ$WqPQJYz_bCO|b1)AOj>4 z{J5s5aQpBs@*;$e{{`b4>+Nquf2&?UG%8#li2fd?gGi>XNt(=QJk%EUR*_{6`N0$h zMvb#1bU*x+RCKTsJ(qa~xsXfux*>MELIf-1qN5t zG>K(k(~x7%urV?tO}!F@{$9oC8cDt!N#5!z714+G$sfPpqF5k6ip!Vi1KWcN!m_%7 z^sSn}=t#{Af!YeX9jnFYi@7!uRLqPyG{aiimuB(m^a_ z4@Oc#@Z>BLj)|ZuwqlDmBQUbX%E+j0!2E}brf}1FhE`R$sgK{+V&e?3BgK_3=_>U! zN~5J_riv+m&GS0X%FqLw(R3XhT4xnbVhsbvNi(9+hjKcls&G@kVeC(}PJ$gNUcQge zK{%b08H;Bzjxq|SGmHRV1s)+USh{8FGRwmgkHU1mXhh~Kcugm<4{@uLhLI32DPF#x z&_Q(8vN=wXL_w5w4bw0MQZ^Yz;#8jFVCFS}kH&RfYg71o^?~T`wExxWt|K@^ipw_$ zY5z$F!4#2~S-~_F&C)PMv~8pnWegL!MRl1oEm6VH6-9r0Cp9u((d$|fz9p`5?-8B_ z1s`z*QGTX_SQ@lcXqJa z_u6(CD@bwq68UzfgP00(e5*WU%o~_@TOz}#iij}-9~c`hti;(IUv&AN85NnY>pe{cK`vMhnP5d#WMOYH-9kcDmX#G*kR?Mylmnx(6ou=WJvMSx zO1OQPRI&P6%wYqhxO|B|{6GgWFyA&TL4nI>@{-OV(7`AoDl~EX!n`4hsaM)_ z6uw^lCGtIXybBw7d|OHf(M-``WkbNk1zr|>26Q$C|HBTe_^N9Hx}j2ZeQ5bSg|BzK zAbe-;UovwYq)Cb^-ypsXr-PUx9BhU$HBB>ZL<(d?lQdhz#8yK<$+o!sxJ@WuQq@P* zK5MlN-I^3{RTIZi`C7Ul!gg}3C}7@XF}%sBHfJh~j35BCP~=&|6ho^jeCW@f6uBzZ z^0-#S6s?-_*!z8B0s&H7zWJ(02N94I*~Tp2lpq?LbPI_=CTUjHAuU)b%R&P@rju0l zOt+dxNEHPIA90bYrcI^th4sMj$MG`aWVUK)ilNHH4d*!plT2BX3{DlI=#}-SMXpNY z!bG!peCth1@fb<*il#3e1d{}Wa7(vwa%59$*8QF?Vr-Se+Mr)22S#)@^bD#R4M zGWP6_a>W7xQe3`4u)jtJQ7~IFL|7i4C8M6ov#Nn~>8h!spR1xIBGWh~FW+iQsUV7~VRDC+6kbK-5g`E1lvPcWb(N7sMHUSSep3`)XaBb&S7mqu z(nP+Eg(m06gDWXszFBmTq9txGx_ncYQTT?BZ)4NG;ljN*qQBX65Q#&;iZMmWHVnf= zl$0fFg91Jy4$a!8j$nZ4@#2VRx@X0dXE1pn#VeW??@;-g8hWvzhpz!r}6=LeE{ywM05XO{k}PAwEgP~f3bvr5 zDm+w06fHz2g%~Ro+pB$anCvd7TK)f`X5@f+*MNAcig>eqjib#7l}{O135I z3(b|tsl?Vh#@yd4>9mKG(4}!t22Szra zc?^a`0aFlL<26h_;2SBrs7K+|m;Zvo*E`oHrgP1rrE|+enxwdVNmpr02SGpPRIF&3 ztcisMtg&F}ShKLApqYxnA!q=HLk;UHy;h%HPX=33+^T~3_CCET%!y@m7e%!#+qQU_ zlQ0jov8to73NK+Z2H7kS#RTH)Hi~G%9k6rRYrif~JC`pJP5m#aAS#D2C0s@Xs5#z3 z(9goEuK))UdoT>pWlgMGd*-f%&$TzxstUK#V@i$K$D>=5;_@Z(Jx>R*5DH*5GMAKO zL`*CJVKmK@B~He28p~t7kGGU4Vguv8rijM7>_SveJoCn`IS5&k;_@Z(T}uZMNDRPW z83lF-wnsNq_~Z<4Nm#rPSe;dQGMgxR*Iu?WGG9AQ zuUU$0vpPcTmVyYNgWD269$goPvPse55&|rx$klf%ya{!c=?W^gAf~U zn<^|Qyibgs*p36XSgtlj4bBnlhHBX!uai`@?E0+Jq>6%qkGO&;$p@%>5yKF8tV-H~ zfZ#L6aD<;3K~d3rG*dMcj>9ANR8_bsy7PNVRpDK`m-fmZ1zcUxkgl!LLC}64?jj-p z2#rhm>!_-QH4>~%=?sTymTa1)C-y?D2rzG5Rc?JE+aLE*s!B@r>~(dv1Kzq5AG|Q7 z4n}Tvx@HXZOe+1-Bj=?!7h%)N2Xot9rrzcE)JV7cH<&s*F=Hkqh-5=EDK#7d0cV40 zHXY>ldrR-V3FYhzEqTZ*mHg&pYMu?ej$}h$zo_b=^AHt-Yy=OriKGvNHmjxw=S?aO zSS6)e13j+OcpS|zt=QiE6@fuY!DDhnRO|;&yOI)9(}sJf_}>#ObCZfAmU#DeGtJ2G z=!gF;o@$;ulYMs@X{I$WV?;`dHXQHec!2yA3R>V$c-servkO9Rcis%zm2+>y(g=)p zEbHG+e<0HBzLyn;WQ4wD;~)l^O3M<^B5Z~V6tv6{Y4FG`k><-JB8va5QQ6c9dy_`AbZEL|1 zM_y`c9=m&22E3k_67VH&-tABu6!HEjDV4hk=}7Hf_}e%4G~N6W+Ozyd%PaBtuzLp% zOdpM%7lW;V14DGu$!L;O{oUCwLaC;Mz=O9q1o7#aLUFXP{3b5L$jO-(t%oD+WsT82&6;%M3FK5#J$Y@mX&zF z&PAB_uM>fJ_<2$;=}0F^5jcnRn?->uoUdQf_c&znC*fPEUIf0*NlGq90^vVPiJ&+f z+tDEWyb4tS?*Nt6I0CJZ6E?iu{AX;q+;$d<<0t5S(0;+yBCz=v5nSg7BQYGEk!hv4 z$A)=n`cIM|SdIv%+#bx&)gw&53tBm!Ci#O<{gb&x)%RL?35@E2z^C}H7nXFYlULUQ F{|Do68TkMJ literal 0 HcmV?d00001 diff --git a/data/simple_add_calculator.zip b/data/simple_add_calculator.zip index 20789d15ec59a45b9cafef3bc796985e565fad6d..05cc1cc6cf933737594b4e4bd7270d4cf3ca94e2 100644 GIT binary patch literal 56538 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�k`l$bNP77p9` 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 literal 8105 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�v;=JNQFLwp)B2zTelPChT8Mx8E=s$EA5%7w&h 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¥3C)2XL(kJ+9LpzVY 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 z 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 literal 0 HcmV?d00001 From f460771978c9997f8e654b14416d42b15cb6d315 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Sat, 13 Jan 2024 10:58:49 +0800 Subject: [PATCH 050/101] Update test cases --- tests/metagpt/test_incremental_dev.py | 81 +++++++++------------------ 1 file changed, 27 insertions(+), 54 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 223b0fb10..44478adc9 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -100,6 +100,33 @@ def test_refined_word_cloud(): raise e +def test_refined_gomoku(): + project_path = f"{DATA_PATH}/Gomoku" + check_or_create_base_tag(project_path) + + args = [ + "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", + "--inc", + "--project-path", + project_path, + ] + result = runner.invoke(app, args) + logger.info(result) + logger.info(result.output) + if "Aborting" in result.output: + assert False + else: + tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if tag == "base": + assert False + else: + assert True + try: + subprocess.run(["git", "tag", "refine"], check=True) + except subprocess.CalledProcessError as e: + raise e + + def test_refined_dice_simulator_1(): project_path = f"{DATA_PATH}/dice_simulator_new" check_or_create_base_tag(project_path) @@ -154,33 +181,6 @@ def test_refined_dice_simulator_2(): raise e -def test_refined_dice_simulator_3(): - project_path = f"{DATA_PATH}/dice_simulator_new" - check_or_create_base_tag(project_path) - - args = [ - "Add functionality to set the number of sides on a die; Add functionality to view the history of scores; Add functionality to perform statistical analysis on all scores. The original dice rolling game could roll the dice multiple times and only display the current game result. But the new requirement add function that players to customize the number of sides of the dice and to view the history of scores and display the statistical analysis", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_3"], check=True) - except subprocess.CalledProcessError as e: - raise e - - def test_refined_pygame_2048_1(): project_path = f"{DATA_PATH}/pygame_2048" check_or_create_base_tag(project_path) @@ -316,33 +316,6 @@ def test_refined_snake_game_2(): raise e -def test_refined_gomoku(): - project_path = f"{DATA_PATH}/Gomoku" - check_or_create_base_tag(project_path) - - args = [ - "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine"], check=True) - except subprocess.CalledProcessError as e: - raise e - - def check_or_create_base_tag(project_path): # Change the current working directory to the specified project path os.chdir(project_path) From f4e39a462ceb99dee6dff5bd523ba0c5f72695f5 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 16 Jan 2024 15:42:48 +0800 Subject: [PATCH 051/101] 1. remove INC prompt in some ActionNode 2. replace Code archive from data to tests/data/incremental_dev_project 3. update test case for ActionNode --- metagpt/actions/design_api_an.py | 47 --- metagpt/actions/project_management_an.py | 60 +--- metagpt/actions/write_code.py | 20 ++ metagpt/actions/write_code_guideline_an.py | 3 +- metagpt/actions/write_prd_an.py | 27 -- metagpt/roles/engineer.py | 14 +- .../data/incremental_dev_project}/Gomoku.zip | Bin .../dice_simulator_new.zip | Bin .../number_guessing_game.zip | Bin .../incremental_dev_project}/pygame_2048.zip | Bin tests/data/incremental_dev_project/readme.md | 3 + .../simple_add_calculator.zip | Bin .../incremental_dev_project}/snake_game.zip | Bin .../incremental_dev_project}/word_cloud.zip | Bin tests/metagpt/actions/test_design_api_an.py | 3 + .../actions/test_project_management_an.py | 4 + .../actions/test_write_code_guideline_an.py | 91 ++++- tests/metagpt/actions/test_write_prd_an.py | 4 + tests/metagpt/test_incremental_dev.py | 338 ++++-------------- 19 files changed, 194 insertions(+), 420 deletions(-) rename {data => tests/data/incremental_dev_project}/Gomoku.zip (100%) rename {data => tests/data/incremental_dev_project}/dice_simulator_new.zip (100%) rename {data => tests/data/incremental_dev_project}/number_guessing_game.zip (100%) rename {data => tests/data/incremental_dev_project}/pygame_2048.zip (100%) create mode 100644 tests/data/incremental_dev_project/readme.md rename {data => tests/data/incremental_dev_project}/simple_add_calculator.zip (100%) rename {data => tests/data/incremental_dev_project}/snake_game.zip (100%) rename {data => tests/data/incremental_dev_project}/word_cloud.zip (100%) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index ee1941350..3890e656a 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -18,14 +18,6 @@ IMPLEMENTATION_APPROACH = ActionNode( example="We will ...", ) -INCREMENTAL_IMPLEMENTATION_APPROACH = ActionNode( - key="Incremental Implementation approach", - expected_type=str, - instruction="Analyze the challenging aspects of the requirements and select a suitable open-source framework. " - "Outline the incremental steps involved in the implementation process with the detailed strategies.", - example="we will ...", -) - REFINED_IMPLEMENTATION_APPROACH = ActionNode( key="Refined Implementation Approach", expected_type=str, @@ -63,16 +55,6 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode( example=MMC1, ) -INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES = ActionNode( - key="Incremental Data structures and interfaces", - expected_type=str, - instruction="Extend the existing mermaid classDiagram code syntax to incorporate new classes, " - "methods (including __init__), and functions with precise type annotations. Clearly delineate additional " - "relationships between classes, maintaining adherence to PEP8 standards. Enhance the level of detail in data " - "structures, ensuring a comprehensive API design that seamlessly integrates with the existing structure.", - example=MMC1_REFINE, -) - REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( key="Refined Data Structures and Interfaces", expected_type=str, @@ -108,32 +90,6 @@ ANYTHING_UNCLEAR = ActionNode( example="Clarification needed on third-party API integration, ...", ) -INC_DESIGN_CONTEXT = """ -## Legacy Content -{old_design} - -## New Requirements -{requirements} - -## PRD Increment Content -{prd_increment} -""" - -MERGE_DESIGN_CONTEXT = """ -Role: You are a professional Architect tasked with overseeing incremental development. -Based on new requirements, review and refine the system design. Integrate existing architecture with incremental design changes, ensuring the refined design encompasses all architectural elements, enhancements, and adjustments. Retain content unrelated to incremental development needs for coherence and clarity. - -# Context -## New Requirements -{requirements} - -## Legacy Content -{old_design} - -## Design Increment Content -{design_increment} -""" - NODES = [ IMPLEMENTATION_APPROACH, # PROJECT_NAME, @@ -143,8 +99,6 @@ NODES = [ ANYTHING_UNCLEAR, ] -INC_NODES = [INCREMENTAL_IMPLEMENTATION_APPROACH, INCREMENTAL_DATA_STRUCTURES_AND_INTERFACES, REFINED_PROGRAM_CALL_FLOW] - REFINE_NODES = [ REFINED_IMPLEMENTATION_APPROACH, REFINED_FILE_LIST, @@ -154,7 +108,6 @@ REFINE_NODES = [ ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) -INCREMENTAL_DESIGN_NODES = ActionNode.from_children("Incremental_Design_API", INC_NODES) REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NODES) diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 559c5ef8e..8b0196707 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -35,24 +35,12 @@ LOGIC_ANALYSIS = ActionNode( ], ) -INCREMENTAL_LOGIC_ANALYSIS = ActionNode( - key="Incremental Logic Analysis", - expected_type=List[List[str]], - instruction="Provide a list of files with the classes/methods/functions to be implemented or modified " - "incrementally. Include thorough dependency analysis, consider potential impacts on existing code, and document" - " necessary imports.", - example=[ - ["new_feature.py", "Introduces NewFeature class and related functions"], - ["utils.py", "Modifies existing utility functions to support incremental changes"], - ], -) - REFINED_LOGIC_ANALYSIS = ActionNode( key="Refined Logic Analysis", expected_type=List[List[str]], instruction="Review and refine the logic analysis by merging the Legacy Content and Incremental Content. " "Provide a comprehensive list of files with classes/methods/functions to be implemented or modified incrementally. " - "Include thorough dependency analysis, consider potential impacts on existing code, and document necessary imports.", + "Include dependency analysis, consider potential impacts on existing code, and document necessary imports.", example=[ ["game.py", "Contains Game class and ... functions"], ["main.py", "Contains main function, from game import Game"], @@ -68,15 +56,6 @@ TASK_LIST = ActionNode( example=["game.py", "main.py"], ) -INCREMENTAL_TASK_LIST = ActionNode( - key="Incremental Task list", - expected_type=List[str], - instruction="Break down the incremental development tasks into a prioritized list of filenames." - "Organize the tasks based on dependency order, ensuring a systematic and efficient implementation." - "Only output filename! Do not include comments in the list ", - example=["new_feature.py", "main.py"], -) - REFINED_TASK_LIST = ActionNode( key="Refined Task list", expected_type=List[str], @@ -101,14 +80,6 @@ SHARED_KNOWLEDGE = ActionNode( example="`game.py` contains functions shared across the project.", ) -INCREMENTAL_SHARED_KNOWLEDGE = ActionNode( - key="Incremental Shared Knowledge", - expected_type=str, - instruction="Document any new shared knowledge generated during incremental development. This includes common " - "utility functions, configuration variables, or any information vital for team collaboration.", - example="`new_module.py` introduces shared utility functions for improved code reusability.", -) - REFINED_SHARED_KNOWLEDGE = ActionNode( key="Refined Shared Knowledge", expected_type=str, @@ -126,32 +97,6 @@ ANYTHING_UNCLEAR_PM = ActionNode( example="Clarification needed on how to start and initialize third-party libraries.", ) -INC_PM_CONTEXT = """ -### Legacy Content -{old_tasks} - -### New Requirements -{requirements} - -### Design Increment Content -{design_increment} -""" - -MERGE_PM_CONTEXT = """ -Role: You are a professional Project Manager tasked with overseeing incremental development. -Based on New Requirements, refine the project context to account for incremental development. Ensure the context offers a comprehensive overview of the project's evolving scope, covering both legacy content and incremental content. Retain any content unrelated to incremental development. - -# Context -## New Requirements -{requirements} - -## Legacy Content -{old_tasks} - -## Increment Content -{tasks_increment} -""" - NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, @@ -162,8 +107,6 @@ NODES = [ ANYTHING_UNCLEAR_PM, ] -INC_NODES = [INCREMENTAL_LOGIC_ANALYSIS, INCREMENTAL_TASK_LIST, INCREMENTAL_SHARED_KNOWLEDGE] - REFINE_NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, @@ -175,7 +118,6 @@ REFINE_NODES = [ ] PM_NODE = ActionNode.from_children("PM_NODE", NODES) -INCREMENTAL_PM_NODES = ActionNode.from_children("Incremental_PM_NODES", INC_NODES) REFINED_PM_NODES = ActionNode.from_children("Refined_PM_NODES", REFINE_NODES) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index f92b72f7f..ce0e2fe3b 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -157,6 +157,24 @@ class WriteCode(Action): @staticmethod async def get_codes(task_doc, exclude, mode="normal") -> str: + """ + Get code snippets based on different modes. + + Attributes: + task_doc (Document): Document object of the task file. + exclude (str): Specifies the filename to be excluded from the code snippets. + mode (str): Specifies the mode, either "normal" or "guide" (default is "normal"). + + Returns: + str: Code snippets. + + Description: + If mode is set to "normal", it returns code snippets for the regular coding phase, + i.e., all the code generated before writing the current file. + + If mode is set to "guide", it returns code snippets for incremental development, + building upon the existing code in the "normal" mode and adding code for the current file's older versions. + """ if not task_doc: return "" if not task_doc.content: @@ -173,6 +191,8 @@ class WriteCode(Action): union_files_list = list(set(src_files) | set(old_files)) for filename in union_files_list: if filename == exclude: + # Exclude unnecessary code to maintain a clean and focused main.py file, ensuring only relevant and + # essential functionality is included for the project’s requirements if filename in old_files and filename != "main.py": # Use legacy code doc = await old_file_repo.get(filename=filename) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index 528b4e8f3..e4afb393d 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -9,6 +9,7 @@ import asyncio from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode +from metagpt.logs import logger GUIDELINES_AND_INCREMENTAL_CHANGE = ActionNode( key="Guidelines and Incremental Change", @@ -455,7 +456,7 @@ async def main(): write_code_guideline = WriteCodeGuideline() node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE) guideline = node.instruct_content.model_dump_json() - print(guideline) + logger.info(guideline) if __name__ == "__main__": diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 7004bfffc..4830076e3 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -130,14 +130,6 @@ REQUIREMENT_ANALYSIS = ActionNode( example="", ) -INCREMENTAL_REQUIREMENT_ANALYSIS = ActionNode( - key="Incremental Requirement Analysis", - expected_type=List[str], - instruction="Propose the comprehensive incremental development requirement analysis on new features and enhanced " - "features for New Requirements.", - example=["Require add/update/modify ..."], -) - REFINED_REQUIREMENT_ANALYSIS = ActionNode( key="Refined Requirement Analysis", expected_type=List[str], @@ -194,22 +186,6 @@ REASON = ActionNode( key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..." ) - -INCREMENTAL_PRD_CONTEXT = """ -Role: You are a professional Product Manager tasked with overseeing incremental development. -Based on New Requirements, output a New PRD that seamlessly integrates both the Legacy Content and the Incremental Content. Ensure the resulting document captures the complete scope of features, enhancements, and retain content unrelated to incremental development needs for coherence and clarity. - -# Context -## New Requirements -{requirements} - -## Legacy Content -{old_prd} - -## PRD Incremental Content -{prd_increment} -""" - REFINE_PRD_TEMPLATE = """ ### Project Name {project_name} @@ -255,11 +231,8 @@ REFINE_NODES = [ ANYTHING_UNCLEAR, ] -INCREMENT_PRD_NODES = [INCREMENTAL_REQUIREMENT_ANALYSIS, REQUIREMENT_POOL] - WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) REFINE_PRD_NODE = ActionNode.from_children("RefinePRD", REFINE_NODES) -INCREMENTAL_PRD_NODE = ActionNode.from_children("IncrementalPRD", INCREMENT_PRD_NODES) WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON]) WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON]) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 772cf0944..a6b51bd00 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -119,18 +119,9 @@ class Engineer(Role): self._init_action_system_message(action) coding_context = await action.run(guideline=guideline) - # Get dependencies + dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} if guideline: - dependencies = { - coding_context.design_doc.root_relative_path, - coding_context.task_doc.root_relative_path, - "code_guideline.json", - } - else: - dependencies = { - coding_context.design_doc.root_relative_path, - coding_context.task_doc.root_relative_path, - } + dependencies.add("code_guideline.json") await src_file_repo.save( coding_context.filename, dependencies=dependencies, @@ -344,6 +335,7 @@ class Engineer(Role): return self.next_todo_action async def _write_code_guideline(self): + """Write some guidelines that guides subsequent WriteCode and WriteCodeReview""" logger.info("Writing code guideline..") user_requirement = str(self.rc.memory.get_by_role("Human")[0]) diff --git a/data/Gomoku.zip b/tests/data/incremental_dev_project/Gomoku.zip similarity index 100% rename from data/Gomoku.zip rename to tests/data/incremental_dev_project/Gomoku.zip diff --git a/data/dice_simulator_new.zip b/tests/data/incremental_dev_project/dice_simulator_new.zip similarity index 100% rename from data/dice_simulator_new.zip rename to tests/data/incremental_dev_project/dice_simulator_new.zip diff --git a/data/number_guessing_game.zip b/tests/data/incremental_dev_project/number_guessing_game.zip similarity index 100% rename from data/number_guessing_game.zip rename to tests/data/incremental_dev_project/number_guessing_game.zip diff --git a/data/pygame_2048.zip b/tests/data/incremental_dev_project/pygame_2048.zip similarity index 100% rename from data/pygame_2048.zip rename to tests/data/incremental_dev_project/pygame_2048.zip diff --git a/tests/data/incremental_dev_project/readme.md b/tests/data/incremental_dev_project/readme.md new file mode 100644 index 000000000..231589028 --- /dev/null +++ b/tests/data/incremental_dev_project/readme.md @@ -0,0 +1,3 @@ +# Code archive + +This folder contains a compressed package for the test_incremental_dev.py file, which is used to demonstrate the process of incremental development. diff --git a/data/simple_add_calculator.zip b/tests/data/incremental_dev_project/simple_add_calculator.zip similarity index 100% rename from data/simple_add_calculator.zip rename to tests/data/incremental_dev_project/simple_add_calculator.zip diff --git a/data/snake_game.zip b/tests/data/incremental_dev_project/snake_game.zip similarity index 100% rename from data/snake_game.zip rename to tests/data/incremental_dev_project/snake_game.zip diff --git a/data/word_cloud.zip b/tests/data/incremental_dev_project/word_cloud.zip similarity index 100% rename from data/word_cloud.zip rename to tests/data/incremental_dev_project/word_cloud.zip diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 5b016ecce..c729c047c 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -101,4 +101,7 @@ def llm(): async def test_write_design_an(): node = await REFINED_DESIGN_NODES.fill(CONTEXT, llm) assert node.instruct_content + assert "Refined Implementation Approach" in node.instruct_content.model_dump_json() + assert "Refined File List" in node.instruct_content.model_dump_json() assert "Refined Data Structures and Interfaces" in node.instruct_content.model_dump_json() + assert "Refined Program call flow" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index e0c1381ec..68d73a3d9 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -141,4 +141,8 @@ def llm(): async def test_project_management_an(llm): node = await REFINED_PM_NODES.fill(CONTEXT, llm) assert node.instruct_content + assert "Required Python Packages" in node.instruct_content.model_dump_json() + assert "Required Other Language Packages" in node.instruct_content.model_dump_json() assert "Refined Logic Analysis" in node.instruct_content.model_dump_json() + assert "Refined Task List" in node.instruct_content.model_dump_json() + assert "Refined Shared Knowledge" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index f8a1ae626..f3fba26cc 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -185,9 +185,94 @@ if __name__ == "__main__": CalculatorApp.main() ```""" -INCREMENTAL_CHANGE_EXAMPLE = """ +GUIDELINES_AND_INCREMENTAL_CHANGE_EXAMPLE = """ { - "Incremental Change": "- operations.py: Implement the Operations class with a method to perform the requested arithmetic operation. This class will be used by the Calculator class to execute the operations.\n```python\n## operations.py\nclass Operations:\n @staticmethod\n def perform_operation(operation: str, number1: float, number2: float) -> float:\n if operation == 'add':\n return number1 + number2\n elif operation == 'subtract':\n return number1 - number2\n elif operation == 'multiply':\n return number1 * number2\n elif operation == 'divide':\n if number2 == 0:\n raise ValueError('Cannot divide by zero')\n return number1 / number2\n else:\n raise ValueError('Invalid operation')\n```\n\n- calculator.py: Extend the Calculator class to include methods for subtraction, multiplication, and division. These methods will utilize the Operations class to perform the actual calculations.\n```python\n## calculator.py\nfrom operations import Operations\nclass Calculator:\n ...\n def subtract(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('subtract', number1, number2)\n\n def multiply(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('multiply', number1, number2)\n\n def divide(self, number1: float, number2: float) -> float:\n return Operations.perform_operation('divide', number1, number2)\n```\n\n- interface.py: Update the Interface class to include buttons for subtraction, multiplication, and division, and link them to the corresponding methods in the Calculator class. Also, handle the display of errors such as division by zero.\n```python\n## interface.py\nimport tkinter as tk\nfrom tkinter import messagebox\nfrom calculator import Calculator\n...\nclass Interface:\n ...\n def create_widgets(self):\n ...\n self.subtract_button = tk.Button(self.root, text='-', command=self.subtract, font=('Arial', 18))\n self.subtract_button.grid(row=3, column=0, sticky='nsew')\n\n self.multiply_button = tk.Button(self.root, text='*', command=self.multiply, font=('Arial', 18))\n self.multiply_button.grid(row=3, column=1, sticky='nsew')\n\n self.divide_button = tk.Button(self.root, text='/', command=self.divide, font=('Arial', 18))\n self.divide_button.grid(row=3, column=2, sticky='nsew')\n ...\n\n def subtract(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.subtract(number1, number2)\n self.display_result(result)\n\n def multiply(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n result = self.calculator.multiply(number1, number2)\n self.display_result(result)\n\n def divide(self):\n number1, number2 = self.get_input()\n if number1 is not None and number2 is not None:\n try:\n result = self.calculator.divide(number1, number2)\n except ValueError as e:\n self.show_error(str(e))\n return\n self.display_result(result)\n```\n\n- main.py: No changes needed in main.py as it serves as the entry point and will run the updated Interface class.\n```python\n## main.py\nfrom interface import Interface\n...\n```\n\nNote: Ensure that the new operations buttons in the Interface class are properly arranged and that the grid layout is adjusted accordingly. Also, make sure to import the messagebox module from tkinter for error handling." + "Guidelines and Incremental Change": " +1. Guideline for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. +```python +class Calculator: + self.result = number1 + number2 + return self.result + +- def sub(self, number1, number2) -> float: ++ def subtract(self, number1: float, number2: float) -> float: ++ ''' ++ Subtracts the second number from the first and returns the result. ++ ++ Args: ++ number1 (float): The number to be subtracted from. ++ number2 (float): The number to subtract. ++ ++ Returns: ++ float: The difference of number1 and number2. ++ ''' ++ self.result = number1 - number2 ++ return self.result ++ + def multiply(self, number1: float, number2: float) -> float: +- pass ++ ''' ++ Multiplies two numbers and returns the result. ++ ++ Args: ++ number1 (float): The first number to multiply. ++ number2 (float): The second number to multiply. ++ ++ Returns: ++ float: The product of number1 and number2. ++ ''' ++ self.result = number1 * number2 ++ return self.result ++ + def divide(self, number1: float, number2: float) -> float: +- pass ++ ''' ++ ValueError: If the second number is zero. ++ ''' ++ if number2 == 0: ++ raise ValueError('Cannot divide by zero') ++ self.result = number1 / number2 ++ return self.result ++ +- def reset_result(self): ++ def clear(self): ++ if self.result != 0.0: ++ print("Result is not zero, clearing...") ++ else: ++ print("Result is already zero, no need to clear.") ++ + self.result = 0.0 +``` + +2. Guideline for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards. +```python +def add_numbers(): + result = calculator.add_numbers(num1, num2) + return jsonify({'result': result}), 200 + +-# TODO: Implement subtraction, multiplication, and division operations ++@app.route('/subtract_numbers', methods=['POST']) ++def subtract_numbers(): ++ data = request.get_json() ++ num1 = data.get('num1', 0) ++ num2 = data.get('num2', 0) ++ result = calculator.subtract_numbers(num1, num2) ++ return jsonify({'result': result}), 200 ++ ++@app.route('/multiply_numbers', methods=['POST']) ++def multiply_numbers(): ++ data = request.get_json() ++ num1 = data.get('num1', 0) ++ num2 = data.get('num2', 0) ++ try: ++ result = calculator.divide_numbers(num1, num2) ++ except ValueError as e: ++ return jsonify({'error': str(e)}), 400 ++ return jsonify({'result': result}), 200 ++ + if __name__ == '__main__': + app.run() +```" } """ @@ -207,7 +292,7 @@ async def test_write_code_guideline_an(): async def test_refine_code(): prompt = REFINED_CODE_TEMPLATE.format( requirement=REQUIREMENT_EXAMPLE, - guideline=INCREMENTAL_CHANGE_EXAMPLE, + guideline=GUIDELINES_AND_INCREMENTAL_CHANGE_EXAMPLE, design=DESIGN_EXAMPLE, tasks=TASKS_EXAMPLE, code=REFINE_CODE_SCRIPT_EXAMPLE, diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 79cd913cd..693f4924d 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -85,4 +85,8 @@ def llm(): async def test_write_prd_an(llm): node = await REFINE_PRD_NODE.fill(CONTEXT, llm) assert node.instruct_content + assert "Refined Requirements" in node.instruct_content.model_dump_json() + assert "Refined Product Goals" in node.instruct_content.model_dump_json() + assert "Refined User Stories" in node.instruct_content.model_dump_json() + assert "Refined Requirement Analysis" in node.instruct_content.model_dump_json() assert "Refined Requirement Pool" in node.instruct_content.model_dump_json() diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 44478adc9..8890137b0 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -11,24 +11,73 @@ import subprocess import pytest from typer.testing import CliRunner -from metagpt.const import DATA_PATH +from metagpt.const import TEST_DATA_PATH from metagpt.logs import logger from metagpt.startup import app runner = CliRunner() +IDEAS = [ + "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", + "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", + "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", + "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", + "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", + "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", + "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", + "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", + "Incremental Idea Gradually increase the speed of the snake as the game progresses. In the current version of the game, the snake’s speed remains constant throughout the gameplay. Implement a feature where the snake’s speed gradually increases over time, making the game more challenging and intense as the player progresses.", + "Introduce power-ups and obstacles to the game. The current version of the game only involves eating food and growing the snake. Add new elements such as power-ups that can enhance the snake’s speed or make it invincible for a short duration. At the same time, introduce obstacles like walls or enemies that the snake must avoid or overcome to continue growing.", +] -def test_refined_simple_calculator(): - project_path = f"{DATA_PATH}/simple_add_calculator" - check_or_create_base_tag(project_path) +PROJECT_NAMES = [ + "calculator", + "word_cloud", + "Gomoku", + "dice_simulator_new", + "dice_simulator_new", + "pygame_2048", + "pygame_2048", + "pygame_2048", + "snake_game", + "snake_game", +] - args = [ - "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) + +def test_refined_calculator(): + result = get_incremental_dev_result(IDEAS[0], PROJECT_NAMES[0]) + log_and_check_result(result) + + +def test_refined_word_cloud(): + result = get_incremental_dev_result(IDEAS[1], PROJECT_NAMES[1]) + log_and_check_result(result) + + +def test_refined_gomoku(): + result = get_incremental_dev_result(IDEAS[2], PROJECT_NAMES[2]) + log_and_check_result(result) + + +def test_refined_dice_simulator_new(): + for idea, project_name in zip(IDEAS[3:5], PROJECT_NAMES[3:5]): + result = get_incremental_dev_result(idea, project_name) + log_and_check_result(result) + + +def test_refined_pygame_2048(): + for idea, project_name in zip(IDEAS[5:8], PROJECT_NAMES[5:8]): + result = get_incremental_dev_result(idea, project_name) + log_and_check_result(result) + + +def test_refined_snake_game(): + for idea, project_name in zip(IDEAS[8:10], PROJECT_NAMES[8:10]): + result = get_incremental_dev_result(idea, project_name) + log_and_check_result(result) + + +def log_and_check_result(result): logger.info(result) logger.info(result.output) if "Aborting" in result.output: @@ -46,274 +95,19 @@ def test_refined_simple_calculator(): raise e -def test_refined_number_guessing_game(): - project_path = f"{DATA_PATH}/number_guessing_game" +def get_incremental_dev_result(idea, project_name): + project_path = TEST_DATA_PATH / "incremental_dev_project" / project_name + if not os.path.exists(project_path): + raise Exception(f"Project {project_name} not exists") check_or_create_base_tag(project_path) - args = [ - "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal", + idea, "--inc", "--project-path", project_path, ] result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_word_cloud(): - project_path = f"{DATA_PATH}/word_cloud" - check_or_create_base_tag(project_path) - - args = [ - "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_gomoku(): - project_path = f"{DATA_PATH}/Gomoku" - check_or_create_base_tag(project_path) - - args = [ - "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_dice_simulator_1(): - project_path = f"{DATA_PATH}/dice_simulator_new" - check_or_create_base_tag(project_path) - - args = [ - "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_1"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_dice_simulator_2(): - project_path = f"{DATA_PATH}/dice_simulator_new" - check_or_create_base_tag(project_path) - - args = [ - "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_2"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_pygame_2048_1(): - project_path = f"{DATA_PATH}/pygame_2048" - check_or_create_base_tag(project_path) - - args = [ - "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_1"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_pygame_2048_2(): - project_path = f"{DATA_PATH}/pygame_2048" - check_or_create_base_tag(project_path) - - args = [ - "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_2"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_pygame_2048_3(): - project_path = f"{DATA_PATH}/pygame_2048" - check_or_create_base_tag(project_path) - - args = [ - "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_3"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_snake_game_1(): - project_path = f"{DATA_PATH}/snake_game" - check_or_create_base_tag(project_path) - - args = [ - "Incremental Idea Gradually increase the speed of the snake as the game progresses. In the current version of the game, the snake’s speed remains constant throughout the gameplay. Implement a feature where the snake’s speed gradually increases over time, making the game more challenging and intense as the player progresses.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_1"], check=True) - except subprocess.CalledProcessError as e: - raise e - - -def test_refined_snake_game_2(): - project_path = f"{DATA_PATH}/snake_game" - check_or_create_base_tag(project_path) - - args = [ - "Introduce power-ups and obstacles to the game. The current version of the game only involves eating food and growing the snake. Add new elements such as power-ups that can enhance the snake’s speed or make it invincible for a short duration. At the same time, introduce obstacles like walls or enemies that the snake must avoid or overcome to continue growing.", - "--inc", - "--project-path", - project_path, - ] - result = runner.invoke(app, args) - logger.info(result) - logger.info(result.output) - if "Aborting" in result.output: - assert False - else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() - if tag == "base": - assert False - else: - assert True - try: - subprocess.run(["git", "tag", "refine_2"], check=True) - except subprocess.CalledProcessError as e: - raise e + return result def check_or_create_base_tag(project_path): From e0839822c0e83117d02f39d7ea8f40d348b09b02 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 16 Jan 2024 18:53:30 +0800 Subject: [PATCH 052/101] update test_incremental_dev.py --- tests/metagpt/test_incremental_dev.py | 51 +++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 8890137b0..2fd319117 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -7,6 +7,7 @@ """ import os import subprocess +import time import pytest from typer.testing import CliRunner @@ -19,6 +20,7 @@ runner = CliRunner() IDEAS = [ "Add subtraction, multiplication and division operations to the calculator. The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator", + "Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal", "Add a feature to remove deprecated words from the word cloud. The current word cloud generator does not support removing deprecated words. Now, The word cloud generator should support removing deprecated words. Customize deactivated words to exclude them from word cloud. Let users see all the words in the text file, and allow users to select the words they want to remove.", "Add an AI opponent with fixed difficulty levels. Currently, the game only allows players to compete against themselves. Implement an AI algorithm that can playing with player. This will provide a more engaging and challenging experience for players.", "Add functionality to view the history of scores. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores", @@ -31,7 +33,8 @@ IDEAS = [ ] PROJECT_NAMES = [ - "calculator", + "simple_add_calculator", + "number_guessing_game", "word_cloud", "Gomoku", "dice_simulator_new", @@ -44,68 +47,72 @@ PROJECT_NAMES = [ ] -def test_refined_calculator(): +def test_simple_add_calculator(): result = get_incremental_dev_result(IDEAS[0], PROJECT_NAMES[0]) log_and_check_result(result) -def test_refined_word_cloud(): +def test_number_guessing_game(): result = get_incremental_dev_result(IDEAS[1], PROJECT_NAMES[1]) log_and_check_result(result) -def test_refined_gomoku(): +def test_word_cloud(): result = get_incremental_dev_result(IDEAS[2], PROJECT_NAMES[2]) log_and_check_result(result) -def test_refined_dice_simulator_new(): - for idea, project_name in zip(IDEAS[3:5], PROJECT_NAMES[3:5]): +def test_gomoku(): + result = get_incremental_dev_result(IDEAS[3], PROJECT_NAMES[3]) + log_and_check_result(result) + + +def test_dice_simulator_new(): + for i, (idea, project_name) in enumerate(zip(IDEAS[4:6], PROJECT_NAMES[4:6]), start=1): result = get_incremental_dev_result(idea, project_name) - log_and_check_result(result) + log_and_check_result(result, "refine_" + str(i)) def test_refined_pygame_2048(): - for idea, project_name in zip(IDEAS[5:8], PROJECT_NAMES[5:8]): + for i, (idea, project_name) in enumerate(zip(IDEAS[6:9], PROJECT_NAMES[6:9]), start=1): result = get_incremental_dev_result(idea, project_name) - log_and_check_result(result) + log_and_check_result(result, "refine_" + str(i)) def test_refined_snake_game(): - for idea, project_name in zip(IDEAS[8:10], PROJECT_NAMES[8:10]): + for i, (idea, project_name) in enumerate(zip(IDEAS[9:11], PROJECT_NAMES[9:11]), start=1): result = get_incremental_dev_result(idea, project_name) - log_and_check_result(result) + log_and_check_result(result, "refine_" + str(i)) -def log_and_check_result(result): +def log_and_check_result(result, tag_name="refine"): logger.info(result) logger.info(result.output) if "Aborting" in result.output: assert False else: - tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() # After running, there will be new commit - if tag == "base": + cur_tag = subprocess.run(["git", "describe", "--tags"], capture_output=True, text=True).stdout.strip() + if cur_tag == "base": assert False else: assert True + if subprocess.run(["git", "show-ref", "--verify", "--quiet", f"refs/tags/{tag_name}"]).returncode == 0: + tag_name += str(int(time.time())) try: - subprocess.run(["git", "tag", "refine"], check=True) + subprocess.run(["git", "tag", tag_name], check=True) except subprocess.CalledProcessError as e: raise e -def get_incremental_dev_result(idea, project_name): +def get_incremental_dev_result(idea, project_name, use_review=True): project_path = TEST_DATA_PATH / "incremental_dev_project" / project_name if not os.path.exists(project_path): raise Exception(f"Project {project_name} not exists") check_or_create_base_tag(project_path) - args = [ - idea, - "--inc", - "--project-path", - project_path, - ] + args = [idea, "--inc", "--project-path", project_path] + if not use_review: + args.append("--no-code-review") result = runner.invoke(app, args) return result From 6c954b8455a8f9f3383a31fee5aa1da3758fda5e Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 17 Jan 2024 11:40:16 +0800 Subject: [PATCH 053/101] update context in write_code_review.py --- metagpt/actions/write_code_review.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 444af51d9..27e4c280e 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -7,7 +7,6 @@ @Modified By: mashenquan, 2023/11/27. Following the think-act principle, solidify the task parameters when creating the WriteCode object, rather than passing them in when calling the run function. """ -import json from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential @@ -15,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions import WriteCode from metagpt.actions.action import Action from metagpt.config import CONFIG -from metagpt.const import DOCS_FILE_REPO, PRDS_FILE_REPO, REQUIREMENT_FILENAME +from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME from metagpt.logs import logger from metagpt.schema import CodingContext from metagpt.utils.common import CodeParser @@ -139,7 +138,8 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.context.code_doc.content - k = CONFIG.code_review_k_times or 1 + # k = CONFIG.code_review_k_times or 1 + k = 1 guideline = kwargs.get("guideline") mode = "guide" if guideline else "normal" for i in range(k): @@ -156,24 +156,14 @@ class WriteCodeReview(Action): ] ) else: - docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) - requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) + requirement_doc = await CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO).get( + filename=REQUIREMENT_FILENAME + ) user_requirement = requirement_doc.content if requirement_doc else "" - prd = await CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO).get_all() - - contents = [] - for doc in prd: - prd_json = json.loads(doc.content) - product_requirement_pool = prd_json.get( - "Requirement Pool", prd_json.get("Refined Requirement Pool") - ) - contents.append(str(product_requirement_pool)) - product_requirement_pools = "\n".join(contents) context = "\n".join( [ "## User New Requirements\n" + str(user_requirement) + "\n", - "## Product Requirement Pool\n" + product_requirement_pools + "\n", "## Guidelines and Incremental Change\n" + guideline + "\n", "## System Design\n" + str(self.context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", From 8831177a22ac3f3e89ddce6bf8528919f2826722 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 17 Jan 2024 11:47:16 +0800 Subject: [PATCH 054/101] update test case in test_incremental_dev.py --- tests/metagpt/test_incremental_dev.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 2fd319117..ed8fc7e78 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -27,7 +27,6 @@ IDEAS = [ "Add functionality to view the history of scores and perform statistical analysis on them. The original dice rolling game could only display the current game result, but the new requirement allows players to view the history of scores and display the statistical analysis results of the current score", "Changed score target for 2048 game from 2048 to 4096. Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8", "Display the history score of the player in the 2048 game. Add a record board that can display players' historical score records so that players can trace their scores", - "Add limited time mode. The original game only had a default classic mode. The improved game should be able to support limited-time mode, allowing users to choose classic mode or limited-time mode from the available options before starting the game.", "Incremental Idea Gradually increase the speed of the snake as the game progresses. In the current version of the game, the snake’s speed remains constant throughout the gameplay. Implement a feature where the snake’s speed gradually increases over time, making the game more challenging and intense as the player progresses.", "Introduce power-ups and obstacles to the game. The current version of the game only involves eating food and growing the snake. Add new elements such as power-ups that can enhance the snake’s speed or make it invincible for a short duration. At the same time, introduce obstacles like walls or enemies that the snake must avoid or overcome to continue growing.", ] @@ -41,7 +40,6 @@ PROJECT_NAMES = [ "dice_simulator_new", "pygame_2048", "pygame_2048", - "pygame_2048", "snake_game", "snake_game", ] @@ -74,13 +72,13 @@ def test_dice_simulator_new(): def test_refined_pygame_2048(): - for i, (idea, project_name) in enumerate(zip(IDEAS[6:9], PROJECT_NAMES[6:9]), start=1): + for i, (idea, project_name) in enumerate(zip(IDEAS[6:8], PROJECT_NAMES[6:8]), start=1): result = get_incremental_dev_result(idea, project_name) log_and_check_result(result, "refine_" + str(i)) def test_refined_snake_game(): - for i, (idea, project_name) in enumerate(zip(IDEAS[9:11], PROJECT_NAMES[9:11]), start=1): + for i, (idea, project_name) in enumerate(zip(IDEAS[8:10], PROJECT_NAMES[8:10]), start=1): result = get_incremental_dev_result(idea, project_name) log_and_check_result(result, "refine_" + str(i)) From a6d56bd7481f22200fd60bec256b705845a7ae28 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 17 Jan 2024 18:47:59 +0800 Subject: [PATCH 055/101] 1. add mock.py in tests\data\incremental_dev_project 2. add mock for test case of ActionNode 3. add path of Guideline file in const.py 4. update engineer.py --- metagpt/actions/design_api_an.py | 6 +- metagpt/actions/write_code_guideline_an.py | 295 +---------- metagpt/const.py | 2 + metagpt/roles/engineer.py | 21 +- tests/data/incremental_dev_project/mock.py | 466 ++++++++++++++++++ tests/metagpt/actions/test_design_api_an.py | 108 +--- .../actions/test_project_management_an.py | 148 +----- .../actions/test_write_code_guideline_an.py | 309 ++---------- tests/metagpt/actions/test_write_prd_an.py | 97 +--- 9 files changed, 599 insertions(+), 853 deletions(-) create mode 100644 tests/data/incremental_dev_project/mock.py diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 3890e656a..3e65cee1b 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -39,7 +39,7 @@ FILE_LIST = ActionNode( ) REFINED_FILE_LIST = ActionNode( - key="Refined File List", + key="Refined File list", expected_type=List[str], instruction="Update and expand the original file list including only relative paths. Up to 2 files can be added." "Ensure that the refined file list reflects the evolving structure of the project.", @@ -56,7 +56,7 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode( ) REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( - key="Refined Data Structures and Interfaces", + key="Refined Data structures and interfaces", expected_type=str, instruction="Update and extend the existing mermaid classDiagram code syntax to incorporate new classes, " "methods (including __init__), and functions with precise type annotations. Delineate additional " @@ -108,7 +108,7 @@ REFINE_NODES = [ ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) -REFINED_DESIGN_NODES = ActionNode.from_children("Refined_Design_API", REFINE_NODES) +REFINED_DESIGN_NODES = ActionNode.from_children("RefinedDesignAPI", REFINE_NODES) def main(): diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index e4afb393d..ff59ee7d6 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -5,11 +5,11 @@ @Author : mannaandpoem @File : write_code_guideline_an.py """ -import asyncio from metagpt.actions.action import Action -from metagpt.actions.action_node import ActionNode -from metagpt.logs import logger +from metagpt.actions.action_node import ActionNode, dict_to_markdown +from metagpt.config import CONFIG +from metagpt.const import CODE_GUIDELINE_FILE_REPO, CODE_GUIDELINE_PDF_FILE_REPO GUIDELINES_AND_INCREMENTAL_CHANGE = ActionNode( key="Guidelines and Incremental Change", @@ -123,271 +123,6 @@ CODE_GUIDELINE_CONTEXT = """ {code} """ -CODE_GUIDELINE_CONTEXT_EXAMPLE = """ -## New Requirements -Add subtraction, multiplication and division operations to the calculator. -The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator - -## Design -{ - "Refined Implementation Approach": "To accommodate the new requirements, we will extend the existing Python-based calculator application. We will enhance the Tkinter-based UI to include buttons for subtraction, multiplication, and division, alongside the existing addition functionality. We will also implement input validation to handle edge cases such as division by zero. The architecture will be modular, with separate components for the UI, calculation logic, and error handling to maintain simplicity and facilitate future enhancements such as a history feature.", - "File list": [ - "main.py", - "calculator.py", - "interface.py", - "operations.py" - ], - "Refined Data Structures and Interfaces": "classDiagram\n class CalculatorApp {\n +main() None\n }\n class Calculator {\n -result float\n +add(number1: float, number2: float) float\n +subtract(number1: float, number2: float) float\n +multiply(number1: float, number2: float) float\n +divide(number1: float, number2: float) float\n +clear() None\n }\n class Interface {\n -calculator Calculator\n +start() None\n +display_result(result: float) None\n +get_input() float\n +show_error(message: str) None\n +update_operation(operation: str) None\n }\n class Operations {\n +perform_operation(operation: str, number1: float, number2: float) float\n }\n CalculatorApp --> Interface\n Interface --> Calculator\n Calculator --> Operations", - "Refined Program call flow": "sequenceDiagram\n participant CA as CalculatorApp\n participant I as Interface\n participant C as Calculator\n participant O as Operations\n CA->>I: start()\n I->>I: get_input()\n I->>I: update_operation(operation)\n loop For Each Operation\n I->>C: perform_operation(operation, number1, number2)\n C->>O: perform_operation(operation, number1, number2)\n O-->>C: return result\n C-->>I: return result\n I->>I: display_result(result)\n end\n I->>I: show_error(message)", - "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." -} - -## Tasks -{ - "Required Python packages": [ - "tkinter" - ], - "Required Other language third-party packages": [ - "No third-party dependencies required" - ], - "Refined Logic Analysis": [ - [ - "main.py", - "Entry point of the application, creates an instance of the Interface class and starts the application." - ], - [ - "calculator.py", - "Contains the Calculator class with add, subtract, multiply, divide and clear methods for performing arithmetic operations." - ], - [ - "interface.py", - "Contains the Interface class responsible for the GUI, interacts with Calculator for the logic and displays results or errors." - ], - [ - "operations.py", - "Contains the Operations class with perform_operation method that delegates the arithmetic operation based on the operation argument." - ] - ], - "Refined Task list": [ - "operations.py", - "calculator.py", - "interface.py", - "main.py" - ], - "Full API spec": "", - "Refined Shared Knowledge": "`interface.py` will use the Calculator class from `calculator.py` to perform operations and display results. `main.py` will be the starting point that initializes the Interface. `calculator.py` will now also interact with `operations.py` to perform the arithmetic operations.", - "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." -} - -## Legacy Code ------ calculator.py -```## calculator.py - -class Calculator: - def __init__(self): - self.result = 0.0 # Default value for the result - - def add(self, number1: float, number2: float) -> float: - ''' - Adds two numbers and returns the result. - - Args: - number1 (float): The first number to add. - number2 (float): The second number to add. - - Returns: - float: The sum of number1 and number2. - ''' - self.result = number1 + number2 - return self.result - - def clear(self) -> None: - ''' - Clears the result to its default value. - ''' - self.result = 0.0 -``` - ----- interface.py -```## interface.py -import tkinter as tk -from calculator import Calculator - -class Interface: - def __init__(self): - self.calculator = Calculator() - self.root = tk.Tk() - self.root.title("Calculator") - self.create_widgets() - - def create_widgets(self): - self.result_var = tk.StringVar() - self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) - self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') - - self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') - - self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') - - self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) - self.add_button.grid(row=2, column=0, sticky='nsew') - - self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) - self.clear_button.grid(row=2, column=1, sticky='nsew') - - self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) - self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') - - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_columnconfigure(0, weight=1) - - def start(self): - self.root.mainloop() - - def display_result(self, result: float): - self.result_var.set(str(result)) - - def get_input(self): - try: - number1 = float(self.entry_number1.get()) - number2 = float(self.entry_number2.get()) - return number1, number2 - except ValueError: - self.show_error("Invalid input! Please enter valid numbers.") - return None, None - - def add(self): - number1, number2 = self.get_input() - if number1 is not None and number2 is not None: - result = self.calculator.add(number1, number2) - self.display_result(result) - - def clear(self): - self.entry_number1.delete(0, tk.END) - self.entry_number2.delete(0, tk.END) - self.result_var.set("") - - def show_error(self, message: str): - tk.messagebox.showerror("Error", message) - -# This code is meant to be used as a module and not as a standalone script. -# The Interface class will be instantiated and started by the main.py file. -``` - ----- main.py -```## main.py -from interface import Interface - - -class CalculatorApp: - @staticmethod - def main(): - interface = Interface() - interface.start() - - -if __name__ == "__main__": - CalculatorApp.main() -``` -""" - -REFINE_CODE_SCRIPT_EXAMPLE = """ ------ calculator.py -```## calculator.py - -class Calculator: - def __init__(self): - self.result = 0.0 # Default value for the result - - def add(self, number1: float, number2: float) -> float: - ''' - Adds two numbers and returns the result. - - Args: - number1 (float): The first number to add. - number2 (float): The second number to add. - - Returns: - float: The sum of number1 and number2. - ''' - self.result = number1 + number2 - return self.result - - def clear(self) -> None: - ''' - Clears the result to its default value. - ''' - self.result = 0.0 -``` - ----- Now, interface.py to be rewritten -```## interface.py -import tkinter as tk -from calculator import Calculator - -class Interface: - def __init__(self): - self.calculator = Calculator() - self.root = tk.Tk() - self.root.title("Calculator") - self.create_widgets() - - def create_widgets(self): - self.result_var = tk.StringVar() - self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) - self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') - - self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') - - self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') - - self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) - self.add_button.grid(row=2, column=0, sticky='nsew') - - self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) - self.clear_button.grid(row=2, column=1, sticky='nsew') - - self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) - self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') - - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_columnconfigure(0, weight=1) - - def start(self): - self.root.mainloop() - - def display_result(self, result: float): - self.result_var.set(str(result)) - - def get_input(self): - try: - number1 = float(self.entry_number1.get()) - number2 = float(self.entry_number2.get()) - return number1, number2 - except ValueError: - self.show_error("Invalid input! Please enter valid numbers.") - return None, None - - def add(self): - number1, number2 = self.get_input() - if number1 is not None and number2 is not None: - result = self.calculator.add(number1, number2) - self.display_result(result) - - def clear(self): - self.entry_number1.delete(0, tk.END) - self.entry_number2.delete(0, tk.END) - self.result_var.set("") - - def show_error(self, message: str): - tk.messagebox.showerror("Error", message) -``` -""" - REFINED_CODE_TEMPLATE = """ NOTICE Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Guidelines and Incremental Change, ensuring the integration of new features. @@ -451,13 +186,21 @@ class WriteCodeGuideline(Action): "meticulously craft comprehensive incremental development guidelines and deliver detailed Incremental Change" return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") + @staticmethod + async def save(guideline): + await WriteCodeGuideline.save_json(guideline) + await WriteCodeGuideline.save_md(guideline) -async def main(): - write_code_guideline = WriteCodeGuideline() - node = await write_code_guideline.run(CODE_GUIDELINE_CONTEXT_EXAMPLE) - guideline = node.instruct_content.model_dump_json() - logger.info(guideline) + @staticmethod + async def save_json(guideline): + filename = "code_guideline.json" + await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_FILE_REPO).save( + filename=filename, content=str(guideline) + ) - -if __name__ == "__main__": - asyncio.run(main()) + @staticmethod + async def save_md(guideline): + filename = "code_guideline.md" + await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_PDF_FILE_REPO).save( + filename=filename, content=dict_to_markdown(guideline) + ) diff --git a/metagpt/const.py b/metagpt/const.py index a57be641b..2ee1267d8 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -92,12 +92,14 @@ DOCS_FILE_REPO = "docs" PRDS_FILE_REPO = "docs/prds" SYSTEM_DESIGN_FILE_REPO = "docs/system_design" TASK_FILE_REPO = "docs/tasks" +CODE_GUIDELINE_FILE_REPO = "docs/code_guideline" COMPETITIVE_ANALYSIS_FILE_REPO = "resources/competitive_analysis" DATA_API_DESIGN_FILE_REPO = "resources/data_api_design" SEQ_FLOW_FILE_REPO = "resources/seq_flow" SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design" PRD_PDF_FILE_REPO = "resources/prd" TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks" +CODE_GUIDELINE_PDF_FILE_REPO = "resources/code_guideline" TEST_CODES_FILE_REPO = "tests" TEST_OUTPUTS_FILE_REPO = "test_outputs" CODE_SUMMARIES_FILE_REPO = "docs/code_summaries" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index a6b51bd00..bb0a2e857 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -26,6 +26,7 @@ from pathlib import Path from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks +from metagpt.actions.action_node import dict_to_markdown from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode from metagpt.actions.write_code_guideline_an import ( @@ -34,6 +35,7 @@ from metagpt.actions.write_code_guideline_an import ( ) from metagpt.config import CONFIG from metagpt.const import ( + CODE_GUIDELINE_PDF_FILE_REPO, CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, PRDS_FILE_REPO, @@ -101,7 +103,7 @@ class Engineer(Role): m = json.loads(task_msg.content) return m.get("Task list") or m.get("Refined Task list") - async def _act_sp_with_cr(self, review=False, guideline="") -> Set[str]: + async def _act_sp_with_cr(self, review=False, guideline=Document()) -> Set[str]: changed_files = set() src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) for todo in self.code_todos: @@ -112,16 +114,16 @@ class Engineer(Role): 3. Do we need other codes (currently needed)? TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ - coding_context = await todo.run(guideline=guideline) + coding_context = await todo.run(guideline=guideline.content) # Code review if review: action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) - coding_context = await action.run(guideline=guideline) + coding_context = await action.run(guideline=guideline.content) dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} - if guideline: - dependencies.add("code_guideline.json") + if guideline.content: + dependencies.add(guideline.root_relative_path) await src_file_repo.save( coding_context.filename, dependencies=dependencies, @@ -364,12 +366,11 @@ class Engineer(Role): code=old_codes, ) node = await WriteCodeGuideline().run(context=context) - guideline = node.instruct_content.model_dump_json() + guideline = node.instruct_content.model_dump() + await WriteCodeGuideline.save(guideline) + guideline = dict_to_markdown(guideline) - await CONFIG.git_repo.new_file_repository(CONFIG.git_repo.workdir).save( - filename="code_guideline.json", content=guideline - ) - return guideline + return Document(root_path=CODE_GUIDELINE_PDF_FILE_REPO, filename="code_guideline.md", content=guideline) @staticmethod async def get_old_codes() -> str: diff --git a/tests/data/incremental_dev_project/mock.py b/tests/data/incremental_dev_project/mock.py new file mode 100644 index 000000000..a7aa2bbe4 --- /dev/null +++ b/tests/data/incremental_dev_project/mock.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/17 +@Author : mannaandpoem +@File : mock.py +""" +NEW_REQUIREMENT_SAMPLE = """ +Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal +""" + +PRD_SAMPLE = """ +## Language + +en_us + +## Programming Language + +Python + +## Original Requirements + +Make a simple number guessing game + +## Product Goals + +- Ensure a user-friendly interface for the game +- Provide a challenging yet enjoyable game experience +- Design the game to be easily extendable for future features + +## User Stories + +- As a player, I want to guess numbers and receive feedback on whether my guess is too high or too low +- As a player, I want to be able to set the difficulty level by choosing the range of possible numbers +- As a player, I want to see my previous guesses to strategize my next guess +- As a player, I want to know how many attempts it took me to guess the number once I get it right + +## Competitive Analysis + +- Guess The Number Game A: Basic text interface, no difficulty levels +- Number Master B: Has difficulty levels, but cluttered interface +- Quick Guess C: Sleek design, but lacks performance tracking +- NumGuess D: Good performance tracking, but not mobile-friendly +- GuessIt E: Mobile-friendly, but too many ads +- Perfect Guess F: Offers hints, but the hints are not very helpful +- SmartGuesser G: Has a learning mode, but lacks a competitive edge + +## Competitive Quadrant Chart + +quadrantChart + title "User Engagement and Game Complexity" + x-axis "Low Complexity" --> "High Complexity" + y-axis "Low Engagement" --> "High Engagement" + quadrant-1 "Too Simple" + quadrant-2 "Niche Appeal" + quadrant-3 "Complex & Unengaging" + quadrant-4 "Sweet Spot" + "Guess The Number Game A": [0.2, 0.4] + "Number Master B": [0.5, 0.3] + "Quick Guess C": [0.6, 0.7] + "NumGuess D": [0.4, 0.6] + "GuessIt E": [0.7, 0.5] + "Perfect Guess F": [0.6, 0.4] + "SmartGuesser G": [0.8, 0.6] + "Our Target Product": [0.5, 0.8] + +## Requirement Analysis + +The game should be simple yet engaging, allowing players of different skill levels to enjoy it. It should provide immediate feedback and track the player's performance. The game should also be designed with a clean and intuitive interface, and it should be easy to add new features in the future. + +## Requirement Pool + +- ['P0', 'Implement the core game logic to randomly select a number and allow the user to guess it'] +- ['P0', 'Design a user interface that displays the game status and results clearly'] +- ['P1', 'Add difficulty levels by varying the range of possible numbers'] +- ['P1', 'Keep track of and display the number of attempts for each game session'] +- ['P2', "Store and show the history of the player's guesses during a game session"] + +## UI Design draft + +The UI will feature a clean and minimalist design with a number input field, submit button, and messages area to provide feedback. There will be options to select the difficulty level and a display showing the number of attempts and history of past guesses. + +## Anything UNCLEAR""" + +DESIGN_SAMPLE = """ +## Implementation approach + +We will create a Python-based number guessing game with a simple command-line interface. For the user interface, we will use the built-in 'input' and 'print' functions for interaction. The random library will be used for generating random numbers. We will structure the code to be modular and easily extendable, separating the game logic from the user interface. + +## File list + +- main.py +- game.py +- ui.py + +## Data structures and interfaces + + +classDiagram + class Game { + -int secret_number + -int min_range + -int max_range + -list attempts + +__init__(difficulty: str) + +start_game() + +check_guess(guess: int) str + +get_attempts() int + +get_history() list + } + class UI { + +start() + +display_message(message: str) + +get_user_input(prompt: str) str + +show_attempts(attempts: int) + +show_history(history: list) + +select_difficulty() str + } + class Main { + +main() + } + Main --> UI + UI --> Game + + +## Program call flow + + +sequenceDiagram + participant M as Main + participant UI as UI + participant G as Game + M->>UI: start() + UI->>UI: select_difficulty() + UI-->>G: __init__(difficulty) + G->>G: start_game() + loop Game Loop + UI->>UI: get_user_input("Enter your guess:") + UI-->>G: check_guess(guess) + G->>UI: display_message(feedback) + G->>UI: show_attempts(attempts) + G->>UI: show_history(history) + end + G->>UI: display_message("Correct! Game over.") + UI->>M: main() # Game session ends + + +## Anything UNCLEAR + +The requirement analysis suggests the need for a clean and intuitive interface. Since we are using a command-line interface, we need to ensure that the text-based UI is as user-friendly as possible. Further clarification on whether a graphical user interface (GUI) is expected in the future would be helpful for planning the extendability of the game.""" + +TASKS_SAMPLE = """ +## Required Python packages + +- random==2.2.1 + +## Required Other language third-party packages + +- No third-party dependencies required + +## Logic Analysis + +- ['game.py', 'Contains Game class with methods __init__, start_game, check_guess, get_attempts, get_history and uses random library for generating secret_number'] +- ['ui.py', 'Contains UI class with methods start, display_message, get_user_input, show_attempts, show_history, select_difficulty and interacts with Game class'] +- ['main.py', 'Contains Main class with method main that initializes UI class and starts the game loop'] + +## Task list + +- game.py +- ui.py +- main.py + +## Full API spec + + + +## Shared Knowledge + +`game.py` contains the core game logic and is used by `ui.py` to interact with the user. `main.py` serves as the entry point to start the game. + +## Anything UNCLEAR + +The requirement analysis suggests the need for a clean and intuitive interface. Since we are using a command-line interface, we need to ensure that the text-based UI is as user-friendly as possible. Further clarification on whether a graphical user interface (GUI) is expected in the future would be helpful for planning the extendability of the game.""" + +OLD_CODE_SAMPLE = """ +--- game.py +```## game.py + +import random + +class Game: + def __init__(self, difficulty: str = 'medium'): + self.min_range, self.max_range = self._set_difficulty(difficulty) + self.secret_number = random.randint(self.min_range, self.max_range) + self.attempts = [] + + def _set_difficulty(self, difficulty: str): + difficulties = { + 'easy': (1, 10), + 'medium': (1, 100), + 'hard': (1, 1000) + } + return difficulties.get(difficulty, (1, 100)) + + def start_game(self): + self.secret_number = random.randint(self.min_range, self.max_range) + self.attempts = [] + + def check_guess(self, guess: int) -> str: + self.attempts.append(guess) + if guess < self.secret_number: + return "It's higher." + elif guess > self.secret_number: + return "It's lower." + else: + return "Correct! Game over." + + def get_attempts(self) -> int: + return len(self.attempts) + + def get_history(self) -> list: + return self.attempts``` + +--- ui.py +```## ui.py + +from game import Game + +class UI: + def start(self): + difficulty = self.select_difficulty() + game = Game(difficulty) + game.start_game() + self.display_welcome_message(game) + + feedback = "" + while feedback != "Correct! Game over.": + guess = self.get_user_input("Enter your guess: ") + if self.is_valid_guess(guess): + feedback = game.check_guess(int(guess)) + self.display_message(feedback) + self.show_attempts(game.get_attempts()) + self.show_history(game.get_history()) + else: + self.display_message("Please enter a valid number.") + + def display_welcome_message(self, game): + print("Welcome to the Number Guessing Game!") + print(f"Guess the number between {game.min_range} and {game.max_range}.") + + def is_valid_guess(self, guess): + return guess.isdigit() + + def display_message(self, message: str): + print(message) + + def get_user_input(self, prompt: str) -> str: + return input(prompt) + + def show_attempts(self, attempts: int): + print(f"Number of attempts: {attempts}") + + def show_history(self, history: list): + print("Guess history:") + for guess in history: + print(guess) + + def select_difficulty(self) -> str: + while True: + difficulty = input("Select difficulty (easy, medium, hard): ").lower() + if difficulty in ['easy', 'medium', 'hard']: + return difficulty + else: + self.display_message("Invalid difficulty. Please choose 'easy', 'medium', or 'hard'.")``` + +--- main.py +```## main.py + +from ui import UI + +class Main: + def main(self): + user_interface = UI() + user_interface.start() + +if __name__ == "__main__": + main_instance = Main() + main_instance.main()``` +""" + +REFINED_PRD_JSON = { + "Language": "en_us", + "Programming Language": "Python", + "Refined Requirements": "Adding graphical interface functionality to enhance the user experience in the number-guessing game.", + "Project Name": "number_guessing_game", + "Refined Product Goals": [ + "Ensure a user-friendly interface for the game with the new graphical interface", + "Provide a challenging yet enjoyable game experience with visual enhancements", + "Design the game to be easily extendable for future features, including graphical elements", + ], + "Refined User Stories": [ + "As a player, I want to interact with a graphical interface to guess numbers and receive visual feedback on my guesses", + "As a player, I want to easily select the difficulty level through the graphical interface", + "As a player, I want to visually track my previous guesses and the number of attempts in the graphical interface", + "As a player, I want to be congratulated with a visually appealing message when I guess the number correctly", + ], + "Competitive Analysis": [ + "Guess The Number Game A: Basic text interface, no difficulty levels", + "Number Master B: Has difficulty levels, but cluttered interface", + "Quick Guess C: Sleek design, but lacks performance tracking", + "NumGuess D: Good performance tracking, but not mobile-friendly", + "GuessIt E: Mobile-friendly, but too many ads", + "Perfect Guess F: Offers hints, but the hints are not very helpful", + "SmartGuesser G: Has a learning mode, but lacks a competitive edge", + "Graphical Guess H: Graphical interface, but poor user experience due to complex design", + ], + "Competitive Quadrant Chart": 'quadrantChart\n title "User Engagement and Game Complexity with Graphical Interface"\n x-axis "Low Complexity" --> "High Complexity"\n y-axis "Low Engagement" --> "High Engagement"\n quadrant-1 "Too Simple"\n quadrant-2 "Niche Appeal"\n quadrant-3 "Complex & Unengaging"\n quadrant-4 "Sweet Spot"\n "Guess The Number Game A": [0.2, 0.4]\n "Number Master B": [0.5, 0.3]\n "Quick Guess C": [0.6, 0.7]\n "NumGuess D": [0.4, 0.6]\n "GuessIt E": [0.7, 0.5]\n "Perfect Guess F": [0.6, 0.4]\n "SmartGuesser G": [0.8, 0.6]\n "Graphical Guess H": [0.7, 0.3]\n "Our Target Product": [0.5, 0.9]', + "Refined Requirement Analysis": [ + "The game should maintain its simplicity while integrating a graphical interface for enhanced engagement.", + "Immediate visual feedback is crucial for user satisfaction in the graphical interface.", + "The interface must be intuitive, allowing for easy navigation and selection of game options.", + "The graphical design should be clean and not detract from the game's core guessing mechanic.", + ], + "Refined Requirement Pool": [ + ["P0", "Implement a graphical user interface (GUI) to replace the command-line interaction"], + [ + "P0", + "Design a user interface that displays the game status, results, and feedback clearly with graphical elements", + ], + ["P1", "Incorporate interactive elements for selecting difficulty levels"], + ["P1", "Visualize the history of the player's guesses and the number of attempts within the game session"], + ["P2", "Create animations for correct or incorrect guesses to enhance user feedback"], + ["P2", "Ensure the GUI is responsive and compatible with various screen sizes"], + ["P2", "Store and show the history of the player's guesses during a game session"], + ], + "UI Design draft": "The UI will feature a modern and minimalist design with a graphical number input field, a submit button with animations, and a dedicated area for visual feedback. It will include interactive elements to select the difficulty level and a visual display for the number of attempts and history of past guesses.", + "Anything UNCLEAR": "", +} + +REFINED_DESIGN_JSON = { + "Refined Implementation Approach": "To accommodate the new graphical user interface (GUI) requirements, we will leverage the Tkinter library, which is included with Python and supports the creation of a user-friendly GUI. The game logic will remain in Python, with Tkinter handling the rendering of the interface. We will ensure that the GUI is responsive and provides immediate visual feedback. The main game loop will be event-driven, responding to user inputs such as button clicks and difficulty selection.", + "Refined File list": ["main.py", "game.py", "ui.py", "gui.py"], + "Refined Data structures and interfaces": "\nclassDiagram\n class Game {\n -int secret_number\n -int min_range\n -int max_range\n -list attempts\n +__init__(difficulty: str)\n +start_game()\n +check_guess(guess: int) str\n +get_attempts() int\n +get_history() list\n }\n class UI {\n +start()\n +display_message(message: str)\n +get_user_input(prompt: str) str\n +show_attempts(attempts: int)\n +show_history(history: list)\n +select_difficulty() str\n }\n class GUI {\n +__init__()\n +setup_window()\n +bind_events()\n +update_feedback(message: str)\n +update_attempts(attempts: int)\n +update_history(history: list)\n +show_difficulty_selector()\n +animate_guess_result(correct: bool)\n }\n class Main {\n +main()\n }\n Main --> UI\n UI --> Game\n UI --> GUI\n GUI --> Game\n", + "Refined Program call flow": '\nsequenceDiagram\n participant M as Main\n participant UI as UI\n participant G as Game\n participant GU as GUI\n M->>UI: start()\n UI->>GU: setup_window()\n GU->>GU: bind_events()\n GU->>UI: select_difficulty()\n UI-->>G: __init__(difficulty)\n G->>G: start_game()\n loop Game Loop\n GU->>GU: show_difficulty_selector()\n GU->>UI: get_user_input("Enter your guess:")\n UI-->>G: check_guess(guess)\n G->>GU: update_feedback(feedback)\n G->>GU: update_attempts(attempts)\n G->>GU: update_history(history)\n GU->>GU: animate_guess_result(correct)\n end\n G->>GU: update_feedback("Correct! Game over.")\n GU->>M: main() # Game session ends\n', + "Anything UNCLEAR": "", +} + +REFINED_TASKS_JSON = { + "Required Python packages": ["random==2.2.1", "Tkinter==8.6"], + "Required Other language third-party packages": ["No third-party dependencies required"], + "Refined Logic Analysis": [ + [ + "game.py", + "Contains Game class with methods __init__, start_game, check_guess, get_attempts, get_history and uses random library for generating secret_number", + ], + [ + "ui.py", + "Contains UI class with methods start, display_message, get_user_input, show_attempts, show_history, select_difficulty and interacts with Game class", + ], + [ + "gui.py", + "Contains GUI class with methods __init__, setup_window, bind_events, update_feedback, update_attempts, update_history, show_difficulty_selector, animate_guess_result and interacts with Game class for GUI rendering", + ], + [ + "main.py", + "Contains Main class with method main that initializes UI class and starts the event-driven game loop", + ], + ], + "Refined Task list": ["game.py", "ui.py", "gui.py", "main.py"], + "Full API spec": "", + "Refined Shared Knowledge": "`game.py` contains the core game logic and is used by `ui.py` to interact with the user. `main.py` serves as the entry point to start the game. `gui.py` is introduced to handle the graphical user interface using Tkinter, which will interact with both `game.py` and `ui.py` for a responsive and user-friendly experience.", + "Anything UNCLEAR": "", +} + +GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE = { + "Guidelines and Incremental Change": '\n1. Guideline for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Guideline for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Guideline for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Guideline for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' +} + +REFINED_CODE_INPUT_SAMPLE = """ +-----Now, game.py to be rewritten +```## game.py + +import random + +class Game: + def __init__(self, difficulty: str = 'medium'): + self.min_range, self.max_range = self._set_difficulty(difficulty) + self.secret_number = random.randint(self.min_range, self.max_range) + self.attempts = [] + + def _set_difficulty(self, difficulty: str): + difficulties = { + 'easy': (1, 10), + 'medium': (1, 100), + 'hard': (1, 1000) + } + return difficulties.get(difficulty, (1, 100)) + + def start_game(self): + self.secret_number = random.randint(self.min_range, self.max_range) + self.attempts = [] + + def check_guess(self, guess: int) -> str: + self.attempts.append(guess) + if guess < self.secret_number: + return "It's higher." + elif guess > self.secret_number: + return "It's lower." + else: + return "Correct! Game over." + + def get_attempts(self) -> int: + return len(self.attempts) + + def get_history(self) -> list: + return self.attempts``` +""" + +REFINED_CODE_SAMPLE = """ +## game.py + +import random + +class Game: + def __init__(self, difficulty: str = 'medium'): + # Set the difficulty level with default value 'medium' + self.min_range, self.max_range = self._set_difficulty(difficulty) + # Initialize the secret number based on the difficulty + self.secret_number = random.randint(self.min_range, self.max_range) + # Initialize the list to keep track of attempts + self.attempts = [] + + def _set_difficulty(self, difficulty: str): + # Define the range of numbers for each difficulty level + difficulties = { + 'easy': (1, 10), + 'medium': (1, 100), + 'hard': (1, 1000) + } + # Return the corresponding range for the selected difficulty, default to 'medium' if not found + return difficulties.get(difficulty, (1, 100)) + + def start_game(self): + # Reset the secret number and attempts list for a new game + self.secret_number = random.randint(self.min_range, self.max_range) + self.attempts.clear() + + def check_guess(self, guess: int) -> str: + # Add the guess to the attempts list + self.attempts.append(guess) + # Provide feedback based on the guess + if guess < self.secret_number: + return "It's higher." + elif guess > self.secret_number: + return "It's lower." + else: + return "Correct! Game over." + + def get_attempts(self) -> int: + # Return the number of attempts made + return len(self.attempts) + + def get_history(self) -> list: + # Return the list of attempts made + return self.attempts +""" diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index c729c047c..2aa123224 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -7,89 +7,15 @@ """ import pytest +from metagpt.actions.action_node import ActionNode, dict_to_markdown +from metagpt.actions.design_api import NEW_REQ_TEMPLATE from metagpt.actions.design_api_an import REFINED_DESIGN_NODES from metagpt.llm import LLM - -CONTEXT = """ -### Legacy Content -{ - "Implementation approach": "We will use Python with the Pygame library to develop the core mechanics of the 2048 game. The game will feature a simple and intuitive user interface, score tracking with high score memory, and an undo move feature. We'll ensure the game has visually appealing graphics and animations while maintaining a minimalist design. The undo feature will allow a single move to be undone without affecting the score, to keep the implementation straightforward.", - "File list": [ - "main.py", - "game.py", - "ui.py", - "constants.py" - ], - "Data structures and interfaces": "classDiagram\n class Main {\n +pygame: PygameInstance\n +game: Game\n +run() void\n }\n class Game {\n -grid: list\n -current_score: int\n -high_score: int\n -last_move: list\n +move(direction: str) bool\n +undo() bool\n +check_game_over() bool\n +reset_game() void\n }\n class UI {\n -screen: PygameSurface\n -font: PygameFont\n +draw_grid(grid: list) void\n +display_score(current_score: int, high_score: int) void\n +show_game_over() void\n +show_undo_button() void\n }\n class Constants {\n +GRID_SIZE: int\n +WINDOW_WIDTH: int\n +WINDOW_HEIGHT: int\n +BACKGROUND_COLOR: tuple\n +TILE_COLORS: dict\n }\n Main --> Game\n Main --> UI\n Game --> Constants\n UI --> Constants", - "Program call flow": "sequenceDiagram\n participant M as Main\n participant G as Game\n participant U as UI\n M->>G: create instance\n M->>U: create instance\n loop game loop\n M->>U: draw_grid(G.grid)\n M->>U: display_score(G.current_score, G.high_score)\n M->>U: show_undo_button()\n M->>G: move(direction)\n alt if move is valid\n G-->>M: return true\n else if move is invalid\n G-->>M: return false\n end\n alt if undo is triggered\n M->>G: undo()\n G-->>M: return true\n else no undo\n G-->>M: return false\n end\n alt if game over\n M->>U: show_game_over()\n M->>G: reset_game()\n end\n end", - "Anything UNCLEAR": "The specifics of the scoring system and how the high score is stored and retrieved need to be clarified. Additionally, the exact graphical assets and animations for the game are not specified." -} - -### New Requirements -{ - "Language": "en_us", - "Programming Language": "Python", - "Refined Requirements": "Update the py2048_game to have a larger 8x8 grid and a new winning score target of 4096, while maintaining the core mechanics and user-friendly interface of the original 2048 game.", - "Project Name": "py2048_game", - "Refined Product Goals": [ - "Develop an enhanced version of the 2048 game with a larger grid and higher score target to provide a new challenge to players", - "Ensure the game remains visually appealing and maintains a consistent theme with the added complexity", - "Implement smooth and responsive game controls suitable for an 8x8 grid interface" - ], - "Refined User Stories": [ - "As a player, I want to experience a clear and simple interface on an 8x8 grid so that I can focus on the gameplay", - "As a player, I want to aim for a higher score target of 4096 to challenge my skills further", - "As a player, I want to see my current and high scores to track my progress on the new larger grid", - "As a player, I want the option to undo my last move to improve my strategy on the 8x8 grid", - "As a player, I want the game to perform smoothly despite the increased complexity of the larger grid" - ], - "Competitive Analysis": [ - "2048 Original: Classic gameplay with minimalistic design, but lacks modern features", - "2048 by Gabriele Cirulli: Open-source version with clean UI, but no additional features", - "2048 Hex: Unique hexagon board, providing a different challenge", - "2048 Multiplayer: Allows playing against others, but the interface is cluttered", - "2048 with AI: Includes AI challenge mode, but the AI is often too difficult for casual players", - "2048.io: Combines 2048 gameplay with .io style, though it can be overwhelming for new players", - "2048 Animated: Features animations, but has performance issues on some devices" - ], - "Competitive Quadrant Chart": "quadrantChart\n title \"2048 Game Market Positioning\"\n x-axis \"Basic Features\" --> \"Advanced Features\"\n y-axis \"Low User Engagement\" --> \"High User Engagement\"\n quadrant-1 \"Niche Innovators\"\n quadrant-2 \"Market Leaders\"\n quadrant-3 \"Emerging Contenders\"\n quadrant-4 \"Falling Behind\"\n \"2048 Original\": [0.2, 0.7]\n \"2048 by Gabriele Cirulli\": [0.3, 0.8]\n \"2048 Hex\": [0.5, 0.4]\n \"2048 Multiplayer\": [0.6, 0.6]\n \"2048 with AI\": [0.7, 0.5]\n \"2048.io\": [0.4, 0.3]\n \"2048 Animated\": [0.3, 0.2]\n \"Our Target Product\": [0.9, 0.9]", - "Incremental Requirement Analysis": [ - "Adjust the game logic to accommodate an 8x8 grid while ensuring performance remains optimal", - "Update the UI to fit the larger grid and include visual cues for the new score target", - "Enhance the scoring system to support the new target of 4096", - "Ensure the undo feature is adapted to work with the larger grid and increased game complexity", - "Test the game thoroughly to maintain a smooth and responsive experience on the new 8x8 grid" - ], - "Refined Requirement Pool": [ - [ - "P0", - "Expand the game grid to 8x8 and adjust the core mechanics accordingly" - ], - [ - "P0", - "Increase the score target to 4096 and update the scoring system" - ], - [ - "P1", - "Redesign the user interface to accommodate the larger grid size" - ], - [ - "P1", - "Ensure the undo move feature is compatible with the new grid and score target" - ], - [ - "P2", - "Optimize game performance for the increased complexity of an 8x8 grid" - ], - [ - "P2", - "Maintain a visually appealing and consistent theme with the updated game features" - ] - ], - "UI Design draft": "The UI will be updated to feature an 8x8 grid while maintaining a minimalist design. The main game screen will display the larger game grid, current score, high score, and an undo button. The color scheme and transitions will be adapted to ensure clarity and pleasant aesthetics despite the increased grid size.", - "Anything UNCLEAR": "The specifics of how the undo feature should work with the larger grid size and whether there should be any limitations on its use need to be clarified." -} -""" +from tests.data.incremental_dev_project.mock import ( + DESIGN_SAMPLE, + REFINED_DESIGN_JSON, + REFINED_PRD_JSON, +) @pytest.fixture() @@ -98,10 +24,16 @@ def llm(): @pytest.mark.asyncio -async def test_write_design_an(): - node = await REFINED_DESIGN_NODES.fill(CONTEXT, llm) - assert node.instruct_content - assert "Refined Implementation Approach" in node.instruct_content.model_dump_json() - assert "Refined File List" in node.instruct_content.model_dump_json() - assert "Refined Data Structures and Interfaces" in node.instruct_content.model_dump_json() - assert "Refined Program call flow" in node.instruct_content.model_dump_json() +async def test_write_design_an(mocker): + root = ActionNode.from_children( + "RefinedDesignAPI", [ActionNode(key="", expected_type=str, instruction="", example="")] + ) + root.instruct_content = REFINED_DESIGN_JSON + + mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODES.fill", return_value=root) + prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) + node = await REFINED_DESIGN_NODES.fill(prompt, llm) + assert "Refined Implementation Approach" in node.instruct_content + assert "Refined File list" in node.instruct_content + assert "Refined Data structures and interfaces" in node.instruct_content + assert "Refined Program call flow" in node.instruct_content diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 68d73a3d9..0540ed6e1 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -7,129 +7,15 @@ """ import pytest +from metagpt.actions.action_node import ActionNode, dict_to_markdown +from metagpt.actions.project_management import NEW_REQ_TEMPLATE from metagpt.actions.project_management_an import REFINED_PM_NODES from metagpt.llm import LLM - -CONTEXT = """ -### Legacy Content -{ - "Required Python packages": [ - "pygame==2.0.1" - ], - "Required Other language third-party packages": [ - "No third-party dependencies required" - ], - "Logic Analysis": [ - [ - "constants.py", - "Contains all the constants like GRID_SIZE, WINDOW_WIDTH, WINDOW_HEIGHT, BACKGROUND_COLOR, TILE_COLORS" - ], - [ - "game.py", - "Contains Game class with methods for game logic such as move, undo, check_game_over, and reset_game" - ], - [ - "ui.py", - "Contains UI class responsible for drawing the grid, displaying scores, showing game over, and undo button" - ], - [ - "main.py", - "Contains Main class which initializes the game loop and orchestrates the interactions between Game and UI classes" - ] - ], - "Task list": [ - "constants.py", - "game.py", - "ui.py", - "main.py" - ], - "Full API spec": "", - "Shared Knowledge": "`constants.py` contains constants shared across `game.py` and `ui.py`. The Main class in `main.py` acts as the controller orchestrating the game flow and UI updates.", - "Anything UNCLEAR": "The specifics of the scoring system and how the high score is stored and retrieved need to be clarified. Additionally, the exact graphical assets and animations for the game are not specified." -} - -### New Requirements -{ - "Refined Implementation Approach": "We will refine our implementation approach to accommodate the new 8x8 grid and the increased winning score target of 4096. This will involve optimizing the game's core logic to handle the larger grid size efficiently and updating the scoring system. We will also enhance the UI to ensure it remains user-friendly and visually appealing with the new grid. The undo feature will be adapted to work seamlessly with the increased complexity of the game.", - "File list": [ - "main.py", - "game.py", - "ui.py", - "constants.py" - ], - "Refined Data Structures and Interfaces": "classDiagram - class Main { - +pygame: PygameInstance - +game: Game - +ui: UI - +run() void - } - class Game { - -grid: list - -current_score: int - -high_score: int - -last_move: list - -target_score: int - +__init__(grid_size: int, target_score: int) - +move(direction: str) bool - +undo() bool - +check_game_over() bool - +reset_game() void - +update_score(value: int) void - } - class UI { - -screen: PygameSurface - -font: PygameFont - +__init__(screen_size: tuple, grid_size: int) - +draw_grid(grid: list) void - +display_score(current_score: int, high_score: int) void - +show_game_over() void - +show_undo_button() void - +update_ui_for_larger_grid() void - } - class Constants { - +GRID_SIZE: int = 8 - +TARGET_SCORE: int = 4096 - +WINDOW_WIDTH: int - +WINDOW_HEIGHT: int - +BACKGROUND_COLOR: tuple - +TILE_COLORS: dict - } - Main --> Game - Main --> UI - Game --> Constants - UI --> Constants", - "Refined Program call flow": "sequenceDiagram - participant M as Main - participant G as Game - participant U as UI - M->>G: create instance(grid_size: Constants.GRID_SIZE, target_score: Constants.TARGET_SCORE) - M->>U: create instance(screen_size: (Constants.WINDOW_WIDTH, Constants.WINDOW_HEIGHT), grid_size: Constants.GRID_SIZE) - loop game loop - M->>U: draw_grid(G.grid) - M->>U: display_score(G.current_score, G.high_score) - M->>U: show_undo_button() - M->>G: move(direction) - alt if move is valid - G-->>M: return true - M->>G: update_score(value) - else if move is invalid - G-->>M: return false - end - alt if undo is triggered - M->>G: undo() - G-->>M: return true - else no undo - G-->>M: return false - end - alt if game over - M->>U: show_game_over() - M->>G: reset_game() - end - end", - "Anything UNCLEAR": "It remains unclear if the undo feature should have limitations on its use, such as a maximum number of undos per game or if it should be available without restriction. Further clarification on this aspect would be beneficial." -} -""" +from tests.data.incremental_dev_project.mock import ( + REFINED_DESIGN_JSON, + REFINED_TASKS_JSON, + TASKS_SAMPLE, +) @pytest.fixture() @@ -138,11 +24,17 @@ def llm(): @pytest.mark.asyncio -async def test_project_management_an(llm): - node = await REFINED_PM_NODES.fill(CONTEXT, llm) +async def test_project_management_an(mocker): + root = ActionNode.from_children( + "RefinedProjectManagement", [ActionNode(key="", expected_type=str, instruction="", example="")] + ) + root.instruct_content = REFINED_TASKS_JSON + + mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODES.fill", return_value=root) + + prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) + node = await REFINED_PM_NODES.fill(prompt, llm) assert node.instruct_content - assert "Required Python Packages" in node.instruct_content.model_dump_json() - assert "Required Other Language Packages" in node.instruct_content.model_dump_json() - assert "Refined Logic Analysis" in node.instruct_content.model_dump_json() - assert "Refined Task List" in node.instruct_content.model_dump_json() - assert "Refined Shared Knowledge" in node.instruct_content.model_dump_json() + assert "Refined Logic Analysis" in node.instruct_content + assert "Refined Task list" in node.instruct_content + assert "Refined Shared Knowledge" in node.instruct_content diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index f3fba26cc..998605c4b 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -7,300 +7,61 @@ """ import pytest +from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_guideline_an import ( CODE_GUIDELINE_CONTEXT, - REFINE_CODE_SCRIPT_EXAMPLE, REFINED_CODE_TEMPLATE, WriteCodeGuideline, ) - -REQUIREMENT_EXAMPLE = """Add subtraction, multiplication and division operations to the calculator. -The current calculator can only perform basic addition operations, and it is necessary to introduce subtraction, multiplication, division operation into the calculator -""" - -DESIGN_EXAMPLE = """ -{ - "Refined Implementation Approach": "To accommodate the new requirements, we will extend the existing Python-based calculator application. We will enhance the Tkinter-based UI to include buttons for subtraction, multiplication, and division, alongside the existing addition functionality. We will also implement input validation to handle edge cases such as division by zero. The architecture will be modular, with separate components for the UI, calculation logic, and error handling to maintain simplicity and facilitate future enhancements such as a history feature.", - "File list": [ - "main.py", - "calculator.py", - "interface.py", - "operations.py" - ], - "Refined Data Structures and Interfaces": "classDiagram\n class CalculatorApp {\n +main() None\n }\n class Calculator {\n -result float\n +add(number1: float, number2: float) float\n +subtract(number1: float, number2: float) float\n +multiply(number1: float, number2: float) float\n +divide(number1: float, number2: float) float\n +clear() None\n }\n class Interface {\n -calculator Calculator\n +start() None\n +display_result(result: float) None\n +get_input() float\n +show_error(message: str) None\n +update_operation(operation: str) None\n }\n class Operations {\n +perform_operation(operation: str, number1: float, number2: float) float\n }\n CalculatorApp --> Interface\n Interface --> Calculator\n Calculator --> Operations", - "Refined Program call flow": "sequenceDiagram\n participant CA as CalculatorApp\n participant I as Interface\n participant C as Calculator\n participant O as Operations\n CA->>I: start()\n I->>I: get_input()\n I->>I: update_operation(operation)\n loop For Each Operation\n I->>C: perform_operation(operation, number1, number2)\n C->>O: perform_operation(operation, number1, number2)\n O-->>C: return result\n C-->>I: return result\n I->>I: display_result(result)\n end\n I->>I: show_error(message)", - "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." -} -""" - -TASKS_EXAMPLE = """ -{ - "Required Python packages": [ - "tkinter" - ], - "Required Other language third-party packages": [ - "No third-party dependencies required" - ], - "Refined Logic Analysis": [ - [ - "main.py", - "Entry point of the application, creates an instance of the Interface class and starts the application." - ], - [ - "calculator.py", - "Contains the Calculator class with add, subtract, multiply, divide and clear methods for performing arithmetic operations." - ], - [ - "interface.py", - "Contains the Interface class responsible for the GUI, interacts with Calculator for the logic and displays results or errors." - ], - [ - "operations.py", - "Contains the Operations class with perform_operation method that delegates the arithmetic operation based on the operation argument." - ] - ], - "Refined Task list": [ - "operations.py", - "calculator.py", - "interface.py", - "main.py" - ], - "Full API spec": "", - "Refined Shared Knowledge": "`interface.py` will use the Calculator class from `calculator.py` to perform operations and display results. `main.py` will be the starting point that initializes the Interface. `calculator.py` will now also interact with `operations.py` to perform the arithmetic operations.", - "Anything UNCLEAR": "The requirement for a history feature is mentioned but not prioritized. It is unclear whether this should be implemented now or in the future. Additionally, there is no specification on the limit to the size of the numbers or the number of operations that can be performed in sequence. These aspects will need clarification for complete implementation." -} -""" - -CODE_GUIDELINE_SCRIPT_EXAMPLE = """ ------ calculator.py -```## calculator.py - -class Calculator: - def __init__(self): - self.result = 0.0 # Default value for the result - - def add(self, number1: float, number2: float) -> float: - ''' - Adds two numbers and returns the result. - - Args: - number1 (float): The first number to add. - number2 (float): The second number to add. - - Returns: - float: The sum of number1 and number2. - ''' - self.result = number1 + number2 - return self.result - - def clear(self) -> None: - ''' - Clears the result to its default value. - ''' - self.result = 0.0 -``` - ----- interface.py -```## interface.py -import tkinter as tk -from calculator import Calculator - -class Interface: - def __init__(self): - self.calculator = Calculator() - self.root = tk.Tk() - self.root.title("Calculator") - self.create_widgets() - - def create_widgets(self): - self.result_var = tk.StringVar() - self.result_display = tk.Entry(self.root, textvariable=self.result_var, state='readonly', justify='right', font=('Arial', 24)) - self.result_display.grid(row=0, column=0, columnspan=4, sticky='nsew') - - self.entry_number1 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number1.grid(row=1, column=0, columnspan=2, sticky='nsew') - - self.entry_number2 = tk.Entry(self.root, justify='right', font=('Arial', 18)) - self.entry_number2.grid(row=1, column=2, columnspan=2, sticky='nsew') - - self.add_button = tk.Button(self.root, text='+', command=self.add, font=('Arial', 18)) - self.add_button.grid(row=2, column=0, sticky='nsew') - - self.clear_button = tk.Button(self.root, text='C', command=self.clear, font=('Arial', 18)) - self.clear_button.grid(row=2, column=1, sticky='nsew') - - self.quit_button = tk.Button(self.root, text='Quit', command=self.root.quit, font=('Arial', 18)) - self.quit_button.grid(row=2, column=2, columnspan=2, sticky='nsew') - - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_columnconfigure(0, weight=1) - - def start(self): - self.root.mainloop() - - def display_result(self, result: float): - self.result_var.set(str(result)) - - def get_input(self): - try: - number1 = float(self.entry_number1.get()) - number2 = float(self.entry_number2.get()) - return number1, number2 - except ValueError: - self.show_error("Invalid input! Please enter valid numbers.") - return None, None - - def add(self): - number1, number2 = self.get_input() - if number1 is not None and number2 is not None: - result = self.calculator.add(number1, number2) - self.display_result(result) - - def clear(self): - self.entry_number1.delete(0, tk.END) - self.entry_number2.delete(0, tk.END) - self.result_var.set("") - - def show_error(self, message: str): - tk.messagebox.showerror("Error", message) - -# This code is meant to be used as a module and not as a standalone script. -# The Interface class will be instantiated and started by the main.py file. -``` - ----- main.py -```## main.py -from interface import Interface - - -class CalculatorApp: - @staticmethod - def main(): - interface = Interface() - interface.start() - - -if __name__ == "__main__": - CalculatorApp.main() -```""" - -GUIDELINES_AND_INCREMENTAL_CHANGE_EXAMPLE = """ -{ - "Guidelines and Incremental Change": " -1. Guideline for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. -```python -class Calculator: - self.result = number1 + number2 - return self.result - -- def sub(self, number1, number2) -> float: -+ def subtract(self, number1: float, number2: float) -> float: -+ ''' -+ Subtracts the second number from the first and returns the result. -+ -+ Args: -+ number1 (float): The number to be subtracted from. -+ number2 (float): The number to subtract. -+ -+ Returns: -+ float: The difference of number1 and number2. -+ ''' -+ self.result = number1 - number2 -+ return self.result -+ - def multiply(self, number1: float, number2: float) -> float: -- pass -+ ''' -+ Multiplies two numbers and returns the result. -+ -+ Args: -+ number1 (float): The first number to multiply. -+ number2 (float): The second number to multiply. -+ -+ Returns: -+ float: The product of number1 and number2. -+ ''' -+ self.result = number1 * number2 -+ return self.result -+ - def divide(self, number1: float, number2: float) -> float: -- pass -+ ''' -+ ValueError: If the second number is zero. -+ ''' -+ if number2 == 0: -+ raise ValueError('Cannot divide by zero') -+ self.result = number1 / number2 -+ return self.result -+ -- def reset_result(self): -+ def clear(self): -+ if self.result != 0.0: -+ print("Result is not zero, clearing...") -+ else: -+ print("Result is already zero, no need to clear.") -+ - self.result = 0.0 -``` - -2. Guideline for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards. -```python -def add_numbers(): - result = calculator.add_numbers(num1, num2) - return jsonify({'result': result}), 200 - --# TODO: Implement subtraction, multiplication, and division operations -+@app.route('/subtract_numbers', methods=['POST']) -+def subtract_numbers(): -+ data = request.get_json() -+ num1 = data.get('num1', 0) -+ num2 = data.get('num2', 0) -+ result = calculator.subtract_numbers(num1, num2) -+ return jsonify({'result': result}), 200 -+ -+@app.route('/multiply_numbers', methods=['POST']) -+def multiply_numbers(): -+ data = request.get_json() -+ num1 = data.get('num1', 0) -+ num2 = data.get('num2', 0) -+ try: -+ result = calculator.divide_numbers(num1, num2) -+ except ValueError as e: -+ return jsonify({'error': str(e)}), 400 -+ return jsonify({'result': result}), 200 -+ - if __name__ == '__main__': - app.run() -```" -} -""" +from tests.data.incremental_dev_project.mock import ( + DESIGN_SAMPLE, + GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE, + NEW_REQUIREMENT_SAMPLE, + OLD_CODE_SAMPLE, + REFINED_CODE_INPUT_SAMPLE, + REFINED_CODE_SAMPLE, + REFINED_DESIGN_JSON, + REFINED_PRD_JSON, + REFINED_TASKS_JSON, + TASKS_SAMPLE, +) @pytest.mark.asyncio -async def test_write_code_guideline_an(): +async def test_write_code_guideline_an(mocker): + root = ActionNode.from_children( + "WriteCodeGuideline", [ActionNode(key="", expected_type=str, instruction="", example="")] + ) + root.instruct_content = GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE + mocker.patch("metagpt.actions.write_code_guideline_an.WriteCodeGuideline.run", return_value=root) + write_code_guideline = WriteCodeGuideline() context = CODE_GUIDELINE_CONTEXT.format( - requirement=REQUIREMENT_EXAMPLE, design=DESIGN_EXAMPLE, tasks=TASKS_EXAMPLE, code=CODE_GUIDELINE_SCRIPT_EXAMPLE + user_requirement=NEW_REQUIREMENT_SAMPLE, + product_requirement_pools=REFINED_PRD_JSON.get("Refined Requirement Pool", ""), + design=REFINED_DESIGN_JSON, + tasks=REFINED_TASKS_JSON, + code=OLD_CODE_SAMPLE, ) node = await write_code_guideline.run(context=context) - assert node.instruct_content - assert "Incremental Change" in node.instruct_content.model_dump_json() + assert "Guidelines and Incremental Change" in node.instruct_content @pytest.mark.asyncio -async def test_refine_code(): +async def test_refine_code(mocker): + mocker.patch("metagpt.actions.write_code.WriteCode.write_code", return_value=REFINED_CODE_SAMPLE) prompt = REFINED_CODE_TEMPLATE.format( - requirement=REQUIREMENT_EXAMPLE, - guideline=GUIDELINES_AND_INCREMENTAL_CHANGE_EXAMPLE, - design=DESIGN_EXAMPLE, - tasks=TASKS_EXAMPLE, - code=REFINE_CODE_SCRIPT_EXAMPLE, + user_requirement=NEW_REQUIREMENT_SAMPLE, + guideline=GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE, + design=DESIGN_SAMPLE, + tasks=TASKS_SAMPLE, + code=REFINED_CODE_INPUT_SAMPLE, logs="", feedback="", - filename="interface.py", + filename="game.py", summary_log="", ) code = await WriteCode().write_code(prompt=prompt) assert code - assert "def create_widgets" in code + assert "def" in code diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 693f4924d..e7f288c68 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -7,73 +7,14 @@ """ import pytest -from metagpt.actions.write_prd_an import REFINE_PRD_NODE +from metagpt.actions.action_node import ActionNode +from metagpt.actions.write_prd_an import REFINE_PRD_NODE, REFINE_PRD_TEMPLATE from metagpt.llm import LLM - -CONTEXT = """ -### New Project Name -py2048_game - -### New Requirements -Changed score target for 2048 game from 2048 to 4096. -Please change the game's score target from 2048 to 4096, and change the interface size from 4*4 to 8*8. - -### Legacy Content -{ - "Language": "en_us", - "Programming Language": "Python", - "Original Requirements": "make a simple 2048 game based on pygame", - "Project Name": "pygame_2048", - "Product Goals": [ - "Develop a user-friendly and intuitive 2048 game", - "Ensure the game is visually appealing and maintains a consistent theme", - "Implement smooth and responsive game controls" - ], - "User Stories": [ - "As a player, I want to experience a clear and simple interface so that I can focus on the gameplay", - "As a player, I want to see my current and high scores to track my progress", - "As a player, I want the option to undo my last move to improve my strategy" - ], - "Competitive Analysis": [ - "2048 Original: Classic gameplay with minimalistic design, but lacks modern features", - "2048 by Gabriele Cirulli: Open-source version with clean UI, but no additional features", - "2048 Hex: Unique hexagon board, providing a different challenge", - "2048 Multiplayer: Allows playing against others, but the interface is cluttered", - "2048 with AI: Includes AI challenge mode, but the AI is often too difficult for casual players", - "2048.io: Combines 2048 gameplay with .io style, though it can be overwhelming for new players", - "2048 Animated: Features animations, but has performance issues on some devices" - ], - "Competitive Quadrant Chart": "quadrantChart\n title \"2048 Game Market Positioning\"\n x-axis \"Basic Features\" --> \"Advanced Features\"\n y-axis \"Low User Engagement\" --> \"High User Engagement\"\n quadrant-1 \"Niche Innovators\"\n quadrant-2 \"Market Leaders\"\n quadrant-3 \"Emerging Contenders\"\n quadrant-4 \"Falling Behind\"\n \"2048 Original\": [0.2, 0.7]\n \"2048 by Gabriele Cirulli\": [0.3, 0.8]\n \"2048 Hex\": [0.5, 0.4]\n \"2048 Multiplayer\": [0.6, 0.6]\n \"2048 with AI\": [0.7, 0.5]\n \"2048.io\": [0.4, 0.3]\n \"2048 Animated\": [0.3, 0.2]\n \"Our Target Product\": [0.8, 0.9]", - "Requirement Analysis": "The game should be simple yet engaging, with a focus on smooth performance and an intuitive user interface. High scores and undo functionality are important to users for a competitive and strategic gameplay experience. Aesthetic appeal and a consistent theme will also contribute to the game's success.", - "Requirement Pool": [ - [ - "P0", - "Develop core 2048 game mechanics using pygame" - ], - [ - "P0", - "Design a clean and intuitive user interface" - ], - [ - "P1", - "Implement score tracking with high score memory" - ], - [ - "P1", - "Add undo move feature for enhanced gameplay strategy" - ], - [ - "P2", - "Create visually appealing graphics and animations" - ] - ], - "UI Design draft": "The UI will feature a minimalist design with a focus on ease of use. The main game screen will display the game grid, current score, high score, and an undo button. The color scheme will be consistent and pleasant to the eye, with smooth transitions for tile movements.", - "Anything UNCLEAR": "The specifics of the undo feature need to be clarified, such as how many moves can be undone and whether it affects the scoring." -} - -### Search Information -- -""" +from tests.data.incremental_dev_project.mock import ( + NEW_REQUIREMENT_SAMPLE, + PRD_SAMPLE, + REFINED_PRD_JSON, +) @pytest.fixture() @@ -82,11 +23,19 @@ def llm(): @pytest.mark.asyncio -async def test_write_prd_an(llm): - node = await REFINE_PRD_NODE.fill(CONTEXT, llm) - assert node.instruct_content - assert "Refined Requirements" in node.instruct_content.model_dump_json() - assert "Refined Product Goals" in node.instruct_content.model_dump_json() - assert "Refined User Stories" in node.instruct_content.model_dump_json() - assert "Refined Requirement Analysis" in node.instruct_content.model_dump_json() - assert "Refined Requirement Pool" in node.instruct_content.model_dump_json() +async def test_write_prd_an(mocker): + root = ActionNode.from_children("RefinePRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) + root.instruct_content = REFINED_PRD_JSON + + mocker.patch("metagpt.actions.write_prd_an.REFINE_PRD_NODE.fill", return_value=root) + prompt = REFINE_PRD_TEMPLATE.format( + requirements=NEW_REQUIREMENT_SAMPLE, + old_prd=PRD_SAMPLE, + project_name="", + ) + node = await REFINE_PRD_NODE.fill(prompt, llm) + assert "Refined Requirements" in node.instruct_content + assert "Refined Product Goals" in node.instruct_content + assert "Refined User Stories" in node.instruct_content + assert "Refined Requirement Analysis" in node.instruct_content + assert "Refined Requirement Pool" in node.instruct_content From db086e47e764cc8303c4b2fd64a0834f6108202a Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 18 Jan 2024 09:42:09 +0800 Subject: [PATCH 056/101] modify: get value of ActionNode by key of ActionNode --- metagpt/actions/design_api.py | 13 ++++++++++--- metagpt/actions/project_management.py | 8 ++++++-- metagpt/actions/write_code.py | 3 ++- metagpt/actions/write_prd.py | 3 ++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 073eb20ae..933c59b74 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -14,7 +14,14 @@ from pathlib import Path from typing import Optional from metagpt.actions import Action, ActionOutput -from metagpt.actions.design_api_an import DESIGN_API_NODE, REFINED_DESIGN_NODES +from metagpt.actions.design_api_an import ( + DATA_STRUCTURES_AND_INTERFACES, + DESIGN_API_NODE, + PROGRAM_CALL_FLOW, + REFINED_DATA_STRUCTURES_AND_INTERFACES, + REFINED_DESIGN_NODES, + REFINED_PROGRAM_CALL_FLOW, +) from metagpt.config import CONFIG from metagpt.const import ( DATA_API_DESIGN_FILE_REPO, @@ -109,7 +116,7 @@ class WriteDesign(Action): @staticmethod async def _save_data_api_design(design_doc): m = json.loads(design_doc.content) - data_api_design = m.get("Data structures and interfaces") or m.get("Refined Data structures and interfaces") + data_api_design = m.get(DATA_STRUCTURES_AND_INTERFACES.key) or m.get(REFINED_DATA_STRUCTURES_AND_INTERFACES.key) if not data_api_design: return pathname = CONFIG.git_repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("") @@ -119,7 +126,7 @@ class WriteDesign(Action): @staticmethod async def _save_seq_flow(design_doc): m = json.loads(design_doc.content) - seq_flow = m.get("Program call flow") or m.get("Refined Program call flow") + seq_flow = m.get(PROGRAM_CALL_FLOW.key) or m.get(REFINED_PROGRAM_CALL_FLOW.key) if not seq_flow: return pathname = CONFIG.git_repo.workdir / Path(SEQ_FLOW_FILE_REPO) / Path(design_doc.filename).with_suffix("") diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 3c379d96f..6e298e500 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -15,7 +15,11 @@ from typing import Optional from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODES +from metagpt.actions.project_management_an import ( + PM_NODE, + REFINED_PM_NODES, + REQUIRED_PYTHON_PACKAGES, +) from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -100,7 +104,7 @@ class WriteTasks(Action): @staticmethod async def _update_requirements(doc): m = json.loads(doc.content) - packages = set(m.get("Required Python third-party packages", set())) + packages = set(m.get(REQUIRED_PYTHON_PACKAGES.key, set())) file_repo = CONFIG.git_repo.new_file_repository() requirement_doc = await file_repo.get(filename=PACKAGE_REQUIREMENTS_FILENAME) if not requirement_doc: diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index ce0e2fe3b..30ebf09f8 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,6 +21,7 @@ from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action +from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST from metagpt.actions.write_code_guideline_an import REFINED_CODE_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( @@ -180,7 +181,7 @@ class WriteCode(Action): if not task_doc.content: task_doc.content = FileRepository.get_file(filename=task_doc.filename, relative_path=TASK_FILE_REPO) m = json.loads(task_doc.content) - code_filenames = m.get("Task list", []) if mode == "normal" else m.get("Refined Task list", []) + code_filenames = m.get(TASK_LIST.key, []) if mode == "normal" else m.get(REFINED_TASK_LIST.key, []) codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 84a9fa13d..e12c1e1ec 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -21,6 +21,7 @@ from metagpt.actions import Action, ActionOutput from metagpt.actions.action_node import ActionNode from metagpt.actions.fix_bug import FixBug from metagpt.actions.write_prd_an import ( + COMPETITIVE_QUADRANT_CHART, PROJECT_NAME, REFINE_PRD_NODE, REFINE_PRD_TEMPLATE, @@ -166,7 +167,7 @@ class WritePRD(Action): @staticmethod async def _save_competitive_analysis(prd_doc): m = json.loads(prd_doc.content) - quadrant_chart = m.get("Competitive Quadrant Chart") + quadrant_chart = m.get(COMPETITIVE_QUADRANT_CHART.key) if not quadrant_chart: return pathname = ( From fe64b23a0ec2f38f8b47368a6ad603079989520b Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 18 Jan 2024 10:01:33 +0800 Subject: [PATCH 057/101] update test file of ActionNode --- tests/metagpt/actions/test_design_api_an.py | 19 +++++++++++------ .../actions/test_project_management_an.py | 17 +++++++++------ .../actions/test_write_code_guideline_an.py | 11 ++++++++-- tests/metagpt/actions/test_write_prd_an.py | 21 ++++++++++++------- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 2aa123224..39de2a595 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -6,6 +6,7 @@ @File : test_design_api_an.py """ import pytest +from openai._models import BaseModel from metagpt.actions.action_node import ActionNode, dict_to_markdown from metagpt.actions.design_api import NEW_REQ_TEMPLATE @@ -23,17 +24,23 @@ def llm(): return LLM() +def mock_refined_design_json(): + return REFINED_DESIGN_JSON + + @pytest.mark.asyncio async def test_write_design_an(mocker): root = ActionNode.from_children( "RefinedDesignAPI", [ActionNode(key="", expected_type=str, instruction="", example="")] ) - root.instruct_content = REFINED_DESIGN_JSON - + root.instruct_content = BaseModel() + root.instruct_content.model_dump = mock_refined_design_json mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODES.fill", return_value=root) + prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) node = await REFINED_DESIGN_NODES.fill(prompt, llm) - assert "Refined Implementation Approach" in node.instruct_content - assert "Refined File list" in node.instruct_content - assert "Refined Data structures and interfaces" in node.instruct_content - assert "Refined Program call flow" in node.instruct_content + + assert "Refined Implementation Approach" in node.instruct_content.model_dump() + assert "Refined File list" in node.instruct_content.model_dump() + assert "Refined Data structures and interfaces" in node.instruct_content.model_dump() + assert "Refined Program call flow" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 0540ed6e1..50dc47067 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -6,6 +6,7 @@ @File : test_project_management_an.py """ import pytest +from openai._models import BaseModel from metagpt.actions.action_node import ActionNode, dict_to_markdown from metagpt.actions.project_management import NEW_REQ_TEMPLATE @@ -23,18 +24,22 @@ def llm(): return LLM() +def mock_refined_tasks_json(): + return REFINED_TASKS_JSON + + @pytest.mark.asyncio async def test_project_management_an(mocker): root = ActionNode.from_children( "RefinedProjectManagement", [ActionNode(key="", expected_type=str, instruction="", example="")] ) - root.instruct_content = REFINED_TASKS_JSON - + root.instruct_content = BaseModel() + root.instruct_content.model_dump = mock_refined_tasks_json mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODES.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) node = await REFINED_PM_NODES.fill(prompt, llm) - assert node.instruct_content - assert "Refined Logic Analysis" in node.instruct_content - assert "Refined Task list" in node.instruct_content - assert "Refined Shared Knowledge" in node.instruct_content + + assert "Refined Logic Analysis" in node.instruct_content.model_dump() + assert "Refined Task list" in node.instruct_content.model_dump() + assert "Refined Shared Knowledge" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_guideline_an.py index 998605c4b..5a4e19d57 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_guideline_an.py @@ -6,6 +6,7 @@ @File : test_write_code_guideline_an.py """ import pytest +from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode @@ -28,12 +29,17 @@ from tests.data.incremental_dev_project.mock import ( ) +def mock_guidelines_and_incremental_change(): + return GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE + + @pytest.mark.asyncio async def test_write_code_guideline_an(mocker): root = ActionNode.from_children( "WriteCodeGuideline", [ActionNode(key="", expected_type=str, instruction="", example="")] ) - root.instruct_content = GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE + root.instruct_content = BaseModel() + root.instruct_content.model_dump = mock_guidelines_and_incremental_change mocker.patch("metagpt.actions.write_code_guideline_an.WriteCodeGuideline.run", return_value=root) write_code_guideline = WriteCodeGuideline() @@ -45,7 +51,8 @@ async def test_write_code_guideline_an(mocker): code=OLD_CODE_SAMPLE, ) node = await write_code_guideline.run(context=context) - assert "Guidelines and Incremental Change" in node.instruct_content + + assert "Guidelines and Incremental Change" in node.instruct_content.model_dump() @pytest.mark.asyncio diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index e7f288c68..1fdaa75c2 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -6,6 +6,7 @@ @File : test_write_prd_an.py """ import pytest +from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_prd_an import REFINE_PRD_NODE, REFINE_PRD_TEMPLATE @@ -22,20 +23,26 @@ def llm(): return LLM() +def mock_refined_prd_json(): + return REFINED_PRD_JSON + + @pytest.mark.asyncio async def test_write_prd_an(mocker): root = ActionNode.from_children("RefinePRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) - root.instruct_content = REFINED_PRD_JSON - + root.instruct_content = BaseModel() + root.instruct_content.model_dump = mock_refined_prd_json mocker.patch("metagpt.actions.write_prd_an.REFINE_PRD_NODE.fill", return_value=root) + prompt = REFINE_PRD_TEMPLATE.format( requirements=NEW_REQUIREMENT_SAMPLE, old_prd=PRD_SAMPLE, project_name="", ) node = await REFINE_PRD_NODE.fill(prompt, llm) - assert "Refined Requirements" in node.instruct_content - assert "Refined Product Goals" in node.instruct_content - assert "Refined User Stories" in node.instruct_content - assert "Refined Requirement Analysis" in node.instruct_content - assert "Refined Requirement Pool" in node.instruct_content + + assert "Refined Requirements" in node.instruct_content.model_dump() + assert "Refined Product Goals" in node.instruct_content.model_dump() + assert "Refined User Stories" in node.instruct_content.model_dump() + assert "Refined Requirement Analysis" in node.instruct_content.model_dump() + assert "Refined Requirement Pool" in node.instruct_content.model_dump() From 2bc88cd71b45a6bcdc6e9c287f05e722f69b44f0 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 18 Jan 2024 10:15:36 +0800 Subject: [PATCH 058/101] update function of save code_guideline file --- metagpt/actions/write_code_guideline_an.py | 15 +++++---------- metagpt/roles/engineer.py | 7 +++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_guideline_an.py index ff59ee7d6..28af592c0 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_guideline_an.py @@ -187,20 +187,15 @@ class WriteCodeGuideline(Action): return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") @staticmethod - async def save(guideline): - await WriteCodeGuideline.save_json(guideline) - await WriteCodeGuideline.save_md(guideline) - - @staticmethod - async def save_json(guideline): - filename = "code_guideline.json" + async def save_json(guideline, filename="code_guideline.json"): await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_FILE_REPO).save( filename=filename, content=str(guideline) ) @staticmethod - async def save_md(guideline): - filename = "code_guideline.md" + async def save_md(guideline, filename="code_guideline.md"): + guideline_md = dict_to_markdown(guideline) await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_PDF_FILE_REPO).save( - filename=filename, content=dict_to_markdown(guideline) + filename=filename, content=guideline_md ) + return guideline_md diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index bb0a2e857..d767b1ebf 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -26,7 +26,6 @@ from pathlib import Path from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks -from metagpt.actions.action_node import dict_to_markdown from metagpt.actions.fix_bug import FixBug from metagpt.actions.summarize_code import SummarizeCode from metagpt.actions.write_code_guideline_an import ( @@ -367,10 +366,10 @@ class Engineer(Role): ) node = await WriteCodeGuideline().run(context=context) guideline = node.instruct_content.model_dump() - await WriteCodeGuideline.save(guideline) - guideline = dict_to_markdown(guideline) + await WriteCodeGuideline.save_json(guideline) + guideline_md = await WriteCodeGuideline.save_md(guideline) - return Document(root_path=CODE_GUIDELINE_PDF_FILE_REPO, filename="code_guideline.md", content=guideline) + return Document(root_path=CODE_GUIDELINE_PDF_FILE_REPO, filename="code_guideline.md", content=guideline_md) @staticmethod async def get_old_codes() -> str: From 6fb48664cb56941f0466dd17b8afc89a62712aa4 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 18 Jan 2024 15:18:29 +0800 Subject: [PATCH 059/101] replace REQUIRED_PYTHON_PACKAGES.key to "Required Python packages" --- metagpt/actions/project_management.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 6e298e500..d6b351d18 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -15,11 +15,7 @@ from typing import Optional from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import ( - PM_NODE, - REFINED_PM_NODES, - REQUIRED_PYTHON_PACKAGES, -) +from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODES from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -104,7 +100,7 @@ class WriteTasks(Action): @staticmethod async def _update_requirements(doc): m = json.loads(doc.content) - packages = set(m.get(REQUIRED_PYTHON_PACKAGES.key, set())) + packages = set(m.get("Required Python packages", set())) file_repo = CONFIG.git_repo.new_file_repository() requirement_doc = await file_repo.get(filename=PACKAGE_REQUIREMENTS_FILENAME) if not requirement_doc: From 1460cff7295bff3234338b5233c8db3d312d5810 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 18 Jan 2024 23:07:43 +0800 Subject: [PATCH 060/101] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61d03f692..6206b7f70 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,12 @@ # MetaGPT: The Multi-Agent Framework

Software Company Multi-Role Schematic (Gradually Implementing)

## News -🚀 Jan. 16, 2024: [MetaGPT paper](https://arxiv.org/abs/2308.00352) accepted for oral presentation **(top 1.2%)** at ICLR 2024, **ranking #1** in the LLM-based Agent category. +🚀 Jan. 16, 2024: Our paper [MetaGPT: Meta Programming for A Multi-Agent Collaborative Framework +](https://arxiv.org/abs/2308.00352) accepted for oral presentation **(top 1.2%)** at ICLR 2024, **ranking #1** in the LLM-based Agent category. -🚀 Jan. 03, 2024: [v0.6.0](https://github.com/geekan/MetaGPT/releases/tag/v0.6.0) released, new features include serialization, upgraded OpenAI package and supported multiple LLM etc. +🚀 Jan. 03, 2024: [v0.6.0](https://github.com/geekan/MetaGPT/releases/tag/v0.6.0) released, new features include serialization, upgraded OpenAI package and supported multiple LLM, provided [minimal example for debate](https://github.com/geekan/MetaGPT/blob/main/examples/debate_simple.py) etc. -🚀 Dec. 15, 2023: [v0.5.0](https://github.com/geekan/MetaGPT/releases/tag/v0.5.0) released, introducing **incremental development**, **multilingual**, **multiple programming languages**, etc. +🚀 Dec. 15, 2023: [v0.5.0](https://github.com/geekan/MetaGPT/releases/tag/v0.5.0) released, introducing some experimental features such as **incremental development**, **multilingual**, **multiple programming languages**, etc. 🔥 Nov. 08, 2023: MetaGPT is selected into [Open100: Top 100 Open Source achievements](https://www.benchcouncil.org/evaluation/opencs/annual.html). From 5389c52556c2065208ef4f67560931ab6c9112a9 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 18 Jan 2024 23:34:46 +0800 Subject: [PATCH 061/101] Update README.md --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6206b7f70..90c586068 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ # MetaGPT: The Multi-Agent Framework

-Assign different roles to GPTs to form a collaborative software entity for complex tasks. +Assign different roles to GPTs to form a collaborative entity for complex tasks.

CN doc EN doc JA doc -Discord Follow License: MIT roadmap +Discord Follow Twitter Follow

@@ -25,14 +25,6 @@ # MetaGPT: The Multi-Agent Framework Hugging Face

-1. MetaGPT takes a **one line requirement** as input and outputs **user stories / competitive analysis / requirements / data structures / APIs / documents, etc.** -2. Internally, MetaGPT includes **product managers / architects / project managers / engineers.** It provides the entire process of a **software company along with carefully orchestrated SOPs.** - 1. `Code = SOP(Team)` is the core philosophy. We materialize SOP and apply it to teams composed of LLMs. - -![A software company consists of LLM-based roles](docs/resources/software_company_cd.jpeg) - -

Software Company Multi-Role Schematic (Gradually Implementing)

- ## News 🚀 Jan. 16, 2024: Our paper [MetaGPT: Meta Programming for A Multi-Agent Collaborative Framework ](https://arxiv.org/abs/2308.00352) accepted for oral presentation **(top 1.2%)** at ICLR 2024, **ranking #1** in the LLM-based Agent category. @@ -49,6 +41,16 @@ ## News 🌟 Apr. 24, 2023: First line of MetaGPT code committed. +## Software Company as Multi-Agent System + +1. MetaGPT takes a **one line requirement** as input and outputs **user stories / competitive analysis / requirements / data structures / APIs / documents, etc.** +2. Internally, MetaGPT includes **product managers / architects / project managers / engineers.** It provides the entire process of a **software company along with carefully orchestrated SOPs.** + 1. `Code = SOP(Team)` is the core philosophy. We materialize SOP and apply it to teams composed of LLMs. + +![A software company consists of LLM-based roles](docs/resources/software_company_cd.jpeg) + +

Software Company Multi-Agent Schematic (Gradually Implementing)

+ ## Install ### Pip installation From 5190dc446238055107725897786969316cb6ee30 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 09:03:24 +0800 Subject: [PATCH 062/101] 1. rename and modify guideline to plan 2. update prompt in ActionNode 3. add code comment 4. refactor Guideline code structure --- metagpt/actions/design_api_an.py | 6 +- metagpt/actions/project_management_an.py | 2 +- metagpt/actions/write_code.py | 39 ++++++----- ..._guideline_an.py => write_code_plan_an.py} | 48 +++++-------- metagpt/actions/write_code_review.py | 27 +++++--- metagpt/const.py | 5 +- metagpt/roles/engineer.py | 64 ++++++++--------- metagpt/utils/mermaid.py | 69 ------------------- tests/data/incremental_dev_project/mock.py | 4 +- ...eline_an.py => test_write_code_plan_an.py} | 32 ++++----- tests/metagpt/test_incremental_dev.py | 2 +- 11 files changed, 112 insertions(+), 186 deletions(-) rename metagpt/actions/{write_code_guideline_an.py => write_code_plan_an.py} (68%) rename tests/metagpt/actions/{test_write_code_guideline_an.py => test_write_code_plan_an.py} (61%) diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 3e65cee1b..b872159e1 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -9,7 +9,7 @@ from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger -from metagpt.utils.mermaid import MMC1, MMC1_REFINE, MMC2, MMC2_REFINE +from metagpt.utils.mermaid import MMC1, MMC2 IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", @@ -62,7 +62,7 @@ REFINED_DATA_STRUCTURES_AND_INTERFACES = ActionNode( "methods (including __init__), and functions with precise type annotations. Delineate additional " "relationships between classes, ensuring clarity and adherence to PEP8 standards." "Retain content that is not related to incremental development but important for consistency and clarity.", - example=MMC1_REFINE, + example=MMC1, ) PROGRAM_CALL_FLOW = ActionNode( @@ -80,7 +80,7 @@ REFINED_PROGRAM_CALL_FLOW = ActionNode( "CRUD and initialization of each object. Ensure correct syntax usage and reflect the incremental changes introduced" "in the classes and API defined above. " "Retain content that is not related to incremental development but important for consistency and clarity.", - example=MMC2_REFINE, + example=MMC2, ) ANYTHING_UNCLEAR = ActionNode( diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 8b0196707..e3d6537ab 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -118,7 +118,7 @@ REFINE_NODES = [ ] PM_NODE = ActionNode.from_children("PM_NODE", NODES) -REFINED_PM_NODES = ActionNode.from_children("Refined_PM_NODES", REFINE_NODES) +REFINED_PM_NODES = ActionNode.from_children("REFINED_PM_NODES", REFINE_NODES) def main(): diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 30ebf09f8..93c7a2f65 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -16,18 +16,21 @@ """ import json +from typing import Literal from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST -from metagpt.actions.write_code_guideline_an import REFINED_CODE_TEMPLATE +from metagpt.actions.write_code_plan_an import REFINED_CODE_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, + PLAN_FILENAME, + PLAN_PDF_FILE_REPO, REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, @@ -104,6 +107,9 @@ class WriteCode(Action): test_doc = await FileRepository.get_file( filename="test_" + coding_context.filename + ".json", relative_path=TEST_OUTPUTS_FILE_REPO ) + plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_PDF_FILE_REPO) + plan = plan_doc.content if plan_doc else "" + requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) summary_doc = None if coding_context.design_doc and coding_context.design_doc.filename: summary_doc = await FileRepository.get_file( @@ -114,21 +120,17 @@ class WriteCode(Action): test_detail = RunCodeResult.loads(test_doc.content) logs = test_detail.stderr - docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO) - requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME) - - guideline = kwargs.get("guideline", "") if bug_feedback: code_context = coding_context.code_doc.content - elif guideline: - code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="guide") + elif plan: + code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="plan") else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) - if guideline: + if plan: prompt = REFINED_CODE_TEMPLATE.format( user_requirement=requirement_doc.content if requirement_doc else "", - guideline=guideline, + plan=plan, design=coding_context.design_doc.content if coding_context.design_doc else "", tasks=coding_context.task_doc.content if coding_context.task_doc else "", code=code_context, @@ -157,14 +159,14 @@ class WriteCode(Action): return coding_context @staticmethod - async def get_codes(task_doc, exclude, mode="normal") -> str: + async def get_codes(task_doc: Document, exclude: str, mode: Literal["normal", "plan"] = "normal") -> str: """ Get code snippets based on different modes. Attributes: task_doc (Document): Document object of the task file. exclude (str): Specifies the filename to be excluded from the code snippets. - mode (str): Specifies the mode, either "normal" or "guide" (default is "normal"). + mode (str): Specifies the mode, either "normal" or "plan" (default is "normal"). Returns: str: Code snippets. @@ -173,7 +175,7 @@ class WriteCode(Action): If mode is set to "normal", it returns code snippets for the regular coding phase, i.e., all the code generated before writing the current file. - If mode is set to "guide", it returns code snippets for incremental development, + If mode is set to "plan", it returns code snippets for incremental development, building upon the existing code in the "normal" mode and adding code for the current file's older versions. """ if not task_doc: @@ -185,31 +187,36 @@ class WriteCode(Action): codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) - if mode == "guide": + if mode == "plan": src_files = src_file_repo.all_files old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) old_files = old_file_repo.all_files + # Get the union of the files in the src and old workspaces union_files_list = list(set(src_files) | set(old_files)) for filename in union_files_list: + # Exclude the current file from the all code snippets to get the context code snippets for generating if filename == exclude: + # If the file is in the old workspace, use the legacy code # Exclude unnecessary code to maintain a clean and focused main.py file, ensuring only relevant and # essential functionality is included for the project’s requirements if filename in old_files and filename != "main.py": # Use legacy code doc = await old_file_repo.get(filename=filename) + # If the file is in the src workspace, skip it else: continue codes.insert(0, f"-----Now, {filename} to be rewritten\n```{doc.content}```\n=====") - + # The context code snippets are generated from the src workspace else: - # Use new code doc = await src_file_repo.get(filename=filename) + # If the file does not exist in the src workspace, skip it if not doc: continue codes.append(f"----- {filename}\n```{doc.content}```") - else: + elif mode == "normal": for filename in code_filenames: + # Exclude the current file to get the context code snippets for generating the current file if filename == exclude: continue doc = await src_file_repo.get(filename=filename) diff --git a/metagpt/actions/write_code_guideline_an.py b/metagpt/actions/write_code_plan_an.py similarity index 68% rename from metagpt/actions/write_code_guideline_an.py rename to metagpt/actions/write_code_plan_an.py index 28af592c0..f3f4177e4 100644 --- a/metagpt/actions/write_code_guideline_an.py +++ b/metagpt/actions/write_code_plan_an.py @@ -3,23 +3,21 @@ """ @Time : 2023/12/26 @Author : mannaandpoem -@File : write_code_guideline_an.py +@File : write_code_plan_an.py """ from metagpt.actions.action import Action -from metagpt.actions.action_node import ActionNode, dict_to_markdown -from metagpt.config import CONFIG -from metagpt.const import CODE_GUIDELINE_FILE_REPO, CODE_GUIDELINE_PDF_FILE_REPO +from metagpt.actions.action_node import ActionNode -GUIDELINES_AND_INCREMENTAL_CHANGE = ActionNode( - key="Guidelines and Incremental Change", +Plan = ActionNode( + key="Plan", expected_type=str, - instruction="Developing comprehensive and step-by-step incremental development guideline, and Write Incremental " + instruction="Developing comprehensive and step-by-step incremental development plan, and write Incremental " "Change by making a code draft that how to implement incremental development including detailed steps based on the " "context. Note: Track incremental changes using mark of '+' or '-' for add/modify/delete code, and conforms to the " "output format of git diff", example=""" -1. Guideline for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. +1. Plan for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero. ```python class Calculator: self.result = number1 + number2 @@ -75,7 +73,7 @@ class Calculator: self.result = 0.0 ``` -2. Guideline for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards. +2. Plan for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards. ```python def add_numbers(): result = calculator.add_numbers(num1, num2) @@ -106,7 +104,7 @@ def add_numbers(): ```""", ) -CODE_GUIDELINE_CONTEXT = """ +CODE_PLAN_CONTEXT = """ ## User New Requirements {user_requirement} @@ -125,14 +123,14 @@ CODE_GUIDELINE_CONTEXT = """ REFINED_CODE_TEMPLATE = """ NOTICE -Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and Guidelines and Incremental Change, ensuring the integration of new features. +Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and plan and Incremental Change, ensuring the integration of new features. # Context ## User New Requirements {user_requirement} -## Guidelines and Incremental Change -{guideline} +## Plan +{plan} ## Design {design} @@ -170,32 +168,18 @@ Role: You are a professional engineer; The main goal is to complete incremental 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Follow Guidelines and Incremental Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the guidelines. +5. Follow plan and Incremental Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. 9. Attention: Retain content that is not related to incremental development but important for consistency and clarity.". """ -WRITE_CODE_GUIDELINE_NODE = ActionNode.from_children("WriteCodeGuideline", [GUIDELINES_AND_INCREMENTAL_CHANGE]) +WRITE_CODE_PLAN_NODE = ActionNode.from_children("WriteCodePlan", [Plan]) -class WriteCodeGuideline(Action): +class WriteCodePlan(Action): async def run(self, context): self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " - "meticulously craft comprehensive incremental development guidelines and deliver detailed Incremental Change" - return await WRITE_CODE_GUIDELINE_NODE.fill(context=context, llm=self.llm, schema="json") - - @staticmethod - async def save_json(guideline, filename="code_guideline.json"): - await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_FILE_REPO).save( - filename=filename, content=str(guideline) - ) - - @staticmethod - async def save_md(guideline, filename="code_guideline.md"): - guideline_md = dict_to_markdown(guideline) - await CONFIG.git_repo.new_file_repository(CODE_GUIDELINE_PDF_FILE_REPO).save( - filename=filename, content=guideline_md - ) - return guideline_md + "meticulously craft comprehensive incremental development plan and deliver detailed Incremental Change" + return await WRITE_CODE_PLAN_NODE.fill(context=context, llm=self.llm, schema="json") diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 27e4c280e..72424c037 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -14,10 +14,16 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions import WriteCode from metagpt.actions.action import Action from metagpt.config import CONFIG -from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME +from metagpt.const import ( + DOCS_FILE_REPO, + PLAN_FILENAME, + PLAN_PDF_FILE_REPO, + REQUIREMENT_FILENAME, +) from metagpt.logs import logger from metagpt.schema import CodingContext from metagpt.utils.common import CodeParser +from metagpt.utils.file_repository import FileRepository PROMPT_TEMPLATE = """ # System @@ -138,16 +144,17 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.context.code_doc.content - # k = CONFIG.code_review_k_times or 1 - k = 1 - guideline = kwargs.get("guideline") - mode = "guide" if guideline else "normal" + k = CONFIG.code_review_k_times or 1 + plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_PDF_FILE_REPO) + plan = plan_doc.content if plan_doc else "" + mode = "plan" if plan else "normal" + for i in range(k): format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename) task_content = self.context.task_doc.content if self.context.task_doc else "" code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename, mode=mode) - if not guideline: + if not plan: context = "\n".join( [ "## System Design\n" + str(self.context.design_doc) + "\n", @@ -156,15 +163,15 @@ class WriteCodeReview(Action): ] ) else: - requirement_doc = await CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO).get( - filename=REQUIREMENT_FILENAME + requirement_doc = await FileRepository.get_file( + filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO ) user_requirement = requirement_doc.content if requirement_doc else "" context = "\n".join( [ - "## User New Requirements\n" + str(user_requirement) + "\n", - "## Guidelines and Incremental Change\n" + guideline + "\n", + "## User New Requirements\n" + user_requirement + "\n", + "## Plan\n" + plan + "\n", "## System Design\n" + str(self.context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", diff --git a/metagpt/const.py b/metagpt/const.py index 2ee1267d8..014193b59 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -87,19 +87,20 @@ MESSAGE_ROUTE_TO_NONE = "" REQUIREMENT_FILENAME = "requirement.txt" BUGFIX_FILENAME = "bugfix.txt" PACKAGE_REQUIREMENTS_FILENAME = "requirements.txt" +PLAN_FILENAME = "plan.json" DOCS_FILE_REPO = "docs" PRDS_FILE_REPO = "docs/prds" SYSTEM_DESIGN_FILE_REPO = "docs/system_design" TASK_FILE_REPO = "docs/tasks" -CODE_GUIDELINE_FILE_REPO = "docs/code_guideline" +PLAN_FILE_REPO = "docs/plan" COMPETITIVE_ANALYSIS_FILE_REPO = "resources/competitive_analysis" DATA_API_DESIGN_FILE_REPO = "resources/data_api_design" SEQ_FLOW_FILE_REPO = "resources/seq_flow" SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design" PRD_PDF_FILE_REPO = "resources/prd" TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks" -CODE_GUIDELINE_PDF_FILE_REPO = "resources/code_guideline" +PLAN_PDF_FILE_REPO = "resources/plan" TEST_CODES_FILE_REPO = "tests" TEST_OUTPUTS_FILE_REPO = "test_outputs" CODE_SUMMARIES_FILE_REPO = "docs/code_summaries" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d767b1ebf..695e9dd2a 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -23,21 +23,21 @@ import json import os from collections import defaultdict from pathlib import Path -from typing import Set +from typing import Literal, Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug +from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST from metagpt.actions.summarize_code import SummarizeCode -from metagpt.actions.write_code_guideline_an import ( - CODE_GUIDELINE_CONTEXT, - WriteCodeGuideline, -) +from metagpt.actions.write_code_plan_an import CODE_PLAN_CONTEXT, WriteCodePlan +from metagpt.actions.write_prd_an import REFINED_REQUIREMENT_POOL, REQUIREMENT_POOL from metagpt.config import CONFIG from metagpt.const import ( - CODE_GUIDELINE_PDF_FILE_REPO, CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, - PRDS_FILE_REPO, + PLAN_FILE_REPO, + PLAN_FILENAME, + PLAN_PDF_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -51,6 +51,7 @@ from metagpt.schema import ( Message, ) from metagpt.utils.common import any_to_name, any_to_str, any_to_str_set +from metagpt.utils.file_repository import FileRepository IS_PASS_PROMPT = """ {context} @@ -100,9 +101,9 @@ class Engineer(Role): @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: m = json.loads(task_msg.content) - return m.get("Task list") or m.get("Refined Task list") + return m.get(TASK_LIST.key) or m.get(REFINED_TASK_LIST.key) - async def _act_sp_with_cr(self, review=False, guideline=Document()) -> Set[str]: + async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "plan"] = "normal") -> Set[str]: changed_files = set() src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) for todo in self.code_todos: @@ -113,16 +114,16 @@ class Engineer(Role): 3. Do we need other codes (currently needed)? TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. """ - coding_context = await todo.run(guideline=guideline.content) + coding_context = await todo.run() # Code review if review: action = WriteCodeReview(context=coding_context, llm=self.llm) self._init_action_system_message(action) - coding_context = await action.run(guideline=guideline.content) + coding_context = await action.run() dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} - if guideline.content: - dependencies.add(guideline.root_relative_path) + if mode == "plan": + dependencies.add(os.path.join(PLAN_PDF_FILE_REPO, PLAN_FILENAME)) await src_file_repo.save( coding_context.filename, dependencies=dependencies, @@ -155,10 +156,8 @@ class Engineer(Role): async def _act_write_code(self): if CONFIG.inc: - code_guideline = await self._write_code_guideline() - changed_files = await self._act_sp_with_cr(review=self.use_code_review, guideline=code_guideline) - else: - changed_files = await self._act_sp_with_cr(review=self.use_code_review) + await self._write_code_plan() + changed_files = await self._act_sp_with_cr(review=self.use_code_review) return Message( content="\n".join(changed_files), role=self.profile, @@ -335,41 +334,38 @@ class Engineer(Role): """AgentStore uses this attribute to display to the user what actions the current role should take.""" return self.next_todo_action - async def _write_code_guideline(self): - """Write some guidelines that guides subsequent WriteCode and WriteCodeReview""" - logger.info("Writing code guideline..") + async def _write_code_plan(self): + """Write code plan that guides subsequent WriteCode and WriteCodeReview""" + logger.info("Writing code plan..") user_requirement = str(self.rc.memory.get_by_role("Human")[0]) - contents = [] - prd = await CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO).get_all() + pool_contents = [] + prd = await FileRepository.get_all_files(relative_path=PLAN_PDF_FILE_REPO) for doc in prd: prd_json = json.loads(doc.content) - product_requirement_pool = prd_json.get("Requirement Pool", prd_json.get("Refined Requirement Pool")) - contents.append(str(product_requirement_pool)) + product_requirement_pool = prd_json.get(REFINED_REQUIREMENT_POOL.key) or prd_json.get(REQUIREMENT_POOL.key) + pool_contents.append(str(product_requirement_pool)) - product_requirement_pools = "\n".join(contents) + product_requirement_pools = "\n".join(pool_contents) - design = await CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO).get_all() + design = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) design = "\n".join([doc.content for doc in design]) - tasks = await CONFIG.git_repo.new_file_repository(TASK_FILE_REPO).get_all() + tasks = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) tasks = "\n".join([doc.content for doc in tasks]) old_codes = await self.get_old_codes() - context = CODE_GUIDELINE_CONTEXT.format( + context = CODE_PLAN_CONTEXT.format( user_requirement=user_requirement, product_requirement_pools=product_requirement_pools, tasks=tasks, design=design, code=old_codes, ) - node = await WriteCodeGuideline().run(context=context) - guideline = node.instruct_content.model_dump() - await WriteCodeGuideline.save_json(guideline) - guideline_md = await WriteCodeGuideline.save_md(guideline) - - return Document(root_path=CODE_GUIDELINE_PDF_FILE_REPO, filename="code_guideline.md", content=guideline_md) + node = await WriteCodePlan().run(context=context) + plan = node.instruct_content.model_dump_json() + CONFIG.git_repo.new_file_repository(PLAN_FILE_REPO).save(filename=PLAN_FILENAME, content=plan) @staticmethod async def get_old_codes() -> str: diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index 25c593e6c..7762784d8 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -121,46 +121,6 @@ classDiagram Index --> KnowledgeBase """ -MMC1_REFINE = """ -classDiagram - class Main { - -SearchEngine search_engine - +main() str - +newMethod() str # Incremental change - } - class SearchEngine { - -Index index - -Ranking ranking - -Summary summary - +search(query: str) str - +newMethod() str # Incremental change - } - class Index { - -KnowledgeBase knowledge_base - +create_index(data: dict) - +query_index(query: str) list - +newMethod() list # Incremental change - } - class Ranking { - +rank_results(results: list) list - +newMethod() list # Incremental change - } - class Summary { - +summarize_results(results: list) str - +newMethod() str # Incremental change - } - class KnowledgeBase { - +update(data: dict) - +fetch_data(query: str) dict - +newMethod() # Incremental change - } - Main --> SearchEngine - SearchEngine --> Index - SearchEngine --> Ranking - SearchEngine --> Summary - Index --> KnowledgeBase -""" - MMC2 = """ sequenceDiagram participant M as Main @@ -180,32 +140,3 @@ sequenceDiagram S-->>SE: return summary SE-->>M: return summary """ - -MMC2_REFINE = """ -sequenceDiagram - participant M as Main - participant SE as SearchEngine - participant I as Index - participant R as Ranking - participant S as Summary - participant KB as KnowledgeBase - M->>SE: search(query) - SE->>I: query_index(query) - I->>KB: fetch_data(query) - KB-->>I: return data - I-->>SE: return results - SE->>R: rank_results(results) - R-->>SE: return ranked_results - SE->>S: summarize_results(ranked_results) - S-->>SE: return summary - SE-->>M: return summary - M->>SE: newMethod() # Incremental change - SE->>I: newMethod() # Incremental change - I->>KB: newMethod() # Incremental change - KB-->>I: newMethod() # Incremental change - SE->>R: newMethod() # Incremental change - R-->>SE: newMethod() # Incremental change - SE->>S: newMethod() # Incremental change - S-->>SE: newMethod() # Incremental change - SE-->>M: newMethod() # Incremental change -""" diff --git a/tests/data/incremental_dev_project/mock.py b/tests/data/incremental_dev_project/mock.py index a7aa2bbe4..bb3d008a1 100644 --- a/tests/data/incremental_dev_project/mock.py +++ b/tests/data/incremental_dev_project/mock.py @@ -372,8 +372,8 @@ REFINED_TASKS_JSON = { "Anything UNCLEAR": "", } -GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE = { - "Guidelines and Incremental Change": '\n1. Guideline for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Guideline for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Guideline for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Guideline for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' +PLAN_SAMPLE = { + "Plan": '\n1. Plan for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Plan for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Plan for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Plan for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' } REFINED_CODE_INPUT_SAMPLE = """ diff --git a/tests/metagpt/actions/test_write_code_guideline_an.py b/tests/metagpt/actions/test_write_code_plan_an.py similarity index 61% rename from tests/metagpt/actions/test_write_code_guideline_an.py rename to tests/metagpt/actions/test_write_code_plan_an.py index 5a4e19d57..0babab7c5 100644 --- a/tests/metagpt/actions/test_write_code_guideline_an.py +++ b/tests/metagpt/actions/test_write_code_plan_an.py @@ -3,23 +3,23 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_code_guideline_an.py +@File : test_write_code_plan_an.py """ import pytest from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode -from metagpt.actions.write_code_guideline_an import ( - CODE_GUIDELINE_CONTEXT, +from metagpt.actions.write_code_plan_an import ( + CODE_PLAN_CONTEXT, REFINED_CODE_TEMPLATE, - WriteCodeGuideline, + WriteCodePlan, ) from tests.data.incremental_dev_project.mock import ( DESIGN_SAMPLE, - GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE, NEW_REQUIREMENT_SAMPLE, OLD_CODE_SAMPLE, + PLAN_SAMPLE, REFINED_CODE_INPUT_SAMPLE, REFINED_CODE_SAMPLE, REFINED_DESIGN_JSON, @@ -29,30 +29,30 @@ from tests.data.incremental_dev_project.mock import ( ) -def mock_guidelines_and_incremental_change(): - return GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE +def mock_plan(): + return PLAN_SAMPLE @pytest.mark.asyncio -async def test_write_code_guideline_an(mocker): +async def test_write_code_plan_an(mocker): root = ActionNode.from_children( - "WriteCodeGuideline", [ActionNode(key="", expected_type=str, instruction="", example="")] + "WriteCodePlan", [ActionNode(key="", expected_type=str, instruction="", example="")] ) root.instruct_content = BaseModel() - root.instruct_content.model_dump = mock_guidelines_and_incremental_change - mocker.patch("metagpt.actions.write_code_guideline_an.WriteCodeGuideline.run", return_value=root) + root.instruct_content.model_dump = mock_plan + mocker.patch("metagpt.actions.write_code_plan_an.WriteCodePlan.run", return_value=root) - write_code_guideline = WriteCodeGuideline() - context = CODE_GUIDELINE_CONTEXT.format( + write_code_plan = WriteCodePlan() + context = CODE_PLAN_CONTEXT.format( user_requirement=NEW_REQUIREMENT_SAMPLE, product_requirement_pools=REFINED_PRD_JSON.get("Refined Requirement Pool", ""), design=REFINED_DESIGN_JSON, tasks=REFINED_TASKS_JSON, code=OLD_CODE_SAMPLE, ) - node = await write_code_guideline.run(context=context) + node = await write_code_plan.run(context=context) - assert "Guidelines and Incremental Change" in node.instruct_content.model_dump() + assert "Plan" in node.instruct_content.model_dump() @pytest.mark.asyncio @@ -60,7 +60,7 @@ async def test_refine_code(mocker): mocker.patch("metagpt.actions.write_code.WriteCode.write_code", return_value=REFINED_CODE_SAMPLE) prompt = REFINED_CODE_TEMPLATE.format( user_requirement=NEW_REQUIREMENT_SAMPLE, - guideline=GUIDELINES_AND_INCREMENTAL_CHANGE_SAMPLE, + plan=PLAN_SAMPLE, design=DESIGN_SAMPLE, tasks=TASKS_SAMPLE, code=REFINED_CODE_INPUT_SAMPLE, diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index ed8fc7e78..41ba785c4 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -105,7 +105,7 @@ def log_and_check_result(result, tag_name="refine"): def get_incremental_dev_result(idea, project_name, use_review=True): project_path = TEST_DATA_PATH / "incremental_dev_project" / project_name - if not os.path.exists(project_path): + if project_path.exists(): raise Exception(f"Project {project_name} not exists") check_or_create_base_tag(project_path) args = [idea, "--inc", "--project-path", project_path] From 42565c39e3f51682dc0521a0a122dd1f1c978feb Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 09:08:29 +0800 Subject: [PATCH 063/101] 1. rename and modify guideline to plan 2. update prompt in ActionNode 3. add code comment 4. refactor Guideline code structure --- metagpt/actions/write_code.py | 4 ++-- metagpt/actions/write_code_review.py | 4 ++-- metagpt/const.py | 1 - metagpt/roles/engineer.py | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 93c7a2f65..093633a8b 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -29,8 +29,8 @@ from metagpt.const import ( BUGFIX_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, + PLAN_FILE_REPO, PLAN_FILENAME, - PLAN_PDF_FILE_REPO, REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, @@ -107,7 +107,7 @@ class WriteCode(Action): test_doc = await FileRepository.get_file( filename="test_" + coding_context.filename + ".json", relative_path=TEST_OUTPUTS_FILE_REPO ) - plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_PDF_FILE_REPO) + plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_FILE_REPO) plan = plan_doc.content if plan_doc else "" requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) summary_doc = None diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 72424c037..2b9c7bd10 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -16,8 +16,8 @@ from metagpt.actions.action import Action from metagpt.config import CONFIG from metagpt.const import ( DOCS_FILE_REPO, + PLAN_FILE_REPO, PLAN_FILENAME, - PLAN_PDF_FILE_REPO, REQUIREMENT_FILENAME, ) from metagpt.logs import logger @@ -145,7 +145,7 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.context.code_doc.content k = CONFIG.code_review_k_times or 1 - plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_PDF_FILE_REPO) + plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_FILE_REPO) plan = plan_doc.content if plan_doc else "" mode = "plan" if plan else "normal" diff --git a/metagpt/const.py b/metagpt/const.py index 014193b59..8f0eeef23 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -100,7 +100,6 @@ SEQ_FLOW_FILE_REPO = "resources/seq_flow" SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design" PRD_PDF_FILE_REPO = "resources/prd" TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks" -PLAN_PDF_FILE_REPO = "resources/plan" TEST_CODES_FILE_REPO = "tests" TEST_OUTPUTS_FILE_REPO = "test_outputs" CODE_SUMMARIES_FILE_REPO = "docs/code_summaries" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 695e9dd2a..2803c8668 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -37,7 +37,7 @@ from metagpt.const import ( CODE_SUMMARIES_PDF_FILE_REPO, PLAN_FILE_REPO, PLAN_FILENAME, - PLAN_PDF_FILE_REPO, + PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -123,7 +123,7 @@ class Engineer(Role): dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} if mode == "plan": - dependencies.add(os.path.join(PLAN_PDF_FILE_REPO, PLAN_FILENAME)) + dependencies.add(os.path.join(PLAN_FILE_REPO, PLAN_FILENAME)) await src_file_repo.save( coding_context.filename, dependencies=dependencies, @@ -340,7 +340,7 @@ class Engineer(Role): user_requirement = str(self.rc.memory.get_by_role("Human")[0]) pool_contents = [] - prd = await FileRepository.get_all_files(relative_path=PLAN_PDF_FILE_REPO) + prd = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) for doc in prd: prd_json = json.loads(doc.content) product_requirement_pool = prd_json.get(REFINED_REQUIREMENT_POOL.key) or prd_json.get(REQUIREMENT_POOL.key) From 95ccd980f8ad2781c0e90e820959548e16b7eb3b Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 11:26:58 +0800 Subject: [PATCH 064/101] 1. rename and modify plan to code plan and change 2. modify name of ActionNode instance --- metagpt/actions/design_api.py | 4 +-- metagpt/actions/design_api_an.py | 6 ++-- metagpt/actions/project_management.py | 4 +-- metagpt/actions/project_management_an.py | 6 ++-- metagpt/actions/write_code.py | 30 +++++++++-------- ...an.py => write_code_plan_and_change_an.py} | 24 +++++++------- metagpt/actions/write_code_review.py | 16 +++++---- metagpt/actions/write_prd.py | 8 ++--- metagpt/actions/write_prd_an.py | 8 ++--- metagpt/const.py | 4 +-- metagpt/roles/engineer.py | 33 +++++++++++-------- tests/data/incremental_dev_project/mock.py | 2 +- tests/metagpt/actions/test_design_api_an.py | 4 +-- .../actions/test_project_management_an.py | 4 +-- .../actions/test_write_code_plan_an.py | 33 ++++++++++--------- tests/metagpt/actions/test_write_prd_an.py | 6 ++-- 16 files changed, 101 insertions(+), 91 deletions(-) rename metagpt/actions/{write_code_plan_an.py => write_code_plan_and_change_an.py} (88%) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 933c59b74..d24cd2ae3 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -19,7 +19,7 @@ from metagpt.actions.design_api_an import ( DESIGN_API_NODE, PROGRAM_CALL_FLOW, REFINED_DATA_STRUCTURES_AND_INTERFACES, - REFINED_DESIGN_NODES, + REFINED_DESIGN_NODE, REFINED_PROGRAM_CALL_FLOW, ) from metagpt.config import CONFIG @@ -89,7 +89,7 @@ class WriteDesign(Action): async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) - node = await REFINED_DESIGN_NODES.fill(context=context, llm=self.llm, schema=schema) + node = await REFINED_DESIGN_NODE.fill(context=context, llm=self.llm, schema=schema) system_design_doc.content = node.instruct_content.model_dump_json() return system_design_doc diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index b872159e1..35b50ef8f 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -99,7 +99,7 @@ NODES = [ ANYTHING_UNCLEAR, ] -REFINE_NODES = [ +REFINED_NODES = [ REFINED_IMPLEMENTATION_APPROACH, REFINED_FILE_LIST, REFINED_DATA_STRUCTURES_AND_INTERFACES, @@ -108,13 +108,13 @@ REFINE_NODES = [ ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) -REFINED_DESIGN_NODES = ActionNode.from_children("RefinedDesignAPI", REFINE_NODES) +REFINED_DESIGN_NODE = ActionNode.from_children("RefinedDesignAPI", REFINED_NODES) def main(): prompt = DESIGN_API_NODE.compile(context="") logger.info(prompt) - prompt = REFINED_DESIGN_NODES.compile(context="") + prompt = REFINED_DESIGN_NODE.compile(context="") logger.info(prompt) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index d6b351d18..448a8afcd 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -15,7 +15,7 @@ from typing import Optional from metagpt.actions import ActionOutput from metagpt.actions.action import Action -from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODES +from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODE from metagpt.config import CONFIG from metagpt.const import ( PACKAGE_REQUIREMENTS_FILENAME, @@ -93,7 +93,7 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) - node = await REFINED_PM_NODES.fill(context, self.llm, schema) + node = await REFINED_PM_NODE.fill(context, self.llm, schema) task_doc.content = node.instruct_content.model_dump_json() return task_doc diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index e3d6537ab..379a23384 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -107,7 +107,7 @@ NODES = [ ANYTHING_UNCLEAR_PM, ] -REFINE_NODES = [ +REFINED_NODES = [ REQUIRED_PYTHON_PACKAGES, REQUIRED_OTHER_LANGUAGE_PACKAGES, REFINED_LOGIC_ANALYSIS, @@ -118,13 +118,13 @@ REFINE_NODES = [ ] PM_NODE = ActionNode.from_children("PM_NODE", NODES) -REFINED_PM_NODES = ActionNode.from_children("REFINED_PM_NODES", REFINE_NODES) +REFINED_PM_NODE = ActionNode.from_children("REFINED_PM_NODE", REFINED_NODES) def main(): prompt = PM_NODE.compile(context="") logger.info(prompt) - prompt = REFINED_PM_NODES.compile(context="") + prompt = REFINED_PM_NODE.compile(context="") logger.info(prompt) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 093633a8b..662524518 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -23,14 +23,14 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action import Action from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST -from metagpt.actions.write_code_plan_an import REFINED_CODE_TEMPLATE +from metagpt.actions.write_code_plan_and_change_an import REFINED_TEMPLATE from metagpt.config import CONFIG from metagpt.const import ( BUGFIX_FILENAME, + CODE_PLAN_AND_CHANGE_FILE_REPO, + CODE_PLAN_AND_CHANGE_FILENAME, CODE_SUMMARIES_FILE_REPO, DOCS_FILE_REPO, - PLAN_FILE_REPO, - PLAN_FILENAME, REQUIREMENT_FILENAME, TASK_FILE_REPO, TEST_OUTPUTS_FILE_REPO, @@ -107,8 +107,10 @@ class WriteCode(Action): test_doc = await FileRepository.get_file( filename="test_" + coding_context.filename + ".json", relative_path=TEST_OUTPUTS_FILE_REPO ) - plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_FILE_REPO) - plan = plan_doc.content if plan_doc else "" + code_plan_and_change_doc = await FileRepository.get_file( + filename=CODE_PLAN_AND_CHANGE_FILENAME, relative_path=CODE_PLAN_AND_CHANGE_FILE_REPO + ) + code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) summary_doc = None if coding_context.design_doc and coding_context.design_doc.filename: @@ -122,15 +124,15 @@ class WriteCode(Action): if bug_feedback: code_context = coding_context.code_doc.content - elif plan: - code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="plan") + elif code_plan_and_change: + code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="guide") else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) - if plan: - prompt = REFINED_CODE_TEMPLATE.format( + if code_plan_and_change: + prompt = REFINED_TEMPLATE.format( user_requirement=requirement_doc.content if requirement_doc else "", - plan=plan, + code_plan_and_change=code_plan_and_change, design=coding_context.design_doc.content if coding_context.design_doc else "", tasks=coding_context.task_doc.content if coding_context.task_doc else "", code=code_context, @@ -159,14 +161,14 @@ class WriteCode(Action): return coding_context @staticmethod - async def get_codes(task_doc: Document, exclude: str, mode: Literal["normal", "plan"] = "normal") -> str: + async def get_codes(task_doc: Document, exclude: str, mode: Literal["normal", "guide"] = "normal") -> str: """ Get code snippets based on different modes. Attributes: task_doc (Document): Document object of the task file. exclude (str): Specifies the filename to be excluded from the code snippets. - mode (str): Specifies the mode, either "normal" or "plan" (default is "normal"). + mode (str): Specifies the mode, either "normal" or "guide" (default is "normal"). Returns: str: Code snippets. @@ -175,7 +177,7 @@ class WriteCode(Action): If mode is set to "normal", it returns code snippets for the regular coding phase, i.e., all the code generated before writing the current file. - If mode is set to "plan", it returns code snippets for incremental development, + If mode is set to "guide", it returns code snippets for generating the code plan and change, building upon the existing code in the "normal" mode and adding code for the current file's older versions. """ if not task_doc: @@ -187,7 +189,7 @@ class WriteCode(Action): codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) - if mode == "plan": + if mode == "guide": src_files = src_file_repo.all_files old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) old_files = old_file_repo.all_files diff --git a/metagpt/actions/write_code_plan_an.py b/metagpt/actions/write_code_plan_and_change_an.py similarity index 88% rename from metagpt/actions/write_code_plan_an.py rename to metagpt/actions/write_code_plan_and_change_an.py index f3f4177e4..1722855d2 100644 --- a/metagpt/actions/write_code_plan_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -3,14 +3,14 @@ """ @Time : 2023/12/26 @Author : mannaandpoem -@File : write_code_plan_an.py +@File : write_code_plan_and_change_an.py """ from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode -Plan = ActionNode( - key="Plan", +CODE_PLAN_AND_CHANGE = ActionNode( + key="Code Plan And Change", expected_type=str, instruction="Developing comprehensive and step-by-step incremental development plan, and write Incremental " "Change by making a code draft that how to implement incremental development including detailed steps based on the " @@ -104,7 +104,7 @@ def add_numbers(): ```""", ) -CODE_PLAN_CONTEXT = """ +CODE_PLAN_AND_CHANGE_CONTEXT = """ ## User New Requirements {user_requirement} @@ -121,7 +121,7 @@ CODE_PLAN_CONTEXT = """ {code} """ -REFINED_CODE_TEMPLATE = """ +REFINED_TEMPLATE = """ NOTICE Role: You are a professional engineer; The main goal is to complete incremental development by combining legacy code and plan and Incremental Change, ensuring the integration of new features. @@ -129,8 +129,8 @@ Role: You are a professional engineer; The main goal is to complete incremental ## User New Requirements {user_requirement} -## Plan -{plan} +## Code Plan And Change +{code_plan_and_change} ## Design {design} @@ -168,18 +168,18 @@ Role: You are a professional engineer; The main goal is to complete incremental 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Follow plan and Incremental Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. +5. Follow Code Plan And Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. 9. Attention: Retain content that is not related to incremental development but important for consistency and clarity.". """ -WRITE_CODE_PLAN_NODE = ActionNode.from_children("WriteCodePlan", [Plan]) +WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", [CODE_PLAN_AND_CHANGE]) -class WriteCodePlan(Action): +class WriteCodePlanAndChange(Action): async def run(self, context): self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " - "meticulously craft comprehensive incremental development plan and deliver detailed Incremental Change" - return await WRITE_CODE_PLAN_NODE.fill(context=context, llm=self.llm, schema="json") + "meticulously craft comprehensive incremental development plan and deliver detailed incremental change" + return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json") diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 2b9c7bd10..6dbcb03fa 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -15,9 +15,9 @@ from metagpt.actions import WriteCode from metagpt.actions.action import Action from metagpt.config import CONFIG from metagpt.const import ( + CODE_PLAN_AND_CHANGE_FILE_REPO, + CODE_PLAN_AND_CHANGE_FILENAME, DOCS_FILE_REPO, - PLAN_FILE_REPO, - PLAN_FILENAME, REQUIREMENT_FILENAME, ) from metagpt.logs import logger @@ -145,16 +145,18 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.context.code_doc.content k = CONFIG.code_review_k_times or 1 - plan_doc = await FileRepository.get_file(filename=PLAN_FILENAME, relative_path=PLAN_FILE_REPO) - plan = plan_doc.content if plan_doc else "" - mode = "plan" if plan else "normal" + code_plan_and_change_doc = await FileRepository.get_file( + filename=CODE_PLAN_AND_CHANGE_FILENAME, relative_path=CODE_PLAN_AND_CHANGE_FILE_REPO + ) + code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" + mode = "guide" if code_plan_and_change else "normal" for i in range(k): format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename) task_content = self.context.task_doc.content if self.context.task_doc else "" code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename, mode=mode) - if not plan: + if not code_plan_and_change: context = "\n".join( [ "## System Design\n" + str(self.context.design_doc) + "\n", @@ -171,7 +173,7 @@ class WriteCodeReview(Action): context = "\n".join( [ "## User New Requirements\n" + user_requirement + "\n", - "## Plan\n" + plan + "\n", + "## Code Plan And Change\n" + code_plan_and_change + "\n", "## System Design\n" + str(self.context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index e12c1e1ec..c92749da0 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -23,8 +23,8 @@ from metagpt.actions.fix_bug import FixBug from metagpt.actions.write_prd_an import ( COMPETITIVE_QUADRANT_CHART, PROJECT_NAME, - REFINE_PRD_NODE, - REFINE_PRD_TEMPLATE, + REFINED_PRD_NODE, + REFINED_TEMPLATE, WP_IS_RELATIVE_NODE, WP_ISSUE_TYPE_NODE, WRITE_PRD_NODE, @@ -135,12 +135,12 @@ class WritePRD(Action): async def _merge(self, new_requirement_doc, prd_doc, schema=CONFIG.prompt_schema) -> Document: if not CONFIG.project_name: CONFIG.project_name = Path(CONFIG.project_path).name - prompt = REFINE_PRD_TEMPLATE.format( + prompt = REFINED_TEMPLATE.format( requirements=new_requirement_doc.content, old_prd=prd_doc.content, project_name=CONFIG.project_name, ) - node = await REFINE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema) + node = await REFINED_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema) prd_doc.content = node.instruct_content.model_dump_json() await self._rename_workspace(node) return prd_doc diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 4830076e3..c65860822 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -186,7 +186,7 @@ REASON = ActionNode( key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..." ) -REFINE_PRD_TEMPLATE = """ +REFINED_TEMPLATE = """ ### Project Name {project_name} @@ -216,7 +216,7 @@ NODES = [ ANYTHING_UNCLEAR, ] -REFINE_NODES = [ +REFINED_NODES = [ LANGUAGE, PROGRAMMING_LANGUAGE, REFINED_REQUIREMENTS, @@ -232,7 +232,7 @@ REFINE_NODES = [ ] WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) -REFINE_PRD_NODE = ActionNode.from_children("RefinePRD", REFINE_NODES) +REFINED_PRD_NODE = ActionNode.from_children("RefinedPRD", REFINED_NODES) WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON]) WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON]) @@ -240,7 +240,7 @@ WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, R def main(): prompt = WRITE_PRD_NODE.compile(context="") logger.info(prompt) - prompt = REFINE_PRD_NODE.compile(context="") + prompt = REFINED_PRD_NODE.compile(context="") logger.info(prompt) diff --git a/metagpt/const.py b/metagpt/const.py index 8f0eeef23..fc3d54296 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -87,13 +87,13 @@ MESSAGE_ROUTE_TO_NONE = "" REQUIREMENT_FILENAME = "requirement.txt" BUGFIX_FILENAME = "bugfix.txt" PACKAGE_REQUIREMENTS_FILENAME = "requirements.txt" -PLAN_FILENAME = "plan.json" +CODE_PLAN_AND_CHANGE_FILENAME = "code_plan_and_change.json" DOCS_FILE_REPO = "docs" PRDS_FILE_REPO = "docs/prds" SYSTEM_DESIGN_FILE_REPO = "docs/system_design" TASK_FILE_REPO = "docs/tasks" -PLAN_FILE_REPO = "docs/plan" +CODE_PLAN_AND_CHANGE_FILE_REPO = "docs/code_plan_and_change" COMPETITIVE_ANALYSIS_FILE_REPO = "resources/competitive_analysis" DATA_API_DESIGN_FILE_REPO = "resources/data_api_design" SEQ_FLOW_FILE_REPO = "resources/seq_flow" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 2803c8668..861c5435e 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -29,14 +29,17 @@ from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug 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_an import CODE_PLAN_CONTEXT, WriteCodePlan +from metagpt.actions.write_code_plan_and_change_an import ( + CODE_PLAN_AND_CHANGE_CONTEXT, + WriteCodePlanAndChange, +) from metagpt.actions.write_prd_an import REFINED_REQUIREMENT_POOL, REQUIREMENT_POOL from metagpt.config import CONFIG from metagpt.const import ( + CODE_PLAN_AND_CHANGE_FILE_REPO, + CODE_PLAN_AND_CHANGE_FILENAME, CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, - PLAN_FILE_REPO, - PLAN_FILENAME, PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, @@ -103,7 +106,7 @@ class Engineer(Role): m = json.loads(task_msg.content) return m.get(TASK_LIST.key) or m.get(REFINED_TASK_LIST.key) - async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "plan"] = "normal") -> Set[str]: + async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "guide"] = "normal") -> Set[str]: changed_files = set() src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) for todo in self.code_todos: @@ -122,8 +125,8 @@ class Engineer(Role): coding_context = await action.run() dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} - if mode == "plan": - dependencies.add(os.path.join(PLAN_FILE_REPO, PLAN_FILENAME)) + if mode == "guide": + dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await src_file_repo.save( coding_context.filename, dependencies=dependencies, @@ -156,7 +159,7 @@ class Engineer(Role): async def _act_write_code(self): if CONFIG.inc: - await self._write_code_plan() + await self._write_code_plan_and_change() changed_files = await self._act_sp_with_cr(review=self.use_code_review) return Message( content="\n".join(changed_files), @@ -334,9 +337,9 @@ class Engineer(Role): """AgentStore uses this attribute to display to the user what actions the current role should take.""" return self.next_todo_action - async def _write_code_plan(self): - """Write code plan that guides subsequent WriteCode and WriteCodeReview""" - logger.info("Writing code plan..") + async def _write_code_plan_and_change(self): + """Write code plan and change that guides subsequent WriteCode and WriteCodeReview""" + logger.info("Writing code plan and change..") user_requirement = str(self.rc.memory.get_by_role("Human")[0]) pool_contents = [] @@ -356,16 +359,18 @@ class Engineer(Role): old_codes = await self.get_old_codes() - context = CODE_PLAN_CONTEXT.format( + context = CODE_PLAN_AND_CHANGE_CONTEXT.format( user_requirement=user_requirement, product_requirement_pools=product_requirement_pools, tasks=tasks, design=design, code=old_codes, ) - node = await WriteCodePlan().run(context=context) - plan = node.instruct_content.model_dump_json() - CONFIG.git_repo.new_file_repository(PLAN_FILE_REPO).save(filename=PLAN_FILENAME, content=plan) + node = await WriteCodePlanAndChange().run(context=context) + code_plan_and_change = node.instruct_content.model_dump_json() + CONFIG.git_repo.new_file_repository(CODE_PLAN_AND_CHANGE_FILE_REPO).save( + filename=CODE_PLAN_AND_CHANGE_FILENAME, content=code_plan_and_change + ) @staticmethod async def get_old_codes() -> str: diff --git a/tests/data/incremental_dev_project/mock.py b/tests/data/incremental_dev_project/mock.py index bb3d008a1..5c5191cf2 100644 --- a/tests/data/incremental_dev_project/mock.py +++ b/tests/data/incremental_dev_project/mock.py @@ -372,7 +372,7 @@ REFINED_TASKS_JSON = { "Anything UNCLEAR": "", } -PLAN_SAMPLE = { +CODE_PLAN_AND_CHANGE_SAMPLE = { "Plan": '\n1. Plan for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Plan for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Plan for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Plan for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' } diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 39de2a595..fcd2ef666 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -10,7 +10,7 @@ from openai._models import BaseModel from metagpt.actions.action_node import ActionNode, dict_to_markdown from metagpt.actions.design_api import NEW_REQ_TEMPLATE -from metagpt.actions.design_api_an import REFINED_DESIGN_NODES +from metagpt.actions.design_api_an import REFINED_DESIGN_NODE from metagpt.llm import LLM from tests.data.incremental_dev_project.mock import ( DESIGN_SAMPLE, @@ -38,7 +38,7 @@ async def test_write_design_an(mocker): mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODES.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) - node = await REFINED_DESIGN_NODES.fill(prompt, llm) + node = await REFINED_DESIGN_NODE.fill(prompt, llm) assert "Refined Implementation Approach" in node.instruct_content.model_dump() assert "Refined File list" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 50dc47067..e230151da 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -10,7 +10,7 @@ from openai._models import BaseModel from metagpt.actions.action_node import ActionNode, dict_to_markdown from metagpt.actions.project_management import NEW_REQ_TEMPLATE -from metagpt.actions.project_management_an import REFINED_PM_NODES +from metagpt.actions.project_management_an import REFINED_PM_NODE from metagpt.llm import LLM from tests.data.incremental_dev_project.mock import ( REFINED_DESIGN_JSON, @@ -38,7 +38,7 @@ async def test_project_management_an(mocker): mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODES.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) - node = await REFINED_PM_NODES.fill(prompt, llm) + node = await REFINED_PM_NODE.fill(prompt, llm) assert "Refined Logic Analysis" in node.instruct_content.model_dump() assert "Refined Task list" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_code_plan_an.py b/tests/metagpt/actions/test_write_code_plan_an.py index 0babab7c5..13bda7cc1 100644 --- a/tests/metagpt/actions/test_write_code_plan_an.py +++ b/tests/metagpt/actions/test_write_code_plan_an.py @@ -10,16 +10,17 @@ from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode -from metagpt.actions.write_code_plan_an import ( - CODE_PLAN_CONTEXT, - REFINED_CODE_TEMPLATE, - WriteCodePlan, +from metagpt.actions.write_code_plan_and_change_an import ( + CODE_PLAN_AND_CHANGE_CONTEXT, + REFINED_TEMPLATE, + WriteCodePlanAndChange, ) +from metagpt.actions.write_prd_an import REQUIREMENT_POOL from tests.data.incremental_dev_project.mock import ( + CODE_PLAN_AND_CHANGE_SAMPLE, DESIGN_SAMPLE, NEW_REQUIREMENT_SAMPLE, OLD_CODE_SAMPLE, - PLAN_SAMPLE, REFINED_CODE_INPUT_SAMPLE, REFINED_CODE_SAMPLE, REFINED_DESIGN_JSON, @@ -29,23 +30,23 @@ from tests.data.incremental_dev_project.mock import ( ) -def mock_plan(): - return PLAN_SAMPLE +def mock_code_plan_and_change(): + return CODE_PLAN_AND_CHANGE_SAMPLE @pytest.mark.asyncio async def test_write_code_plan_an(mocker): root = ActionNode.from_children( - "WriteCodePlan", [ActionNode(key="", expected_type=str, instruction="", example="")] + "WriteCodePlanAndChange", [ActionNode(key="", expected_type=str, instruction="", example="")] ) root.instruct_content = BaseModel() - root.instruct_content.model_dump = mock_plan - mocker.patch("metagpt.actions.write_code_plan_an.WriteCodePlan.run", return_value=root) + root.instruct_content.model_dump = mock_code_plan_and_change + mocker.patch("metagpt.actions.write_code_plan_an.WriteCodePlanAndChange.run", return_value=root) - write_code_plan = WriteCodePlan() - context = CODE_PLAN_CONTEXT.format( + write_code_plan = WriteCodePlanAndChange() + context = CODE_PLAN_AND_CHANGE_CONTEXT.format( user_requirement=NEW_REQUIREMENT_SAMPLE, - product_requirement_pools=REFINED_PRD_JSON.get("Refined Requirement Pool", ""), + product_requirement_pools=REFINED_PRD_JSON.get(REQUIREMENT_POOL.key), design=REFINED_DESIGN_JSON, tasks=REFINED_TASKS_JSON, code=OLD_CODE_SAMPLE, @@ -57,10 +58,10 @@ async def test_write_code_plan_an(mocker): @pytest.mark.asyncio async def test_refine_code(mocker): - mocker.patch("metagpt.actions.write_code.WriteCode.write_code", return_value=REFINED_CODE_SAMPLE) - prompt = REFINED_CODE_TEMPLATE.format( + mocker.patch("metagpt.actions.write_code.WriteCodePlanAndChange.write_code", return_value=REFINED_CODE_SAMPLE) + prompt = REFINED_TEMPLATE.format( user_requirement=NEW_REQUIREMENT_SAMPLE, - plan=PLAN_SAMPLE, + code_plan_and_change=CODE_PLAN_AND_CHANGE_SAMPLE, design=DESIGN_SAMPLE, tasks=TASKS_SAMPLE, code=REFINED_CODE_INPUT_SAMPLE, diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 1fdaa75c2..806f88d36 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -9,7 +9,7 @@ import pytest from openai._models import BaseModel from metagpt.actions.action_node import ActionNode -from metagpt.actions.write_prd_an import REFINE_PRD_NODE, REFINE_PRD_TEMPLATE +from metagpt.actions.write_prd_an import REFINED_PRD_NODE, REFINED_TEMPLATE from metagpt.llm import LLM from tests.data.incremental_dev_project.mock import ( NEW_REQUIREMENT_SAMPLE, @@ -34,12 +34,12 @@ async def test_write_prd_an(mocker): root.instruct_content.model_dump = mock_refined_prd_json mocker.patch("metagpt.actions.write_prd_an.REFINE_PRD_NODE.fill", return_value=root) - prompt = REFINE_PRD_TEMPLATE.format( + prompt = REFINED_TEMPLATE.format( requirements=NEW_REQUIREMENT_SAMPLE, old_prd=PRD_SAMPLE, project_name="", ) - node = await REFINE_PRD_NODE.fill(prompt, llm) + node = await REFINED_PRD_NODE.fill(prompt, llm) assert "Refined Requirements" in node.instruct_content.model_dump() assert "Refined Product Goals" in node.instruct_content.model_dump() From 1959743d0beed9bfeed7074ac346e6ba44efc2db Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 13:34:47 +0800 Subject: [PATCH 065/101] update write_code_plan_and_change_an.py and add it to _think and _act process --- .../actions/write_code_plan_and_change_an.py | 32 ++++- metagpt/const.py | 1 + metagpt/roles/engineer.py | 111 ++++++++++-------- metagpt/schema.py | 8 ++ .../actions/test_write_code_plan_an.py | 5 +- 5 files changed, 101 insertions(+), 56 deletions(-) diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index 1722855d2..151b2dc25 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -5,9 +5,14 @@ @Author : mannaandpoem @File : write_code_plan_and_change_an.py """ +import os + +from pydantic import Field from metagpt.actions.action import Action from metagpt.actions.action_node import ActionNode +from metagpt.config import CONFIG +from metagpt.schema import CodePlanAndChangeContext CODE_PLAN_AND_CHANGE = ActionNode( key="Code Plan And Change", @@ -106,10 +111,10 @@ def add_numbers(): CODE_PLAN_AND_CHANGE_CONTEXT = """ ## User New Requirements -{user_requirement} +{requirement} -## Product Requirement Pool -{product_requirement_pools} +## PRD +{prd} ## Design {design} @@ -179,7 +184,26 @@ WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChan class WriteCodePlanAndChange(Action): - async def run(self, context): + name: str = "WriteCodePlanAndChange" + context: CodePlanAndChangeContext = Field(default_factory=CodePlanAndChangeContext) + + async def run(self, *args, **kwargs): self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " "meticulously craft comprehensive incremental development plan and deliver detailed incremental change" + requirement = self.context.requirement_doc.content + prd = "\n".join([doc.content for doc in self.context.prd_docs]) + design = "\n".join([doc.content for doc in self.context.design_docs]) + tasks = "\n".join([doc.content for doc in self.context.task_docs]) + code_text = self.get_old_codes() + context = CODE_PLAN_AND_CHANGE_CONTEXT.format( + requirement=requirement, prd=prd, design=design, tasks=tasks, code=code_text + ) return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json") + + @staticmethod + async def get_old_codes() -> str: + CONFIG.old_workspace = CONFIG.git_repo.workdir / os.path.basename(CONFIG.project_path) + old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) + old_codes = await old_file_repo.get_all() + codes = [f"----- {code.filename}\n```{code.content}```" for code in old_codes] + return "\n".join(codes) diff --git a/metagpt/const.py b/metagpt/const.py index fc3d54296..7011f9c6f 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -100,6 +100,7 @@ SEQ_FLOW_FILE_REPO = "resources/seq_flow" SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design" PRD_PDF_FILE_REPO = "resources/prd" TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks" +CODE_PLAN_AND_CHANGE_PDF_FILE_REPO = "resources/code_plan_and_change" TEST_CODES_FILE_REPO = "tests" TEST_OUTPUTS_FILE_REPO = "test_outputs" CODE_SUMMARIES_FILE_REPO = "docs/code_summaries" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 861c5435e..ac6e3b720 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -29,24 +29,24 @@ from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug 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 ( - CODE_PLAN_AND_CHANGE_CONTEXT, - WriteCodePlanAndChange, -) -from metagpt.actions.write_prd_an import REFINED_REQUIREMENT_POOL, REQUIREMENT_POOL +from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange from metagpt.config import CONFIG from metagpt.const import ( CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME, + CODE_PLAN_AND_CHANGE_PDF_FILE_REPO, CODE_SUMMARIES_FILE_REPO, CODE_SUMMARIES_PDF_FILE_REPO, + DOCS_FILE_REPO, PRDS_FILE_REPO, + REQUIREMENT_FILENAME, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import ( + CodePlanAndChangeContext, CodeSummarizeContext, CodingContext, Document, @@ -149,6 +149,9 @@ class Engineer(Role): """Determines the mode of action based on whether code review is used.""" if self.rc.todo is None: return None + if isinstance(self.rc.todo, WriteCodePlanAndChange): + self.next_todo_action = any_to_name(WriteCode) + return await self._act_code_plan_and_change() if isinstance(self.rc.todo, WriteCode): self.next_todo_action = any_to_name(SummarizeCode) return await self._act_write_code() @@ -212,6 +215,40 @@ class Engineer(Role): content=json.dumps(tasks), role=self.profile, cause_by=SummarizeCode, send_to=self, sent_from=self ) + async def _act_code_plan_and_change(self): + """Write code plan and change that guides subsequent WriteCode and WriteCodeReview""" + logger.info("Writing code plan and change..") + code_plan_and_change_file_repo = CONFIG.git_repo.new_file_repository(CODE_PLAN_AND_CHANGE_FILE_REPO) + code_plan_and_change_pdf_file_repo = CONFIG.git_repo.new_file_repository(CODE_PLAN_AND_CHANGE_PDF_FILE_REPO) + + node = await self.rc.todo.run() + code_plan_and_change = node.instruct_content.model_dump_json() + + dependencies = { + self.rc.todo.context.requirement_filename, + self.rc.todo.context.prd_filename, + self.rc.todo.context.design_filename, + self.rc.todo.context.task_filename, + } + + code_plan_and_change_filename = os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME) + await code_plan_and_change_file_repo.save( + filename=code_plan_and_change_filename, content=code_plan_and_change, dependencies=dependencies + ) + await code_plan_and_change_pdf_file_repo.save( + filename=Path(code_plan_and_change_filename).with_suffix(".md").name, + content=node.content, + dependencies=dependencies, + ) + + return Message( + content=code_plan_and_change, + role=self.profile, + cause_by=WriteCodePlanAndChange, + send_to=self, + sent_from=self, + ) + async def _is_pass(self, summary) -> (str, str): rsp = await self.llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False) logger.info(rsp) @@ -222,11 +259,16 @@ class Engineer(Role): async def _think(self) -> Action | None: if not CONFIG.src_workspace: CONFIG.src_workspace = CONFIG.git_repo.workdir / CONFIG.git_repo.workdir.name - write_code_filters = any_to_str_set([WriteTasks, SummarizeCode, FixBug]) + write_plan_and_change_filters = any_to_str_set([WriteTasks]) + write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode, FixBug]) summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview]) if not self.rc.news: return None msg = self.rc.news[0] + if 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() + return self.rc.todo if msg.cause_by in write_code_filters: logger.debug(f"TODO WriteCode:{msg.model_dump_json()}") await self._new_code_actions(bug_fix=msg.cause_by == any_to_str(FixBug)) @@ -332,50 +374,21 @@ class Engineer(Role): if self.summarize_todos: self.rc.todo = self.summarize_todos[0] + async def _new_code_plan_and_change_action(self): + """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" + requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) + prd_docs = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) + design_docs = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) + tasks_docs = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) + code_plan_and_change_context = CodePlanAndChangeContext( + requirement_doc=requirement_doc, + prd_docs=prd_docs, + design_docs=design_docs, + tasks_docs=tasks_docs, + ) + self.rc.todo = WriteCodePlanAndChange(context=code_plan_and_change_context, llm=self.llm) + @property def todo(self) -> str: """AgentStore uses this attribute to display to the user what actions the current role should take.""" return self.next_todo_action - - async def _write_code_plan_and_change(self): - """Write code plan and change that guides subsequent WriteCode and WriteCodeReview""" - logger.info("Writing code plan and change..") - - user_requirement = str(self.rc.memory.get_by_role("Human")[0]) - pool_contents = [] - prd = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) - for doc in prd: - prd_json = json.loads(doc.content) - product_requirement_pool = prd_json.get(REFINED_REQUIREMENT_POOL.key) or prd_json.get(REQUIREMENT_POOL.key) - pool_contents.append(str(product_requirement_pool)) - - product_requirement_pools = "\n".join(pool_contents) - - design = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) - design = "\n".join([doc.content for doc in design]) - - tasks = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) - tasks = "\n".join([doc.content for doc in tasks]) - - old_codes = await self.get_old_codes() - - context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - user_requirement=user_requirement, - product_requirement_pools=product_requirement_pools, - tasks=tasks, - design=design, - code=old_codes, - ) - node = await WriteCodePlanAndChange().run(context=context) - code_plan_and_change = node.instruct_content.model_dump_json() - CONFIG.git_repo.new_file_repository(CODE_PLAN_AND_CHANGE_FILE_REPO).save( - filename=CODE_PLAN_AND_CHANGE_FILENAME, content=code_plan_and_change - ) - - @staticmethod - async def get_old_codes() -> str: - CONFIG.old_workspace = CONFIG.git_repo.workdir / os.path.basename(CONFIG.project_path) - old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) - old_codes = await old_file_repo.get_all() - codes = [f"----- {code.filename}\n```{code.content}```" for code in old_codes] - return "\n".join(codes) diff --git a/metagpt/schema.py b/metagpt/schema.py index e36bef395..5daaffdeb 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -451,3 +451,11 @@ class CodeSummarizeContext(BaseModel): class BugFixContext(BaseContext): filename: str = "" + + +class CodePlanAndChangeContext(BaseContext): + filename: str = "" + requirement_doc: Document + prd_docs: List[Document] + design_docs: List[Document] + task_docs: List[Document] diff --git a/tests/metagpt/actions/test_write_code_plan_an.py b/tests/metagpt/actions/test_write_code_plan_an.py index 13bda7cc1..44679e561 100644 --- a/tests/metagpt/actions/test_write_code_plan_an.py +++ b/tests/metagpt/actions/test_write_code_plan_an.py @@ -15,7 +15,6 @@ from metagpt.actions.write_code_plan_and_change_an import ( REFINED_TEMPLATE, WriteCodePlanAndChange, ) -from metagpt.actions.write_prd_an import REQUIREMENT_POOL from tests.data.incremental_dev_project.mock import ( CODE_PLAN_AND_CHANGE_SAMPLE, DESIGN_SAMPLE, @@ -45,8 +44,8 @@ async def test_write_code_plan_an(mocker): write_code_plan = WriteCodePlanAndChange() context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - user_requirement=NEW_REQUIREMENT_SAMPLE, - product_requirement_pools=REFINED_PRD_JSON.get(REQUIREMENT_POOL.key), + requirement=NEW_REQUIREMENT_SAMPLE, + PRD=REFINED_PRD_JSON, design=REFINED_DESIGN_JSON, tasks=REFINED_TASKS_JSON, code=OLD_CODE_SAMPLE, From 81b59bfe0dd2d84df40781cffa61eab8d4053841 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 14:44:06 +0800 Subject: [PATCH 066/101] update CodePlanAndChangeContext --- metagpt/roles/engineer.py | 7 ++++--- metagpt/schema.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index ac6e3b720..4d265f4df 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -125,6 +125,7 @@ class Engineer(Role): coding_context = await action.run() dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} + # TODO: Add code plan and change file to context when _think if mode == "guide": dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await src_file_repo.save( @@ -161,8 +162,6 @@ class Engineer(Role): return None async def _act_write_code(self): - if CONFIG.inc: - await self._write_code_plan_and_change() changed_files = await self._act_sp_with_cr(review=self.use_code_review) return Message( content="\n".join(changed_files), @@ -376,10 +375,12 @@ class Engineer(Role): async def _new_code_plan_and_change_action(self): """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" + # FIXME: The following code is not robust enough requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) prd_docs = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) design_docs = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) - tasks_docs = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) + tasks_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) + tasks_docs = await tasks_file_repo.get_all() code_plan_and_change_context = CodePlanAndChangeContext( requirement_doc=requirement_doc, prd_docs=prd_docs, diff --git a/metagpt/schema.py b/metagpt/schema.py index 5daaffdeb..3fb934d93 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -454,7 +454,6 @@ class BugFixContext(BaseContext): class CodePlanAndChangeContext(BaseContext): - filename: str = "" requirement_doc: Document prd_docs: List[Document] design_docs: List[Document] From 69ad2f414757a0df131d102dd9effbe99016fae5 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 14:53:19 +0800 Subject: [PATCH 067/101] update mode from "guide" to "incremental" in get_codes function of write_code.py --- metagpt/actions/write_code.py | 12 +++++++----- metagpt/actions/write_code_plan_and_change_an.py | 2 +- metagpt/actions/write_code_review.py | 2 +- metagpt/roles/engineer.py | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 662524518..489c4ccb6 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -125,7 +125,9 @@ class WriteCode(Action): if bug_feedback: code_context = coding_context.code_doc.content elif code_plan_and_change: - code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename, mode="guide") + code_context = await self.get_codes( + coding_context.task_doc, exclude=self.context.filename, mode="incremental" + ) else: code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename) @@ -161,14 +163,14 @@ class WriteCode(Action): return coding_context @staticmethod - async def get_codes(task_doc: Document, exclude: str, mode: Literal["normal", "guide"] = "normal") -> str: + async def get_codes(task_doc: Document, exclude: str, mode: Literal["normal", "incremental"] = "normal") -> str: """ Get code snippets based on different modes. Attributes: task_doc (Document): Document object of the task file. exclude (str): Specifies the filename to be excluded from the code snippets. - mode (str): Specifies the mode, either "normal" or "guide" (default is "normal"). + mode (str): Specifies the mode, either "normal" or "incremental" (default is "normal"). Returns: str: Code snippets. @@ -177,7 +179,7 @@ class WriteCode(Action): If mode is set to "normal", it returns code snippets for the regular coding phase, i.e., all the code generated before writing the current file. - If mode is set to "guide", it returns code snippets for generating the code plan and change, + If mode is set to "incremental", it returns code snippets for generating the code plan and change, building upon the existing code in the "normal" mode and adding code for the current file's older versions. """ if not task_doc: @@ -189,7 +191,7 @@ class WriteCode(Action): codes = [] src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) - if mode == "guide": + if mode == "incremental": src_files = src_file_repo.all_files old_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.old_workspace) old_files = old_file_repo.all_files diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index 151b2dc25..db0b73554 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -194,7 +194,7 @@ class WriteCodePlanAndChange(Action): prd = "\n".join([doc.content for doc in self.context.prd_docs]) design = "\n".join([doc.content for doc in self.context.design_docs]) tasks = "\n".join([doc.content for doc in self.context.task_docs]) - code_text = self.get_old_codes() + code_text = await self.get_old_codes() context = CODE_PLAN_AND_CHANGE_CONTEXT.format( requirement=requirement, prd=prd, design=design, tasks=tasks, code=code_text ) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 6dbcb03fa..80f9dd36d 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -149,7 +149,7 @@ class WriteCodeReview(Action): filename=CODE_PLAN_AND_CHANGE_FILENAME, relative_path=CODE_PLAN_AND_CHANGE_FILE_REPO ) code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" - mode = "guide" if code_plan_and_change else "normal" + mode = "incremental" if code_plan_and_change else "normal" for i in range(k): format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 4d265f4df..44db2e8a0 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -106,7 +106,7 @@ class Engineer(Role): m = json.loads(task_msg.content) return m.get(TASK_LIST.key) or m.get(REFINED_TASK_LIST.key) - async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "guide"] = "normal") -> Set[str]: + async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "incremental"] = "normal") -> Set[str]: changed_files = set() src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) for todo in self.code_todos: @@ -126,7 +126,7 @@ class Engineer(Role): dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} # TODO: Add code plan and change file to context when _think - if mode == "guide": + if mode == "incremental": dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await src_file_repo.save( coding_context.filename, From 134791ca35078e535677e817ec19cdda01ca9ef5 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 15:05:30 +0800 Subject: [PATCH 068/101] 1. update mode from "guide" to "incremental" in get_codes function of write_code.py 2. update _new_code_plan_and_change_action function --- metagpt/actions/write_code_plan_and_change_an.py | 2 +- metagpt/roles/engineer.py | 10 +++++----- metagpt/schema.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index db0b73554..8bf20e494 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -193,7 +193,7 @@ class WriteCodePlanAndChange(Action): requirement = self.context.requirement_doc.content prd = "\n".join([doc.content for doc in self.context.prd_docs]) design = "\n".join([doc.content for doc in self.context.design_docs]) - tasks = "\n".join([doc.content for doc in self.context.task_docs]) + tasks = "\n".join([doc.content for doc in self.context.tasks_docs]) code_text = await self.get_old_codes() context = CODE_PLAN_AND_CHANGE_CONTEXT.format( requirement=requirement, prd=prd, design=design, tasks=tasks, code=code_text diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 44db2e8a0..e1184dfb7 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -222,12 +222,12 @@ class Engineer(Role): node = await self.rc.todo.run() code_plan_and_change = node.instruct_content.model_dump_json() - + # FIXME: define a load function dependencies = { - self.rc.todo.context.requirement_filename, - self.rc.todo.context.prd_filename, - self.rc.todo.context.design_filename, - self.rc.todo.context.task_filename, + self.rc.todo.context.requirement_doc.filename, + self.rc.todo.context.prd_docs[0].filename, + self.rc.todo.context.design_docs[0].filename, + self.rc.todo.context.tasks_docs[0].filename, } code_plan_and_change_filename = os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME) diff --git a/metagpt/schema.py b/metagpt/schema.py index 3fb934d93..dd0e0a01e 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -454,7 +454,8 @@ class BugFixContext(BaseContext): class CodePlanAndChangeContext(BaseContext): + filename: str = "" requirement_doc: Document prd_docs: List[Document] design_docs: List[Document] - task_docs: List[Document] + tasks_docs: List[Document] From 12b9f51abf289abe988ead22f49e3ecd574ddabe Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 16:00:59 +0800 Subject: [PATCH 069/101] update _watch in engineer.py --- metagpt/roles/engineer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index e1184dfb7..9bbd32649 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -96,7 +96,7 @@ class Engineer(Role): super().__init__(**kwargs) self._init_actions([WriteCode]) - self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug]) + self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug, WriteCodePlanAndChange]) self.code_todos = [] self.summarize_todos = [] self.next_todo_action = any_to_name(WriteCode) From 39c00848b9acb58eec0f740a45e99cc0cdbb50ff Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 19:39:59 +0800 Subject: [PATCH 070/101] update test case of ActionNode --- metagpt/roles/engineer.py | 4 +-- tests/metagpt/actions/test_design_api_an.py | 2 +- .../actions/test_project_management_an.py | 2 +- ... => test_write_code_plan_and_change_an.py} | 30 +++++++++---------- tests/metagpt/actions/test_write_prd_an.py | 4 +-- 5 files changed, 20 insertions(+), 22 deletions(-) rename tests/metagpt/actions/{test_write_code_plan_an.py => test_write_code_plan_and_change_an.py} (70%) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 9bbd32649..75393bc2b 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -379,8 +379,8 @@ class Engineer(Role): requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) prd_docs = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) design_docs = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) - tasks_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) - tasks_docs = await tasks_file_repo.get_all() + tasks_docs = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) + code_plan_and_change_context = CodePlanAndChangeContext( requirement_doc=requirement_doc, prd_docs=prd_docs, diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index fcd2ef666..3d11f200d 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -35,7 +35,7 @@ async def test_write_design_an(mocker): ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_design_json - mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODES.fill", return_value=root) + mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) node = await REFINED_DESIGN_NODE.fill(prompt, llm) diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index e230151da..aa759aec8 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -35,7 +35,7 @@ async def test_project_management_an(mocker): ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_tasks_json - mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODES.fill", return_value=root) + mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) node = await REFINED_PM_NODE.fill(prompt, llm) diff --git a/tests/metagpt/actions/test_write_code_plan_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py similarity index 70% rename from tests/metagpt/actions/test_write_code_plan_an.py rename to tests/metagpt/actions/test_write_code_plan_and_change_an.py index 44679e561..33114dfcf 100644 --- a/tests/metagpt/actions/test_write_code_plan_an.py +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_code_plan_an.py +@File : test_write_code_plan_and_change_an.py """ import pytest from openai._models import BaseModel @@ -11,20 +11,16 @@ from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_plan_and_change_an import ( - CODE_PLAN_AND_CHANGE_CONTEXT, REFINED_TEMPLATE, WriteCodePlanAndChange, ) +from metagpt.schema import CodePlanAndChangeContext, Document from tests.data.incremental_dev_project.mock import ( CODE_PLAN_AND_CHANGE_SAMPLE, DESIGN_SAMPLE, NEW_REQUIREMENT_SAMPLE, - OLD_CODE_SAMPLE, REFINED_CODE_INPUT_SAMPLE, REFINED_CODE_SAMPLE, - REFINED_DESIGN_JSON, - REFINED_PRD_JSON, - REFINED_TASKS_JSON, TASKS_SAMPLE, ) @@ -34,23 +30,25 @@ def mock_code_plan_and_change(): @pytest.mark.asyncio -async def test_write_code_plan_an(mocker): +async def test_write_code_plan_and_change_an(mocker): root = ActionNode.from_children( "WriteCodePlanAndChange", [ActionNode(key="", expected_type=str, instruction="", example="")] ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_code_plan_and_change - mocker.patch("metagpt.actions.write_code_plan_an.WriteCodePlanAndChange.run", return_value=root) + mocker.patch("metagpt.actions.write_code_plan_and_change_an.WriteCodePlanAndChange.run", return_value=root) - write_code_plan = WriteCodePlanAndChange() - context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - requirement=NEW_REQUIREMENT_SAMPLE, - PRD=REFINED_PRD_JSON, - design=REFINED_DESIGN_JSON, - tasks=REFINED_TASKS_JSON, - code=OLD_CODE_SAMPLE, + requirement_doc = Document() + prd_docs = [Document()] + design_docs = [Document()] + tasks_docs = [Document()] + code_plan_and_change_context = CodePlanAndChangeContext( + requirement_doc=requirement_doc, + prd_docs=prd_docs, + design_docs=design_docs, + tasks_docs=tasks_docs, ) - node = await write_code_plan.run(context=context) + node = await WriteCodePlanAndChange(context=code_plan_and_change_context).run() assert "Plan" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 806f88d36..3bfb4d3be 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -29,10 +29,10 @@ def mock_refined_prd_json(): @pytest.mark.asyncio async def test_write_prd_an(mocker): - root = ActionNode.from_children("RefinePRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) + root = ActionNode.from_children("RefinedPRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_prd_json - mocker.patch("metagpt.actions.write_prd_an.REFINE_PRD_NODE.fill", return_value=root) + mocker.patch("metagpt.actions.write_prd_an.REFINED_PRD_NODE.fill", return_value=root) prompt = REFINED_TEMPLATE.format( requirements=NEW_REQUIREMENT_SAMPLE, From 4bb9c006c282db05ced4cdf1ebfa0db72927c192 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Fri, 19 Jan 2024 19:39:59 +0800 Subject: [PATCH 071/101] update test case of ActionNode --- metagpt/roles/engineer.py | 7 ++--- tests/metagpt/actions/test_design_api_an.py | 2 +- .../actions/test_project_management_an.py | 2 +- ... => test_write_code_plan_and_change_an.py} | 30 +++++++++---------- tests/metagpt/actions/test_write_prd_an.py | 4 +-- 5 files changed, 20 insertions(+), 25 deletions(-) rename tests/metagpt/actions/{test_write_code_plan_an.py => test_write_code_plan_and_change_an.py} (70%) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 9bbd32649..b1588f9ef 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -125,7 +125,6 @@ class Engineer(Role): coding_context = await action.run() dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} - # TODO: Add code plan and change file to context when _think if mode == "incremental": dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await src_file_repo.save( @@ -222,7 +221,6 @@ class Engineer(Role): node = await self.rc.todo.run() code_plan_and_change = node.instruct_content.model_dump_json() - # FIXME: define a load function dependencies = { self.rc.todo.context.requirement_doc.filename, self.rc.todo.context.prd_docs[0].filename, @@ -375,12 +373,11 @@ class Engineer(Role): async def _new_code_plan_and_change_action(self): """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" - # FIXME: The following code is not robust enough requirement_doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO) prd_docs = await FileRepository.get_all_files(relative_path=PRDS_FILE_REPO) design_docs = await FileRepository.get_all_files(relative_path=SYSTEM_DESIGN_FILE_REPO) - tasks_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO) - tasks_docs = await tasks_file_repo.get_all() + tasks_docs = await FileRepository.get_all_files(relative_path=TASK_FILE_REPO) + code_plan_and_change_context = CodePlanAndChangeContext( requirement_doc=requirement_doc, prd_docs=prd_docs, diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index fcd2ef666..3d11f200d 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -35,7 +35,7 @@ async def test_write_design_an(mocker): ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_design_json - mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODES.fill", return_value=root) + mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) node = await REFINED_DESIGN_NODE.fill(prompt, llm) diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index e230151da..aa759aec8 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -35,7 +35,7 @@ async def test_project_management_an(mocker): ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_tasks_json - mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODES.fill", return_value=root) + mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) node = await REFINED_PM_NODE.fill(prompt, llm) diff --git a/tests/metagpt/actions/test_write_code_plan_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py similarity index 70% rename from tests/metagpt/actions/test_write_code_plan_an.py rename to tests/metagpt/actions/test_write_code_plan_and_change_an.py index 44679e561..33114dfcf 100644 --- a/tests/metagpt/actions/test_write_code_plan_an.py +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -3,7 +3,7 @@ """ @Time : 2024/01/03 @Author : mannaandpoem -@File : test_write_code_plan_an.py +@File : test_write_code_plan_and_change_an.py """ import pytest from openai._models import BaseModel @@ -11,20 +11,16 @@ from openai._models import BaseModel from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_plan_and_change_an import ( - CODE_PLAN_AND_CHANGE_CONTEXT, REFINED_TEMPLATE, WriteCodePlanAndChange, ) +from metagpt.schema import CodePlanAndChangeContext, Document from tests.data.incremental_dev_project.mock import ( CODE_PLAN_AND_CHANGE_SAMPLE, DESIGN_SAMPLE, NEW_REQUIREMENT_SAMPLE, - OLD_CODE_SAMPLE, REFINED_CODE_INPUT_SAMPLE, REFINED_CODE_SAMPLE, - REFINED_DESIGN_JSON, - REFINED_PRD_JSON, - REFINED_TASKS_JSON, TASKS_SAMPLE, ) @@ -34,23 +30,25 @@ def mock_code_plan_and_change(): @pytest.mark.asyncio -async def test_write_code_plan_an(mocker): +async def test_write_code_plan_and_change_an(mocker): root = ActionNode.from_children( "WriteCodePlanAndChange", [ActionNode(key="", expected_type=str, instruction="", example="")] ) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_code_plan_and_change - mocker.patch("metagpt.actions.write_code_plan_an.WriteCodePlanAndChange.run", return_value=root) + mocker.patch("metagpt.actions.write_code_plan_and_change_an.WriteCodePlanAndChange.run", return_value=root) - write_code_plan = WriteCodePlanAndChange() - context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - requirement=NEW_REQUIREMENT_SAMPLE, - PRD=REFINED_PRD_JSON, - design=REFINED_DESIGN_JSON, - tasks=REFINED_TASKS_JSON, - code=OLD_CODE_SAMPLE, + requirement_doc = Document() + prd_docs = [Document()] + design_docs = [Document()] + tasks_docs = [Document()] + code_plan_and_change_context = CodePlanAndChangeContext( + requirement_doc=requirement_doc, + prd_docs=prd_docs, + design_docs=design_docs, + tasks_docs=tasks_docs, ) - node = await write_code_plan.run(context=context) + node = await WriteCodePlanAndChange(context=code_plan_and_change_context).run() assert "Plan" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 806f88d36..3bfb4d3be 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -29,10 +29,10 @@ def mock_refined_prd_json(): @pytest.mark.asyncio async def test_write_prd_an(mocker): - root = ActionNode.from_children("RefinePRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) + root = ActionNode.from_children("RefinedPRD", [ActionNode(key="", expected_type=str, instruction="", example="")]) root.instruct_content = BaseModel() root.instruct_content.model_dump = mock_refined_prd_json - mocker.patch("metagpt.actions.write_prd_an.REFINE_PRD_NODE.fill", return_value=root) + mocker.patch("metagpt.actions.write_prd_an.REFINED_PRD_NODE.fill", return_value=root) prompt = REFINED_TEMPLATE.format( requirements=NEW_REQUIREMENT_SAMPLE, From b4e09341b354d12d419977eb5907d310eb3d226a Mon Sep 17 00:00:00 2001 From: Arnaud Gelas Date: Thu, 18 Jan 2024 20:47:33 +0100 Subject: [PATCH 072/101] Stop generating unit test for non python files When trying to create a simple HelloWorld with test, metagpt creates test for README.md --- metagpt/roles/qa_engineer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 0e323893e..45a1c7715 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -65,6 +65,8 @@ class QaEngineer(Role): code_doc = await src_file_repo.get(filename) if not code_doc: continue + if not code_doc.filename.endswith(".py"): + continue test_doc = await tests_file_repo.get("test_" + code_doc.filename) if not test_doc: test_doc = Document( From 6a9bd4a3914b565e98b04752d11cc58ee1f4afe2 Mon Sep 17 00:00:00 2001 From: Arnaud Gelas Date: Thu, 18 Jan 2024 20:48:35 +0100 Subject: [PATCH 073/101] Do not try installing requirements if there are none Do not try running pip install -r requirements.txt if the file does not exist or is empty. It avoids seeing an error in the log. --- metagpt/actions/run_code.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/run_code.py b/metagpt/actions/run_code.py index 30b06f1a6..4a26e5137 100644 --- a/metagpt/actions/run_code.py +++ b/metagpt/actions/run_code.py @@ -16,6 +16,7 @@ class. """ import subprocess +from pathlib import Path from typing import Tuple from pydantic import Field @@ -152,11 +153,23 @@ class RunCode(Action): return subprocess.run(cmd, check=check, cwd=cwd, env=env) @staticmethod - def _install_dependencies(working_directory, env): + def _install_requirements(working_directory, env): + file_path = Path(working_directory) / "requirements.txt" + if not file_path.exists(): + return + if file_path.stat().st_size == 0: + return install_command = ["python", "-m", "pip", "install", "-r", "requirements.txt"] logger.info(" ".join(install_command)) RunCode._install_via_subprocess(install_command, check=True, cwd=working_directory, env=env) + @staticmethod + def _install_pytest(working_directory, env): install_pytest_command = ["python", "-m", "pip", "install", "pytest"] logger.info(" ".join(install_pytest_command)) RunCode._install_via_subprocess(install_pytest_command, check=True, cwd=working_directory, env=env) + + @staticmethod + def _install_dependencies(working_directory, env): + RunCode._install_requirements(working_directory, env) + RunCode._install_pytest(working_directory, env) From f3d295787eeaeeb14b17424a32b21b48470b4adc Mon Sep 17 00:00:00 2001 From: geekan Date: Mon, 22 Jan 2024 15:37:00 +0800 Subject: [PATCH 074/101] resolve zhipu 2.0.1 --- .../metagpt/provider/zhipuai/test_zhipu_model_api.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py index abaafb402..15673c51c 100644 --- a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py +++ b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py @@ -6,8 +6,6 @@ from typing import Any, Tuple import pytest import zhipuai -from zhipuai.model_api.api import InvokeType -from zhipuai.utils.http_client import headers as zhipuai_default_headers from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI @@ -23,14 +21,7 @@ async def mock_requestor_arequest(self, **kwargs) -> Tuple[Any, Any, str]: @pytest.mark.asyncio async def test_zhipu_model_api(mocker): - header = ZhiPuModelAPI.get_header() - zhipuai_default_headers.update({"Authorization": api_key}) - assert header == zhipuai_default_headers - - ZhiPuModelAPI.get_sse_header() - # assert len(sse_header["Authorization"]) == 191 - - url_prefix, url_suffix = ZhiPuModelAPI.split_zhipu_api_url(InvokeType.SYNC, kwargs={"model": "chatglm_turbo"}) + url_prefix, url_suffix = ZhiPuModelAPI(api_key=api_key).split_zhipu_api_url() assert url_prefix == "https://open.bigmodel.cn/api" assert url_suffix == "/paas/v4/chat/completions" From 47af1967b46370da055c42cf39ea4f0a70ba542b Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Mon, 22 Jan 2024 15:45:19 +0800 Subject: [PATCH 075/101] fix pybrowsers not found --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b3fd62178..1ba08c636 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ extras_require["test"] = [ "grpcio-status==1.48.2", "mock==5.1.0", "pylint==3.0.3", + "pybrowsers", ] extras_require["pyppeteer"] = [ From e8b3e6762b494d1b2117a4cfac930e95dd1f6528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 22 Jan 2024 17:13:20 +0800 Subject: [PATCH 076/101] feat: replace global CONTEXT with Config() fixbug: unit test --- metagpt/actions/write_code_review.py | 2 +- metagpt/config2.py | 1 + metagpt/context.py | 4 ---- metagpt/context_mixin.py | 6 +++--- metagpt/learn/skill_loader.py | 7 ++++--- metagpt/llm.py | 9 +++++---- metagpt/roles/assistant.py | 3 +-- metagpt/startup.py | 6 ++++-- metagpt/team.py | 17 ++++++++++++----- tests/data/audio/hello.mp3 | Bin 0 -> 31391 bytes tests/metagpt/roles/test_engineer.py | 2 +- .../metagpt/serialize_deserialize/test_team.py | 4 ++++ tests/metagpt/test_context.py | 7 ++++--- tests/metagpt/test_environment.py | 7 +++---- tests/metagpt/test_role.py | 2 +- 15 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 tests/data/audio/hello.mp3 diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index ec56afc61..8fe2cc5b5 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -161,7 +161,7 @@ class WriteCodeReview(Action): format_example=format_example, ) len1 = len(iterative_code) if iterative_code else 0 - len2 = len(self.context.code_doc.content) if self.context.code_doc.content else 0 + len2 = len(self.i_context.code_doc.content) if self.i_context.code_doc.content else 0 logger.info( f"Code review and rewrite {self.i_context.code_doc.filename}: {i + 1}/{k} | len(iterative_code)={len1}, " f"len(self.i_context.code_doc.content)={len2}" diff --git a/metagpt/config2.py b/metagpt/config2.py index 92dd98bad..5a556cc52 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -38,6 +38,7 @@ class CLIParams(BaseModel): if self.project_path: self.inc = True self.project_name = self.project_name or Path(self.project_path).name + return self class Config(CLIParams, YamlModel): diff --git a/metagpt/context.py b/metagpt/context.py index 8e9749d66..3dfd52d58 100644 --- a/metagpt/context.py +++ b/metagpt/context.py @@ -95,7 +95,3 @@ class Context(BaseModel): if llm.cost_manager is None: llm.cost_manager = self.cost_manager return llm - - -# Global context, not in Env -CONTEXT = Context() diff --git a/metagpt/context_mixin.py b/metagpt/context_mixin.py index 1d239d2e4..bdf2d0734 100644 --- a/metagpt/context_mixin.py +++ b/metagpt/context_mixin.py @@ -10,7 +10,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict, Field from metagpt.config2 import Config -from metagpt.context import CONTEXT, Context +from metagpt.context import Context from metagpt.provider.base_llm import BaseLLM @@ -34,7 +34,7 @@ class ContextMixin(BaseModel): def __init__( self, - context: Optional[Context] = CONTEXT, + context: Optional[Context] = None, config: Optional[Config] = None, llm: Optional[BaseLLM] = None, **kwargs, @@ -81,7 +81,7 @@ class ContextMixin(BaseModel): """Role context: role context > context""" if self.private_context: return self.private_context - return CONTEXT + return Context() @context.setter def context(self, context: Context) -> None: diff --git a/metagpt/learn/skill_loader.py b/metagpt/learn/skill_loader.py index ddcd7ccba..bcf28bb87 100644 --- a/metagpt/learn/skill_loader.py +++ b/metagpt/learn/skill_loader.py @@ -13,7 +13,7 @@ import aiofiles import yaml from pydantic import BaseModel, Field -from metagpt.context import CONTEXT, Context +from metagpt.context import Context class Example(BaseModel): @@ -73,14 +73,15 @@ class SkillsDeclaration(BaseModel): skill_data = yaml.safe_load(data) return SkillsDeclaration(**skill_data) - def get_skill_list(self, entity_name: str = "Assistant", context: Context = CONTEXT) -> Dict: + def get_skill_list(self, entity_name: str = "Assistant", context: Context = None) -> Dict: """Return the skill name based on the skill description.""" entity = self.entities.get(entity_name) if not entity: return {} # List of skills that the agent chooses to activate. - agent_skills = context.kwargs.agent_skills + ctx = context or Context() + agent_skills = ctx.kwargs.agent_skills if not agent_skills: return {} diff --git a/metagpt/llm.py b/metagpt/llm.py index 30ced25d2..a3fc5613a 100644 --- a/metagpt/llm.py +++ b/metagpt/llm.py @@ -8,12 +8,13 @@ from typing import Optional from metagpt.configs.llm_config import LLMConfig -from metagpt.context import CONTEXT +from metagpt.context import Context from metagpt.provider.base_llm import BaseLLM -def LLM(llm_config: Optional[LLMConfig] = None) -> BaseLLM: +def LLM(llm_config: Optional[LLMConfig] = None, context: Context = None) -> BaseLLM: """get the default llm provider if name is None""" + ctx = context or Context() if llm_config is not None: - CONTEXT.llm_with_cost_manager_from_llm_config(llm_config) - return CONTEXT.llm() + ctx.llm_with_cost_manager_from_llm_config(llm_config) + return ctx.llm() diff --git a/metagpt/roles/assistant.py b/metagpt/roles/assistant.py index 2e9ec9bf7..2774bd9b6 100644 --- a/metagpt/roles/assistant.py +++ b/metagpt/roles/assistant.py @@ -22,7 +22,6 @@ from pydantic import Field from metagpt.actions.skill_action import ArgumentsParingAction, SkillAction from metagpt.actions.talk_action import TalkAction -from metagpt.context import CONTEXT from metagpt.learn.skill_loader import SkillsDeclaration from metagpt.logs import logger from metagpt.memory.brain_memory import BrainMemory @@ -48,7 +47,7 @@ class Assistant(Role): def __init__(self, **kwargs): super().__init__(**kwargs) - language = kwargs.get("language") or self.context.kwargs.language or CONTEXT.kwargs.language + language = kwargs.get("language") or self.context.kwargs.language self.constraints = self.constraints.format(language=language) async def think(self) -> bool: diff --git a/metagpt/startup.py b/metagpt/startup.py index 771cde80c..000b3c5d4 100644 --- a/metagpt/startup.py +++ b/metagpt/startup.py @@ -8,6 +8,7 @@ import typer from metagpt.config2 import config from metagpt.const import CONFIG_ROOT, METAGPT_ROOT +from metagpt.context import Context app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False) @@ -37,9 +38,10 @@ def generate_repo( from metagpt.team import Team config.update_via_cli(project_path, project_name, inc, reqa_file, max_auto_summarize_code) + ctx = Context(config=config) if not recover_path: - company = Team() + company = Team(context=ctx) company.hire( [ ProductManager(), @@ -58,7 +60,7 @@ def generate_repo( if not stg_path.exists() or not str(stg_path).endswith("team"): raise FileNotFoundError(f"{recover_path} not exists or not endswith `team`") - company = Team.deserialize(stg_path=stg_path) + company = Team.deserialize(stg_path=stg_path, context=ctx) idea = company.idea company.invest(investment) diff --git a/metagpt/team.py b/metagpt/team.py index aec72970b..35f987b57 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -10,12 +10,13 @@ import warnings from pathlib import Path -from typing import Any +from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field from metagpt.actions import UserRequirement from metagpt.const import MESSAGE_ROUTE_TO_ALL, SERDESER_PATH +from metagpt.context import Context from metagpt.environment import Environment from metagpt.logs import logger from metagpt.roles import Role @@ -36,12 +37,17 @@ class Team(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - env: Environment = Field(default_factory=Environment) + env: Optional[Environment] = None investment: float = Field(default=10.0) idea: str = Field(default="") - def __init__(self, **data: Any): + def __init__(self, context: Context = None, **data: Any): super(Team, self).__init__(**data) + ctx = context or Context() + if not self.env: + self.env = Environment(context=ctx) + else: + self.env.context = ctx # The `env` object is allocated by deserialization if "roles" in data: self.hire(data["roles"]) if "env_desc" in data: @@ -54,7 +60,7 @@ class Team(BaseModel): write_json_file(team_info_path, self.model_dump()) @classmethod - def deserialize(cls, stg_path: Path) -> "Team": + def deserialize(cls, stg_path: Path, context: Context = None) -> "Team": """stg_path = ./storage/team""" # recover team_info team_info_path = stg_path.joinpath("team.json") @@ -64,7 +70,8 @@ class Team(BaseModel): ) team_info: dict = read_json_file(team_info_path) - team = Team(**team_info) + ctx = context or Context() + team = Team(**team_info, context=ctx) return team def hire(self, roles: list[Role]): diff --git a/tests/data/audio/hello.mp3 b/tests/data/audio/hello.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7b3aab0a439e9eb4e6c32c64964aa0a68674771d GIT binary patch literal 31391 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 Date: Mon, 22 Jan 2024 18:52:55 +0800 Subject: [PATCH 077/101] fix bugs --- .gitignore | 1 + examples/example.faiss | Bin 12333 -> 0 bytes examples/example.pkl | Bin 624 -> 0 bytes metagpt/actions/write_code_review.py | 2 +- tests/data/rsp_cache.json | 3 ++- tests/metagpt/provider/test_zhipuai_api.py | 4 ++-- tests/metagpt/roles/test_engineer.py | 2 +- tests/metagpt/test_context_mixin.py | 1 + tests/metagpt/test_role.py | 2 +- 9 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 examples/example.faiss delete mode 100644 examples/example.pkl diff --git a/.gitignore b/.gitignore index ed45cb260..3c762de4c 100644 --- a/.gitignore +++ b/.gitignore @@ -174,5 +174,6 @@ htmlcov htmlcov.* *.dot *.pkl +*.faiss *-structure.csv *-structure.json diff --git a/examples/example.faiss b/examples/example.faiss deleted file mode 100644 index 58094619004ac7b01cf52e596e1bb4bf254f4322..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12333 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 bool:\n return isinstance(param1, int)\n\nclass ExampleError(Exception):\n def __init__(self, msg: str):\n self.msg = msg\n```\n\n### Output Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n \"\"\"Example function with PEP 484 type annotations.\n\n Extended description of function.\n\n Args:\n param1: The first parameter.\n\n Returns:\n The return value. True for success, False otherwise.\n \"\"\"\n ...\n\nclass ExampleError(Exception):\n \"\"\"Exceptions are documented in the same way as classes.\n\n The __init__ method was documented in the class level docstring.\n\n Args:\n msg: Human readable string describing the exception.\n\n Attributes:\n msg: Human readable string describing the exception.\n \"\"\"\n ...\n```\n#SYSTEM_MSG_END#```python\nimport pytest\nfrom metagpt.actions.write_docstring import WriteDocstring\ncode = '\\ndef add_numbers(a: int, b: int):\\n return a + b\\n\\n\\nclass Person:\\n def __init__(self, name: str, age: int):\\n self.name = name\\n self.age = age\\n\\n def greet(self):\\n return f\"Hello, my name is {self.name} and I am {self.age} years old.\"\\n'\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(('style', 'part'), [('google', 'Args:'), ('numpy', 'Parameters'), ('sphinx', ':param name:')], ids=['google', 'numpy', 'sphinx'])\nasync def test_write_docstring(style: str, part: str, context):\n ret = await WriteDocstring(context=context).run(code, style=style)\n assert part in ret\n\n@pytest.mark.asyncio\nasync def test_write():\n code = await WriteDocstring.write_docstring(__file__)\n assert code\n```": "```python\ndef add_numbers(a: int, b: int):\n \"\"\"Adds two numbers together.\n\n Args:\n a: The first number.\n b: The second number.\n\n Returns:\n The sum of a and b.\n \"\"\"\n ...\n\nclass Person:\n \"\"\"Represents a person.\n\n Args:\n name: The name of the person.\n age: The age of the person.\n\n Attributes:\n name: The name of the person.\n age: The age of the person.\n \"\"\"\n\n def __init__(self, name: str, age: int):\n ...\n \n def greet(self):\n \"\"\"Generates a greeting message.\n\n Returns:\n A greeting string.\n \"\"\"\n ...\n```" } \ No newline at end of file diff --git a/tests/metagpt/provider/test_zhipuai_api.py b/tests/metagpt/provider/test_zhipuai_api.py index ad2ececa2..798209710 100644 --- a/tests/metagpt/provider/test_zhipuai_api.py +++ b/tests/metagpt/provider/test_zhipuai_api.py @@ -17,7 +17,7 @@ default_resp = { } -async def mock_zhipuai_acreate_stream(self, **kwargs): +async def mock_zhipuai_acreate_stream(**kwargs): class MockResponse(object): async def _aread(self): class Iterator(object): @@ -37,7 +37,7 @@ async def mock_zhipuai_acreate_stream(self, **kwargs): return MockResponse() -async def mock_zhipuai_acreate(self, **kwargs) -> dict: +async def mock_zhipuai_acreate(**kwargs) -> dict: return default_resp diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index 383d28096..d263a8a2f 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -99,7 +99,7 @@ def test_parse_code(): def test_todo(): role = Engineer() - assert role.todo == any_to_name(WriteCode) + assert role.action_description == any_to_name(WriteCode) @pytest.mark.asyncio diff --git a/tests/metagpt/test_context_mixin.py b/tests/metagpt/test_context_mixin.py index 1ef0e4832..4389dc251 100644 --- a/tests/metagpt/test_context_mixin.py +++ b/tests/metagpt/test_context_mixin.py @@ -109,6 +109,7 @@ async def test_config_priority(): if not home_dir.exists(): assert gpt4t is None gpt35 = Config.default() + gpt35.llm.model = "gpt-3.5-turbo-1106" gpt4 = Config.default() gpt4.llm.model = "gpt-4-0613" diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 1b843795c..7e707803b 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -131,7 +131,7 @@ async def test_recover(): role.recovered = True role.latest_observed_msg = Message(content="recover_test") role.rc.state = 0 - assert role.first_action == any_to_name(MockAction) + assert role.action_description == any_to_name(MockAction) rsp = await role.run() assert rsp.cause_by == any_to_str(MockAction) From 246d88748ebedc64af7b135bad3c9839623a4d3d Mon Sep 17 00:00:00 2001 From: geekan Date: Mon, 22 Jan 2024 19:11:33 +0800 Subject: [PATCH 078/101] update rsp cache --- tests/data/rsp_cache.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/data/rsp_cache.json b/tests/data/rsp_cache.json index af1c93b23..462d4b084 100644 --- a/tests/data/rsp_cache.json +++ b/tests/data/rsp_cache.json @@ -262,5 +262,19 @@ "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n150\n": "It seems like you have evaluated the given numbers correctly. Here are the results:\n\n10 14: Sure\n11 12: Impossible\n4 4 10: Sure\n4 9 11: Sure\n5 7 8: Likely\n5 6 6: Likely\n10 10 11: Impossible\n1 3 3: Impossible\n\nAs for the number 150, it is not clear what operation or combination of numbers it is associated with. If you have a specific question or context for the number 150, please provide more details so that I can assist you further.", "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n5\n": "The numbers 10 and 14 can reach 24, so that is sure. The numbers 11 and 12 cannot reach 24, so that is impossible. The numbers 4, 4, and 10 can reach 24, so that is sure. The numbers 4, 9, and 11 can reach 24, so that is sure. The numbers 5, 7, and 8 cannot reach 24, but they are within a reasonable range, so that is likely. The numbers 5, 6, and 6 cannot reach 24, but they are within a reasonable range, so that is likely. The numbers 10, 10, and 11 cannot reach 24, so that is impossible. The numbers 1, 3, and 3 cannot reach 24, so that is impossible. The number 5 by itself cannot reach 24.", "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n7 9 10\n": "7 * 9 - 10 = 63 - 10 = 53\n(9 - 7) * 10 = 2 * 10 = 20\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely", - "### Requirements\n1. Add docstrings to the given code following the google style.\n2. Replace the function body with an Ellipsis object(...) to reduce output.\n3. If the types are already annotated, there is no need to include them in the docstring.\n4. Extract only class, function or the docstrings for the module parts from the given Python code, avoiding any other text.\n\n### Input Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n return isinstance(param1, int)\n\nclass ExampleError(Exception):\n def __init__(self, msg: str):\n self.msg = msg\n```\n\n### Output Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n \"\"\"Example function with PEP 484 type annotations.\n\n Extended description of function.\n\n Args:\n param1: The first parameter.\n\n Returns:\n The return value. True for success, False otherwise.\n \"\"\"\n ...\n\nclass ExampleError(Exception):\n \"\"\"Exceptions are documented in the same way as classes.\n\n The __init__ method was documented in the class level docstring.\n\n Args:\n msg: Human readable string describing the exception.\n\n Attributes:\n msg: Human readable string describing the exception.\n \"\"\"\n ...\n```\n#SYSTEM_MSG_END#```python\nimport pytest\nfrom metagpt.actions.write_docstring import WriteDocstring\ncode = '\\ndef add_numbers(a: int, b: int):\\n return a + b\\n\\n\\nclass Person:\\n def __init__(self, name: str, age: int):\\n self.name = name\\n self.age = age\\n\\n def greet(self):\\n return f\"Hello, my name is {self.name} and I am {self.age} years old.\"\\n'\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(('style', 'part'), [('google', 'Args:'), ('numpy', 'Parameters'), ('sphinx', ':param name:')], ids=['google', 'numpy', 'sphinx'])\nasync def test_write_docstring(style: str, part: str, context):\n ret = await WriteDocstring(context=context).run(code, style=style)\n assert part in ret\n\n@pytest.mark.asyncio\nasync def test_write():\n code = await WriteDocstring.write_docstring(__file__)\n assert code\n```": "```python\ndef add_numbers(a: int, b: int):\n \"\"\"Adds two numbers together.\n\n Args:\n a: The first number.\n b: The second number.\n\n Returns:\n The sum of a and b.\n \"\"\"\n ...\n\nclass Person:\n \"\"\"Represents a person.\n\n Args:\n name: The name of the person.\n age: The age of the person.\n\n Attributes:\n name: The name of the person.\n age: The age of the person.\n \"\"\"\n\n def __init__(self, name: str, age: int):\n ...\n \n def greet(self):\n \"\"\"Generates a greeting message.\n\n Returns:\n A greeting string.\n \"\"\"\n ...\n```" + "### Requirements\n1. Add docstrings to the given code following the google style.\n2. Replace the function body with an Ellipsis object(...) to reduce output.\n3. If the types are already annotated, there is no need to include them in the docstring.\n4. Extract only class, function or the docstrings for the module parts from the given Python code, avoiding any other text.\n\n### Input Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n return isinstance(param1, int)\n\nclass ExampleError(Exception):\n def __init__(self, msg: str):\n self.msg = msg\n```\n\n### Output Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n \"\"\"Example function with PEP 484 type annotations.\n\n Extended description of function.\n\n Args:\n param1: The first parameter.\n\n Returns:\n The return value. True for success, False otherwise.\n \"\"\"\n ...\n\nclass ExampleError(Exception):\n \"\"\"Exceptions are documented in the same way as classes.\n\n The __init__ method was documented in the class level docstring.\n\n Args:\n msg: Human readable string describing the exception.\n\n Attributes:\n msg: Human readable string describing the exception.\n \"\"\"\n ...\n```\n#SYSTEM_MSG_END#```python\nimport pytest\nfrom metagpt.actions.write_docstring import WriteDocstring\ncode = '\\ndef add_numbers(a: int, b: int):\\n return a + b\\n\\n\\nclass Person:\\n def __init__(self, name: str, age: int):\\n self.name = name\\n self.age = age\\n\\n def greet(self):\\n return f\"Hello, my name is {self.name} and I am {self.age} years old.\"\\n'\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(('style', 'part'), [('google', 'Args:'), ('numpy', 'Parameters'), ('sphinx', ':param name:')], ids=['google', 'numpy', 'sphinx'])\nasync def test_write_docstring(style: str, part: str, context):\n ret = await WriteDocstring(context=context).run(code, style=style)\n assert part in ret\n\n@pytest.mark.asyncio\nasync def test_write():\n code = await WriteDocstring.write_docstring(__file__)\n assert code\n```": "```python\ndef add_numbers(a: int, b: int):\n \"\"\"Adds two numbers together.\n\n Args:\n a: The first number.\n b: The second number.\n\n Returns:\n The sum of a and b.\n \"\"\"\n ...\n\nclass Person:\n \"\"\"Represents a person.\n\n Args:\n name: The name of the person.\n age: The age of the person.\n\n Attributes:\n name: The name of the person.\n age: The age of the person.\n \"\"\"\n\n def __init__(self, name: str, age: int):\n ...\n \n def greet(self):\n \"\"\"Generates a greeting message.\n\n Returns:\n A greeting string.\n \"\"\"\n ...\n```", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: It isn't difficult to do a handstand if you just stand on your hands. It caught him off guard that space smelled of seared steak. When she didn’t like a guy who was trying to pick her up, she started using sign language. Each person who knows you has a different perception of who you are.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Introduce the concept of doing a handstand.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Describe the surprising smell of space.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Introduce a woman using sign language to deter unwanted attention.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Discuss the varied perceptions people have of an individual.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"It isn't difficult to do a handstand if you just stand on your hands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"It caught him off guard that space smelled of seared steak.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"When she didn’t like a guy who was trying to pick her up, she started using sign language.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Each person who knows you has a different perception of who you are.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nDiscuss the varied perceptions people have of an individual.\n": "Choice 4: Discuss the varied perceptions people have of an individual.\n\nAnalyzing this choice, discussing the varied perceptions people have of an individual can be promising as it allows for a deeper understanding of how different people perceive and interpret the actions, behaviors, and characteristics of an individual. This can lead to insights into the complexities of human interactions and relationships, and can provide valuable information for personal growth and self-awareness.\n\nBy exploring the reasons behind differing perceptions, it can also lead to increased empathy and understanding of others, which is beneficial for building stronger relationships and effective communication.\n\nFurthermore, discussing varied perceptions can also shed light on societal influences, biases, and stereotypes that may impact how individuals are perceived, which can be valuable for promoting inclusivity and diversity.\n\nThe best choice is 4.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nIntroduce a woman using sign language to deter unwanted attention.\n": "Choice 3: Introduce a woman using sign language to deter unwanted attention.\n\nAnalysis:\nIntroducing a woman using sign language can be a promising choice to deter unwanted attention. Sign language is a visual and expressive form of communication, and it can be an effective way to convey a message without drawing unnecessary attention. By using sign language, the woman can communicate with others discreetly and avoid attracting unwanted attention from individuals who may be seeking to engage her in conversation or interaction.\n\nAdditionally, using sign language can create a sense of privacy and exclusivity, as it may not be easily understood by everyone in the vicinity. This can help the woman maintain a level of personal space and discourage unwelcome advances or interactions.\n\nFurthermore, introducing sign language into the interaction can also serve as a way to promote inclusivity and awareness of different forms of communication, which can contribute to a more understanding and respectful environment.\n\nConclusion:\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 1:\nIntroduce the concept of doing a handstand.\n": "Choice 1: Introduce the concept of doing a handstand.\n\nAnalyzing the choice:\nIntroducing the concept of doing a handstand can be a fun and engaging way to promote physical activity and coordination. It can also help build strength and balance. However, it's important to ensure that proper safety measures and instructions are provided to prevent any potential injuries.\n\nConclusion:\nThe best choice is 1.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nDescribe the surprising smell of space.\n": "Choice 2: \n\nThe surprising smell of space has been described by astronauts as a mix of gunpowder, seared steak, hot metal, and raspberries. This unexpected combination of scents is due to the volatile organic compounds that are released from the materials in the spacecraft when they are exposed to the vacuum of space. These compounds react with the radiation from the sun, creating the unique smell that astronauts have reported. The smell of space is intriguing and unexpected, providing a sensory experience that is truly out of this world.\n\nThe best choice is 2.", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: Introduce the concept of doing a handstand.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Warm up your body with some stretching exercises.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Practice against a wall to build strength and balance.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Gradually work on holding the handstand position without support.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Before attempting a handstand, it's important to decide on a suitable location to practice. A clear, open space with a soft surface, such as a yoga mat or grass, is ideal for safety and comfort.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Warm up your body with some stretching exercises.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"To prepare your body for the physical demands of a handstand, start with a series of stretching exercises. Focus on the wrists, shoulders, and core muscles to ensure they are adequately warmed up and flexible.\"\n },\n {\n \"node_id\": \"5\",\n \"node_state_instruction\": \"Practice against a wall to build strength and balance.\"\n },\n {\n \"node_id\": \"6\",\n \"node_state_instruction\": \"When learning to do a handstand, it can be helpful to practice against a wall. This provides support and allows you to focus on building strength and balance in the proper alignment. Gradually work on kicking up into the handstand position and holding it for short periods of time.\"\n },\n {\n \"node_id\": \"7\",\n \"node_state_instruction\": \"Gradually work on holding the handstand position without support.\"\n },\n {\n \"node_id\": \"8\",\n \"node_state_instruction\": \"As you gain confidence and strength, begin to practice holding the handstand position without the support of a wall. Focus on engaging your core and maintaining a straight line from your wrists to your hips. With consistent practice, you'll be able to hold a handstand with control and stability.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 1:\nDecide on a suitable location to practice handstands.\n": "Choice 1: Find a spacious and flat area with soft ground, such as a grassy field or a padded gym floor. This will minimize the risk of injury and provide a stable surface for practicing handstands.\n\nThe best choice is 1", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nWarm up your body with some stretching exercises.\n": "Stretching exercises are a great way to warm up the body before engaging in physical activity. They help to increase blood flow to the muscles, improve flexibility, and reduce the risk of injury. By performing stretching exercises, you can prepare your body for more strenuous activities and improve your overall performance.\n\nThe best choice is 2.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nPractice against a wall to build strength and balance.\n": "Choice 3: Practice against a wall to build strength and balance.\n\nAnalyzing choice 3, practicing against a wall can be a promising option for building strength and balance. The wall provides a stable surface to push against, which can help in developing strength in the muscles. It also offers support, allowing for balance practice without the risk of falling. Additionally, using a wall as a tool for practice can help in improving posture and body alignment.\n\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nGradually work on holding the handstand position without support.\n": "Choice 4: Gradually work on holding the handstand position without support.\n\nAnalyzing this choice, working on holding the handstand position without support is a promising option for improving strength, balance, and body control. This choice allows for gradual progression, which is important for building the necessary strength and stability to maintain the handstand position. It also encourages the development of proprioception and spatial awareness, which are essential for mastering this challenging skill.\n\nThe best choice is 4.", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: Decide on a suitable location to practice handstands.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nEnsure there is enough space around you to avoid any obstacles.\n": "Choice 3: \n\nAnalyzing this choice, ensuring there is enough space around you to avoid any obstacles is a crucial step in maintaining safety and preventing accidents. By creating a buffer zone, you can react to unexpected obstacles or changes in the environment without risking collisions or other hazards.\n\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nConsider a flat and stable surface, such as a grassy field or a gym mat.\n": "Choice 2: Considering a flat and stable surface, such as a grassy field or a gym mat, is the most promising option. This choice provides a safe and secure environment for the activity, reducing the risk of injury. A grassy field offers a natural and soft surface, while a gym mat provides cushioning and stability. Both options allow for comfortable movement and can accommodate various physical activities.\n\nThe best choice is 2.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nPractice near a wall or with a spotter for safety.\n": "Choice 4: \n\nPractice near a wall or with a spotter for safety.\n\nAnalyzing this choice, practicing near a wall or with a spotter provides a safety measure to prevent accidents or injuries. When practicing a new skill or exercise, having a wall nearby can provide support and stability, reducing the risk of falling or losing balance. Similarly, having a spotter can offer assistance and guidance, ensuring that the practice is done safely and effectively.\n\nThe best choice is 4." } \ No newline at end of file From 8b1378308500fbec611d7d5e661ab59d6c30c9b7 Mon Sep 17 00:00:00 2001 From: geekan Date: Mon, 22 Jan 2024 19:33:58 +0800 Subject: [PATCH 079/101] add rsp cache --- tests/data/rsp_cache.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/data/rsp_cache.json b/tests/data/rsp_cache.json index 462d4b084..92a600b67 100644 --- a/tests/data/rsp_cache.json +++ b/tests/data/rsp_cache.json @@ -276,5 +276,6 @@ "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: Decide on a suitable location to practice handstands.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```", "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nEnsure there is enough space around you to avoid any obstacles.\n": "Choice 3: \n\nAnalyzing this choice, ensuring there is enough space around you to avoid any obstacles is a crucial step in maintaining safety and preventing accidents. By creating a buffer zone, you can react to unexpected obstacles or changes in the environment without risking collisions or other hazards.\n\nThe best choice is 3.", "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nConsider a flat and stable surface, such as a grassy field or a gym mat.\n": "Choice 2: Considering a flat and stable surface, such as a grassy field or a gym mat, is the most promising option. This choice provides a safe and secure environment for the activity, reducing the risk of injury. A grassy field offers a natural and soft surface, while a gym mat provides cushioning and stability. Both options allow for comfortable movement and can accommodate various physical activities.\n\nThe best choice is 2.", - "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nPractice near a wall or with a spotter for safety.\n": "Choice 4: \n\nPractice near a wall or with a spotter for safety.\n\nAnalyzing this choice, practicing near a wall or with a spotter provides a safety measure to prevent accidents or injuries. When practicing a new skill or exercise, having a wall nearby can provide support and stability, reducing the risk of falling or losing balance. Similarly, having a spotter can offer assistance and guidance, ensuring that the practice is done safely and effectively.\n\nThe best choice is 4." + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nPractice near a wall or with a spotter for safety.\n": "Choice 4: \n\nPractice near a wall or with a spotter for safety.\n\nAnalyzing this choice, practicing near a wall or with a spotter provides a safety measure to prevent accidents or injuries. When practicing a new skill or exercise, having a wall nearby can provide support and stability, reducing the risk of falling or losing balance. Similarly, having a spotter can offer assistance and guidance, ensuring that the practice is done safely and effectively.\n\nThe best choice is 4.", + "### Requirements\n1. Please summarize the latest dialogue based on the reference information (secondary) and dialogue history (primary). Do not include text that is irrelevant to the conversation.\n- The context is for reference only. If it is irrelevant to the user's search request history, please reduce its reference and usage.\n2. If there are citable links in the context, annotate them in the main text in the format [main text](citation link). If there are none in the context, do not write links.\n3. The reply should be graceful, clear, non-repetitive, smoothly written, and of moderate length, in {LANG}.\n\n### Dialogue History (For example)\nA: MLOps competitors\n\n### Current Question (For example)\nA: MLOps competitors\n\n### Current Reply (For example)\n1. Alteryx Designer: etc. if any\n2. Matlab: ditto\n3. IBM SPSS Statistics\n4. RapidMiner Studio\n5. DataRobot AI Platform\n6. Databricks Lakehouse Platform\n7. Amazon SageMaker\n8. Dataiku\n#SYSTEM_MSG_END#\n### Reference Information\nABC cleanser is preferred by many with oily skin.\nL'Oreal is a popular brand with many positive reviews.\n\n### Dialogue History\n\nuser: Which facial cleanser is good for oily skin?\n\n### Current Question\nuser: Which facial cleanser is good for oily skin?\n\n### Current Reply: Based on the information, please write the reply to the Question\n\n\n": "Based on the information provided, ABC cleanser is preferred by many with oily skin. It is a popular choice for individuals with oily skin due to its effectiveness. Additionally, L'Oreal is a well-known brand with many positive reviews, and they offer a range of products suitable for oily skin. Both of these options could be good choices for individuals with oily skin." } \ No newline at end of file From 0e4197395d6f75f2cc51b98339e8109ede008185 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 23 Jan 2024 19:11:58 +0800 Subject: [PATCH 080/101] 1. update CodePlanAndChangeContext in schema.py 2. add 'await' word in _update_prd function of write_prd.py --- metagpt/actions/action.py | 5 +++- .../actions/write_code_plan_and_change_an.py | 21 +++++++------ metagpt/actions/write_prd.py | 2 +- metagpt/actions/write_prd_an.py | 2 +- metagpt/roles/engineer.py | 30 +++++++------------ metagpt/schema.py | 30 +++++++++++++++---- 6 files changed, 52 insertions(+), 38 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index addc672bc..1b93213f7 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -15,6 +15,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from metagpt.actions.action_node import ActionNode from metagpt.context_mixin import ContextMixin from metagpt.schema import ( + CodePlanAndChangeContext, CodeSummarizeContext, CodingContext, RunCodeContext, @@ -28,7 +29,9 @@ class Action(SerializationMixin, ContextMixin, BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) name: str = "" - i_context: Union[dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, str, None] = "" + i_context: Union[ + dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, CodePlanAndChangeContext, str, None + ] = "" prefix: str = "" # aask*时会加上prefix,作为system_message desc: str = "" # for skill manager node: ActionNode = Field(default=None, exclude=True) diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index 188520ba8..3fac22242 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -118,8 +118,8 @@ CODE_PLAN_AND_CHANGE_CONTEXT = """ ## Design {design} -## Tasks -{tasks} +## Task +{task} ## Legacy Code {code} @@ -139,8 +139,8 @@ Role: You are a professional engineer; The main goal is to complete incremental ## Design {design} -## Tasks -{tasks} +## Task +{task} ## Legacy Code ```Code @@ -189,13 +189,16 @@ class WriteCodePlanAndChange(Action): async def run(self, *args, **kwargs): self.llm.system_prompt = "You are a professional software engineer, your primary responsibility is to " "meticulously craft comprehensive incremental development plan and deliver detailed incremental change" - requirement = self.i_context.requirement_doc.content - prd = "\n".join([doc.content for doc in self.i_context.prd_docs]) - design = "\n".join([doc.content for doc in self.i_context.design_docs]) - tasks = "\n".join([doc.content for doc in self.i_context.tasks_docs]) + prd_doc = await self.repo.docs.prd.get(filename=self.i_context.prd_filename) + design_doc = await self.repo.docs.system_design.get(filename=self.i_context.design_filename) + task_doc = await self.repo.docs.task.get(filename=self.i_context.task_filename) code_text = await self.get_old_codes() context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - requirement=requirement, prd=prd, design=design, tasks=tasks, code=code_text + requirement=self.i_context.requirement, + prd=prd_doc.content, + design=design_doc.content, + task=task_doc.content, + code=code_text, ) return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json") diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 000c1731e..823786893 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -147,7 +147,7 @@ class WritePRD(Action): async def _update_prd(self, req: Document, prd_doc: Document) -> Document: new_prd_doc: Document = await self._merge(req, prd_doc) - self.repo.docs.prd.save_doc(doc=new_prd_doc) + await self.repo.docs.prd.save_doc(doc=new_prd_doc) await self._save_competitive_analysis(new_prd_doc) await self.repo.resources.prd.save_pdf(doc=new_prd_doc) return new_prd_doc diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 4baa46b12..9898be55b 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -33,7 +33,7 @@ ORIGINAL_REQUIREMENTS = ActionNode( REFINED_REQUIREMENTS = ActionNode( key="Refined Requirements", expected_type=str, - instruction="Place the New user's requirements here.", + instruction="Place the New user's original requirements here.", example="Create a 2048 game with a new feature that ...", ) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 3cf52fc35..ae4f40ac7 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -210,18 +210,16 @@ class Engineer(Role): node = await self.rc.todo.run() code_plan_and_change = node.instruct_content.model_dump_json() dependencies = { - self.rc.todo.i_context.requirement_doc.filename, - self.rc.todo.i_context.prd_docs[0].filename, - self.rc.todo.i_context.design_docs[0].filename, - self.rc.todo.i_context.tasks_docs[0].filename, + REQUIREMENT_FILENAME, + self.rc.todo.i_context.prd_filename, + self.rc.todo.i_context.design_filename, + self.rc.todo.i_context.task_filename, } - - code_plan_and_change_filename = os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME) await self.project_repo.resources.code_plan_and_change.save( - filename=code_plan_and_change_filename, content=code_plan_and_change, dependencies=dependencies + filename=self.rc.todo.i_context.filename, content=code_plan_and_change, dependencies=dependencies ) await self.project_repo.docs.code_plan_and_change.save( - filename=Path(code_plan_and_change_filename).with_suffix(".md").name, + filename=Path(self.rc.todo.i_context.filename).with_suffix(".md").name, content=node.content, dependencies=dependencies, ) @@ -350,18 +348,10 @@ class Engineer(Role): async def _new_code_plan_and_change_action(self): """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" - requirement_doc = await self.project_repo.docs.requirement.get(REQUIREMENT_FILENAME) - prd_docs = await self.project_repo.docs.prd.get_all() - design_docs = await self.project_repo.docs.system_design.get_all() - task_docs = await self.project_repo.docs.task.get_all() - - code_plan_and_change_context = CodePlanAndChangeContext( - requirement_doc=requirement_doc, - prd_docs=prd_docs, - design_docs=design_docs, - task_docs=task_docs, - ) - self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_context, llm=self.llm) + files = self.project_repo.all_files + requirement = str(self.rc.memory.get_by_role("Human")[0]) + code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, requirement=requirement) + self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_ctx, context=self.context, llm=self.llm) @property def action_description(self) -> str: diff --git a/metagpt/schema.py b/metagpt/schema.py index 88e1712fc..ee194afbd 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -37,10 +37,12 @@ from pydantic import ( ) from metagpt.const import ( + CODE_PLAN_AND_CHANGE_FILENAME, MESSAGE_ROUTE_CAUSE_BY, MESSAGE_ROUTE_FROM, MESSAGE_ROUTE_TO, MESSAGE_ROUTE_TO_ALL, + PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -470,12 +472,28 @@ class BugFixContext(BaseContext): filename: str = "" -class CodePlanAndChangeContext(BaseContext): - filename: str = "" - requirement_doc: Document - prd_docs: List[Document] - design_docs: List[Document] - task_docs: List[Document] +class CodePlanAndChangeContext(BaseModel): + filename: str = CODE_PLAN_AND_CHANGE_FILENAME + requirement: str = "" + prd_filename: str = "" + design_filename: str = "" + task_filename: str = "" + + @staticmethod + def loads(filenames: List, **kwargs) -> CodePlanAndChangeContext: + ctx = CodePlanAndChangeContext(requirement=kwargs.get("requirement", "")) + for filename in filenames: + filename = Path(filename) + if filename.is_relative_to(PRDS_FILE_REPO): + ctx.prd_filename = filename.name + continue + if filename.is_relative_to(SYSTEM_DESIGN_FILE_REPO): + ctx.design_filename = filename.name + continue + if filename.is_relative_to(TASK_FILE_REPO): + ctx.task_filename = filename.name + continue + return ctx # mermaid class view From 62c128602fa8ffdae5c4dc4daf488d003053d39d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 09:51:18 +0800 Subject: [PATCH 081/101] 1. update bug of getting requirement_doc 2. modify prompt --- metagpt/actions/write_code.py | 2 +- metagpt/actions/write_code_review.py | 13 +++++------ metagpt/roles/engineer.py | 7 +++--- tests/data/incremental_dev_project/mock.py | 2 +- .../test_write_code_plan_and_change_an.py | 23 +++++++++---------- tests/metagpt/actions/test_write_prd_an.py | 1 - tests/metagpt/test_incremental_dev.py | 2 +- 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index df1e383e7..ef6d1708d 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -101,7 +101,7 @@ class WriteCode(Action): test_doc = await self.repo.test_outputs.get(filename="test_" + coding_context.filename + ".json") code_plan_and_change_doc = await self.repo.docs.code_plan_and_change.get(filename=CODE_PLAN_AND_CHANGE_FILENAME) code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" - requirement_doc = await self.repo.docs.requirement.get(filename=REQUIREMENT_FILENAME) + requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME) summary_doc = None if coding_context.design_doc and coding_context.design_doc.filename: summary_doc = await self.repo.docs.code_summary.get(filename=coding_context.design_doc.filename) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index f130542f9..b320be6ee 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -137,7 +137,8 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.i_context.code_doc.content - k = self.context.config.code_review_k_times or 1 + # k = self.context.config.code_review_k_times or 1 + k = 1 code_plan_and_change_doc = await self.repo.get(filename=CODE_PLAN_AND_CHANGE_FILENAME) code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" mode = "incremental" if code_plan_and_change else "normal" @@ -154,20 +155,18 @@ class WriteCodeReview(Action): if not code_plan_and_change: context = "\n".join( [ - "## System Design\n" + str(self.context.design_doc) + "\n", + "## System Design\n" + str(self.i_context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", ] ) else: - requirement_doc = await self.repo.get(filename=REQUIREMENT_FILENAME) - user_requirement = requirement_doc.content if requirement_doc else "" - + requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME) context = "\n".join( [ - "## User New Requirements\n" + user_requirement + "\n", + "## User New Requirements\n" + str(requirement_doc) + "\n", "## Code Plan And Change\n" + code_plan_and_change + "\n", - "## System Design\n" + str(self.context.design_doc) + "\n", + "## System Design\n" + str(self.i_context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", ] diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index ae4f40ac7..8635e5fcf 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -215,10 +215,10 @@ class Engineer(Role): self.rc.todo.i_context.design_filename, self.rc.todo.i_context.task_filename, } - await self.project_repo.resources.code_plan_and_change.save( + await self.project_repo.docs.code_plan_and_change.save( filename=self.rc.todo.i_context.filename, content=code_plan_and_change, dependencies=dependencies ) - await self.project_repo.docs.code_plan_and_change.save( + await self.project_repo.resources.code_plan_and_change.save( filename=Path(self.rc.todo.i_context.filename).with_suffix(".md").name, content=node.content, dependencies=dependencies, @@ -349,7 +349,8 @@ class Engineer(Role): async def _new_code_plan_and_change_action(self): """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" files = self.project_repo.all_files - requirement = str(self.rc.memory.get_by_role("Human")[0]) + requirement_doc = await self.project_repo.docs.get(REQUIREMENT_FILENAME) + requirement = requirement_doc.content if requirement_doc else "" code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, requirement=requirement) self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_ctx, context=self.context, llm=self.llm) diff --git a/tests/data/incremental_dev_project/mock.py b/tests/data/incremental_dev_project/mock.py index 5c5191cf2..f2eb71359 100644 --- a/tests/data/incremental_dev_project/mock.py +++ b/tests/data/incremental_dev_project/mock.py @@ -373,7 +373,7 @@ REFINED_TASKS_JSON = { } CODE_PLAN_AND_CHANGE_SAMPLE = { - "Plan": '\n1. Plan for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Plan for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Plan for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Plan for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' + "Code Plan And Change": '\n1. Plan for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Plan for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Plan for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Plan for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' } REFINED_CODE_INPUT_SAMPLE = """ diff --git a/tests/metagpt/actions/test_write_code_plan_and_change_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py index 33114dfcf..383e791b8 100644 --- a/tests/metagpt/actions/test_write_code_plan_and_change_an.py +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -14,7 +14,7 @@ from metagpt.actions.write_code_plan_and_change_an import ( REFINED_TEMPLATE, WriteCodePlanAndChange, ) -from metagpt.schema import CodePlanAndChangeContext, Document +from metagpt.schema import CodePlanAndChangeContext from tests.data.incremental_dev_project.mock import ( CODE_PLAN_AND_CHANGE_SAMPLE, DESIGN_SAMPLE, @@ -38,19 +38,19 @@ async def test_write_code_plan_and_change_an(mocker): root.instruct_content.model_dump = mock_code_plan_and_change mocker.patch("metagpt.actions.write_code_plan_and_change_an.WriteCodePlanAndChange.run", return_value=root) - requirement_doc = Document() - prd_docs = [Document()] - design_docs = [Document()] - tasks_docs = [Document()] + requirement = "New requirement" + prd_filename = "prd.md" + design_filename = "design.md" + task_filename = "task.md" code_plan_and_change_context = CodePlanAndChangeContext( - requirement_doc=requirement_doc, - prd_docs=prd_docs, - design_docs=design_docs, - tasks_docs=tasks_docs, + requirement=requirement, + prd_filename=prd_filename, + design_filename=design_filename, + task_filename=task_filename, ) - node = await WriteCodePlanAndChange(context=code_plan_and_change_context).run() + node = await WriteCodePlanAndChange(i_context=code_plan_and_change_context).run() - assert "Plan" in node.instruct_content.model_dump() + assert "Code Plan And Change" in node.instruct_content.model_dump() @pytest.mark.asyncio @@ -68,5 +68,4 @@ async def test_refine_code(mocker): summary_log="", ) code = await WriteCode().write_code(prompt=prompt) - assert code assert "def" in code diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index e8e347e5c..378ce42c3 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -38,7 +38,6 @@ async def test_write_prd_an(mocker): prompt = NEW_REQ_TEMPLATE.format( requirements=NEW_REQUIREMENT_SAMPLE, old_prd=PRD_SAMPLE, - project_name="", ) node = await REFINED_PRD_NODE.fill(prompt, llm) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 41ba785c4..7bc319ed2 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -108,7 +108,7 @@ def get_incremental_dev_result(idea, project_name, use_review=True): if project_path.exists(): raise Exception(f"Project {project_name} not exists") check_or_create_base_tag(project_path) - args = [idea, "--inc", "--project-path", project_path] + args = [idea, "--inc", "--project-path", project_path, "--n-round", "20"] if not use_review: args.append("--no-code-review") result = runner.invoke(app, args) From e42dc522c2b2dd88c1fb7247fd893d54e23cfb85 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 10:59:32 +0800 Subject: [PATCH 082/101] Fix bug in WriteCode --- metagpt/actions/write_code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index ef6d1708d..a7f53badf 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -114,7 +114,7 @@ class WriteCode(Action): code_context = coding_context.code_doc.content elif code_plan_and_change: code_context = await self.get_codes( - coding_context.task_doc, exclude=self.context.filename, project_repo=self.repo, mode="incremental" + coding_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo, mode="incremental" ) else: code_context = await self.get_codes( @@ -128,17 +128,17 @@ class WriteCode(Action): user_requirement=requirement_doc.content if requirement_doc else "", code_plan_and_change=code_plan_and_change, design=coding_context.design_doc.content if coding_context.design_doc else "", - tasks=coding_context.task_doc.content if coding_context.task_doc else "", + task=coding_context.task_doc.content if coding_context.task_doc else "", code=code_context, logs=logs, feedback=bug_feedback.content if bug_feedback else "", - filename=self.context.filename, + filename=self.i_context.filename, summary_log=summary_doc.content if summary_doc else "", ) else: prompt = PROMPT_TEMPLATE.format( design=coding_context.design_doc.content if coding_context.design_doc else "", - tasks=coding_context.task_doc.content if coding_context.task_doc else "", + task=coding_context.task_doc.content if coding_context.task_doc else "", code=code_context, logs=logs, feedback=bug_feedback.content if bug_feedback else "", From 094d7c26df35907f99fdaaf79a9f3260f2e92f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 24 Jan 2024 16:30:55 +0800 Subject: [PATCH 083/101] fixbug: The startup parameters of the program have been lost. --- metagpt/roles/engineer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 7c91ec6f9..0a50bb26d 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -292,7 +292,7 @@ class Engineer(Role): summarizations[ctx].append(filename) for ctx, filenames in summarizations.items(): ctx.codes_filenames = filenames - self.summarize_todos.append(SummarizeCode(i_context=ctx, llm=self.llm)) + self.summarize_todos.append(SummarizeCode(i_context=ctx, context=self.context, llm=self.llm)) if self.summarize_todos: self.set_todo(self.summarize_todos[0]) From 3450c240c96c7162b232698e3f46c3fc1aae644b Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 16:44:10 +0800 Subject: [PATCH 084/101] remove mode of get_codes function --- metagpt/actions/write_code.py | 26 +++++++++----------------- metagpt/actions/write_code_review.py | 14 ++++++-------- metagpt/roles/engineer.py | 6 +++--- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index a7f53badf..f0eb699bc 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -16,7 +16,6 @@ """ import json -from typing import Literal from pydantic import Field from tenacity import retry, stop_after_attempt, wait_random_exponential @@ -114,7 +113,7 @@ class WriteCode(Action): code_context = coding_context.code_doc.content elif code_plan_and_change: code_context = await self.get_codes( - coding_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo, mode="incremental" + coding_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo, use_inc=True ) else: code_context = await self.get_codes( @@ -155,39 +154,31 @@ class WriteCode(Action): return coding_context @staticmethod - async def get_codes( - task_doc: Document, exclude: str, project_repo: ProjectRepo, mode: Literal["normal", "incremental"] = "normal" - ) -> str: + async def get_codes(task_doc: Document, exclude: str, project_repo: ProjectRepo, use_inc: bool = False) -> str: """ - Get code snippets based on different modes. + Get code snippets that meet the requirements in various scenarios. Attributes: task_doc (Document): Document object of the task file. exclude (str): Specifies the filename to be excluded from the code snippets. project_repo (ProjectRepo): ProjectRepo object of the project. - mode (str): Specifies the mode, either "normal" or "incremental" (default is "normal"). + use_inc (bool): Specifies whether is incremental development. Returns: str: Code snippets. - - Description: - If mode is set to "normal", it returns code snippets for the regular coding phase, - i.e., all the code generated before writing the current file. - - If mode is set to "incremental", it returns code snippets for generating the code plan and change, - building upon the existing code in the "normal" mode and adding code for the current file's older versions. """ if not task_doc: return "" if not task_doc.content: task_doc = project_repo.docs.task.get(filename=task_doc.filename) m = json.loads(task_doc.content) - code_filenames = m.get(TASK_LIST.key, []) if mode == "normal" else m.get(REFINED_TASK_LIST.key, []) + code_filenames = m.get(TASK_LIST.key, []) if use_inc else m.get(REFINED_TASK_LIST.key, []) codes = [] src_file_repo = project_repo.srcs - if mode == "incremental": + if use_inc: src_files = src_file_repo.all_files + # Get the old workspace that are created by the previous WriteCodePlanAndChange action old_file_repo = project_repo.git_repo.new_file_repository(relative_path=project_repo.old_workspace) old_files = old_file_repo.all_files # Get the union of the files in the src and old workspaces @@ -213,7 +204,7 @@ class WriteCode(Action): continue codes.append(f"----- {filename}\n```{doc.content}```") - elif mode == "normal": + else: for filename in code_filenames: # Exclude the current file to get the context code snippets for generating the current file if filename == exclude: @@ -222,4 +213,5 @@ class WriteCode(Action): if not doc: continue codes.append(f"----- {filename}\n```{doc.content}```") + return "\n".join(codes) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index b320be6ee..b8c350f11 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -137,11 +137,8 @@ class WriteCodeReview(Action): async def run(self, *args, **kwargs) -> CodingContext: iterative_code = self.i_context.code_doc.content - # k = self.context.config.code_review_k_times or 1 - k = 1 - code_plan_and_change_doc = await self.repo.get(filename=CODE_PLAN_AND_CHANGE_FILENAME) - code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else "" - mode = "incremental" if code_plan_and_change else "normal" + k = self.context.config.code_review_k_times or 1 + for i in range(k): format_example = FORMAT_EXAMPLE.format(filename=self.i_context.code_doc.filename) task_content = self.i_context.task_doc.content if self.i_context.task_doc else "" @@ -149,10 +146,10 @@ class WriteCodeReview(Action): self.i_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo.with_src_path(self.context.src_workspace), - mode=mode, + use_inc=self.config.inc, ) - if not code_plan_and_change: + if not self.config.inc: context = "\n".join( [ "## System Design\n" + str(self.i_context.design_doc) + "\n", @@ -162,10 +159,11 @@ class WriteCodeReview(Action): ) else: requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME) + code_plan_and_change_doc = await self.repo.get(filename=CODE_PLAN_AND_CHANGE_FILENAME) context = "\n".join( [ "## User New Requirements\n" + str(requirement_doc) + "\n", - "## Code Plan And Change\n" + code_plan_and_change + "\n", + "## Code Plan And Change\n" + str(code_plan_and_change_doc) + "\n", "## System Design\n" + str(self.i_context.design_doc) + "\n", "## Tasks\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 8635e5fcf..3475a95fd 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -23,7 +23,7 @@ import json import os from collections import defaultdict from pathlib import Path -from typing import Literal, Set +from typing import Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug @@ -100,7 +100,7 @@ class Engineer(Role): m = json.loads(task_msg.content) return m.get(TASK_LIST.key) or m.get(REFINED_TASK_LIST.key) - async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "incremental"] = "normal") -> Set[str]: + async def _act_sp_with_cr(self, review=False) -> Set[str]: changed_files = set() for todo in self.code_todos: """ @@ -118,7 +118,7 @@ class Engineer(Role): coding_context = await action.run() dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path} - if mode == "incremental": + if self.config.inc: dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await self.project_repo.srcs.save( filename=coding_context.filename, From db8ae71afacc93c77ebe35408eee750dca620d4e Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 17:17:40 +0800 Subject: [PATCH 085/101] update comment in get_codes function --- metagpt/actions/write_code.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index f0eb699bc..05fffd4fc 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -156,16 +156,16 @@ class WriteCode(Action): @staticmethod async def get_codes(task_doc: Document, exclude: str, project_repo: ProjectRepo, use_inc: bool = False) -> str: """ - Get code snippets that meet the requirements in various scenarios. + Get codes for generating the current file in various scenarios. Attributes: task_doc (Document): Document object of the task file. - exclude (str): Specifies the filename to be excluded from the code snippets. + exclude (str): The file to be generated. Specifies the filename to be excluded from the code snippets. project_repo (ProjectRepo): ProjectRepo object of the project. - use_inc (bool): Specifies whether is incremental development. + use_inc (bool): Whether is the incremental development scenario. Defaults to False. Returns: - str: Code snippets. + str: Code snippets for generating the current file. """ if not task_doc: return "" From 92384d77dd189cbe498f9d905dc49470d7fbf128 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 17:17:40 +0800 Subject: [PATCH 086/101] update comment in get_codes function --- metagpt/actions/write_code.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index f0eb699bc..750e0cfa5 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -156,16 +156,16 @@ class WriteCode(Action): @staticmethod async def get_codes(task_doc: Document, exclude: str, project_repo: ProjectRepo, use_inc: bool = False) -> str: """ - Get code snippets that meet the requirements in various scenarios. + Get codes for generating the exclude file in various scenarios. Attributes: task_doc (Document): Document object of the task file. - exclude (str): Specifies the filename to be excluded from the code snippets. + exclude (str): The file to be generated. Specifies the filename to be excluded from the code snippets. project_repo (ProjectRepo): ProjectRepo object of the project. - use_inc (bool): Specifies whether is incremental development. + use_inc (bool): Whether is the incremental development scenario. Defaults to False. Returns: - str: Code snippets. + str: Codes for generating the exclude file. """ if not task_doc: return "" @@ -176,27 +176,28 @@ class WriteCode(Action): codes = [] src_file_repo = project_repo.srcs + # Incremental development scenario if use_inc: src_files = src_file_repo.all_files - # Get the old workspace that are created by the previous WriteCodePlanAndChange action + # Get the old workspace contained the old codes and old workspace are created in previous CodePlanAndChange old_file_repo = project_repo.git_repo.new_file_repository(relative_path=project_repo.old_workspace) old_files = old_file_repo.all_files # Get the union of the files in the src and old workspaces union_files_list = list(set(src_files) | set(old_files)) for filename in union_files_list: - # Exclude the current file from the all code snippets to get the context code snippets for generating + # Exclude the current file from the all code snippets if filename == exclude: - # If the file is in the old workspace, use the legacy code + # If the file is in the old workspace, use the old code # Exclude unnecessary code to maintain a clean and focused main.py file, ensuring only relevant and # essential functionality is included for the project’s requirements if filename in old_files and filename != "main.py": - # Use legacy code + # Use old code doc = await old_file_repo.get(filename=filename) # If the file is in the src workspace, skip it else: continue codes.insert(0, f"-----Now, {filename} to be rewritten\n```{doc.content}```\n=====") - # The context code snippets are generated from the src workspace + # The code snippets are generated from the src workspace else: doc = await src_file_repo.get(filename=filename) # If the file does not exist in the src workspace, skip it @@ -204,9 +205,10 @@ class WriteCode(Action): continue codes.append(f"----- {filename}\n```{doc.content}```") + # Normal scenario else: for filename in code_filenames: - # Exclude the current file to get the context code snippets for generating the current file + # Exclude the current file to get the code snippets for generating the current file if filename == exclude: continue doc = await src_file_repo.get(filename=filename) From 48cb6c6a78a44ff3bd308d838cd09623e43535c6 Mon Sep 17 00:00:00 2001 From: voidking Date: Wed, 24 Jan 2024 17:42:11 +0800 Subject: [PATCH 087/101] feat: support config2.yaml --- .github/workflows/unittest.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index fd56c42fb..dbad0441f 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -50,6 +50,7 @@ jobs: run: | export ALLOW_OPENAI_API_CALL=0 echo "${{ secrets.METAGPT_KEY_YAML }}" | base64 -d > config/key.yaml + echo "${{ secrets.METAGPT_CONFIG2_YAML }}" | base64 -d > ~/.metagpt/config2.yaml pytest tests/ --doctest-modules --cov=./metagpt/ --cov-report=xml:cov.xml --cov-report=html:htmlcov --durations=20 | tee unittest.txt - name: Show coverage report run: | From 5af94ae1331d5c4e0cbaeb4292f85702e86dee23 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 17:51:17 +0800 Subject: [PATCH 088/101] update comment in get_codes function --- metagpt/actions/write_code.py | 2 +- metagpt/roles/engineer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 750e0cfa5..fcadd5f72 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -162,7 +162,7 @@ class WriteCode(Action): task_doc (Document): Document object of the task file. exclude (str): The file to be generated. Specifies the filename to be excluded from the code snippets. project_repo (ProjectRepo): ProjectRepo object of the project. - use_inc (bool): Whether is the incremental development scenario. Defaults to False. + use_inc (bool): Indicates whether the scenario involves incremental development. Defaults to False. Returns: str: Codes for generating the exclude file. diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 3475a95fd..ee1a019bd 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -342,7 +342,7 @@ class Engineer(Role): summarizations[ctx].append(filename) for ctx, filenames in summarizations.items(): ctx.codes_filenames = filenames - self.summarize_todos.append(SummarizeCode(i_context=ctx, llm=self.llm)) + self.summarize_todos.append(SummarizeCode(i_context=ctx, context=self.context, llm=self.llm)) if self.summarize_todos: self.set_todo(self.summarize_todos[0]) From 448215613d994143c7cee65cb7acf67fd5663ad4 Mon Sep 17 00:00:00 2001 From: voidking Date: Wed, 24 Jan 2024 18:00:54 +0800 Subject: [PATCH 089/101] feat: support config2.yaml --- .github/workflows/unittest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index dbad0441f..87ccbf144 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -50,7 +50,7 @@ jobs: run: | export ALLOW_OPENAI_API_CALL=0 echo "${{ secrets.METAGPT_KEY_YAML }}" | base64 -d > config/key.yaml - echo "${{ secrets.METAGPT_CONFIG2_YAML }}" | base64 -d > ~/.metagpt/config2.yaml + mkdir -p ~/.metagpt && echo "${{ secrets.METAGPT_CONFIG2_YAML }}" | base64 -d > ~/.metagpt/config2.yaml pytest tests/ --doctest-modules --cov=./metagpt/ --cov-report=xml:cov.xml --cov-report=html:htmlcov --durations=20 | tee unittest.txt - name: Show coverage report run: | From 02b4608f8408f255f7a114c4f326ec7dae7bb912 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 18:04:26 +0800 Subject: [PATCH 090/101] changed {tasks} to {task} in PROMPT_TEMPLATE --- metagpt/actions/summarize_code.py | 2 +- metagpt/actions/write_code.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/summarize_code.py b/metagpt/actions/summarize_code.py index 2b5546546..644d5d6a9 100644 --- a/metagpt/actions/summarize_code.py +++ b/metagpt/actions/summarize_code.py @@ -28,7 +28,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ----- # Tasks ```text -{tasks} +{task} ``` ----- {code_blocks} diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index fcadd5f72..7f5dae414 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -44,7 +44,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc {design} ## Tasks -{tasks} +{task} ## Legacy Code ```Code From 7e328e543197b6ae07ea643429bd4bf70fffa96d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 18:05:26 +0800 Subject: [PATCH 091/101] changed {tasks} to {task} in PROMPT_TEMPLATE --- metagpt/actions/summarize_code.py | 2 +- metagpt/actions/write_code.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/summarize_code.py b/metagpt/actions/summarize_code.py index 644d5d6a9..0f6e0e69d 100644 --- a/metagpt/actions/summarize_code.py +++ b/metagpt/actions/summarize_code.py @@ -26,7 +26,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc {system_design} ``` ----- -# Tasks +# Task ```text {task} ``` diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 7f5dae414..0b86ac1bb 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -43,7 +43,7 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ## Design {design} -## Tasks +## Task {task} ## Legacy Code From 431e53617e0d5803a336ac7f641d521b27357326 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 24 Jan 2024 18:16:47 +0800 Subject: [PATCH 092/101] changed tasks to task --- metagpt/actions/project_management.py | 4 ++-- metagpt/actions/summarize_code.py | 2 +- metagpt/actions/write_code_review.py | 4 ++-- tests/metagpt/actions/test_write_code_plan_and_change_an.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index d417bf538..67a614d6f 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -22,7 +22,7 @@ from metagpt.schema import Document, Documents NEW_REQ_TEMPLATE = """ ### Legacy Content -{old_tasks} +{old_task} ### New Requirements {context} @@ -77,7 +77,7 @@ class WriteTasks(Action): return node async def _merge(self, system_design_doc, task_doc) -> Document: - context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) + context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_task=task_doc.content) node = await REFINED_PM_NODE.fill(context, self.llm, schema=self.prompt_schema) task_doc.content = node.instruct_content.model_dump_json() return task_doc diff --git a/metagpt/actions/summarize_code.py b/metagpt/actions/summarize_code.py index 0f6e0e69d..d21b62f83 100644 --- a/metagpt/actions/summarize_code.py +++ b/metagpt/actions/summarize_code.py @@ -110,7 +110,7 @@ class SummarizeCode(Action): format_example = FORMAT_EXAMPLE prompt = PROMPT_TEMPLATE.format( system_design=design_doc.content, - tasks=task_doc.content, + task=task_doc.content, code_blocks="\n".join(code_blocks), format_example=format_example, ) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index b8c350f11..da636eb36 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -153,7 +153,7 @@ class WriteCodeReview(Action): context = "\n".join( [ "## System Design\n" + str(self.i_context.design_doc) + "\n", - "## Tasks\n" + task_content + "\n", + "## Task\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", ] ) @@ -165,7 +165,7 @@ class WriteCodeReview(Action): "## User New Requirements\n" + str(requirement_doc) + "\n", "## Code Plan And Change\n" + str(code_plan_and_change_doc) + "\n", "## System Design\n" + str(self.i_context.design_doc) + "\n", - "## Tasks\n" + task_content + "\n", + "## Task\n" + task_content + "\n", "## Code Files\n" + code_context + "\n", ] ) diff --git a/tests/metagpt/actions/test_write_code_plan_and_change_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py index 383e791b8..741773e2b 100644 --- a/tests/metagpt/actions/test_write_code_plan_and_change_an.py +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -60,7 +60,7 @@ async def test_refine_code(mocker): user_requirement=NEW_REQUIREMENT_SAMPLE, code_plan_and_change=CODE_PLAN_AND_CHANGE_SAMPLE, design=DESIGN_SAMPLE, - tasks=TASKS_SAMPLE, + task=TASKS_SAMPLE, code=REFINED_CODE_INPUT_SAMPLE, logs="", feedback="", From d51d262238f9327819c06a402e0579e33c986093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 24 Jan 2024 20:02:07 +0800 Subject: [PATCH 093/101] fixbug: module 'mock' has no attribute 'patch' fixbug: unit test --- setup.py | 1 - .../actions/test_project_management_an.py | 2 +- .../actions/test_rebuild_sequence_view.py | 1 + .../test_write_code_plan_and_change_an.py | 2 +- tests/metagpt/utils/test_redis.py | 24 ++++++++++--------- tests/metagpt/utils/test_s3.py | 17 ++++++------- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index 1ba08c636..d1445e3f8 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,6 @@ extras_require["test"] = [ "chromadb==0.4.14", "gradio==3.0.0", "grpcio-status==1.48.2", - "mock==5.1.0", "pylint==3.0.3", "pybrowsers", ] diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index aa759aec8..ddbb56569 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -37,7 +37,7 @@ async def test_project_management_an(mocker): root.instruct_content.model_dump = mock_refined_tasks_json mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODE.fill", return_value=root) - prompt = NEW_REQ_TEMPLATE.format(old_tasks=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) + prompt = NEW_REQ_TEMPLATE.format(old_task=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) node = await REFINED_PM_NODE.fill(prompt, llm) assert "Refined Logic Analysis" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_rebuild_sequence_view.py b/tests/metagpt/actions/test_rebuild_sequence_view.py index 49d444f2f..e0e65c3cc 100644 --- a/tests/metagpt/actions/test_rebuild_sequence_view.py +++ b/tests/metagpt/actions/test_rebuild_sequence_view.py @@ -17,6 +17,7 @@ from metagpt.utils.git_repository import ChangeType @pytest.mark.asyncio +@pytest.mark.skip async def test_rebuild(context): # Mock data = await aread(filename=Path(__file__).parent / "../../data/graph_db/networkx.json") diff --git a/tests/metagpt/actions/test_write_code_plan_and_change_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py index 741773e2b..9cd51398f 100644 --- a/tests/metagpt/actions/test_write_code_plan_and_change_an.py +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -55,7 +55,7 @@ async def test_write_code_plan_and_change_an(mocker): @pytest.mark.asyncio async def test_refine_code(mocker): - mocker.patch("metagpt.actions.write_code.WriteCodePlanAndChange.write_code", return_value=REFINED_CODE_SAMPLE) + mocker.patch.object(WriteCode, "_aask", return_value=REFINED_CODE_SAMPLE) prompt = REFINED_TEMPLATE.format( user_requirement=NEW_REQUIREMENT_SAMPLE, code_plan_and_change=CODE_PLAN_AND_CHANGE_SAMPLE, diff --git a/tests/metagpt/utils/test_redis.py b/tests/metagpt/utils/test_redis.py index 748c44f54..6fd4250a6 100644 --- a/tests/metagpt/utils/test_redis.py +++ b/tests/metagpt/utils/test_redis.py @@ -9,23 +9,25 @@ from unittest.mock import AsyncMock import pytest -from metagpt.config2 import Config from metagpt.utils.redis import Redis -async def async_mock_from_url(*args, **kwargs): - mock_client = AsyncMock() - mock_client.set.return_value = None - mock_client.get.side_effect = [b"test", b""] - return mock_client - - @pytest.mark.asyncio async def test_redis(mocker): - redis = Config.default().redis - mocker.patch("aioredis.from_url", return_value=async_mock_from_url()) + async def async_mock_from_url(*args, **kwargs): + mock_client = AsyncMock() + mock_client.set.return_value = None + mock_client.get.return_value = b"test" + return mock_client - conn = Redis(redis) + mocker.patch("aioredis.from_url", return_value=async_mock_from_url()) + mock_config = mocker.Mock() + mock_config.to_url.return_value = "http://mock.com" + mock_config.username = "mockusername" + mock_config.password = "mockpwd" + mock_config.db = "0" + + conn = Redis(mock_config) await conn.set("test", "test", timeout_sec=0) assert await conn.get("test") == b"test" await conn.close() diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index 4dc3b1e42..b26ebe94d 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -8,8 +8,8 @@ import uuid from pathlib import Path +import aioboto3 import aiofiles -import mock import pytest from metagpt.config2 import Config @@ -18,21 +18,18 @@ from metagpt.utils.s3 import S3 @pytest.mark.asyncio -@mock.patch("aioboto3.Session") -async def test_s3(mock_session_class): +async def test_s3(mocker): # Set up the mock response data = await aread(__file__, "utf-8") - mock_session_object = mock.Mock() - reader_mock = mock.AsyncMock() + reader_mock = mocker.AsyncMock() reader_mock.read.side_effect = [data.encode("utf-8"), b"", data.encode("utf-8")] - type(reader_mock).url = mock.PropertyMock(return_value="https://mock") - mock_client = mock.AsyncMock() + type(reader_mock).url = mocker.PropertyMock(return_value="https://mock") + mock_client = mocker.AsyncMock() mock_client.put_object.return_value = None mock_client.get_object.return_value = {"Body": reader_mock} mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None - mock_session_object.client.return_value = mock_client - mock_session_class.return_value = mock_session_object + mocker.patch.object(aioboto3.Session, "client", return_value=mock_client) # Prerequisites s3 = Config.default().s3 @@ -55,7 +52,7 @@ async def test_s3(mock_session_class): # Mock session env s3.access_key = "ABC" - type(reader_mock).url = mock.PropertyMock(return_value="") + type(reader_mock).url = mocker.PropertyMock(return_value="") try: conn = S3(s3) res = await conn.cache("ABC", ".bak", "script") From f9f6dbefa8bf94be64c1c5b917402f5de939453f Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 25 Jan 2024 10:33:46 +0800 Subject: [PATCH 094/101] Add the tar command to extract the .zip file --- tests/metagpt/test_incremental_dev.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 7bc319ed2..1baa82a72 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -105,8 +105,16 @@ def log_and_check_result(result, tag_name="refine"): def get_incremental_dev_result(idea, project_name, use_review=True): project_path = TEST_DATA_PATH / "incremental_dev_project" / project_name - if project_path.exists(): - raise Exception(f"Project {project_name} not exists") + # Check if the project path exists + if not project_path.exists(): + # If the project does not exist, extract the project file + try: + # Use the tar command to extract the .zip file + subprocess.run(["tar", "-xf", f"{project_path}.zip", "-C", str(project_path.parent)], check=True) + except subprocess.CalledProcessError as e: + # If the extraction fails, throw an exception + raise Exception(f"Failed to unzip project {project_name}. Error: {e}") + check_or_create_base_tag(project_path) args = [idea, "--inc", "--project-path", project_path, "--n-round", "20"] if not use_review: @@ -146,14 +154,18 @@ def check_or_create_base_tag(project_path): logger.info("Base tag doesn't exist.") # Add and commit the current code if 'base' tag doesn't exist add_cmd = ["git", "add", "."] - commit_cmd = ["git", "commit", "-m", "Initial commit"] try: subprocess.run(add_cmd, check=True) + logger.info("Files added successfully.") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to add files: {e}") + + commit_cmd = ["git", "commit", "-m", "Initial commit"] + try: subprocess.run(commit_cmd, check=True) - logger.info("Added and committed all files with the message 'Initial commit'.") - except Exception as e: - logger.error("Failed to add and commit all files.") - raise e + logger.info("Committed all files with the message 'Initial commit'.") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to commit: {e.stderr}") # Add 'base' tag add_base_tag_cmd = ["git", "tag", "base"] From 0faae9368f18a27c690a92c652013578cb2dda3f Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 25 Jan 2024 11:52:14 +0800 Subject: [PATCH 095/101] Add the tar command to extract the .zip file --- tests/metagpt/test_incremental_dev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 1baa82a72..6a26f9b83 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -113,7 +113,7 @@ def get_incremental_dev_result(idea, project_name, use_review=True): subprocess.run(["tar", "-xf", f"{project_path}.zip", "-C", str(project_path.parent)], check=True) except subprocess.CalledProcessError as e: # If the extraction fails, throw an exception - raise Exception(f"Failed to unzip project {project_name}. Error: {e}") + raise Exception(f"Failed to extract project {project_name}. Error: {e}") check_or_create_base_tag(project_path) args = [idea, "--inc", "--project-path", project_path, "--n-round", "20"] From 51169d7a69f8ce48a34ac8674f6f310d04cd2acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 25 Jan 2024 15:40:16 +0800 Subject: [PATCH 096/101] feat: + compatible with windows path --- metagpt/actions/design_api.py | 2 +- metagpt/actions/prepare_documents.py | 2 +- metagpt/roles/engineer.py | 2 +- metagpt/utils/dependency_file.py | 20 +++++++++++++------- metagpt/utils/file_repository.py | 19 +++++++++++++------ 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 4060d211c..cb6013538 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -46,7 +46,7 @@ class WriteDesign(Action): ) async def run(self, with_messages: Message, schema: str = None): - # Use `git status` to identify which PRD documents have been modified in the `docs/prds` directory. + # Use `git status` to identify which PRD documents have been modified in the `docs/prd` directory. changed_prds = self.repo.docs.prd.changed_files # Use `git status` to identify which design documents in the `docs/system_designs` directory have undergone # changes. diff --git a/metagpt/actions/prepare_documents.py b/metagpt/actions/prepare_documents.py index 84a4fc1d7..ab069dc11 100644 --- a/metagpt/actions/prepare_documents.py +++ b/metagpt/actions/prepare_documents.py @@ -48,5 +48,5 @@ class PrepareDocuments(Action): # 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/prds/`. + # `docs/requirement.txt` and `docs/prd/`. return ActionOutput(content=doc.content, instruct_content=doc) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index ee1a019bd..40ade2110 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -178,7 +178,7 @@ class Engineer(Role): is_pass, reason = await self._is_pass(summary) if not is_pass: todo.i_context.reason = reason - tasks.append(todo.i_context.dict()) + tasks.append(todo.i_context.model_dump()) await self.project_repo.docs.code_summary.save( filename=Path(todo.i_context.design_filename).name, diff --git a/metagpt/utils/dependency_file.py b/metagpt/utils/dependency_file.py index 7cf9a1d49..c8b3bc4a4 100644 --- a/metagpt/utils/dependency_file.py +++ b/metagpt/utils/dependency_file.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import re from pathlib import Path from typing import Set @@ -36,7 +37,9 @@ class DependencyFile: """Load dependencies from the file asynchronously.""" if not self._filename.exists(): return - self._dependencies = json.loads(await aread(self._filename)) + json_data = await aread(self._filename) + json_data = re.sub(r"\\+", "/", json_data) # Compatible with windows path + self._dependencies = json.loads(json_data) @handle_exception async def save(self): @@ -60,17 +63,20 @@ class DependencyFile: key = Path(filename).relative_to(root) except ValueError: key = filename - + skey = re.sub(r"\\+", "/", str(key)) # Compatible with windows path if dependencies: relative_paths = [] for i in dependencies: try: - relative_paths.append(str(Path(i).relative_to(root))) + s = str(Path(i).relative_to(root)) except ValueError: - relative_paths.append(str(i)) - self._dependencies[str(key)] = relative_paths - elif str(key) in self._dependencies: - del self._dependencies[str(key)] + s = str(i) + s = re.sub(r"\\+", "/", s) # Compatible with windows path + relative_paths.append(s) + + self._dependencies[skey] = relative_paths + elif skey in self._dependencies: + del self._dependencies[skey] if persist: await self.save() diff --git a/metagpt/utils/file_repository.py b/metagpt/utils/file_repository.py index 94d6fe76d..d2a06963a 100644 --- a/metagpt/utils/file_repository.py +++ b/metagpt/utils/file_repository.py @@ -101,21 +101,28 @@ class FileRepository: path_name = self.workdir / filename if not path_name.exists(): return None + if not path_name.is_file(): + return None doc.content = await aread(path_name) return doc - async def get_all(self) -> List[Document]: + async def get_all(self, filter_ignored=True) -> List[Document]: """Get the content of all files in the repository. :return: List of Document instances representing files. """ docs = [] - for root, dirs, files in os.walk(str(self.workdir)): - for file in files: - file_path = Path(root) / file - relative_path = file_path.relative_to(self.workdir) - doc = await self.get(relative_path) + if filter_ignored: + for f in self.all_files: + doc = await self.get(f) docs.append(doc) + else: + for root, dirs, files in os.walk(str(self.workdir)): + for file in files: + file_path = Path(root) / file + relative_path = file_path.relative_to(self.workdir) + doc = await self.get(relative_path) + docs.append(doc) return docs @property From 1f90bc58cc56485b0355243bba98398a8d9547b3 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 25 Jan 2024 16:25:23 +0800 Subject: [PATCH 097/101] 1. Update the code compression package 2. Modify the prompt of REFINED_TEMPLATE --- .../actions/write_code_plan_and_change_an.py | 4 ++-- tests/data/incremental_dev_project/Gomoku.zip | Bin 94089 -> 94000 bytes .../dice_simulator_new.zip | Bin 56384 -> 56393 bytes .../number_guessing_game.zip | Bin 53755 -> 53764 bytes .../incremental_dev_project/pygame_2048.zip | Bin 60632 -> 63452 bytes .../simple_add_calculator.zip | Bin 56538 -> 56546 bytes .../incremental_dev_project/snake_game.zip | Bin 119511 -> 108388 bytes .../incremental_dev_project/word_cloud.zip | Bin 12762 -> 12752 bytes 8 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index 3fac22242..708808050 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -172,11 +172,11 @@ Role: You are a professional engineer; The main goal is to complete incremental 2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets. 3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import. 4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. -5. Follow Code Plan And Change: If there is any Incremental Change or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. +5. Follow Code Plan And Change: If there is any Incremental Change that is marked by the git diff format using '+' and '-' for add/modify/delete code, or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. 6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. 7. Before using a external variable/module, make sure you import it first. 8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. -9. Attention: Retain content that is not related to incremental development but important for consistency and clarity.". +9. Attention: Retain details that are not related to incremental development but are important for maintaining the consistency and clarity of the old code. """ WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", [CODE_PLAN_AND_CHANGE]) diff --git a/tests/data/incremental_dev_project/Gomoku.zip b/tests/data/incremental_dev_project/Gomoku.zip index d6c6b8d16148047dadbc180ad3974b612c48dac5..23649565ab58c4b07ee08680cb20370d933c1b39 100644 GIT binary patch delta 3940 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 diff --git a/tests/data/incremental_dev_project/dice_simulator_new.zip b/tests/data/incremental_dev_project/dice_simulator_new.zip index 4b8d3f038addf84e74e0d1a15a5209d3a263e921..4752ab4c55876bdb8d17a6ebc34aa7e2cf006786 100644 GIT binary patch delta 3861 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 delta 3564 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 delta 2540 zcmY+F30M=?7RP5U5P>2D5FR^7NSKU-2neW!Kz*WRHG&lp6wrr0Ktv3+fJLl6>xb0m zasX>ZlvR+;5O$G8CF}^cw6!kPDyX$}se2!V-prs+^L=yj`~T0mXL4umz4=eE+W(5w zOr-v5>az%f&?Hv2@mA%@+e*{0`jdqABnt>E)Ia3oq9#;U!yg+RjZF7;2Q{6gYHR1g zkny-+!}y4@Z#5)EnM0<`6mB|e$e(R-)Kc{++?HzB1qtGu49+fM^mzKR=CS^7bL$el zVs73$C9i*6)Ht<~b0vG?&bq&L6=ttX=c(}w)&8lrp0|FzjOQ}T{x(rI9y*~j6dU*Q z(-VoomcoYI{+kyQj;-M6pLy+5zNh4Bc=ldZ zpx=`Rlk!K$#(Pg@aNHW3YMbm_-6i~b6BE1Q?Zjw)Nmyb2i~nZHCMRkZZadfTU@TLp z6`->3E@e}sv*YvgFe>o3Rp=ZaQSJXmC|}a# z>Eu2TIXf7K-l$dvc5nTCLt;>)hf$8vUk=L#Rv9phX!w9?5os%`oEKWF;Z>ygb$Q+l zP1SNLE*@J2-tmqwvqh*F3(GczED0Acx(rZ_sp-}vWgtv8C!OI7WWs7iOmdnQ>_n=x zhDLI6n59w>4|u`0BMTSYWF#iF>_3+byfZh*Qzyej2_qSy?Dav19{#}u?n zXw*9sOCE%ke29v4F$$m+9QXi z{Iy1m3oXE+T#O=}8H~l4$6`ad8`|%}xR2*)fZk~u_5EK)oOVQ~=1pgHV1Abw44u|R z_up9B;NhwXIu#Uha-FWyhKLF`WaS15uFQKD4nC!)o0!xViqHAch zMZ(-yUL!&Z7S+#$ptIhLn|?-wCRrTDuKFVE3L!{Np`|LchZAxsnG28Q!Q>SjDG)7Y zyY5=djK5VYLR~Bx`v_r;f<~KtV0JwpI(*cjRzV>-E7g`zAgrTN*b*k2R_B27SbT^x z&+BN?9c=1@$ut}(7+T8S#n2a`1NdOj;DA=LSmsN?{stQT=*MJk;EcIHg9-i=gf-IW ziAvf-CB}IH7z5`>{Yf;7Q2iGW%zwd1jppY94T4Zz{*0>%HuxFM-{_g_g52<9smsDUSKcFVBmRWjuqMb+OCQTr^E0_$&k%F479Qb#I4m|9#A_uW!3u2`h z>B(@hIc!%Nzz}mv>7unVNJ_PUUL}okS^Qb156=h8@cF2&qk3%w!Nu2gCaJz<>%U|W z-)+ftW-w)=XIi>(| z97kL59`rm&M*KcWM}gItjG=pwbew+#aGK7gcQ3i-ZkX+d5d w(~~QDuV>})x8pKKRL`Fai$=`Q%Xe*G)LOt)O%&+C;}LD``(lEaC{XqJ8*8o&q5uE@ diff --git a/tests/data/incremental_dev_project/pygame_2048.zip b/tests/data/incremental_dev_project/pygame_2048.zip index 2f0457be626f4f60d27563c5bdc7c86940260c35..93e9cf0fe6fa29b344701c93dc7b5b34d3e41e87 100644 GIT binary patch delta 6280 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 TU;9z0Sor@j0fpjgqFVn8F&a!% delta 3332 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 lq5h*V`hbbN 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 diff --git a/tests/data/incremental_dev_project/word_cloud.zip b/tests/data/incremental_dev_project/word_cloud.zip index d15c1cf74cea5a7ac15ed29f75574351949df1cc..d8747d14d884677004c7f29ce2422319cb679eaa 100644 GIT binary patch delta 781 zcmY*VTSyd97@l)nYjww+!JVDeZPZoUcJ{c=>_%oQ-JqaQABK>yki^tNBndNcOL8mg zAu}((MQG7eS(J1!qYDb9_@Yu0V%SBYhl&Jyum_Wk=FDo~59jcG|Ns5h>GF5w4y_=@ zBrptPW~#!$;(_5mY_rY|w>xqgAMbnP%%8k-yQ)3??i0l+#u=^w|)tN2PoVOZ3+vL zkB2zF6)z=2ic^9dzl;KK$iE9!>VGoaawfvKpWvbtDD@}7m6WxR6d?F{nn6nfxwz16 Rz}`S+{8SvneD%;Z{sHx1=7#_P delta 758 zcmZ`#Ur1A76#u@vbatKFmFZQlo4Yz~_s`tjEl6Tz5C!7WM8TpW%T3eFj7>=;)I^F9 ziFIDGz`(GF2+AzMg8n!Jkp{jL&nc{tZR6aRWcffUQ>{DfIz%X;`jBi}c=!h+`Gn2vCm4fNq?}P0H1N*k$ z+k3eho$H-&)}*#nA1tvATyzXxi+0qNjyx*5(QN+O(_3)@MtA&Nd~)YkkLNJ42f2x9 zk$EThI0me^0mkVR{(LAHR{mPQNvtnKA7AI<5UNQ;NPuC?;xI6B%E%mENB}=)ffqO~ zuK85BAq=y0CKidry1^Nl9dRbUwk7>@0$6n&$CHuw9DEQt=o8HNht>lR1c`Ld3WZQC z(D4kpRxO4uoJ#iX5$%jX3R+;qlia`g#K=6{7PF{t@sMnEl92+n(+H);9Pmg271AIN zYpex$nSmvdak1JEJ+u|~X@o8`g@RRqvrR2QCD;@QE9;+$Np_dGp+x%5`TuGexe&YH zkX(dMg6d9(hq8o$AQ|G_5`IfiE%w5)J0B(pZzZUnqRsHdod)e@2DK On&`Q$2rarav%dj0DeW8p From 4680ff5e6232a9e17953c2db02140ab59f4baa80 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Thu, 25 Jan 2024 17:33:23 +0800 Subject: [PATCH 098/101] Only retain test_simple_add_calculator, skip other test cases --- tests/metagpt/test_incremental_dev.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 6a26f9b83..3e4a1b901 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -50,33 +50,39 @@ def test_simple_add_calculator(): log_and_check_result(result) +@pytest.mark.skip def test_number_guessing_game(): result = get_incremental_dev_result(IDEAS[1], PROJECT_NAMES[1]) log_and_check_result(result) +@pytest.mark.skip def test_word_cloud(): result = get_incremental_dev_result(IDEAS[2], PROJECT_NAMES[2]) log_and_check_result(result) +@pytest.mark.skip def test_gomoku(): result = get_incremental_dev_result(IDEAS[3], PROJECT_NAMES[3]) log_and_check_result(result) +@pytest.mark.skip def test_dice_simulator_new(): for i, (idea, project_name) in enumerate(zip(IDEAS[4:6], PROJECT_NAMES[4:6]), start=1): result = get_incremental_dev_result(idea, project_name) log_and_check_result(result, "refine_" + str(i)) +@pytest.mark.skip def test_refined_pygame_2048(): for i, (idea, project_name) in enumerate(zip(IDEAS[6:8], PROJECT_NAMES[6:8]), start=1): result = get_incremental_dev_result(idea, project_name) log_and_check_result(result, "refine_" + str(i)) +@pytest.mark.skip def test_refined_snake_game(): for i, (idea, project_name) in enumerate(zip(IDEAS[8:10], PROJECT_NAMES[8:10]), start=1): result = get_incremental_dev_result(idea, project_name) From cfadd54a3a5aee02416ea092087cdb65b6171611 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 26 Jan 2024 15:02:34 +0800 Subject: [PATCH 099/101] Update token_counter.py --- metagpt/utils/token_counter.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 885eb37d7..feec20928 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -4,10 +4,11 @@ @Time : 2023/5/18 00:40 @Author : alexanderwu @File : token_counter.py -ref1: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb -ref2: https://github.com/Significant-Gravitas/Auto-GPT/blob/master/autogpt/llm/token_counter.py -ref3: https://github.com/hwchase17/langchain/blob/master/langchain/chat_models/openai.py -ref4: https://ai.google.dev/models/gemini +ref1: https://openai.com/pricing +ref2: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb +ref3: https://github.com/Significant-Gravitas/Auto-GPT/blob/master/autogpt/llm/token_counter.py +ref4: https://github.com/hwchase17/langchain/blob/master/langchain/chat_models/openai.py +ref5: https://ai.google.dev/models/gemini """ import tiktoken @@ -25,7 +26,10 @@ TOKEN_COSTS = { "gpt-4-32k": {"prompt": 0.06, "completion": 0.12}, "gpt-4-32k-0314": {"prompt": 0.06, "completion": 0.12}, "gpt-4-0613": {"prompt": 0.06, "completion": 0.12}, + "gpt-4-turbo-preview": {"prompt": 0.01, "completion": 0.03}, + "gpt-4-0125-preview": {"prompt": 0.01, "completion": 0.03}, "gpt-4-1106-preview": {"prompt": 0.01, "completion": 0.03}, + "gpt-4-1106-vision-preview": {"prompt": 0.01, "completion": 0.03}, "text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0}, "glm-3-turbo": {"prompt": 0.0, "completion": 0.0007}, # 128k version, prompt + completion tokens=0.005¥/k-tokens "glm-4": {"prompt": 0.0, "completion": 0.014}, # 128k version, prompt + completion tokens=0.1¥/k-tokens @@ -47,7 +51,10 @@ TOKEN_MAX = { "gpt-4-32k": 32768, "gpt-4-32k-0314": 32768, "gpt-4-0613": 8192, + "gpt-4-turbo-preview": 128000, + "gpt-4-0125-preview": 128000, "gpt-4-1106-preview": 128000, + "gpt-4-1106-vision-preview": 128000, "text-embedding-ada-002": 8192, "chatglm_turbo": 32768, "gemini-pro": 32768, From 59afc5301f55037e7b379497767f4af62fd65b31 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 26 Jan 2024 15:08:08 +0800 Subject: [PATCH 100/101] update token counter --- metagpt/utils/token_counter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index feec20928..94506e373 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -79,7 +79,10 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0613"): "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613", + "gpt-4-turbo-preview", + "gpt-4-0125-preview", "gpt-4-1106-preview", + "gpt-4-1106-vision-preview", }: tokens_per_message = 3 # # every reply is primed with <|start|>assistant<|message|> tokens_per_name = 1 From a6bdd0201765e3f6b1dca8aa398f25496d3158b3 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 26 Jan 2024 15:03:17 +0800 Subject: [PATCH 101/101] add ActionNode.from_pydantic --- metagpt/actions/action_node.py | 79 +++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index ca41c76a5..162ab90eb 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -12,7 +12,7 @@ import json from enum import Enum from typing import Any, Dict, List, Optional, Tuple, Type, Union -from pydantic import BaseModel, create_model, model_validator +from pydantic import BaseModel, Field, create_model, model_validator from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_outcls_registry import register_action_outcls @@ -186,11 +186,27 @@ class ActionNode: obj.add_children(nodes) return obj - def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: + def get_children_mapping_old(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: """获得子ActionNode的字典,以key索引""" exclude = exclude or [] return {k: (v.expected_type, ...) for k, v in self.children.items() if k not in exclude} + def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: + """获得子ActionNode的字典,以key索引,支持多级结构""" + exclude = exclude or [] + mapping = {} + + def _get_mapping(node: "ActionNode", prefix: str = ""): + for key, child in node.children.items(): + if key in exclude: + continue + full_key = f"{prefix}{key}" + mapping[full_key] = (child.expected_type, ...) + _get_mapping(child, prefix=f"{full_key}.") + + _get_mapping(self) + return mapping + def get_self_mapping(self) -> Dict[str, Tuple[Type, Any]]: """get self key: type mapping""" return {self.key: (self.expected_type, ...)} @@ -616,3 +632,62 @@ class ActionNode: self.update_instruct_content(revise_contents) return revise_contents + + @classmethod + def from_pydantic(cls, model: Type[BaseModel], key: str = None): + """ + Creates an ActionNode tree from a Pydantic model. + + Args: + model (Type[BaseModel]): The Pydantic model to convert. + + Returns: + ActionNode: The root node of the created ActionNode tree. + """ + key = key or model.__name__ + root_node = cls(key=model.__name__, expected_type=Type[model], instruction="", example="") + + for field_name, field_model in model.model_fields.items(): + # Extracting field details + expected_type = field_model.annotation + instruction = field_model.description or "" + example = field_model.default + + # Check if the field is a Pydantic model itself. + # Use isinstance to avoid typing.List, typing.Dict, etc. (they are instances of type, not subclasses) + if isinstance(expected_type, type) and issubclass(expected_type, BaseModel): + # Recursively process the nested model + child_node = cls.from_pydantic(expected_type, key=field_name) + else: + child_node = cls(key=field_name, expected_type=expected_type, instruction=instruction, example=example) + + root_node.add_child(child_node) + + return root_node + + +class ToolUse(BaseModel): + tool_name: str = Field(default="a", description="tool name", examples=[]) + + +class Task(BaseModel): + task_id: int = Field(default="1", description="task id", examples=[1, 2, 3]) + name: str = Field(default="Get data from ...", description="task name", examples=[]) + dependent_task_ids: List[int] = Field(default=[], description="dependent task ids", examples=[1, 2, 3]) + tool: ToolUse = Field(default=ToolUse(), description="tool use", examples=[]) + + +class Tasks(BaseModel): + tasks: List[Task] = Field(default=[], description="tasks", examples=[]) + + +if __name__ == "__main__": + node = ActionNode.from_pydantic(Tasks) + print("Tasks") + print(Tasks.model_json_schema()) + print("Task") + print(Task.model_json_schema()) + print(node) + prompt = node.compile(context="") + node.create_children_class() + print(prompt)