diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index fd56c42fb..87ccbf144 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -50,6 +50,7 @@ jobs: run: | export ALLOW_OPENAI_API_CALL=0 echo "${{ secrets.METAGPT_KEY_YAML }}" | base64 -d > config/key.yaml + mkdir -p ~/.metagpt && echo "${{ secrets.METAGPT_CONFIG2_YAML }}" | base64 -d > ~/.metagpt/config2.yaml pytest tests/ --doctest-modules --cov=./metagpt/ --cov-report=xml:cov.xml --cov-report=html:htmlcov --durations=20 | tee unittest.txt - name: Show coverage report run: | diff --git a/.gitignore b/.gitignore index ed45cb260..3c762de4c 100644 --- a/.gitignore +++ b/.gitignore @@ -174,5 +174,6 @@ htmlcov htmlcov.* *.dot *.pkl +*.faiss *-structure.csv *-structure.json diff --git a/README.md b/README.md index 61d03f692..90c586068 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ # MetaGPT: The Multi-Agent Framework

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

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

@@ -25,20 +25,13 @@ # MetaGPT: The Multi-Agent Framework Hugging Face

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

Software Company Multi-Role Schematic (Gradually Implementing)

- ## News -🚀 Jan. 16, 2024: [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) + +

Software Company Multi-Agent Schematic (Gradually Implementing)

