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