merge main to dev

This commit is contained in:
mannaandpoem 2024-01-22 13:50:21 +08:00
commit 76d4451fc6
28 changed files with 1479 additions and 55 deletions

View file

@ -6,16 +6,16 @@ # MetaGPT: The Multi-Agent Framework
</p>
<p align="center">
<b>Assign different roles to GPTs to form a collaborative software entity for complex tasks.</b>
<b>Assign different roles to GPTs to form a collaborative entity for complex tasks.</b>
</p>
<p align="center">
<a href="docs/README_CN.md"><img src="https://img.shields.io/badge/文档-中文版-blue.svg" alt="CN doc"></a>
<a href="README.md"><img src="https://img.shields.io/badge/document-English-blue.svg" alt="EN doc"></a>
<a href="docs/README_JA.md"><img src="https://img.shields.io/badge/ドキュメント-日本語-blue.svg" alt="JA doc"></a>
<a href="https://discord.gg/DYn29wFk9z"><img src="https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat" alt="Discord Follow"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
<a href="docs/ROADMAP.md"><img src="https://img.shields.io/badge/ROADMAP-路线图-blue" alt="roadmap"></a>
<a href="https://discord.gg/DYn29wFk9z"><img src="https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat" alt="Discord Follow"></a>
<a href="https://twitter.com/MetaGPT_"><img src="https://img.shields.io/twitter/follow/MetaGPT?style=social" alt="Twitter Follow"></a>
</p>
@ -25,20 +25,13 @@ # MetaGPT: The Multi-Agent Framework
<a href="https://huggingface.co/spaces/deepwisdom/MetaGPT" target="_blank"><img alt="Hugging Face" src="https://img.shields.io/badge/%F0%9F%A4%97%20-Hugging%20Face-blue?color=blue&logoColor=white" /></a>
</p>
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)
<p align="center">Software Company Multi-Role Schematic (Gradually Implementing)</p>
## 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).
@ -48,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)
<p align="center">Software Company Multi-Agent Schematic (Gradually Implementing)</p>
## Install
### Pip installation

View file

@ -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
from metagpt.actions.design_api_an import (
DATA_STRUCTURES_AND_INTERFACES,
DESIGN_API_NODE,
PROGRAM_CALL_FLOW,
REFINED_DATA_STRUCTURES_AND_INTERFACES,
REFINED_DESIGN_NODE,
REFINED_PROGRAM_CALL_FLOW,
)
from metagpt.const import DATA_API_DESIGN_FILE_REPO, SEQ_FLOW_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import Document, Documents, Message
@ -68,7 +75,7 @@ class WriteDesign(Action):
async def _merge(self, prd_doc, system_design_doc):
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)
node = await REFINED_DESIGN_NODE.fill(context=context, llm=self.llm)
system_design_doc.content = node.instruct_content.model_dump_json()
return system_design_doc
@ -92,7 +99,7 @@ class WriteDesign(Action):
async def _save_data_api_design(self, 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.key) or m.get(REFINED_DATA_STRUCTURES_AND_INTERFACES.key)
if not data_api_design:
return
pathname = self.repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("")
@ -101,7 +108,7 @@ class WriteDesign(Action):
async def _save_seq_flow(self, design_doc):
m = json.loads(design_doc.content)
seq_flow = m.get("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 = self.repo.workdir / Path(SEQ_FLOW_FILE_REPO) / Path(design_doc.filename).with_suffix("")

View file

@ -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, MMC2
IMPLEMENTATION_APPROACH = ActionNode(
@ -17,6 +18,15 @@ IMPLEMENTATION_APPROACH = ActionNode(
example="We will ...",
)
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. Outline the steps involved in the implementation process with the "
"detailed strategies.",
example="We will refine ...",
)
PROJECT_NAME = ActionNode(
key="Project name", expected_type=str, instruction="The project name with underline", example="game_2048"
)
@ -28,6 +38,14 @@ FILE_LIST = ActionNode(
example=["main.py", "game.py"],
)
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.",
example=["main.py", "game.py", "new_feature.py"],
)
DATA_STRUCTURES_AND_INTERFACES = ActionNode(
key="Data structures and interfaces",
expected_type=str,
@ -37,6 +55,16 @@ DATA_STRUCTURES_AND_INTERFACES = ActionNode(
example=MMC1,
)
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."
"Retain content that is not related to incremental development but important for consistency and clarity.",
example=MMC1,
)
PROGRAM_CALL_FLOW = ActionNode(
key="Program call flow",
expected_type=str,
@ -45,6 +73,16 @@ PROGRAM_CALL_FLOW = ActionNode(
example=MMC2,
)
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 that is not related to incremental development but important for consistency and clarity.",
example=MMC2,
)
ANYTHING_UNCLEAR = ActionNode(
key="Anything UNCLEAR",
expected_type=str,
@ -61,4 +99,24 @@ NODES = [
ANYTHING_UNCLEAR,
]
REFINED_NODES = [
REFINED_IMPLEMENTATION_APPROACH,
REFINED_FILE_LIST,
REFINED_DATA_STRUCTURES_AND_INTERFACES,
REFINED_PROGRAM_CALL_FLOW,
ANYTHING_UNCLEAR,
]
DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES)
REFINED_DESIGN_NODE = ActionNode.from_children("RefinedDesignAPI", REFINED_NODES)
def main():
prompt = DESIGN_API_NODE.compile(context="")
logger.info(prompt)
prompt = REFINED_DESIGN_NODE.compile(context="")
logger.info(prompt)
if __name__ == "__main__":
main()