+ ## Install ### Pip installation diff --git a/examples/example.faiss b/examples/example.faiss deleted file mode 100644 index 580946190..000000000 Binary files a/examples/example.faiss and /dev/null differ diff --git a/examples/example.pkl b/examples/example.pkl deleted file mode 100644 index f1912a973..000000000 Binary files a/examples/example.pkl and /dev/null differ diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index addc672bc..1b93213f7 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -15,6 +15,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from metagpt.actions.action_node import ActionNode from metagpt.context_mixin import ContextMixin from metagpt.schema import ( + CodePlanAndChangeContext, CodeSummarizeContext, CodingContext, RunCodeContext, @@ -28,7 +29,9 @@ class Action(SerializationMixin, ContextMixin, BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) name: str = "" - i_context: Union[dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, str, None] = "" + i_context: Union[ + dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, CodePlanAndChangeContext, str, None + ] = "" prefix: str = "" # aask*时会加上prefix,作为system_message desc: str = "" # for skill manager node: ActionNode = Field(default=None, exclude=True) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index ca41c76a5..162ab90eb 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -12,7 +12,7 @@ import json from enum import Enum from typing import Any, Dict, List, Optional, Tuple, Type, Union -from pydantic import BaseModel, create_model, model_validator +from pydantic import BaseModel, Field, create_model, model_validator from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_outcls_registry import register_action_outcls @@ -186,11 +186,27 @@ class ActionNode: obj.add_children(nodes) return obj - def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: + def get_children_mapping_old(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: """获得子ActionNode的字典,以key索引""" exclude = exclude or [] return {k: (v.expected_type, ...) for k, v in self.children.items() if k not in exclude} + def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: + """获得子ActionNode的字典,以key索引,支持多级结构""" + exclude = exclude or [] + mapping = {} + + def _get_mapping(node: "ActionNode", prefix: str = ""): + for key, child in node.children.items(): + if key in exclude: + continue + full_key = f"{prefix}{key}" + mapping[full_key] = (child.expected_type, ...) + _get_mapping(child, prefix=f"{full_key}.") + + _get_mapping(self) + return mapping + def get_self_mapping(self) -> Dict[str, Tuple[Type, Any]]: """get self key: type mapping""" return {self.key: (self.expected_type, ...)} @@ -616,3 +632,62 @@ class ActionNode: self.update_instruct_content(revise_contents) return revise_contents + + @classmethod + def from_pydantic(cls, model: Type[BaseModel], key: str = None): + """ + Creates an ActionNode tree from a Pydantic model. + + Args: + model (Type[BaseModel]): The Pydantic model to convert. + + Returns: + ActionNode: The root node of the created ActionNode tree. + """ + key = key or model.__name__ + root_node = cls(key=model.__name__, expected_type=Type[model], instruction="", example="") + + for field_name, field_model in model.model_fields.items(): + # Extracting field details + expected_type = field_model.annotation + instruction = field_model.description or "" + example = field_model.default + + # Check if the field is a Pydantic model itself. + # Use isinstance to avoid typing.List, typing.Dict, etc. (they are instances of type, not subclasses) + if isinstance(expected_type, type) and issubclass(expected_type, BaseModel): + # Recursively process the nested model + child_node = cls.from_pydantic(expected_type, key=field_name) + else: + child_node = cls(key=field_name, expected_type=expected_type, instruction=instruction, example=example) + + root_node.add_child(child_node) + + return root_node + + +class ToolUse(BaseModel): + tool_name: str = Field(default="a", description="tool name", examples=[]) + + +class Task(BaseModel): + task_id: int = Field(default="1", description="task id", examples=[1, 2, 3]) + name: str = Field(default="Get data from ...", description="task name", examples=[]) + dependent_task_ids: List[int] = Field(default=[], description="dependent task ids", examples=[1, 2, 3]) + tool: ToolUse = Field(default=ToolUse(), description="tool use", examples=[]) + + +class Tasks(BaseModel): + tasks: List[Task] = Field(default=[], description="tasks", examples=[]) + + +if __name__ == "__main__": + node = ActionNode.from_pydantic(Tasks) + print("Tasks") + print(Tasks.model_json_schema()) + print("Task") + print(Task.model_json_schema()) + print(node) + prompt = node.compile(context="") + node.create_children_class() + print(prompt) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index c6f608b7e..cb6013538 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -14,7 +14,14 @@ from pathlib import Path from typing import Optional from metagpt.actions import Action, ActionOutput -from metagpt.actions.design_api_an import DESIGN_API_NODE +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 @@ -39,7 +46,7 @@ class WriteDesign(Action): ) async def run(self, with_messages: Message, schema: str = None): - # Use `git status` to identify which PRD documents have been modified in the `docs/prds` directory. + # Use `git status` to identify which PRD documents have been modified in the `docs/prd` directory. changed_prds = self.repo.docs.prd.changed_files # Use `git status` to identify which design documents in the `docs/system_designs` directory have undergone # changes. @@ -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("") diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 3737203cf..35b50ef8f 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -8,6 +8,7 @@ from typing import List from metagpt.actions.action_node import ActionNode +from metagpt.logs import logger from metagpt.utils.mermaid import MMC1, 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() diff --git a/metagpt/actions/prepare_documents.py b/metagpt/actions/prepare_documents.py index 84a4fc1d7..ab069dc11 100644 --- a/metagpt/actions/prepare_documents.py +++ b/metagpt/actions/prepare_documents.py @@ -48,5 +48,5 @@ class PrepareDocuments(Action): # Write the newly added requirements from the main parameter idea to `docs/requirement.txt`. doc = await self.repo.docs.save(filename=REQUIREMENT_FILENAME, content=with_messages[0].content) # Send a Message notification to the WritePRD action, instructing it to process requirements using - # `docs/requirement.txt` and `docs/prds/`. + # `docs/requirement.txt` and `docs/prd/`. return ActionOutput(content=doc.content, instruct_content=doc) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 7988dd4e8..67a614d6f 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -15,14 +15,14 @@ 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 NEW_REQ_TEMPLATE = """ ### Legacy Content -{old_tasks} +{old_task} ### New Requirements {context} @@ -77,8 +77,8 @@ class WriteTasks(Action): return node async def _merge(self, system_design_doc, task_doc) -> Document: - context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) - node = await PM_NODE.fill(context, self.llm, schema=self.prompt_schema) + context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_task=task_doc.content) + node = await REFINED_PM_NODE.fill(context, self.llm, schema=self.prompt_schema) task_doc.content = node.instruct_content.model_dump_json() return task_doc diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 215a67202..379a23384 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -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__": diff --git a/metagpt/actions/run_code.py b/metagpt/actions/run_code.py index 7b84a79bb..3b84cc9f2 100644 --- a/metagpt/actions/run_code.py +++ b/metagpt/actions/run_code.py @@ -16,6 +16,7 @@ class. """ import subprocess +from pathlib import Path from typing import Tuple from pydantic import Field @@ -150,11 +151,23 @@ class RunCode(Action): return subprocess.run(cmd, check=check, cwd=cwd, env=env) @staticmethod - def _install_dependencies(working_directory, env): + def _install_requirements(working_directory, env): + file_path = Path(working_directory) / "requirements.txt" + if not file_path.exists(): + return + if file_path.stat().st_size == 0: + return install_command = ["python", "-m", "pip", "install", "-r", "requirements.txt"] logger.info(" ".join(install_command)) RunCode._install_via_subprocess(install_command, check=True, cwd=working_directory, env=env) + @staticmethod + def _install_pytest(working_directory, env): install_pytest_command = ["python", "-m", "pip", "install", "pytest"] logger.info(" ".join(install_pytest_command)) RunCode._install_via_subprocess(install_pytest_command, check=True, cwd=working_directory, env=env) + + @staticmethod + def _install_dependencies(working_directory, env): + RunCode._install_requirements(working_directory, env) + RunCode._install_pytest(working_directory, env) diff --git a/metagpt/actions/summarize_code.py b/metagpt/actions/summarize_code.py index 2b5546546..d21b62f83 100644 --- a/metagpt/actions/summarize_code.py +++ b/metagpt/actions/summarize_code.py @@ -26,9 +26,9 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc {system_design} ``` ----- -# Tasks +# Task ```text -{tasks} +{task} ``` ----- {code_blocks} @@ -110,7 +110,7 @@ class SummarizeCode(Action): format_example = FORMAT_EXAMPLE prompt = PROMPT_TEMPLATE.format( system_design=design_doc.content, - tasks=task_doc.content, + task=task_doc.content, code_blocks="\n".join(code_blocks), format_example=format_example, ) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index aaaa9648a..0b86ac1bb 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -21,10 +21,17 @@ 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 @@ -36,8 +43,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc ## Design {design} -## Tasks -{tasks} +## Task +{task} ## Legacy Code ```Code @@ -91,6 +98,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.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 +111,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.i_context.filename, project_repo=self.repo, use_inc=True + ) else: code_context = await self.get_codes( coding_context.task_doc, @@ -108,15 +122,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 "", + task=coding_context.task_doc.content if coding_context.task_doc else "", + code=code_context, + logs=logs, + feedback=bug_feedback.content if bug_feedback else "", + filename=self.i_context.filename, + summary_log=summary_doc.content if summary_doc else "", + ) + else: + prompt = PROMPT_TEMPLATE.format( + design=coding_context.design_doc.content if coding_context.design_doc else "", + task=coding_context.task_doc.content if coding_context.task_doc else "", + code=code_context, + logs=logs, + feedback=bug_feedback.content if bug_feedback else "", + filename=self.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 +154,66 @@ 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, use_inc: bool = False) -> str: + """ + Get codes for generating the exclude file in various scenarios. + + Attributes: + task_doc (Document): Document object of the task file. + exclude (str): The file to be generated. Specifies the filename to be excluded from the code snippets. + project_repo (ProjectRepo): ProjectRepo object of the project. + use_inc (bool): Indicates whether the scenario involves incremental development. Defaults to False. + + Returns: + str: Codes for generating the exclude file. + """ 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 use_inc 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) + + # Incremental development scenario + if use_inc: + src_files = src_file_repo.all_files + # Get the old workspace contained the old codes and old workspace are created in previous CodePlanAndChange + old_file_repo = project_repo.git_repo.new_file_repository(relative_path=project_repo.old_workspace) + old_files = old_file_repo.all_files + # Get the union of the files in the src and old workspaces + union_files_list = list(set(src_files) | set(old_files)) + for filename in union_files_list: + # Exclude the current file from the all code snippets + if filename == exclude: + # If the file is in the old workspace, use the old code + # Exclude unnecessary code to maintain a clean and focused main.py file, ensuring only relevant and + # essential functionality is included for the project’s requirements + if filename in old_files and filename != "main.py": + # Use old code + doc = await old_file_repo.get(filename=filename) + # If the file is in the src workspace, skip it + else: + continue + codes.insert(0, f"-----Now, {filename} to be rewritten\n```{doc.content}```\n=====") + # The 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}```") + + # Normal scenario + else: + for filename in code_filenames: + # Exclude the current file to get the code snippets for generating the current file + if filename == exclude: + continue + doc = await src_file_repo.get(filename=filename) + if not doc: + continue + codes.append(f"----- {filename}\n```{doc.content}```") + return "\n".join(codes) diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py new file mode 100644 index 000000000..708808050 --- /dev/null +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -0,0 +1,210 @@ +#!/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} + +## Task +{task} + +## 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} + +## Task +{task} + +## 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 that is marked by the git diff format using '+' and '-' for add/modify/delete code, or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan. +6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE. +7. Before using a external variable/module, make sure you import it first. +8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. +9. Attention: Retain details that are not related to incremental development but are important for maintaining the consistency and clarity of the old code. +""" + +WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", [CODE_PLAN_AND_CHANGE]) + + +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" + prd_doc = await self.repo.docs.prd.get(filename=self.i_context.prd_filename) + design_doc = await self.repo.docs.system_design.get(filename=self.i_context.design_filename) + task_doc = await self.repo.docs.task.get(filename=self.i_context.task_filename) + code_text = await self.get_old_codes() + context = CODE_PLAN_AND_CHANGE_CONTEXT.format( + requirement=self.i_context.requirement, + prd=prd_doc.content, + design=design_doc.content, + task=task_doc.content, + code=code_text, + ) + return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json") + + 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) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index ec56afc61..da636eb36 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -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,7 @@ 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 + 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 +146,30 @@ class WriteCodeReview(Action): self.i_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo.with_src_path(self.context.src_workspace), + use_inc=self.config.inc, ) - 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 self.config.inc: + context = "\n".join( + [ + "## System Design\n" + str(self.i_context.design_doc) + "\n", + "## Task\n" + task_content + "\n", + "## Code Files\n" + code_context + "\n", + ] + ) + else: + requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME) + code_plan_and_change_doc = await self.repo.get(filename=CODE_PLAN_AND_CHANGE_FILENAME) + context = "\n".join( + [ + "## User New Requirements\n" + str(requirement_doc) + "\n", + "## Code Plan And Change\n" + str(code_plan_and_change_doc) + "\n", + "## System Design\n" + str(self.i_context.design_doc) + "\n", + "## Task\n" + task_content + "\n", + "## Code Files\n" + code_context + "\n", + ] + ) + context_prompt = PROMPT_TEMPLATE.format( context=context, code=iterative_code, @@ -161,7 +179,7 @@ class WriteCodeReview(Action): format_example=format_example, ) len1 = len(iterative_code) if iterative_code else 0 - len2 = len(self.context.code_doc.content) if self.context.code_doc.content else 0 + len2 = len(self.i_context.code_doc.content) if self.i_context.code_doc.content else 0 logger.info( f"Code review and rewrite {self.i_context.code_doc.filename}: {i + 1}/{k} | len(iterative_code)={len1}, " f"len(self.i_context.code_doc.content)={len2}" diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index d401cc588..823786893 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -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,21 +140,21 @@ 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 async def _update_prd(self, req: Document, prd_doc: Document) -> Document: new_prd_doc: Document = await self._merge(req, prd_doc) - self.repo.docs.prd.save_doc(doc=new_prd_doc) + await self.repo.docs.prd.save_doc(doc=new_prd_doc) await self._save_competitive_analysis(new_prd_doc) await self.repo.resources.prd.save_pdf(doc=new_prd_doc) return new_prd_doc 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 diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 715e8fc55..9898be55b 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -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 original 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]) diff --git a/metagpt/config2.py b/metagpt/config2.py index 92dd98bad..5a556cc52 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -38,6 +38,7 @@ class CLIParams(BaseModel): if self.project_path: self.inc = True self.project_name = self.project_name or Path(self.project_path).name + return self class Config(CLIParams, YamlModel): diff --git a/metagpt/const.py b/metagpt/const.py index 0ae425a47..a1c650ce3 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -82,17 +82,20 @@ MESSAGE_ROUTE_TO_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" diff --git a/metagpt/context.py b/metagpt/context.py index 8e9749d66..3dfd52d58 100644 --- a/metagpt/context.py +++ b/metagpt/context.py @@ -95,7 +95,3 @@ class Context(BaseModel): if llm.cost_manager is None: llm.cost_manager = self.cost_manager return llm - - -# Global context, not in Env -CONTEXT = Context() diff --git a/metagpt/context_mixin.py b/metagpt/context_mixin.py index 1d239d2e4..bdf2d0734 100644 --- a/metagpt/context_mixin.py +++ b/metagpt/context_mixin.py @@ -10,7 +10,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict, Field from metagpt.config2 import Config -from metagpt.context import CONTEXT, Context +from metagpt.context import Context from metagpt.provider.base_llm import BaseLLM @@ -34,7 +34,7 @@ class ContextMixin(BaseModel): def __init__( self, - context: Optional[Context] = CONTEXT, + context: Optional[Context] = None, config: Optional[Config] = None, llm: Optional[BaseLLM] = None, **kwargs, @@ -81,7 +81,7 @@ class ContextMixin(BaseModel): """Role context: role context > context""" if self.private_context: return self.private_context - return CONTEXT + return Context() @context.setter def context(self, context: Context) -> None: diff --git a/metagpt/learn/skill_loader.py b/metagpt/learn/skill_loader.py index ddcd7ccba..bcf28bb87 100644 --- a/metagpt/learn/skill_loader.py +++ b/metagpt/learn/skill_loader.py @@ -13,7 +13,7 @@ import aiofiles import yaml from pydantic import BaseModel, Field -from metagpt.context import CONTEXT, Context +from metagpt.context import Context class Example(BaseModel): @@ -73,14 +73,15 @@ class SkillsDeclaration(BaseModel): skill_data = yaml.safe_load(data) return SkillsDeclaration(**skill_data) - def get_skill_list(self, entity_name: str = "Assistant", context: Context = CONTEXT) -> Dict: + def get_skill_list(self, entity_name: str = "Assistant", context: Context = None) -> Dict: """Return the skill name based on the skill description.""" entity = self.entities.get(entity_name) if not entity: return {} # List of skills that the agent chooses to activate. - agent_skills = context.kwargs.agent_skills + ctx = context or Context() + agent_skills = ctx.kwargs.agent_skills if not agent_skills: return {} diff --git a/metagpt/llm.py b/metagpt/llm.py index 30ced25d2..a3fc5613a 100644 --- a/metagpt/llm.py +++ b/metagpt/llm.py @@ -8,12 +8,13 @@ from typing import Optional from metagpt.configs.llm_config import LLMConfig -from metagpt.context import CONTEXT +from metagpt.context import Context from metagpt.provider.base_llm import BaseLLM -def LLM(llm_config: Optional[LLMConfig] = None) -> BaseLLM: +def LLM(llm_config: Optional[LLMConfig] = None, context: Context = None) -> BaseLLM: """get the default llm provider if name is None""" + ctx = context or Context() if llm_config is not None: - CONTEXT.llm_with_cost_manager_from_llm_config(llm_config) - return CONTEXT.llm() + ctx.llm_with_cost_manager_from_llm_config(llm_config) + return ctx.llm() diff --git a/metagpt/roles/assistant.py b/metagpt/roles/assistant.py index 2e9ec9bf7..2774bd9b6 100644 --- a/metagpt/roles/assistant.py +++ b/metagpt/roles/assistant.py @@ -22,7 +22,6 @@ from pydantic import Field from metagpt.actions.skill_action import ArgumentsParingAction, SkillAction from metagpt.actions.talk_action import TalkAction -from metagpt.context import CONTEXT from metagpt.learn.skill_loader import SkillsDeclaration from metagpt.logs import logger from metagpt.memory.brain_memory import BrainMemory @@ -48,7 +47,7 @@ class Assistant(Role): def __init__(self, **kwargs): super().__init__(**kwargs) - language = kwargs.get("language") or self.context.kwargs.language or CONTEXT.kwargs.language + language = kwargs.get("language") or self.context.kwargs.language self.constraints = self.constraints.format(language=language) async def think(self) -> bool: diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 7c91ec6f9..40ade2110 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -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 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,7 +98,7 @@ class Engineer(Role): @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: m = json.loads(task_msg.content) - return m.get("Task list") + return m.get(TASK_LIST.key) or m.get(REFINED_TASK_LIST.key) async def _act_sp_with_cr(self, review=False) -> Set[str]: changed_files = set() @@ -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 self.config.inc: + dependencies.add(os.path.join(CODE_PLAN_AND_CHANGE_FILE_REPO, CODE_PLAN_AND_CHANGE_FILENAME)) await self.project_repo.srcs.save( filename=coding_context.filename, - 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() @@ -161,7 +178,7 @@ class Engineer(Role): is_pass, reason = await self._is_pass(summary) if not is_pass: todo.i_context.reason = reason - tasks.append(todo.i_context.dict()) + tasks.append(todo.i_context.model_dump()) await self.project_repo.docs.code_summary.save( filename=Path(todo.i_context.design_filename).name, @@ -187,6 +204,34 @@ 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 = { + REQUIREMENT_FILENAME, + self.rc.todo.i_context.prd_filename, + self.rc.todo.i_context.design_filename, + self.rc.todo.i_context.task_filename, + } + await self.project_repo.docs.code_plan_and_change.save( + filename=self.rc.todo.i_context.filename, content=code_plan_and_change, dependencies=dependencies + ) + await self.project_repo.resources.code_plan_and_change.save( + filename=Path(self.rc.todo.i_context.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 +242,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)) @@ -292,10 +342,18 @@ class Engineer(Role): summarizations[ctx].append(filename) for ctx, filenames in summarizations.items(): ctx.codes_filenames = filenames - self.summarize_todos.append(SummarizeCode(i_context=ctx, llm=self.llm)) + self.summarize_todos.append(SummarizeCode(i_context=ctx, context=self.context, llm=self.llm)) if self.summarize_todos: self.set_todo(self.summarize_todos[0]) + async def _new_code_plan_and_change_action(self): + """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" + files = self.project_repo.all_files + requirement_doc = await self.project_repo.docs.get(REQUIREMENT_FILENAME) + requirement = requirement_doc.content if requirement_doc else "" + code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, requirement=requirement) + self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_ctx, context=self.context, llm=self.llm) + @property def action_description(self) -> str: """AgentStore uses this attribute to display to the user what actions the current role should take.""" diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 42faa0cd4..c73c10ef3 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -57,6 +57,8 @@ class QaEngineer(Role): code_doc = await src_file_repo.get(filename) if not code_doc: continue + if not code_doc.filename.endswith(".py"): + continue test_doc = await self.project_repo.tests.get("test_" + code_doc.filename) if not test_doc: test_doc = Document( diff --git a/metagpt/schema.py b/metagpt/schema.py index 05a76cc26..0d444606b 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -37,10 +37,12 @@ from pydantic import ( ) from metagpt.const import ( + CODE_PLAN_AND_CHANGE_FILENAME, MESSAGE_ROUTE_CAUSE_BY, MESSAGE_ROUTE_FROM, MESSAGE_ROUTE_TO, MESSAGE_ROUTE_TO_ALL, + PRDS_FILE_REPO, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO, ) @@ -471,6 +473,30 @@ class BugFixContext(BaseContext): filename: str = "" +class CodePlanAndChangeContext(BaseModel): + filename: str = CODE_PLAN_AND_CHANGE_FILENAME + requirement: str = "" + prd_filename: str = "" + design_filename: str = "" + task_filename: str = "" + + @staticmethod + def loads(filenames: List, **kwargs) -> CodePlanAndChangeContext: + ctx = CodePlanAndChangeContext(requirement=kwargs.get("requirement", "")) + for filename in filenames: + filename = Path(filename) + if filename.is_relative_to(PRDS_FILE_REPO): + ctx.prd_filename = filename.name + continue + if filename.is_relative_to(SYSTEM_DESIGN_FILE_REPO): + ctx.design_filename = filename.name + continue + if filename.is_relative_to(TASK_FILE_REPO): + ctx.task_filename = filename.name + continue + return ctx + + # mermaid class view class UMLClassMeta(BaseModel): name: str = "" diff --git a/metagpt/startup.py b/metagpt/startup.py index 771cde80c..000b3c5d4 100644 --- a/metagpt/startup.py +++ b/metagpt/startup.py @@ -8,6 +8,7 @@ import typer from metagpt.config2 import config from metagpt.const import CONFIG_ROOT, METAGPT_ROOT +from metagpt.context import Context app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False) @@ -37,9 +38,10 @@ def generate_repo( from metagpt.team import Team config.update_via_cli(project_path, project_name, inc, reqa_file, max_auto_summarize_code) + ctx = Context(config=config) if not recover_path: - company = Team() + company = Team(context=ctx) company.hire( [ ProductManager(), @@ -58,7 +60,7 @@ def generate_repo( if not stg_path.exists() or not str(stg_path).endswith("team"): raise FileNotFoundError(f"{recover_path} not exists or not endswith `team`") - company = Team.deserialize(stg_path=stg_path) + company = Team.deserialize(stg_path=stg_path, context=ctx) idea = company.idea company.invest(investment) diff --git a/metagpt/team.py b/metagpt/team.py index aec72970b..35f987b57 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -10,12 +10,13 @@ import warnings from pathlib import Path -from typing import Any +from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field from metagpt.actions import UserRequirement from metagpt.const import MESSAGE_ROUTE_TO_ALL, SERDESER_PATH +from metagpt.context import Context from metagpt.environment import Environment from metagpt.logs import logger from metagpt.roles import Role @@ -36,12 +37,17 @@ class Team(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - env: Environment = Field(default_factory=Environment) + env: Optional[Environment] = None investment: float = Field(default=10.0) idea: str = Field(default="") - def __init__(self, **data: Any): + def __init__(self, context: Context = None, **data: Any): super(Team, self).__init__(**data) + ctx = context or Context() + if not self.env: + self.env = Environment(context=ctx) + else: + self.env.context = ctx # The `env` object is allocated by deserialization if "roles" in data: self.hire(data["roles"]) if "env_desc" in data: @@ -54,7 +60,7 @@ class Team(BaseModel): write_json_file(team_info_path, self.model_dump()) @classmethod - def deserialize(cls, stg_path: Path) -> "Team": + def deserialize(cls, stg_path: Path, context: Context = None) -> "Team": """stg_path = ./storage/team""" # recover team_info team_info_path = stg_path.joinpath("team.json") @@ -64,7 +70,8 @@ class Team(BaseModel): ) team_info: dict = read_json_file(team_info_path) - team = Team(**team_info) + ctx = context or Context() + team = Team(**team_info, context=ctx) return team def hire(self, roles: list[Role]): diff --git a/metagpt/utils/dependency_file.py b/metagpt/utils/dependency_file.py index 7cf9a1d49..c8b3bc4a4 100644 --- a/metagpt/utils/dependency_file.py +++ b/metagpt/utils/dependency_file.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import re from pathlib import Path from typing import Set @@ -36,7 +37,9 @@ class DependencyFile: """Load dependencies from the file asynchronously.""" if not self._filename.exists(): return - self._dependencies = json.loads(await aread(self._filename)) + json_data = await aread(self._filename) + json_data = re.sub(r"\\+", "/", json_data) # Compatible with windows path + self._dependencies = json.loads(json_data) @handle_exception async def save(self): @@ -60,17 +63,20 @@ class DependencyFile: key = Path(filename).relative_to(root) except ValueError: key = filename - + skey = re.sub(r"\\+", "/", str(key)) # Compatible with windows path if dependencies: relative_paths = [] for i in dependencies: try: - relative_paths.append(str(Path(i).relative_to(root))) + s = str(Path(i).relative_to(root)) except ValueError: - relative_paths.append(str(i)) - self._dependencies[str(key)] = relative_paths - elif str(key) in self._dependencies: - del self._dependencies[str(key)] + s = str(i) + s = re.sub(r"\\+", "/", s) # Compatible with windows path + relative_paths.append(s) + + self._dependencies[skey] = relative_paths + elif skey in self._dependencies: + del self._dependencies[skey] if persist: await self.save() diff --git a/metagpt/utils/file_repository.py b/metagpt/utils/file_repository.py index 94d6fe76d..d2a06963a 100644 --- a/metagpt/utils/file_repository.py +++ b/metagpt/utils/file_repository.py @@ -101,21 +101,28 @@ class FileRepository: path_name = self.workdir / filename if not path_name.exists(): return None + if not path_name.is_file(): + return None doc.content = await aread(path_name) return doc - async def get_all(self) -> List[Document]: + async def get_all(self, filter_ignored=True) -> List[Document]: """Get the content of all files in the repository. :return: List of Document instances representing files. """ docs = [] - for root, dirs, files in os.walk(str(self.workdir)): - for file in files: - file_path = Path(root) / file - relative_path = file_path.relative_to(self.workdir) - doc = await self.get(relative_path) + if filter_ignored: + for f in self.all_files: + doc = await self.get(f) docs.append(doc) + else: + for root, dirs, files in os.walk(str(self.workdir)): + for file in files: + file_path = Path(root) / file + relative_path = file_path.relative_to(self.workdir) + doc = await self.get(relative_path) + docs.append(doc) return docs @property diff --git a/metagpt/utils/project_repo.py b/metagpt/utils/project_repo.py index 77ac4f897..72bca7ea0 100644 --- a/metagpt/utils/project_repo.py +++ b/metagpt/utils/project_repo.py @@ -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): diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 885eb37d7..94506e373 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -4,10 +4,11 @@ @Time : 2023/5/18 00:40 @Author : alexanderwu @File : token_counter.py -ref1: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb -ref2: https://github.com/Significant-Gravitas/Auto-GPT/blob/master/autogpt/llm/token_counter.py -ref3: https://github.com/hwchase17/langchain/blob/master/langchain/chat_models/openai.py -ref4: https://ai.google.dev/models/gemini +ref1: https://openai.com/pricing +ref2: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb +ref3: https://github.com/Significant-Gravitas/Auto-GPT/blob/master/autogpt/llm/token_counter.py +ref4: https://github.com/hwchase17/langchain/blob/master/langchain/chat_models/openai.py +ref5: https://ai.google.dev/models/gemini """ import tiktoken @@ -25,7 +26,10 @@ TOKEN_COSTS = { "gpt-4-32k": {"prompt": 0.06, "completion": 0.12}, "gpt-4-32k-0314": {"prompt": 0.06, "completion": 0.12}, "gpt-4-0613": {"prompt": 0.06, "completion": 0.12}, + "gpt-4-turbo-preview": {"prompt": 0.01, "completion": 0.03}, + "gpt-4-0125-preview": {"prompt": 0.01, "completion": 0.03}, "gpt-4-1106-preview": {"prompt": 0.01, "completion": 0.03}, + "gpt-4-1106-vision-preview": {"prompt": 0.01, "completion": 0.03}, "text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0}, "glm-3-turbo": {"prompt": 0.0, "completion": 0.0007}, # 128k version, prompt + completion tokens=0.005¥/k-tokens "glm-4": {"prompt": 0.0, "completion": 0.014}, # 128k version, prompt + completion tokens=0.1¥/k-tokens @@ -47,7 +51,10 @@ TOKEN_MAX = { "gpt-4-32k": 32768, "gpt-4-32k-0314": 32768, "gpt-4-0613": 8192, + "gpt-4-turbo-preview": 128000, + "gpt-4-0125-preview": 128000, "gpt-4-1106-preview": 128000, + "gpt-4-1106-vision-preview": 128000, "text-embedding-ada-002": 8192, "chatglm_turbo": 32768, "gemini-pro": 32768, @@ -72,7 +79,10 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0613"): "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613", + "gpt-4-turbo-preview", + "gpt-4-0125-preview", "gpt-4-1106-preview", + "gpt-4-1106-vision-preview", }: tokens_per_message = 3 # # every reply is primed with <|start|>assistant<|message|> tokens_per_name = 1 diff --git a/setup.py b/setup.py index b3fd62178..d1445e3f8 100644 --- a/setup.py +++ b/setup.py @@ -46,8 +46,8 @@ extras_require["test"] = [ "chromadb==0.4.14", "gradio==3.0.0", "grpcio-status==1.48.2", - "mock==5.1.0", "pylint==3.0.3", + "pybrowsers", ] extras_require["pyppeteer"] = [ diff --git a/tests/data/audio/hello.mp3 b/tests/data/audio/hello.mp3 new file mode 100644 index 000000000..7b3aab0a4 Binary files /dev/null and b/tests/data/audio/hello.mp3 differ diff --git a/tests/data/incremental_dev_project/Gomoku.zip b/tests/data/incremental_dev_project/Gomoku.zip new file mode 100644 index 000000000..23649565a Binary files /dev/null and b/tests/data/incremental_dev_project/Gomoku.zip differ diff --git a/tests/data/incremental_dev_project/dice_simulator_new.zip b/tests/data/incremental_dev_project/dice_simulator_new.zip new file mode 100644 index 000000000..4752ab4c5 Binary files /dev/null and b/tests/data/incremental_dev_project/dice_simulator_new.zip differ diff --git a/tests/data/incremental_dev_project/mock.py b/tests/data/incremental_dev_project/mock.py new file mode 100644 index 000000000..f2eb71359 --- /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": "", +} + +CODE_PLAN_AND_CHANGE_SAMPLE = { + "Code Plan And Change": '\n1. Plan for gui.py: Develop the GUI using Tkinter to replace the command-line interface. Start by setting up the main window and event handling. Then, add widgets for displaying the game status, results, and feedback. Implement interactive elements for difficulty selection and visualize the guess history. Finally, create animations for guess feedback and ensure responsiveness across different screen sizes.\n```python\nclass GUI:\n- pass\n+ def __init__(self):\n+ self.setup_window()\n+\n+ def setup_window(self):\n+ # Initialize the main window using Tkinter\n+ pass\n+\n+ def bind_events(self):\n+ # Bind button clicks and other events\n+ pass\n+\n+ def update_feedback(self, message: str):\n+ # Update the feedback label with the given message\n+ pass\n+\n+ def update_attempts(self, attempts: int):\n+ # Update the attempts label with the number of attempts\n+ pass\n+\n+ def update_history(self, history: list):\n+ # Update the history view with the list of past guesses\n+ pass\n+\n+ def show_difficulty_selector(self):\n+ # Show buttons or a dropdown for difficulty selection\n+ pass\n+\n+ def animate_guess_result(self, correct: bool):\n+ # Trigger an animation for correct or incorrect guesses\n+ pass\n```\n\n2. Plan for main.py: Modify the main.py to initialize the GUI and start the event-driven game loop. Ensure that the GUI is the primary interface for user interaction.\n```python\nclass Main:\n def main(self):\n- user_interface = UI()\n- user_interface.start()\n+ graphical_user_interface = GUI()\n+ graphical_user_interface.setup_window()\n+ graphical_user_interface.bind_events()\n+ # Start the Tkinter main loop\n+ pass\n\n if __name__ == "__main__":\n main_instance = Main()\n main_instance.main()\n```\n\n3. Plan for ui.py: Refactor ui.py to work with the new GUI class. Remove command-line interactions and delegate display and input tasks to the GUI.\n```python\nclass UI:\n- def display_message(self, message: str):\n- print(message)\n+\n+ def display_message(self, message: str):\n+ # This method will now pass the message to the GUI to display\n+ pass\n\n- def get_user_input(self, prompt: str) -> str:\n- return input(prompt)\n+\n+ def get_user_input(self, prompt: str) -> str:\n+ # This method will now trigger the GUI to get user input\n+ pass\n\n- def show_attempts(self, attempts: int):\n- print(f"Number of attempts: {attempts}")\n+\n+ def show_attempts(self, attempts: int):\n+ # This method will now update the GUI with the number of attempts\n+ pass\n\n- def show_history(self, history: list):\n- print("Guess history:")\n- for guess in history:\n- print(guess)\n+\n+ def show_history(self, history: list):\n+ # This method will now update the GUI with the guess history\n+ pass\n```\n\n4. Plan for game.py: Ensure game.py remains mostly unchanged as it contains the core game logic. However, make minor adjustments if necessary to integrate with the new GUI.\n```python\nclass Game:\n # No changes required for now\n```\n' +} + +REFINED_CODE_INPUT_SAMPLE = """ +-----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/data/incremental_dev_project/number_guessing_game.zip b/tests/data/incremental_dev_project/number_guessing_game.zip new file mode 100644 index 000000000..7bbe07713 Binary files /dev/null and b/tests/data/incremental_dev_project/number_guessing_game.zip differ diff --git a/tests/data/incremental_dev_project/pygame_2048.zip b/tests/data/incremental_dev_project/pygame_2048.zip new file mode 100644 index 000000000..93e9cf0fe Binary files /dev/null and b/tests/data/incremental_dev_project/pygame_2048.zip differ diff --git a/tests/data/incremental_dev_project/readme.md b/tests/data/incremental_dev_project/readme.md new file mode 100644 index 000000000..231589028 --- /dev/null +++ b/tests/data/incremental_dev_project/readme.md @@ -0,0 +1,3 @@ +# Code archive + +This folder contains a compressed package for the test_incremental_dev.py file, which is used to demonstrate the process of incremental development. diff --git a/tests/data/incremental_dev_project/simple_add_calculator.zip b/tests/data/incremental_dev_project/simple_add_calculator.zip new file mode 100644 index 000000000..e6e73f8f9 Binary files /dev/null and b/tests/data/incremental_dev_project/simple_add_calculator.zip differ diff --git a/tests/data/incremental_dev_project/snake_game.zip b/tests/data/incremental_dev_project/snake_game.zip new file mode 100644 index 000000000..997203a18 Binary files /dev/null and b/tests/data/incremental_dev_project/snake_game.zip differ diff --git a/tests/data/incremental_dev_project/word_cloud.zip b/tests/data/incremental_dev_project/word_cloud.zip new file mode 100644 index 000000000..d8747d14d Binary files /dev/null and b/tests/data/incremental_dev_project/word_cloud.zip differ diff --git a/tests/data/rsp_cache.json b/tests/data/rsp_cache.json index 9ebe50a3c..92a600b67 100644 --- a/tests/data/rsp_cache.json +++ b/tests/data/rsp_cache.json @@ -261,5 +261,21 @@ "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n10 12\n": "It seems like you have evaluated the combinations correctly. Based on the evaluations you provided, the combinations that can reach 24 are:\n\n- 10 14 (sure)\n- 4 4 10 (sure)\n- 4 9 11 (sure)\n- 5 7 8 (likely)\n- 5 6 6 (likely)\n\nThe combinations that cannot reach 24 are:\n\n- 11 12 (impossible)\n- 10 10 11 (impossible)\n- 1 3 3 (impossible)\n\nAs for the combination \"10 12,\" it seems like it was cut off. If you provide the complete expression, I can help evaluate it for you.", "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n150\n": "It seems like you have evaluated the given numbers correctly. Here are the results:\n\n10 14: Sure\n11 12: Impossible\n4 4 10: Sure\n4 9 11: Sure\n5 7 8: Likely\n5 6 6: Likely\n10 10 11: Impossible\n1 3 3: Impossible\n\nAs for the number 150, it is not clear what operation or combination of numbers it is associated with. If you have a specific question or context for the number 150, please provide more details so that I can assist you further.", "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n5\n": "The numbers 10 and 14 can reach 24, so that is sure. The numbers 11 and 12 cannot reach 24, so that is impossible. The numbers 4, 4, and 10 can reach 24, so that is sure. The numbers 4, 9, and 11 can reach 24, so that is sure. The numbers 5, 7, and 8 cannot reach 24, but they are within a reasonable range, so that is likely. The numbers 5, 6, and 6 cannot reach 24, but they are within a reasonable range, so that is likely. The numbers 10, 10, and 11 cannot reach 24, so that is impossible. The numbers 1, 3, and 3 cannot reach 24, so that is impossible. The number 5 by itself cannot reach 24.", - "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n7 9 10\n": "7 * 9 - 10 = 63 - 10 = 53\n(9 - 7) * 10 = 2 * 10 = 20\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely" + "Evaluate if given numbers can reach 24 (sure/likely/impossible)\n10 14\n10 + 14 = 24\nsure\n11 12\n11 + 12 = 23\n12 - 11 = 1\n11 * 12 = 132\n11 / 12 = 0.91\nimpossible\n4 4 10\n4 + 4 + 10 = 8 + 10 = 18\n4 * 10 - 4 = 40 - 4 = 36\n(10 - 4) * 4 = 6 * 4 = 24\nsure\n4 9 11\n9 + 11 + 4 = 20 + 4 = 24\nsure\n5 7 8\n5 + 7 + 8 = 12 + 8 = 20\n(8 - 5) * 7 = 3 * 7 = 21\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n5 6 6\n5 + 6 + 6 = 17\n(6 - 5) * 6 = 1 * 6 = 6\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely\n10 10 11\n10 + 10 + 11 = 31\n(11 - 10) * 10 = 10\n10 10 10 are all too big\nimpossible\n1 3 3\n1 * 3 * 3 = 9\n(1 + 3) * 3 = 12\n1 3 3 are all too small\nimpossible\n7 9 10\n": "7 * 9 - 10 = 63 - 10 = 53\n(9 - 7) * 10 = 2 * 10 = 20\nI cannot obtain 24 now, but numbers are within a reasonable range\nlikely", + "### Requirements\n1. Add docstrings to the given code following the google style.\n2. Replace the function body with an Ellipsis object(...) to reduce output.\n3. If the types are already annotated, there is no need to include them in the docstring.\n4. Extract only class, function or the docstrings for the module parts from the given Python code, avoiding any other text.\n\n### Input Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n return isinstance(param1, int)\n\nclass ExampleError(Exception):\n def __init__(self, msg: str):\n self.msg = msg\n```\n\n### Output Example\n```python\ndef function_with_pep484_type_annotations(param1: int) -> bool:\n \"\"\"Example function with PEP 484 type annotations.\n\n Extended description of function.\n\n Args:\n param1: The first parameter.\n\n Returns:\n The return value. True for success, False otherwise.\n \"\"\"\n ...\n\nclass ExampleError(Exception):\n \"\"\"Exceptions are documented in the same way as classes.\n\n The __init__ method was documented in the class level docstring.\n\n Args:\n msg: Human readable string describing the exception.\n\n Attributes:\n msg: Human readable string describing the exception.\n \"\"\"\n ...\n```\n#SYSTEM_MSG_END#```python\nimport pytest\nfrom metagpt.actions.write_docstring import WriteDocstring\ncode = '\\ndef add_numbers(a: int, b: int):\\n return a + b\\n\\n\\nclass Person:\\n def __init__(self, name: str, age: int):\\n self.name = name\\n self.age = age\\n\\n def greet(self):\\n return f\"Hello, my name is {self.name} and I am {self.age} years old.\"\\n'\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(('style', 'part'), [('google', 'Args:'), ('numpy', 'Parameters'), ('sphinx', ':param name:')], ids=['google', 'numpy', 'sphinx'])\nasync def test_write_docstring(style: str, part: str, context):\n ret = await WriteDocstring(context=context).run(code, style=style)\n assert part in ret\n\n@pytest.mark.asyncio\nasync def test_write():\n code = await WriteDocstring.write_docstring(__file__)\n assert code\n```": "```python\ndef add_numbers(a: int, b: int):\n \"\"\"Adds two numbers together.\n\n Args:\n a: The first number.\n b: The second number.\n\n Returns:\n The sum of a and b.\n \"\"\"\n ...\n\nclass Person:\n \"\"\"Represents a person.\n\n Args:\n name: The name of the person.\n age: The age of the person.\n\n Attributes:\n name: The name of the person.\n age: The age of the person.\n \"\"\"\n\n def __init__(self, name: str, age: int):\n ...\n \n def greet(self):\n \"\"\"Generates a greeting message.\n\n Returns:\n A greeting string.\n \"\"\"\n ...\n```", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: It isn't difficult to do a handstand if you just stand on your hands. It caught him off guard that space smelled of seared steak. When she didn’t like a guy who was trying to pick her up, she started using sign language. Each person who knows you has a different perception of who you are.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Introduce the concept of doing a handstand.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Describe the surprising smell of space.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Introduce a woman using sign language to deter unwanted attention.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Discuss the varied perceptions people have of an individual.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"It isn't difficult to do a handstand if you just stand on your hands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"It caught him off guard that space smelled of seared steak.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"When she didn’t like a guy who was trying to pick her up, she started using sign language.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Each person who knows you has a different perception of who you are.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nDiscuss the varied perceptions people have of an individual.\n": "Choice 4: Discuss the varied perceptions people have of an individual.\n\nAnalyzing this choice, discussing the varied perceptions people have of an individual can be promising as it allows for a deeper understanding of how different people perceive and interpret the actions, behaviors, and characteristics of an individual. This can lead to insights into the complexities of human interactions and relationships, and can provide valuable information for personal growth and self-awareness.\n\nBy exploring the reasons behind differing perceptions, it can also lead to increased empathy and understanding of others, which is beneficial for building stronger relationships and effective communication.\n\nFurthermore, discussing varied perceptions can also shed light on societal influences, biases, and stereotypes that may impact how individuals are perceived, which can be valuable for promoting inclusivity and diversity.\n\nThe best choice is 4.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nIntroduce a woman using sign language to deter unwanted attention.\n": "Choice 3: Introduce a woman using sign language to deter unwanted attention.\n\nAnalysis:\nIntroducing a woman using sign language can be a promising choice to deter unwanted attention. Sign language is a visual and expressive form of communication, and it can be an effective way to convey a message without drawing unnecessary attention. By using sign language, the woman can communicate with others discreetly and avoid attracting unwanted attention from individuals who may be seeking to engage her in conversation or interaction.\n\nAdditionally, using sign language can create a sense of privacy and exclusivity, as it may not be easily understood by everyone in the vicinity. This can help the woman maintain a level of personal space and discourage unwelcome advances or interactions.\n\nFurthermore, introducing sign language into the interaction can also serve as a way to promote inclusivity and awareness of different forms of communication, which can contribute to a more understanding and respectful environment.\n\nConclusion:\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 1:\nIntroduce the concept of doing a handstand.\n": "Choice 1: Introduce the concept of doing a handstand.\n\nAnalyzing the choice:\nIntroducing the concept of doing a handstand can be a fun and engaging way to promote physical activity and coordination. It can also help build strength and balance. However, it's important to ensure that proper safety measures and instructions are provided to prevent any potential injuries.\n\nConclusion:\nThe best choice is 1.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nDescribe the surprising smell of space.\n": "Choice 2: \n\nThe surprising smell of space has been described by astronauts as a mix of gunpowder, seared steak, hot metal, and raspberries. This unexpected combination of scents is due to the volatile organic compounds that are released from the materials in the spacecraft when they are exposed to the vacuum of space. These compounds react with the radiation from the sun, creating the unique smell that astronauts have reported. The smell of space is intriguing and unexpected, providing a sensory experience that is truly out of this world.\n\nThe best choice is 2.", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: Introduce the concept of doing a handstand.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Warm up your body with some stretching exercises.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Practice against a wall to build strength and balance.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Gradually work on holding the handstand position without support.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Before attempting a handstand, it's important to decide on a suitable location to practice. A clear, open space with a soft surface, such as a yoga mat or grass, is ideal for safety and comfort.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Warm up your body with some stretching exercises.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"To prepare your body for the physical demands of a handstand, start with a series of stretching exercises. Focus on the wrists, shoulders, and core muscles to ensure they are adequately warmed up and flexible.\"\n },\n {\n \"node_id\": \"5\",\n \"node_state_instruction\": \"Practice against a wall to build strength and balance.\"\n },\n {\n \"node_id\": \"6\",\n \"node_state_instruction\": \"When learning to do a handstand, it can be helpful to practice against a wall. This provides support and allows you to focus on building strength and balance in the proper alignment. Gradually work on kicking up into the handstand position and holding it for short periods of time.\"\n },\n {\n \"node_id\": \"7\",\n \"node_state_instruction\": \"Gradually work on holding the handstand position without support.\"\n },\n {\n \"node_id\": \"8\",\n \"node_state_instruction\": \"As you gain confidence and strength, begin to practice holding the handstand position without the support of a wall. Focus on engaging your core and maintaining a straight line from your wrists to your hips. With consistent practice, you'll be able to hold a handstand with control and stability.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 1:\nDecide on a suitable location to practice handstands.\n": "Choice 1: Find a spacious and flat area with soft ground, such as a grassy field or a padded gym floor. This will minimize the risk of injury and provide a stable surface for practicing handstands.\n\nThe best choice is 1", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nWarm up your body with some stretching exercises.\n": "Stretching exercises are a great way to warm up the body before engaging in physical activity. They help to increase blood flow to the muscles, improve flexibility, and reduce the risk of injury. By performing stretching exercises, you can prepare your body for more strenuous activities and improve your overall performance.\n\nThe best choice is 2.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nPractice against a wall to build strength and balance.\n": "Choice 3: Practice against a wall to build strength and balance.\n\nAnalyzing choice 3, practicing against a wall can be a promising option for building strength and balance. The wall provides a stable surface to push against, which can help in developing strength in the muscles. It also offers support, allowing for balance practice without the risk of falling. Additionally, using a wall as a tool for practice can help in improving posture and body alignment.\n\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nGradually work on holding the handstand position without support.\n": "Choice 4: Gradually work on holding the handstand position without support.\n\nAnalyzing this choice, working on holding the handstand position without support is a promising option for improving strength, balance, and body control. This choice allows for gradual progression, which is important for building the necessary strength and stability to maintain the handstand position. It also encourages the development of proprioception and spatial awareness, which are essential for mastering this challenging skill.\n\nThe best choice is 4.", + "\nWrite a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: Decide on a suitable location to practice handstands.\n\nMake a plan then write. Your output should be like:\n\nPlan:\n\n\nPassage:\n\n\n\nEach output should be strictly a list of nodes, in json format, like this:\n```json\n [\n {\n \"node_id\": str = \"unique identifier for a solution, can be an ordinal\",\n \"node_state_instruction\": \"specified sample of solution\",\n },\n ...\n ]\n```\n": "Plan:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```\n\nPassage:\n```json\n[\n {\n \"node_id\": \"1\",\n \"node_state_instruction\": \"Decide on a suitable location to practice handstands.\"\n },\n {\n \"node_id\": \"2\",\n \"node_state_instruction\": \"Consider a flat and stable surface, such as a grassy field or a gym mat.\"\n },\n {\n \"node_id\": \"3\",\n \"node_state_instruction\": \"Ensure there is enough space around you to avoid any obstacles.\"\n },\n {\n \"node_id\": \"4\",\n \"node_state_instruction\": \"Practice near a wall or with a spotter for safety.\"\n }\n]\n```", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 3:\nEnsure there is enough space around you to avoid any obstacles.\n": "Choice 3: \n\nAnalyzing this choice, ensuring there is enough space around you to avoid any obstacles is a crucial step in maintaining safety and preventing accidents. By creating a buffer zone, you can react to unexpected obstacles or changes in the environment without risking collisions or other hazards.\n\nThe best choice is 3.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 2:\nConsider a flat and stable surface, such as a grassy field or a gym mat.\n": "Choice 2: Considering a flat and stable surface, such as a grassy field or a gym mat, is the most promising option. This choice provides a safe and secure environment for the activity, reducing the risk of injury. A grassy field offers a natural and soft surface, while a gym mat provides cushioning and stability. Both options allow for comfortable movement and can accommodate various physical activities.\n\nThe best choice is 2.", + "Given an instruction and several choices, decide which choice is most promising. Analyze each choice in detail, then conclude in the last line \"The best choice is {s}\", where s the integer id of the choice.\nChoice 4:\nPractice near a wall or with a spotter for safety.\n": "Choice 4: \n\nPractice near a wall or with a spotter for safety.\n\nAnalyzing this choice, practicing near a wall or with a spotter provides a safety measure to prevent accidents or injuries. When practicing a new skill or exercise, having a wall nearby can provide support and stability, reducing the risk of falling or losing balance. Similarly, having a spotter can offer assistance and guidance, ensuring that the practice is done safely and effectively.\n\nThe best choice is 4.", + "### Requirements\n1. Please summarize the latest dialogue based on the reference information (secondary) and dialogue history (primary). Do not include text that is irrelevant to the conversation.\n- The context is for reference only. If it is irrelevant to the user's search request history, please reduce its reference and usage.\n2. If there are citable links in the context, annotate them in the main text in the format [main text](citation link). If there are none in the context, do not write links.\n3. The reply should be graceful, clear, non-repetitive, smoothly written, and of moderate length, in {LANG}.\n\n### Dialogue History (For example)\nA: MLOps competitors\n\n### Current Question (For example)\nA: MLOps competitors\n\n### Current Reply (For example)\n1. Alteryx Designer: etc. if any\n2. Matlab: ditto\n3. IBM SPSS Statistics\n4. RapidMiner Studio\n5. DataRobot AI Platform\n6. Databricks Lakehouse Platform\n7. Amazon SageMaker\n8. Dataiku\n#SYSTEM_MSG_END#\n### Reference Information\nABC cleanser is preferred by many with oily skin.\nL'Oreal is a popular brand with many positive reviews.\n\n### Dialogue History\n\nuser: Which facial cleanser is good for oily skin?\n\n### Current Question\nuser: Which facial cleanser is good for oily skin?\n\n### Current Reply: Based on the information, please write the reply to the Question\n\n\n": "Based on the information provided, ABC cleanser is preferred by many with oily skin. It is a popular choice for individuals with oily skin due to its effectiveness. Additionally, L'Oreal is a well-known brand with many positive reviews, and they offer a range of products suitable for oily skin. Both of these options could be good choices for individuals with oily skin." } \ No newline at end of file diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py new file mode 100644 index 000000000..3d11f200d --- /dev/null +++ b/tests/metagpt/actions/test_design_api_an.py @@ -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() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py new file mode 100644 index 000000000..ddbb56569 --- /dev/null +++ b/tests/metagpt/actions/test_project_management_an.py @@ -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_task=TASKS_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) + node = await REFINED_PM_NODE.fill(prompt, llm) + + assert "Refined Logic Analysis" in node.instruct_content.model_dump() + assert "Refined Task list" in node.instruct_content.model_dump() + assert "Refined Shared Knowledge" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_code_plan_and_change_an.py b/tests/metagpt/actions/test_write_code_plan_and_change_an.py new file mode 100644 index 000000000..9cd51398f --- /dev/null +++ b/tests/metagpt/actions/test_write_code_plan_and_change_an.py @@ -0,0 +1,71 @@ +#!/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 +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 = "New requirement" + prd_filename = "prd.md" + design_filename = "design.md" + task_filename = "task.md" + code_plan_and_change_context = CodePlanAndChangeContext( + requirement=requirement, + prd_filename=prd_filename, + design_filename=design_filename, + task_filename=task_filename, + ) + node = await WriteCodePlanAndChange(i_context=code_plan_and_change_context).run() + + assert "Code Plan And Change" in node.instruct_content.model_dump() + + +@pytest.mark.asyncio +async def test_refine_code(mocker): + mocker.patch.object(WriteCode, "_aask", return_value=REFINED_CODE_SAMPLE) + prompt = REFINED_TEMPLATE.format( + user_requirement=NEW_REQUIREMENT_SAMPLE, + code_plan_and_change=CODE_PLAN_AND_CHANGE_SAMPLE, + design=DESIGN_SAMPLE, + task=TASKS_SAMPLE, + code=REFINED_CODE_INPUT_SAMPLE, + logs="", + feedback="", + filename="game.py", + summary_log="", + ) + code = await WriteCode().write_code(prompt=prompt) + assert "def" in code diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py new file mode 100644 index 000000000..378ce42c3 --- /dev/null +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -0,0 +1,48 @@ +#!/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, + ) + 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() diff --git a/tests/metagpt/provider/test_zhipuai_api.py b/tests/metagpt/provider/test_zhipuai_api.py index ad2ececa2..798209710 100644 --- a/tests/metagpt/provider/test_zhipuai_api.py +++ b/tests/metagpt/provider/test_zhipuai_api.py @@ -17,7 +17,7 @@ default_resp = { } -async def mock_zhipuai_acreate_stream(self, **kwargs): +async def mock_zhipuai_acreate_stream(**kwargs): class MockResponse(object): async def _aread(self): class Iterator(object): @@ -37,7 +37,7 @@ async def mock_zhipuai_acreate_stream(self, **kwargs): return MockResponse() -async def mock_zhipuai_acreate(self, **kwargs) -> dict: +async def mock_zhipuai_acreate(**kwargs) -> dict: return default_resp diff --git a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py index abaafb402..15673c51c 100644 --- a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py +++ b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py @@ -6,8 +6,6 @@ from typing import Any, Tuple import pytest import zhipuai -from zhipuai.model_api.api import InvokeType -from zhipuai.utils.http_client import headers as zhipuai_default_headers from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI @@ -23,14 +21,7 @@ async def mock_requestor_arequest(self, **kwargs) -> Tuple[Any, Any, str]: @pytest.mark.asyncio async def test_zhipu_model_api(mocker): - header = ZhiPuModelAPI.get_header() - zhipuai_default_headers.update({"Authorization": api_key}) - assert header == zhipuai_default_headers - - ZhiPuModelAPI.get_sse_header() - # assert len(sse_header["Authorization"]) == 191 - - url_prefix, url_suffix = ZhiPuModelAPI.split_zhipu_api_url(InvokeType.SYNC, kwargs={"model": "chatglm_turbo"}) + url_prefix, url_suffix = ZhiPuModelAPI(api_key=api_key).split_zhipu_api_url() assert url_prefix == "https://open.bigmodel.cn/api" assert url_suffix == "/paas/v4/chat/completions" diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index 383d28096..d263a8a2f 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -99,7 +99,7 @@ def test_parse_code(): def test_todo(): role = Engineer() - assert role.todo == any_to_name(WriteCode) + assert role.action_description == any_to_name(WriteCode) @pytest.mark.asyncio diff --git a/tests/metagpt/serialize_deserialize/test_team.py b/tests/metagpt/serialize_deserialize/test_team.py index d45c8cf21..dbd38422d 100644 --- a/tests/metagpt/serialize_deserialize/test_team.py +++ b/tests/metagpt/serialize_deserialize/test_team.py @@ -144,3 +144,7 @@ async def test_team_recover_multi_roles_save(mocker, context): assert new_company.env.get_role(role_b.profile).rc.state == 1 await new_company.run(n_round=4) + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/test_context.py b/tests/metagpt/test_context.py index d90d0b686..f8218c44d 100644 --- a/tests/metagpt/test_context.py +++ b/tests/metagpt/test_context.py @@ -6,7 +6,7 @@ @File : test_context.py """ from metagpt.configs.llm_config import LLMType -from metagpt.context import CONTEXT, AttrDict, Context +from metagpt.context import AttrDict, Context def test_attr_dict_1(): @@ -51,11 +51,12 @@ def test_context_1(): def test_context_2(): - llm = CONTEXT.config.get_openai_llm() + ctx = Context() + llm = ctx.config.get_openai_llm() assert llm is not None assert llm.api_type == LLMType.OPENAI - kwargs = CONTEXT.kwargs + kwargs = ctx.kwargs assert kwargs is not None kwargs.test_key = "test_value" diff --git a/tests/metagpt/test_context_mixin.py b/tests/metagpt/test_context_mixin.py index 1ef0e4832..4389dc251 100644 --- a/tests/metagpt/test_context_mixin.py +++ b/tests/metagpt/test_context_mixin.py @@ -109,6 +109,7 @@ async def test_config_priority(): if not home_dir.exists(): assert gpt4t is None gpt35 = Config.default() + gpt35.llm.model = "gpt-3.5-turbo-1106" gpt4 = Config.default() gpt4.llm.model = "gpt-4-0613" diff --git a/tests/metagpt/test_environment.py b/tests/metagpt/test_environment.py index 10839a2a5..7559655d3 100644 --- a/tests/metagpt/test_environment.py +++ b/tests/metagpt/test_environment.py @@ -11,7 +11,6 @@ from pathlib import Path import pytest from metagpt.actions import UserRequirement -from metagpt.context import CONTEXT from metagpt.environment import Environment from metagpt.logs import logger from metagpt.roles import Architect, ProductManager, Role @@ -44,9 +43,9 @@ def test_get_roles(env: Environment): @pytest.mark.asyncio async def test_publish_and_process_message(env: Environment): - if CONTEXT.git_repo: - CONTEXT.git_repo.delete_repository() - CONTEXT.git_repo = None + if env.context.git_repo: + env.context.git_repo.delete_repository() + env.context.git_repo = None product_manager = ProductManager(name="Alice", profile="Product Manager", goal="做AI Native产品", constraints="资源有限") architect = Architect( diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py new file mode 100644 index 000000000..3e4a1b901 --- /dev/null +++ b/tests/metagpt/test_incremental_dev.py @@ -0,0 +1,189 @@ +#!/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 snake’s speed remains constant throughout the gameplay. Implement a feature where the snake’s speed gradually increases over time, making the game more challenging and intense as the player progresses.", + "Introduce power-ups and obstacles to the game. The current version of the game only involves eating food and growing the snake. Add new elements such as power-ups that can enhance the snake’s speed or make it invincible for a short duration. At the same time, introduce obstacles like walls or enemies that the snake must avoid or overcome to continue growing.", +] + +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) + + +@pytest.mark.skip +def test_number_guessing_game(): + result = get_incremental_dev_result(IDEAS[1], PROJECT_NAMES[1]) + log_and_check_result(result) + + +@pytest.mark.skip +def test_word_cloud(): + result = get_incremental_dev_result(IDEAS[2], PROJECT_NAMES[2]) + log_and_check_result(result) + + +@pytest.mark.skip +def test_gomoku(): + result = get_incremental_dev_result(IDEAS[3], PROJECT_NAMES[3]) + log_and_check_result(result) + + +@pytest.mark.skip +def test_dice_simulator_new(): + for i, (idea, project_name) in enumerate(zip(IDEAS[4:6], PROJECT_NAMES[4:6]), start=1): + result = get_incremental_dev_result(idea, project_name) + log_and_check_result(result, "refine_" + str(i)) + + +@pytest.mark.skip +def test_refined_pygame_2048(): + for i, (idea, project_name) in enumerate(zip(IDEAS[6:8], PROJECT_NAMES[6:8]), start=1): + result = get_incremental_dev_result(idea, project_name) + log_and_check_result(result, "refine_" + str(i)) + + +@pytest.mark.skip +def test_refined_snake_game(): + for i, (idea, project_name) in enumerate(zip(IDEAS[8:10], PROJECT_NAMES[8:10]), start=1): + result = get_incremental_dev_result(idea, project_name) + 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 + # Check if the project path exists + if not project_path.exists(): + # If the project does not exist, extract the project file + try: + # Use the tar command to extract the .zip file + subprocess.run(["tar", "-xf", f"{project_path}.zip", "-C", str(project_path.parent)], check=True) + except subprocess.CalledProcessError as e: + # If the extraction fails, throw an exception + raise Exception(f"Failed to extract project {project_name}. Error: {e}") + + check_or_create_base_tag(project_path) + args = [idea, "--inc", "--project-path", project_path, "--n-round", "20"] + if not use_review: + 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", "."] + try: + subprocess.run(add_cmd, check=True) + logger.info("Files added successfully.") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to add files: {e}") + + commit_cmd = ["git", "commit", "-m", "Initial commit"] + try: + subprocess.run(commit_cmd, check=True) + logger.info("Committed all files with the message 'Initial commit'.") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to commit: {e.stderr}") + + # Add 'base' tag + add_base_tag_cmd = ["git", "tag", "base"] + + # 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"]) diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 1b843795c..7e707803b 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -131,7 +131,7 @@ async def test_recover(): role.recovered = True role.latest_observed_msg = Message(content="recover_test") role.rc.state = 0 - assert role.first_action == any_to_name(MockAction) + assert role.action_description == any_to_name(MockAction) rsp = await role.run() assert rsp.cause_by == any_to_str(MockAction) diff --git a/tests/metagpt/utils/test_redis.py b/tests/metagpt/utils/test_redis.py index 748c44f54..6fd4250a6 100644 --- a/tests/metagpt/utils/test_redis.py +++ b/tests/metagpt/utils/test_redis.py @@ -9,23 +9,25 @@ from unittest.mock import AsyncMock import pytest -from metagpt.config2 import Config from metagpt.utils.redis import Redis -async def async_mock_from_url(*args, **kwargs): - mock_client = AsyncMock() - mock_client.set.return_value = None - mock_client.get.side_effect = [b"test", b""] - return mock_client - - @pytest.mark.asyncio async def test_redis(mocker): - redis = Config.default().redis - mocker.patch("aioredis.from_url", return_value=async_mock_from_url()) + async def async_mock_from_url(*args, **kwargs): + mock_client = AsyncMock() + mock_client.set.return_value = None + mock_client.get.return_value = b"test" + return mock_client - conn = Redis(redis) + mocker.patch("aioredis.from_url", return_value=async_mock_from_url()) + mock_config = mocker.Mock() + mock_config.to_url.return_value = "http://mock.com" + mock_config.username = "mockusername" + mock_config.password = "mockpwd" + mock_config.db = "0" + + conn = Redis(mock_config) await conn.set("test", "test", timeout_sec=0) assert await conn.get("test") == b"test" await conn.close() diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index 4dc3b1e42..b26ebe94d 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -8,8 +8,8 @@ import uuid from pathlib import Path +import aioboto3 import aiofiles -import mock import pytest from metagpt.config2 import Config @@ -18,21 +18,18 @@ from metagpt.utils.s3 import S3 @pytest.mark.asyncio -@mock.patch("aioboto3.Session") -async def test_s3(mock_session_class): +async def test_s3(mocker): # Set up the mock response data = await aread(__file__, "utf-8") - mock_session_object = mock.Mock() - reader_mock = mock.AsyncMock() + reader_mock = mocker.AsyncMock() reader_mock.read.side_effect = [data.encode("utf-8"), b"", data.encode("utf-8")] - type(reader_mock).url = mock.PropertyMock(return_value="https://mock") - mock_client = mock.AsyncMock() + type(reader_mock).url = mocker.PropertyMock(return_value="https://mock") + mock_client = mocker.AsyncMock() mock_client.put_object.return_value = None mock_client.get_object.return_value = {"Body": reader_mock} mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None - mock_session_object.client.return_value = mock_client - mock_session_class.return_value = mock_session_object + mocker.patch.object(aioboto3.Session, "client", return_value=mock_client) # Prerequisites s3 = Config.default().s3 @@ -55,7 +52,7 @@ async def test_s3(mock_session_class): # Mock session env s3.access_key = "ABC" - type(reader_mock).url = mock.PropertyMock(return_value="") + type(reader_mock).url = mocker.PropertyMock(return_value="") try: conn = S3(s3) res = await conn.cache("ABC", ".bak", "script")