View file

@ -15,7 +15,7 @@ from typing import Optional
from metagpt.actions.action import Action
from metagpt.actions.action_output import ActionOutput
from metagpt.actions.project_management_an import PM_NODE
from metagpt.actions.project_management_an import PM_NODE, REFINED_PM_NODE
from metagpt.const import PACKAGE_REQUIREMENTS_FILENAME
from metagpt.logs import logger
from metagpt.schema import Document, Documents
@ -78,7 +78,7 @@ class WriteTasks(Action):
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)
node = await PM_NODE.fill(context, self.llm, schema=self.prompt_schema)
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

View file

@ -35,6 +35,20 @@ LOGIC_ANALYSIS = ActionNode(
],
)
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 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"],
["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 +56,15 @@ TASK_LIST = ActionNode(
example=["game.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, "
"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"],
)
FULL_API_SPEC = ActionNode(
key="Full API spec",
expected_type=str,
@ -54,9 +77,19 @@ 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.",
)
REFINED_SHARED_KNOWLEDGE = ActionNode(
key="Refined Shared Knowledge",
expected_type=str,
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.",
)
ANYTHING_UNCLEAR_PM = ActionNode(
key="Anything UNCLEAR",
expected_type=str,
@ -74,13 +107,25 @@ NODES = [
ANYTHING_UNCLEAR_PM,
]
REFINED_NODES = [
REQUIRED_PYTHON_PACKAGES,
REQUIRED_OTHER_LANGUAGE_PACKAGES,
REFINED_LOGIC_ANALYSIS,
REFINED_TASK_LIST,
FULL_API_SPEC,
REFINED_SHARED_KNOWLEDGE,
ANYTHING_UNCLEAR_PM,
]
PM_NODE = ActionNode.from_children("PM_NODE", 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_NODE.compile(context="")
logger.info(prompt)
if __name__ == "__main__":

View file

@ -16,15 +16,23 @@
"""
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.const import BUGFIX_FILENAME
from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST
from metagpt.actions.write_code_plan_and_change_an import REFINED_TEMPLATE
from metagpt.const import (
BUGFIX_FILENAME,
CODE_PLAN_AND_CHANGE_FILENAME,
REQUIREMENT_FILENAME,
)
from metagpt.logs import logger
from metagpt.schema import CodingContext, Document, RunCodeResult
from metagpt.utils.common import CodeParser
from metagpt.utils.project_repo import ProjectRepo
PROMPT_TEMPLATE = """
NOTICE
@ -91,6 +99,9 @@ class WriteCode(Action):
bug_feedback = await self.repo.docs.get(filename=BUGFIX_FILENAME)
coding_context = CodingContext.loads(self.i_context.content)
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)
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)
@ -101,6 +112,10 @@ 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, project_repo=self.repo, mode="incremental"
)
else:
code_context = await self.get_codes(
coding_context.task_doc,
@ -108,15 +123,28 @@ class WriteCode(Action):
project_repo=self.repo.with_src_path(self.context.src_workspace),
)
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.i_context.filename,
summary_log=summary_doc.content if summary_doc else "",
)
if code_plan_and_change:
prompt = REFINED_TEMPLATE.format(
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 "",
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.i_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:
@ -127,20 +155,71 @@ class WriteCode(Action):
return coding_context
@staticmethod
async def get_codes(task_doc, exclude, project_repo) -> str:
async def get_codes(
task_doc: Document, exclude: str, project_repo: ProjectRepo, 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.
project_repo (ProjectRepo): ProjectRepo object of the project.
mode (str): Specifies the mode, either "normal" or "incremental" (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 "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", [])
code_filenames = m.get(TASK_LIST.key, []) if mode == "normal" else m.get(REFINED_TASK_LIST.key, [])
codes = []
src_file_repo = project_repo.srcs
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)
if mode == "incremental":
src_files = src_file_repo.all_files
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
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 projects 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:
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}```")
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)
if not doc:
continue
codes.append(f"----- {filename}\n```{doc.content}```")
return "\n".join(codes)

View file

@ -0,0 +1,207 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/12/26
@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.schema import CodePlanAndChangeContext
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 "
"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. 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
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. 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)
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()
```""",
)
CODE_PLAN_AND_CHANGE_CONTEXT = """
## User New Requirements
{requirement}
## PRD
{prd}
## Design
{design}
## Tasks
{tasks}
## Legacy Code
{code}
"""
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.
# Context
## User New Requirements
{user_requirement}
## Code Plan And Change
{code_plan_and_change}
## 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 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.
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 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_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", [CODE_PLAN_AND_CHANGE])
class WriteCodePlanAndChange(Action):
name: str = "WriteCodePlanAndChange"
i_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.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])
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
)
return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json")
async def get_old_codes(self) -> str:
self.repo.old_workspace = self.repo.git_repo.workdir / os.path.basename(self.config.project_path)
old_file_repo = self.repo.git_repo.new_file_repository(relative_path=self.repo.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)

View file

@ -13,6 +13,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential
from metagpt.actions import WriteCode
from metagpt.actions.action import Action
from metagpt.const import CODE_PLAN_AND_CHANGE_FILENAME, REQUIREMENT_FILENAME
from metagpt.logs import logger
from metagpt.schema import CodingContext
from metagpt.utils.common import CodeParser
@ -137,6 +138,9 @@ 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
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"
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 ""
@ -144,14 +148,31 @@ 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,
)
context = "\n".join(
[
"## System Design\n" + str(self.i_context.design_doc) + "\n",
"## Tasks\n" + task_content + "\n",
"## Code Files\n" + code_context + "\n",
]
)
if not code_plan_and_change:
context = "\n".join(
[
"## System Design\n" + str(self.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 ""
context = "\n".join(
[
"## User New Requirements\n" + user_requirement + "\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",
]
)
context_prompt = PROMPT_TEMPLATE.format(
context=context,
code=iterative_code,

View file

@ -20,7 +20,9 @@ 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,
REFINED_PRD_NODE,
WP_IS_RELATIVE_NODE,
WP_ISSUE_TYPE_NODE,
WRITE_PRD_NODE,
@ -138,7 +140,7 @@ class WritePRD(Action):
if not self.project_name:
self.project_name = Path(self.project_path).name
prompt = NEW_REQ_TEMPLATE.format(requirements=req.content, old_prd=related_doc.content)
node = await WRITE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=self.prompt_schema)
node = await REFINED_PRD_NODE.fill(context=prompt, llm=self.llm, schema=self.prompt_schema)
related_doc.content = node.instruct_content.model_dump_json()
await self._rename_workspace(node)
return related_doc
@ -152,7 +154,7 @@ class WritePRD(Action):
async def _save_competitive_analysis(self, prd_doc: Document):
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 = self.repo.workdir / COMPETITIVE_ANALYSIS_FILE_REPO / Path(prd_doc.filename).stem

View file

@ -30,6 +30,13 @@ ORIGINAL_REQUIREMENTS = ActionNode(
example="Create a 2048 game",
)
REFINED_REQUIREMENTS = ActionNode(
key="Refined Requirements",
expected_type=str,
instruction="Place the New user's requirements here.",
example="Create a 2048 game with a new feature that ...",
)
PROJECT_NAME = ActionNode(
key="Project Name",
expected_type=str,
@ -45,6 +52,18 @@ PRODUCT_GOALS = ActionNode(
example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"],
)
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.",
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 +77,20 @@ USER_STORIES = ActionNode(
],
)
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. 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",
"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],
@ -97,6 +130,15 @@ REQUIREMENT_ANALYSIS = ActionNode(
example="",
)
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]],
@ -104,6 +146,14 @@ REQUIREMENT_POOL = ActionNode(
example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]],
)
REFINED_REQUIREMENT_POOL = ActionNode(
key="Refined Requirement Pool",
expected_type=List[List[str]],
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 ..."]],
)
UI_DESIGN_DRAFT = ActionNode(
key="UI Design draft",
expected_type=str,
@ -152,6 +202,22 @@ NODES = [
ANYTHING_UNCLEAR,
]
REFINED_NODES = [
LANGUAGE,
PROGRAMMING_LANGUAGE,
REFINED_REQUIREMENTS,
PROJECT_NAME,
REFINED_PRODUCT_GOALS,
REFINED_USER_STORIES,
COMPETITIVE_ANALYSIS,
COMPETITIVE_QUADRANT_CHART,
REFINED_REQUIREMENT_ANALYSIS,
REFINED_REQUIREMENT_POOL,
UI_DESIGN_DRAFT,
ANYTHING_UNCLEAR,
]
WRITE_PRD_NODE = ActionNode.from_children("WritePRD", 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])

View file

@ -82,17 +82,20 @@ MESSAGE_ROUTE_TO_NONE = "<none>"
REQUIREMENT_FILENAME = "requirement.txt"
BUGFIX_FILENAME = "bugfix.txt"
PACKAGE_REQUIREMENTS_FILENAME = "requirements.txt"
CODE_PLAN_AND_CHANGE_FILENAME = "code_plan_and_change.json"
DOCS_FILE_REPO = "docs"
PRDS_FILE_REPO = "docs/prd"
SYSTEM_DESIGN_FILE_REPO = "docs/system_design"
TASK_FILE_REPO = "docs/task"
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"
SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design"
PRD_PDF_FILE_REPO = "resources/prd"
TASK_PDF_FILE_REPO = "resources/api_spec_and_task"
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_summary"

View file

@ -20,17 +20,27 @@
from __future__ import annotations
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.const import SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO
from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange
from metagpt.const import (
CODE_PLAN_AND_CHANGE_FILE_REPO,
CODE_PLAN_AND_CHANGE_FILENAME,
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,
@ -80,7 +90,7 @@ class Engineer(Role):
super().__init__(**kwargs)
self.set_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)
@ -88,9 +98,9 @@ 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.key) or m.get(REFINED_TASK_LIST.key)
async def _act_sp_with_cr(self, review=False) -> Set[str]:
async def _act_sp_with_cr(self, review=False, mode: Literal["normal", "incremental"] = "normal") -> Set[str]:
changed_files = set()
for todo in self.code_todos:
"""
@ -106,9 +116,13 @@ class Engineer(Role):
action = WriteCodeReview(i_context=coding_context, context=self.context, llm=self.llm)
self._init_action(action)
coding_context = await action.run()
dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path}
if mode == "incremental":
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,
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(
@ -128,6 +142,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()
@ -187,6 +204,36 @@ 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..")
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,
}
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
)
await self.project_repo.docs.code_plan_and_change.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)
@ -197,11 +244,16 @@ class Engineer(Role):
async def _think(self) -> Action | None:
if not self.src_workspace:
self.src_workspace = self.git_repo.workdir / self.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 self.config.inc and msg.cause_by in write_plan_and_change_filters:
logger.debug(f"TODO WriteCodePlanAndChange:{msg.model_dump_json()}")
await self._new_code_plan_and_change_action()
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))
@ -296,6 +348,21 @@ class Engineer(Role):
if self.summarize_todos:
self.set_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 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)
@property
def todo(self) -> str:
"""AgentStore uses this attribute to display to the user what actions the current role should take."""

View file

@ -470,6 +470,14 @@ 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]
# mermaid class view
class ClassMeta(BaseModel):
name: str = ""

View file

@ -13,6 +13,8 @@ from pathlib import Path
from metagpt.const import (
CLASS_VIEW_FILE_REPO,
CODE_PLAN_AND_CHANGE_FILE_REPO,
CODE_PLAN_AND_CHANGE_PDF_FILE_REPO,
CODE_SUMMARIES_FILE_REPO,
CODE_SUMMARIES_PDF_FILE_REPO,
COMPETITIVE_ANALYSIS_FILE_REPO,
@ -43,6 +45,7 @@ class DocFileRepositories(FileRepository):
code_summary: FileRepository
graph_repo: FileRepository
class_view: FileRepository
code_plan_and_change: FileRepository
def __init__(self, git_repo):
super().__init__(git_repo=git_repo, relative_path=DOCS_FILE_REPO)
@ -53,6 +56,7 @@ class DocFileRepositories(FileRepository):
self.code_summary = git_repo.new_file_repository(relative_path=CODE_SUMMARIES_FILE_REPO)
self.graph_repo = git_repo.new_file_repository(relative_path=GRAPH_REPO_FILE_REPO)
self.class_view = git_repo.new_file_repository(relative_path=CLASS_VIEW_FILE_REPO)
self.code_plan_and_change = git_repo.new_file_repository(relative_path=CODE_PLAN_AND_CHANGE_FILE_REPO)
class ResourceFileRepositories(FileRepository):
@ -64,6 +68,7 @@ class ResourceFileRepositories(FileRepository):
api_spec_and_task: FileRepository
code_summary: FileRepository
sd_output: FileRepository
code_plan_and_change: FileRepository
def __init__(self, git_repo):
super().__init__(git_repo=git_repo, relative_path=RESOURCES_FILE_REPO)
@ -76,6 +81,7 @@ class ResourceFileRepositories(FileRepository):
self.api_spec_and_task = git_repo.new_file_repository(relative_path=TASK_PDF_FILE_REPO)
self.code_summary = git_repo.new_file_repository(relative_path=CODE_SUMMARIES_PDF_FILE_REPO)
self.sd_output = git_repo.new_file_repository(relative_path=SD_OUTPUT_FILE_REPO)
self.code_plan_and_change = git_repo.new_file_repository(relative_path=CODE_PLAN_AND_CHANGE_PDF_FILE_REPO)
class ProjectRepo(FileRepository):

Binary file not shown.

View file

@ -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": "",
}
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'
}
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
"""

Binary file not shown.

View file

@ -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.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,46 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/01/03
@Author : mannaandpoem
@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
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,
REFINED_DESIGN_JSON,
REFINED_PRD_JSON,
)
@pytest.fixture()
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 = BaseModel()
root.instruct_content.model_dump = mock_refined_design_json
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)
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()

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/01/03
@Author : mannaandpoem
@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
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,
REFINED_TASKS_JSON,
TASKS_SAMPLE,
)
@pytest.fixture()
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 = BaseModel()
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))
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()
assert "Refined Shared Knowledge" in node.instruct_content.model_dump()

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/01/03
@Author : mannaandpoem
@File : test_write_code_plan_and_change_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_plan_and_change_an import (
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,
REFINED_CODE_INPUT_SAMPLE,
REFINED_CODE_SAMPLE,
TASKS_SAMPLE,
)
def mock_code_plan_and_change():
return CODE_PLAN_AND_CHANGE_SAMPLE
@pytest.mark.asyncio
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_and_change_an.WriteCodePlanAndChange.run", return_value=root)
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 WriteCodePlanAndChange(context=code_plan_and_change_context).run()
assert "Plan" in node.instruct_content.model_dump()
@pytest.mark.asyncio
async def test_refine_code(mocker):
mocker.patch("metagpt.actions.write_code.WriteCodePlanAndChange.write_code", return_value=REFINED_CODE_SAMPLE)
prompt = REFINED_TEMPLATE.format(
user_requirement=NEW_REQUIREMENT_SAMPLE,
code_plan_and_change=CODE_PLAN_AND_CHANGE_SAMPLE,
design=DESIGN_SAMPLE,
tasks=TASKS_SAMPLE,
code=REFINED_CODE_INPUT_SAMPLE,
logs="",
feedback="",
filename="game.py",
summary_log="",
)
code = await WriteCode().write_code(prompt=prompt)
assert code
assert "def" in code

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/01/03
@Author : mannaandpoem
@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 import NEW_REQ_TEMPLATE
from metagpt.actions.write_prd_an import REFINED_PRD_NODE
from metagpt.llm import LLM
from tests.data.incremental_dev_project.mock import (
NEW_REQUIREMENT_SAMPLE,
PRD_SAMPLE,
REFINED_PRD_JSON,
)
@pytest.fixture()
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("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.REFINED_PRD_NODE.fill", return_value=root)
prompt = NEW_REQ_TEMPLATE.format(
requirements=NEW_REQUIREMENT_SAMPLE,
old_prd=PRD_SAMPLE,
project_name="",
)
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()
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()

View file

@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/01/03
@Author : mannaandpoem
@File : test_incremental_dev.py
"""
import os
import subprocess
import time
import pytest
from typer.testing import CliRunner
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",
"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",
"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",
"Incremental Idea Gradually increase the speed of the snake as the game progresses. In the current version of the game, the snakes speed remains constant throughout the gameplay. Implement a feature where the snakes 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 snakes 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.",
]
PROJECT_NAMES = [
"simple_add_calculator",
"number_guessing_game",
"word_cloud",
"Gomoku",
"dice_simulator_new",
"dice_simulator_new",
"pygame_2048",
"pygame_2048",
"snake_game",
"snake_game",
]
def test_simple_add_calculator():
result = get_incremental_dev_result(IDEAS[0], PROJECT_NAMES[0])
log_and_check_result(result)
def test_number_guessing_game():
result = get_incremental_dev_result(IDEAS[1], PROJECT_NAMES[1])
log_and_check_result(result)
def test_word_cloud():
result = get_incremental_dev_result(IDEAS[2], PROJECT_NAMES[2])
log_and_check_result(result)
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, "refine_" + str(i))
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))
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)
log_and_check_result(result, "refine_" + str(i))
def log_and_check_result(result, tag_name="refine"):
logger.info(result)
logger.info(result.output)
if "Aborting" in result.output:
assert False
else:
# After running, there will be new commit
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", tag_name], check=True)
except subprocess.CalledProcessError as e:
raise e
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_or_create_base_tag(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
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
subprocess.run(["git", "init"], check=True)
# Check if the 'base' tag exists
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
try:
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")
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"]
try:
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.error("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
try:
subprocess.run(add_base_tag_cmd, check=True)
logger.info("Added 'base' tag.")
except Exception as e:
logger.error("Failed to add 'base' tag.")
raise e
if __name__ == "__main__":
pytest.main([__file__, "-s"])