diff --git a/.gitignore b/.gitignore index e03eab3d3..0ac318ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ cover/ # Django stuff: *.log +logs local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 96c175ccb..ae40913e0 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -5,13 +5,12 @@ @Author : alexanderwu @File : action_node.py """ -import re -from typing import Dict, Type, List, Any, Tuple, Optional import json +import re +from typing import Any, Dict, List, Optional, Type from pydantic import BaseModel, create_model, root_validator, validator -# , model_validator, field_validator -from tenacity import wait_random_exponential, stop_after_attempt, retry +from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions import ActionOutput from metagpt.llm import BaseGPTAPI @@ -51,6 +50,7 @@ def dict_to_markdown(d, prefix="-", postfix="\n"): class ActionNode: """ActionNode is a tree of nodes.""" + # Action Strgy # - sop: 仅使用一级SOP # - complex: 使用一级SOP+自定义策略填槽 @@ -72,8 +72,7 @@ class ActionNode: content: str instruct_content: BaseModel - def __init__(self, key, expected_type, instruction, example, content="", - children=None): + def __init__(self, key, expected_type, instruction, example, content="", children=None): self.key = key self.expected_type = expected_type self.instruction = instruction @@ -82,8 +81,9 @@ class ActionNode: self.children = children if children is not None else {} def __str__(self): - return f"{self.key}, {self.expected_type}, {self.instruction}, {self.example}" \ - f", {self.content}, {self.children}" + return ( + f"{self.key}, {self.expected_type}, {self.instruction}, {self.example}" f", {self.content}, {self.children}" + ) def __repr__(self): return self.__str__() @@ -136,7 +136,7 @@ class ActionNode: """基于pydantic v2的模型动态生成,用来检验结果类型正确性,待验证""" new_class = create_model(class_name, **mapping) - @model_validator(mode='before') + @model_validator(mode="before") def check_missing_fields(data): required_fields = set(mapping.keys()) missing_fields = required_fields - set(data.keys()) @@ -144,7 +144,7 @@ class ActionNode: raise ValueError(f"Missing fields: {missing_fields}") return data - @field_validator('*') + @field_validator("*") def check_name(v: Any, field: str) -> Any: if field not in mapping.keys(): raise ValueError(f"Unrecognized block: {field}") @@ -230,8 +230,9 @@ class ActionNode: # FIXME: json instruction会带来 "Project name": "web_2048 # 项目名称使用下划线", self.instruction = self.compile_instruction(to="markdown", mode=mode) self.example = self.compile_example(to=to, tag="CONTENT", mode=mode) - prompt = template.format(context=context, example=self.example, instruction=self.instruction, - constraint=CONSTRAINT) + prompt = template.format( + context=context, example=self.example, instruction=self.instruction, constraint=CONSTRAINT + ) return prompt @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) @@ -284,9 +285,7 @@ class ActionNode: def action_node_from_tuple_example(): # 示例:列表中包含元组 - list_of_tuples = [ - ("key1", str, "Instruction 1", "Example 1") - ] + list_of_tuples = [("key1", str, "Instruction 1", "Example 1")] # 从列表中创建 ActionNode 实例 nodes = [ActionNode(*data) for data in list_of_tuples] @@ -294,5 +293,5 @@ def action_node_from_tuple_example(): logger.info(i) -if __name__ == '__main__': +if __name__ == "__main__": action_node_from_tuple_example() diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 2db203606..0a303cdd5 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -6,52 +6,49 @@ @File : design_api_an.py """ from metagpt.actions.action_node import ActionNode -from metagpt.utils.mermaid import MMC1, MMC2 from metagpt.logs import logger +from metagpt.utils.mermaid import MMC1, MMC2 IMPLEMENTATION_APPROACH = ActionNode( key="Implementation approach", expected_type=str, instruction="Analyze the difficult points of the requirements, select the appropriate open-source framework", - example="We will ..." + example="We will ...", ) PROJECT_NAME = ActionNode( - key="Project name", - expected_type=str, - instruction="The project name with underline", - example="game_2048" + key="Project name", expected_type=str, instruction="The project name with underline", example="game_2048" ) FILE_LIST = ActionNode( key="File list", expected_type=list[str], instruction="Only need relative paths. ALWAYS write a main.py or app.py here", - example=['main.py', 'game.py'] + example=["main.py", "game.py"], ) DATA_STRUCTURES_AND_INTERFACES = ActionNode( key="Data structures and interfaces", expected_type=str, instruction="Use mermaid classDiagram code syntax, including classes, method(__init__ etc.) and functions with type" - " annotations, CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. " - "The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.", - example=MMC1 + " annotations, CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. " + "The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.", + example=MMC1, ) PROGRAM_CALL_FLOW = ActionNode( key="Program call flow", expected_type=str, instruction="Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE " - "accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.", - example=MMC2 + "accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.", + example=MMC2, ) ANYTHING_UNCLEAR = ActionNode( key="Anything UNCLEAR", expected_type=str, instruction="Mention unclear project aspects, then try to clarify it.", - example="Clarification needed on third-party API integration, ..." + example="Clarification needed on third-party API integration, ...", ) NODES = [ @@ -60,7 +57,7 @@ NODES = [ FILE_LIST, DATA_STRUCTURES_AND_INTERFACES, PROGRAM_CALL_FLOW, - ANYTHING_UNCLEAR + ANYTHING_UNCLEAR, ] DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES) @@ -71,5 +68,5 @@ def main(): logger.info(prompt) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 29e3bed3e..c95be4012 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -10,7 +10,6 @@ 3. According to the design in Section 2.2.3.5.4 of RFC 135, add incremental iteration functionality. """ import json -# from typing import List from metagpt.actions import ActionOutput from metagpt.actions.action import Action @@ -25,6 +24,9 @@ from metagpt.const import ( from metagpt.logs import logger from metagpt.schema import Document, Documents from metagpt.utils.file_repository import FileRepository + +# from typing import List + # from metagpt.utils.get_template import get_template NEW_REQ_TEMPLATE = """ @@ -97,7 +99,8 @@ class WriteTasks(Action): async def _merge(self, system_design_doc, task_doc, format=CONFIG.prompt_format) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content) node = await PM_NODE.fill(context, self.llm, format) - return node + task_doc.content = node.content + return task_doc @staticmethod async def _update_requirements(doc): diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index aa7cdcde2..e03af36d7 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -12,51 +12,53 @@ REQUIRED_PYTHON_PACKAGES = ActionNode( key="Required Python packages", expected_type=list[str], instruction="Provide required Python packages in requirements.txt format.", - example=["flask==1.1.2", "bcrypt==3.2.0"] + example=["flask==1.1.2", "bcrypt==3.2.0"], ) REQUIRED_OTHER_LANGUAGE_PACKAGES = ActionNode( key="Required Other language third-party packages", expected_type=list[str], instruction="List down the required packages for languages other than Python.", - example=["No third-party dependencies required"] + example=["No third-party dependencies required"], ) LOGIC_ANALYSIS = ActionNode( key="Logic Analysis", expected_type=list[list[str]], instruction="Provide a list of files with the classes/methods/functions to be implemented, " - "including dependency analysis and imports.", - example=[["game.py", "Contains Game class and ... functions"], - ["main.py", "Contains main function, from game import Game"]] + "including dependency analysis and imports.", + example=[ + ["game.py", "Contains Game class and ... functions"], + ["main.py", "Contains main function, from game import Game"], + ], ) TASK_LIST = ActionNode( key="Task list", expected_type=list[str], instruction="Break down the tasks into a list of filenames, prioritized by dependency order.", - example=["game.py", "main.py"] + example=["game.py", "main.py"], ) FULL_API_SPEC = ActionNode( key="Full API spec", expected_type=str, instruction="Describe all APIs using OpenAPI 3.0 spec that may be used by both frontend and backend.", - example="openapi: 3.0.0 ..." + example="openapi: 3.0.0 ...", ) 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.", ) ANYTHING_UNCLEAR_PM = ActionNode( key="Anything UNCLEAR", expected_type=str, instruction="Mention any unclear aspects in the project management context and try to clarify them.", - example="Clarification needed on how to start and initialize third-party libraries." + example="Clarification needed on how to start and initialize third-party libraries.", ) NODES = [ @@ -66,7 +68,7 @@ NODES = [ TASK_LIST, FULL_API_SPEC, SHARED_KNOWLEDGE, - ANYTHING_UNCLEAR_PM + ANYTHING_UNCLEAR_PM, ] @@ -78,5 +80,5 @@ def main(): logger.info(prompt) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 0781760ba..849150f6c 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -13,45 +13,45 @@ LANGUAGE = ActionNode( key="Language", expected_type=str, instruction="Provide the language used in the project, typically matching the user's requirement language.", - example="en_us" + example="en_us", ) ORIGINAL_REQUIREMENTS = ActionNode( key="Original Requirements", expected_type=str, instruction="Place the polished, complete original requirements here.", - example="The game should have a leaderboard and multiple difficulty levels." + example="The game should have a leaderboard and multiple difficulty levels.", ) PROJECT_NAME = ActionNode( key="Project Name", expected_type=str, instruction="Name the project using snake case style, like 'game_2048' or 'simple_crm'.", - example="game_2048" + example="game_2048", ) PRODUCT_GOALS = ActionNode( key="Product Goals", expected_type=list[str], instruction="Provide up to three clear, orthogonal product goals.", - example=["Create an engaging user experience", - "Ensure high performance", - "Provide customizable features"] + example=["Create an engaging user experience", "Ensure high performance", "Provide customizable features"], ) USER_STORIES = ActionNode( key="User Stories", expected_type=list[str], instruction="Provide up to five scenario-based user stories.", - example=["As a user, I want to be able to choose difficulty levels", - "As a player, I want to see my score after each game"] + example=[ + "As a user, I want to be able to choose difficulty levels", + "As a player, I want to see my score after each game", + ], ) COMPETITIVE_ANALYSIS = ActionNode( key="Competitive Analysis", expected_type=list[str], instruction="Provide analyses for up to seven competitive products.", - example=["Python Snake Game: Simple interface, lacks advanced features"] + example=["Python Snake Game: Simple interface, lacks advanced features"], ) COMPETITIVE_QUADRANT_CHART = ActionNode( @@ -72,56 +72,53 @@ COMPETITIVE_QUADRANT_CHART = ActionNode( "Campaign D": [0.78, 0.34] "Campaign E": [0.40, 0.34] "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6]""" + "Our Target Product": [0.5, 0.6]""", ) REQUIREMENT_ANALYSIS = ActionNode( key="Requirement Analysis", expected_type=str, instruction="Provide a detailed analysis of the requirements.", - example="The product should be user-friendly and performance-optimized." + example="The product should be user-friendly and performance-optimized.", ) REQUIREMENT_POOL = ActionNode( key="Requirement Pool", expected_type=list[list[str]], instruction="List down the requirements with their priority (P0, P1, P2).", - example=[["P0", "High priority requirement"], ["P1", "Medium priority requirement"]] + example=[["P0", "High priority requirement"], ["P1", "Medium priority requirement"]], ) UI_DESIGN_DRAFT = ActionNode( key="UI Design draft", expected_type=str, instruction="Provide a simple description of UI elements, functions, style, and layout.", - example="Basic function description with a simple style and layout." + example="Basic function description with a simple style and layout.", ) ANYTHING_UNCLEAR = ActionNode( key="Anything UNCLEAR", expected_type=str, instruction="Mention any aspects of the project that are unclear and try to clarify them.", - example="..." + example="...", ) ISSUE_TYPE = ActionNode( key="issue_type", expected_type=str, instruction="Answer BUG/REQUIREMENT. If it is a bugfix, answer BUG, otherwise answer Requirement", - example="BUG" + example="BUG", ) IS_RELATIVE = ActionNode( key="is_relative", expected_type=str, instruction="Answer YES/NO. If the requirement is related to the old PRD, answer YES, otherwise NO", - example="YES" + example="YES", ) REASON = ActionNode( - key="reason", - expected_type=str, - instruction="Explain the reasoning process from question to answer", - example="..." + key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..." ) @@ -136,7 +133,7 @@ NODES = [ REQUIREMENT_ANALYSIS, REQUIREMENT_POOL, UI_DESIGN_DRAFT, - ANYTHING_UNCLEAR + ANYTHING_UNCLEAR, ] WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES) @@ -149,5 +146,5 @@ def main(): logger.info(prompt) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/metagpt/provider/fireworks_api.py b/metagpt/provider/fireworks_api.py index 23126af2d..47ac9cf61 100644 --- a/metagpt/provider/fireworks_api.py +++ b/metagpt/provider/fireworks_api.py @@ -5,11 +5,10 @@ import openai from metagpt.config import CONFIG -from metagpt.provider.openai_api import OpenAIGPTAPI, CostManager, RateLimiter +from metagpt.provider.openai_api import CostManager, OpenAIGPTAPI, RateLimiter class FireWorksGPTAPI(OpenAIGPTAPI): - def __init__(self): self.__init_fireworks(CONFIG) self.llm = openai diff --git a/metagpt/provider/open_llm_api.py b/metagpt/provider/open_llm_api.py index a6820b42b..f421e30c8 100644 --- a/metagpt/provider/open_llm_api.py +++ b/metagpt/provider/open_llm_api.py @@ -4,13 +4,13 @@ import openai -from metagpt.logs import logger from metagpt.config import CONFIG -from metagpt.provider.openai_api import OpenAIGPTAPI, CostManager, RateLimiter +from metagpt.logs import logger +from metagpt.provider.openai_api import CostManager, OpenAIGPTAPI, RateLimiter class OpenLLMCostManager(CostManager): - """ open llm model is self-host, it's free and without cost""" + """open llm model is self-host, it's free and without cost""" def update_cost(self, prompt_tokens, completion_tokens, model): """ @@ -32,7 +32,6 @@ class OpenLLMCostManager(CostManager): class OpenLLMGPTAPI(OpenAIGPTAPI): - def __init__(self): self.__init_openllm(CONFIG) self.llm = openai diff --git a/metagpt/provider/postprecess/base_postprecess_plugin.py b/metagpt/provider/postprecess/base_postprecess_plugin.py index 702a03194..0d1cfbb11 100644 --- a/metagpt/provider/postprecess/base_postprecess_plugin.py +++ b/metagpt/provider/postprecess/base_postprecess_plugin.py @@ -5,13 +5,15 @@ from typing import Union from metagpt.logs import logger -from metagpt.utils.repair_llm_raw_output import RepairType -from metagpt.utils.repair_llm_raw_output import repair_llm_raw_output, extract_content_from_output, \ - retry_parse_json_text +from metagpt.utils.repair_llm_raw_output import ( + RepairType, + extract_content_from_output, + repair_llm_raw_output, + retry_parse_json_text, +) class BasePostPrecessPlugin(object): - model = None # the plugin of the `model`, use to judge in `llm_postprecess` def run_repair_llm_output(self, output: str, schema: dict, req_key: str = "[/CONTENT]") -> Union[dict, list]: @@ -33,15 +35,15 @@ class BasePostPrecessPlugin(object): return parsed_data def run_repair_llm_raw_output(self, content: str, req_keys: list[str], repair_type: str = None) -> str: - """ inherited class can re-implement the function""" + """inherited class can re-implement the function""" return repair_llm_raw_output(content, req_keys=req_keys, repair_type=repair_type) def run_extract_content_from_output(self, content: str, right_key: str) -> str: - """ inherited class can re-implement the function""" + """inherited class can re-implement the function""" return extract_content_from_output(content, right_key=right_key) def run_retry_parse_json_text(self, content: str) -> Union[dict, list]: - """ inherited class can re-implement the function""" + """inherited class can re-implement the function""" logger.info(f"extracted json CONTENT from output:\n{content}") parsed_data = retry_parse_json_text(output=content) # should use output=content return parsed_data @@ -64,9 +66,5 @@ class BasePostPrecessPlugin(object): assert "/" in req_key # current, postprocess only deal the repair_llm_raw_output - new_output = self.run_repair_llm_output( - output=output, - schema=schema, - req_key=req_key - ) + new_output = self.run_repair_llm_output(output=output, schema=schema, req_key=req_key) return new_output diff --git a/metagpt/provider/postprecess/llm_output_postprecess.py b/metagpt/provider/postprecess/llm_output_postprecess.py index 4b5955061..85405543d 100644 --- a/metagpt/provider/postprecess/llm_output_postprecess.py +++ b/metagpt/provider/postprecess/llm_output_postprecess.py @@ -7,17 +7,14 @@ from typing import Union from metagpt.provider.postprecess.base_postprecess_plugin import BasePostPrecessPlugin -def llm_output_postprecess(output: str, schema: dict, req_key: str = "[/CONTENT]", - model_name: str = None) -> Union[dict, str]: +def llm_output_postprecess( + output: str, schema: dict, req_key: str = "[/CONTENT]", model_name: str = None +) -> Union[dict, str]: """ default use BasePostPrecessPlugin if there is not matched plugin. """ # TODO choose different model's plugin according to the model_name postprecess_plugin = BasePostPrecessPlugin() - result = postprecess_plugin.run( - output=output, - schema=schema, - req_key=req_key - ) + result = postprecess_plugin.run(output=output, schema=schema, req_key=req_key) return result diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index b80ef85be..2c0bdd1d6 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -27,7 +27,7 @@ class Architect(Role): name: str = "Bob", profile: str = "Architect", goal: str = "design a concise, usable, complete software system", - constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries" + constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries", ) -> None: """Initializes the Architect with given attributes.""" super().__init__(name, profile, goal, constraints) diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 37090b24f..bfe1be251 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -26,7 +26,7 @@ class ProjectManager(Role): name: str = "Eve", profile: str = "Project Manager", goal: str = "break down tasks according to PRD/technical design, generate a task list, and analyze task " - "dependencies to start with the prerequisite modules", + "dependencies to start with the prerequisite modules", constraints: str = "", ) -> None: """ diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 15a01b9e9..c1573e63b 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -14,9 +14,7 @@ @Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results of SummarizeCode. """ -from metagpt.actions import DebugError, RunCode, WriteCode, WriteCodeReview, WriteTest - -# from metagpt.const import WORKSPACE_ROOT +from metagpt.actions import DebugError, RunCode, WriteTest from metagpt.actions.summarize_code import SummarizeCode from metagpt.config import CONFIG from metagpt.const import ( diff --git a/metagpt/schema.py b/metagpt/schema.py index 25281e399..baed5582b 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -97,14 +97,14 @@ class Message(BaseModel): send_to: Set = Field(default_factory={MESSAGE_ROUTE_TO_ALL}) def __init__( - self, - content, - instruct_content=None, - role="user", - cause_by="", - sent_from="", - send_to=MESSAGE_ROUTE_TO_ALL, - **kwargs, + self, + content, + instruct_content=None, + role="user", + cause_by="", + sent_from="", + send_to=MESSAGE_ROUTE_TO_ALL, + **kwargs, ): """ Parameters not listed below will be stored as meta info, including custom parameters. diff --git a/metagpt/utils/ahttp_client.py b/metagpt/utils/ahttp_client.py index d4f9f94e5..b4a33e9d7 100644 --- a/metagpt/utils/ahttp_client.py +++ b/metagpt/utils/ahttp_client.py @@ -2,29 +2,24 @@ # -*- coding: utf-8 -*- # @Desc : pure async http_client -from typing import Optional, Any, Mapping, Union +from typing import Any, Mapping, Optional, Union -from aiohttp.client import DEFAULT_TIMEOUT import aiohttp +from aiohttp.client import DEFAULT_TIMEOUT -async def apost(url: str, - params: Optional[Mapping[str, str]] = None, - json: Any = None, - data: Any = None, - headers: Optional[dict] = None, - as_json: bool = False, - encoding: str = "utf-8", - timeout: int = DEFAULT_TIMEOUT.total) -> Union[str, dict]: +async def apost( + url: str, + params: Optional[Mapping[str, str]] = None, + json: Any = None, + data: Any = None, + headers: Optional[dict] = None, + as_json: bool = False, + encoding: str = "utf-8", + timeout: int = DEFAULT_TIMEOUT.total, +) -> Union[str, dict]: async with aiohttp.ClientSession() as session: - async with session.post( - url=url, - params=params, - json=json, - data=data, - headers=headers, - timeout=timeout - ) as resp: + async with session.post(url=url, params=params, json=json, data=data, headers=headers, timeout=timeout) as resp: if as_json: data = await resp.json() else: @@ -33,13 +28,15 @@ async def apost(url: str, return data -async def apost_stream(url: str, - params: Optional[Mapping[str, str]] = None, - json: Any = None, - data: Any = None, - headers: Optional[dict] = None, - encoding: str = "utf-8", - timeout: int = DEFAULT_TIMEOUT.total) -> Any: +async def apost_stream( + url: str, + params: Optional[Mapping[str, str]] = None, + json: Any = None, + data: Any = None, + headers: Optional[dict] = None, + encoding: str = "utf-8", + timeout: int = DEFAULT_TIMEOUT.total, +) -> Any: """ usage: result = astream(url="xx") @@ -47,13 +44,6 @@ async def apost_stream(url: str, deal_with(line) """ async with aiohttp.ClientSession() as session: - async with session.post( - url=url, - params=params, - json=json, - data=data, - headers=headers, - timeout=timeout - ) as resp: + async with session.post(url=url, params=params, json=json, data=data, headers=headers, timeout=timeout) as resp: async for line in resp.content: yield line.decode(encoding) diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index 9827b8252..1340b1768 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -8,13 +8,15 @@ """ from __future__ import annotations -from gitignore_parser import parse_gitignore, rule_from_pattern, handle_negation import shutil from enum import Enum from pathlib import Path from typing import Dict, List + from git.repo import Repo from git.repo.fun import is_git_dir +from gitignore_parser import parse_gitignore + from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.logs import logger from metagpt.utils.dependency_file import DependencyFile @@ -236,8 +238,9 @@ class GitRepository: rpath = file_path.relative_to(root_relative_path) files.append(str(rpath)) else: - subfolder_files = self.get_files(relative_path=file_path, root_relative_path=root_relative_path, - filter_ignored=False) + subfolder_files = self.get_files( + relative_path=file_path, root_relative_path=root_relative_path, filter_ignored=False + ) files.extend(subfolder_files) except Exception as e: logger.error(f"Error: {e}") diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index 0a461d360..4aafd8e66 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -4,12 +4,13 @@ import copy from enum import Enum -from typing import Union, Callable -import regex as re -from tenacity import retry, stop_after_attempt, wait_fixed, after_log, RetryCallState +from typing import Callable, Union + +import regex as re +from tenacity import RetryCallState, retry, stop_after_attempt, wait_fixed -from metagpt.logs import logger from metagpt.config import CONFIG +from metagpt.logs import logger from metagpt.utils.custom_decoder import CustomDecoder @@ -33,7 +34,7 @@ def repair_case_sensitivity(output: str, req_key: str) -> str: if req_key_lower in output_lower: # find the sub-part index, and replace it with raw req_key lidx = output_lower.find(req_key_lower) - source = output[lidx: lidx + len(req_key_lower)] + source = output[lidx : lidx + len(req_key_lower)] output = output.replace(source, req_key) logger.info(f"repair_case_sensitivity: {req_key}") @@ -73,7 +74,7 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - sc = "/" # special char if req_key.startswith("[") and req_key.endswith("]"): if sc in req_key: - left_key = req_key.replace(sc, "") # `[/req_key]` -> `[req_key]` + left_key = req_key.replace(sc, "") # `[/req_key]` -> `[req_key]` right_key = req_key else: left_key = req_key @@ -82,6 +83,7 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - if left_key not in output: output = left_key + "\n" + output if right_key not in output: + def judge_potential_json(routput: str, left_key: str) -> Union[str, None]: ridx = routput.rfind(left_key) if ridx < 0: @@ -90,7 +92,7 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - idx1 = sub_output.rfind("}") idx2 = sub_output.rindex("]") idx = idx1 if idx1 >= idx2 else idx2 - sub_output = sub_output[: idx+1] + sub_output = sub_output[: idx + 1] return sub_output if output.strip().endswith("}") or (output.strip().endswith("]") and not output.strip().endswith(left_key)): @@ -155,9 +157,7 @@ def repair_llm_raw_output(output: str, req_keys: list[str], repair_type: RepairT # do the repairation usually for non-openai models for req_key in req_keys: - output = _repair_llm_raw_output(output=output, - req_key=req_key, - repair_type=repair_type) + output = _repair_llm_raw_output(output=output, req_key=req_key, repair_type=repair_type) return output @@ -187,7 +187,7 @@ def repair_invalid_json(output: str, error: str) -> str: new_line = line.replace("}", "") elif line.endswith("},") and output.endswith("},"): new_line = line[:-1] - elif '",' not in line and ',' not in line: + elif '",' not in line and "," not in line: new_line = f'{line}",' elif "," not in line: # problem, miss char `,` at the end. @@ -228,8 +228,10 @@ def run_after_exp_and_passon_next_retry(logger: "loguru.Logger") -> Callable[["R elif retry_state.kwargs: func_param_output = retry_state.kwargs.get("output", "") exp_str = str(retry_state.outcome.exception()) - logger.warning(f"parse json from content inside [CONTENT][/CONTENT] failed at retry " - f"{retry_state.attempt_number}, try to fix it, exp: {exp_str}") + logger.warning( + f"parse json from content inside [CONTENT][/CONTENT] failed at retry " + f"{retry_state.attempt_number}, try to fix it, exp: {exp_str}" + ) repaired_output = repair_invalid_json(func_param_output, exp_str) retry_state.kwargs["output"] = repaired_output @@ -260,7 +262,8 @@ def retry_parse_json_text(output: str) -> Union[list, dict]: def extract_content_from_output(content: str, right_key: str = "[/CONTENT]"): - """ extract xxx from [CONTENT](xxx)[/CONTENT] using regex pattern """ + """extract xxx from [CONTENT](xxx)[/CONTENT] using regex pattern""" + def re_extract_content(cont: str, pattern: str) -> str: matches = re.findall(pattern, cont, re.DOTALL) for match in matches: diff --git a/metagpt/utils/utils.py b/metagpt/utils/utils.py index f479ec3b8..5ceed65d9 100644 --- a/metagpt/utils/utils.py +++ b/metagpt/utils/utils.py @@ -4,7 +4,7 @@ import typing -from tenacity import after_log, _utils +from tenacity import _utils def general_after_log(logger: "loguru.Logger", sec_format: str = "%0.3f") -> typing.Callable[["RetryCallState"], None]: @@ -13,7 +13,10 @@ def general_after_log(logger: "loguru.Logger", sec_format: str = "%0.3f") -> typ fn_name = "" else: fn_name = _utils.get_callback_name(retry_state.fn) - logger.error(f"Finished call to '{fn_name}' after {sec_format % retry_state.seconds_since_start}(s), " - f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it. " - f"exp: {retry_state.outcome.exception()}") + logger.error( + f"Finished call to '{fn_name}' after {sec_format % retry_state.seconds_since_start}(s), " + f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it. " + f"exp: {retry_state.outcome.exception()}" + ) + return log_it diff --git a/tests/metagpt/test_llm.py b/tests/metagpt/test_llm.py index 49969a2af..408fd3162 100644 --- a/tests/metagpt/test_llm.py +++ b/tests/metagpt/test_llm.py @@ -33,5 +33,6 @@ async def test_llm_acompletion(llm): assert len(await llm.acompletion_batch([hello_msg])) > 0 assert len(await llm.acompletion_batch_text([hello_msg])) > 0 + # if __name__ == "__main__": # pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_ahttp_client.py b/tests/metagpt/utils/test_ahttp_client.py index 15159423a..a595d645f 100644 --- a/tests/metagpt/utils/test_ahttp_client.py +++ b/tests/metagpt/utils/test_ahttp_client.py @@ -9,30 +9,21 @@ from metagpt.utils.ahttp_client import apost, apost_stream @pytest.mark.asyncio async def test_apost(): - result = await apost( - url="https://www.baidu.com/" - ) + result = await apost(url="https://www.baidu.com/") assert "百度一下" in result result = await apost( - url="http://aider.meizu.com/app/weather/listWeather", - data={"cityIds": "101240101"}, - as_json=True + url="http://aider.meizu.com/app/weather/listWeather", data={"cityIds": "101240101"}, as_json=True ) assert result["code"] == "200" @pytest.mark.asyncio async def test_apost_stream(): - result = apost_stream( - url="https://www.baidu.com/" - ) + result = apost_stream(url="https://www.baidu.com/") async for line in result: assert len(line) >= 0 - result = apost_stream( - url="http://aider.meizu.com/app/weather/listWeather", - data={"cityIds": "101240101"} - ) + result = apost_stream(url="http://aider.meizu.com/app/weather/listWeather", data={"cityIds": "101240101"}) async for line in result: assert len(line) >= 0 diff --git a/tests/metagpt/utils/test_repair_llm_raw_output.py b/tests/metagpt/utils/test_repair_llm_raw_output.py index a2dd18516..21bbee921 100644 --- a/tests/metagpt/utils/test_repair_llm_raw_output.py +++ b/tests/metagpt/utils/test_repair_llm_raw_output.py @@ -4,10 +4,15 @@ from metagpt.config import CONFIG -CONFIG.repair_llm_output = True +from metagpt.utils.repair_llm_raw_output import ( + RepairType, + extract_content_from_output, + repair_invalid_json, + repair_llm_raw_output, + retry_parse_json_text, +) -from metagpt.utils.repair_llm_raw_output import repair_llm_raw_output, RepairType, repair_invalid_json,\ - extract_content_from_output, retry_parse_json_text +CONFIG.repair_llm_output = True def test_repair_case_sensitivity(): @@ -26,8 +31,7 @@ def test_repair_case_sensitivity(): "Requirement Analysis": "The 2048 game should be simple to play" }""" req_keys = ["Original Requirements", "Search Information", "Competitive Quadrant Chart", "Requirement Analysis"] - output = repair_llm_raw_output(output=raw_output, - req_keys=req_keys) + output = repair_llm_raw_output(output=raw_output, req_keys=req_keys) assert output == target_output @@ -40,8 +44,7 @@ def test_repair_special_character_missing(): "Anything UNCLEAR": "No unclear requirements or information." [/CONTENT]""" req_keys = ["[/CONTENT]"] - output = repair_llm_raw_output(output=raw_output, - req_keys=req_keys) + output = repair_llm_raw_output(output=raw_output, req_keys=req_keys) assert output == target_output raw_output = """[CONTENT] tag @@ -56,15 +59,13 @@ def test_repair_special_character_missing(): "Anything UNCLEAR": "No unclear requirements or information." } [/CONTENT]""" - output = repair_llm_raw_output(output=raw_output, - req_keys=req_keys) + output = repair_llm_raw_output(output=raw_output, req_keys=req_keys) assert output == target_output raw_output = '[CONTENT] {"a": "b"} [CONTENT]' target_output = '[CONTENT] {"a": "b"} [/CONTENT]' - output = repair_llm_raw_output(output=raw_output, - req_keys=["[/CONTENT]"]) + output = repair_llm_raw_output(output=raw_output, req_keys=["[/CONTENT]"]) print("output\n", output) assert output == target_output @@ -73,38 +74,35 @@ def test_required_key_pair_missing(): raw_output = '[CONTENT] {"a": "b"}' target_output = '[CONTENT] {"a": "b"}\n[/CONTENT]' - output = repair_llm_raw_output(output=raw_output, - req_keys=["[/CONTENT]"]) + output = repair_llm_raw_output(output=raw_output, req_keys=["[/CONTENT]"]) assert output == target_output - raw_output = '''[CONTENT] + raw_output = """[CONTENT] { "key": "value" -]''' - target_output = '''[CONTENT] +]""" + target_output = """[CONTENT] { "key": "value" ] -[/CONTENT]''' +[/CONTENT]""" - output = repair_llm_raw_output(output=raw_output, - req_keys=["[/CONTENT]"]) + output = repair_llm_raw_output(output=raw_output, req_keys=["[/CONTENT]"]) assert output == target_output - raw_output = '''[CONTENT] tag + raw_output = """[CONTENT] tag [CONTENT] { "key": "value" } xxx -''' - target_output = '''[CONTENT] +""" + target_output = """[CONTENT] { "key": "value" } -[/CONTENT]''' - output = repair_llm_raw_output(output=raw_output, - req_keys=["[/CONTENT]"]) +[/CONTENT]""" + output = repair_llm_raw_output(output=raw_output, req_keys=["[/CONTENT]"]) assert output == target_output @@ -112,25 +110,19 @@ def test_repair_json_format(): raw_output = "{ xxx }]" target_output = "{ xxx }" - output = repair_llm_raw_output(output=raw_output, - req_keys=[None], - repair_type=RepairType.JSON) + output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) assert output == target_output raw_output = "[{ xxx }" target_output = "{ xxx }" - output = repair_llm_raw_output(output=raw_output, - req_keys=[None], - repair_type=RepairType.JSON) + output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) assert output == target_output raw_output = "{ xxx ]" target_output = "{ xxx }" - output = repair_llm_raw_output(output=raw_output, - req_keys=[None], - repair_type=RepairType.JSON) + output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) assert output == target_output @@ -186,7 +178,7 @@ def test_retry_parse_json_text(): target_json = { "Original Requirements": "Create a 2048 game", "Competitive Quadrant Chart": "quadrantChart\n\ttitle Reach and engagement of campaigns\n\t\tx-axis", - "Requirement Analysis": "The requirements are clear and well-defined" + "Requirement Analysis": "The requirements are clear and well-defined", } output = retry_parse_json_text(output=invalid_json_text) assert output == target_json @@ -200,7 +192,7 @@ def test_retry_parse_json_text(): target_json = { "Original Requirements": "Create a 2048 game", "Competitive Quadrant Chart": "quadrantChart\n\ttitle Reach and engagement of campaigns\n\t\tx-axis", - "Requirement Analysis": "The requirements are clear and well-defined" + "Requirement Analysis": "The requirements are clear and well-defined", } output = retry_parse_json_text(output=invalid_json_text) assert output == target_json @@ -214,84 +206,88 @@ def test_extract_content_from_output(): xxx [CONTENT] xxxx [/CONTENT] xxx [CONTENT][/CONTENT] xxx [CONTENT][/CONTENT] # target pair is the last one """ - output = 'Sure! Here is the properly formatted JSON output based on the given context:\n\n[CONTENT]\n{\n"' \ - 'Required Python third-party packages": [\n"pygame==2.0.4",\n"pytest"\n],\n"Required Other language ' \ - 'third-party packages": [\n"No third-party packages are required."\n],\n"Full API spec": "\nopenapi: ' \ - '3.0.0\n\ndescription: A JSON object representing the game state.\n\npaths:\n game:\n get:\n ' \ - 'summary: Get the current game state.\n responses:\n 200:\n description: Game state.' \ - '\n\n moves:\n post:\n summary: Make a move.\n requestBody:\n description: Move to be ' \ - 'made.\n content:\n applicationjson:\n schema:\n type: object\n ' \ - ' properties:\n x:\n type: integer\n y:\n ' \ - ' type: integer\n tile:\n type: object\n ' \ - 'properties:\n value:\n type: integer\n x:\n ' \ - ' type: integer\n y:\n type: integer\n\n ' \ - 'undo-move:\n post:\n summary: Undo the last move.\n responses:\n 200:\n ' \ - ' description: Undone move.\n\n end-game:\n post:\n summary: End the game.\n responses:\n ' \ - ' 200:\n description: Game ended.\n\n start-game:\n post:\n summary: Start a new ' \ - 'game.\n responses:\n 200:\n description: Game started.\n\n game-over:\n get:\n ' \ - ' summary: Check if the game is over.\n responses:\n 200:\n description: Game ' \ - 'over.\n 404:\n description: Game not over.\n\n score:\n get:\n summary: Get the ' \ - 'current score.\n responses:\n 200:\n description: Score.\n\n tile:\n get:\n ' \ - 'summary: Get a specific tile.\n parameters:\n tile_id:\n type: integer\n ' \ - 'description: ID of the tile to get.\n responses:\n 200:\n description: Tile.\n\n ' \ - 'tiles:\n get:\n summary: Get all tiles.\n responses:\n 200:\n description: ' \ - 'Tiles.\n\n level:\n get:\n summary: Get the current level.\n responses:\n 200:\n ' \ - ' description: Level.\n\n level-up:\n post:\n summary: Level up.\n responses:\n ' \ - '200:\n description: Level up successful.\n\n level-down:\n post:\n summary: Level ' \ - 'down.\n responses:\n 200:\n description: Level down successful.\n\n restart:\n ' \ - 'post:\n summary: Restart the game.\n responses:\n 200:\n description: Game ' \ - 'restarted.\n\n help:\n get:\n summary: Get help.\n responses:\n 200:\n ' \ - 'description: Help.\n\n version:\n get:\n summary: Get the version of the game.\n ' \ - 'responses:\n 200:\n description: Version.\n\n}\n\n"Logic Analysis": [\n"game.py",' \ - '\n"Contains the game logic."\n],\n"Task list": [\n"game.py",\n"Contains the game logic and should be ' \ - 'done first."\n],\n"Shared Knowledge": "\n\'game.py\' contains the game logic.\n",\n"Anything ' \ - 'UNCLEAR": "How to start the game."\n]\n\n[/CONTENT] Great! Your JSON output is properly formatted ' \ - 'and correctly includes all the required sections. Here\'s a breakdown of what each section ' \ - 'contains:\n\nRequired Python third-party packages:\n\n* pygame==2.0.4\n* pytest\n\nRequired Other ' \ - 'language third-party packages:\n\n* No third-party packages are required.\n\nFull API spec:\n\n* ' \ - 'openapi: 3.0.0\n* description: A JSON object representing the game state.\n* paths:\n + game: ' \ - 'Get the current game state.\n + moves: Make a move.\n + undo-move: Undo the last move.\n + ' \ - 'end-game: End the game.\n + start-game: Start a new game.\n + game-over: Check if the game is ' \ - 'over.\n + score: Get the current score.\n + tile: Get a specific tile.\n + tiles: Get all tiles.\n ' \ - '+ level: Get the current level.\n + level-up: Level up.\n + level-down: Level down.\n + restart: ' \ - 'Restart the game.\n + help: Get help.\n + version: Get the version of the game.\n\nLogic ' \ - 'Analysis:\n\n* game.py contains the game logic.\n\nTask list:\n\n* game.py contains the game logic ' \ - 'and should be done first.\n\nShared Knowledge:\n\n* \'game.py\' contains the game logic.\n\nAnything ' \ - 'UNCLEAR:\n\n* How to start the game.\n\nGreat job! This JSON output should provide a clear and ' \ - 'comprehensive overview of the project\'s requirements and dependencies.' + output = ( + 'Sure! Here is the properly formatted JSON output based on the given context:\n\n[CONTENT]\n{\n"' + 'Required Python third-party packages": [\n"pygame==2.0.4",\n"pytest"\n],\n"Required Other language ' + 'third-party packages": [\n"No third-party packages are required."\n],\n"Full API spec": "\nopenapi: ' + "3.0.0\n\ndescription: A JSON object representing the game state.\n\npaths:\n game:\n get:\n " + "summary: Get the current game state.\n responses:\n 200:\n description: Game state." + "\n\n moves:\n post:\n summary: Make a move.\n requestBody:\n description: Move to be " + "made.\n content:\n applicationjson:\n schema:\n type: object\n " + " properties:\n x:\n type: integer\n y:\n " + " type: integer\n tile:\n type: object\n " + "properties:\n value:\n type: integer\n x:\n " + " type: integer\n y:\n type: integer\n\n " + "undo-move:\n post:\n summary: Undo the last move.\n responses:\n 200:\n " + " description: Undone move.\n\n end-game:\n post:\n summary: End the game.\n responses:\n " + " 200:\n description: Game ended.\n\n start-game:\n post:\n summary: Start a new " + "game.\n responses:\n 200:\n description: Game started.\n\n game-over:\n get:\n " + " summary: Check if the game is over.\n responses:\n 200:\n description: Game " + "over.\n 404:\n description: Game not over.\n\n score:\n get:\n summary: Get the " + "current score.\n responses:\n 200:\n description: Score.\n\n tile:\n get:\n " + "summary: Get a specific tile.\n parameters:\n tile_id:\n type: integer\n " + "description: ID of the tile to get.\n responses:\n 200:\n description: Tile.\n\n " + "tiles:\n get:\n summary: Get all tiles.\n responses:\n 200:\n description: " + "Tiles.\n\n level:\n get:\n summary: Get the current level.\n responses:\n 200:\n " + " description: Level.\n\n level-up:\n post:\n summary: Level up.\n responses:\n " + "200:\n description: Level up successful.\n\n level-down:\n post:\n summary: Level " + "down.\n responses:\n 200:\n description: Level down successful.\n\n restart:\n " + "post:\n summary: Restart the game.\n responses:\n 200:\n description: Game " + "restarted.\n\n help:\n get:\n summary: Get help.\n responses:\n 200:\n " + "description: Help.\n\n version:\n get:\n summary: Get the version of the game.\n " + 'responses:\n 200:\n description: Version.\n\n}\n\n"Logic Analysis": [\n"game.py",' + '\n"Contains the game logic."\n],\n"Task list": [\n"game.py",\n"Contains the game logic and should be ' + 'done first."\n],\n"Shared Knowledge": "\n\'game.py\' contains the game logic.\n",\n"Anything ' + 'UNCLEAR": "How to start the game."\n]\n\n[/CONTENT] Great! Your JSON output is properly formatted ' + "and correctly includes all the required sections. Here's a breakdown of what each section " + "contains:\n\nRequired Python third-party packages:\n\n* pygame==2.0.4\n* pytest\n\nRequired Other " + "language third-party packages:\n\n* No third-party packages are required.\n\nFull API spec:\n\n* " + "openapi: 3.0.0\n* description: A JSON object representing the game state.\n* paths:\n + game: " + "Get the current game state.\n + moves: Make a move.\n + undo-move: Undo the last move.\n + " + "end-game: End the game.\n + start-game: Start a new game.\n + game-over: Check if the game is " + "over.\n + score: Get the current score.\n + tile: Get a specific tile.\n + tiles: Get all tiles.\n " + "+ level: Get the current level.\n + level-up: Level up.\n + level-down: Level down.\n + restart: " + "Restart the game.\n + help: Get help.\n + version: Get the version of the game.\n\nLogic " + "Analysis:\n\n* game.py contains the game logic.\n\nTask list:\n\n* game.py contains the game logic " + "and should be done first.\n\nShared Knowledge:\n\n* 'game.py' contains the game logic.\n\nAnything " + "UNCLEAR:\n\n* How to start the game.\n\nGreat job! This JSON output should provide a clear and " + "comprehensive overview of the project's requirements and dependencies." + ) output = extract_content_from_output(output) - assert output.startswith('{\n"Required Python third-party packages') and \ - output.endswith('UNCLEAR": "How to start the game."\n]') + assert output.startswith('{\n"Required Python third-party packages') and output.endswith( + 'UNCLEAR": "How to start the game."\n]' + ) - output = 'Sure, I would be happy to help! Here is the information you provided, formatted as a JSON object ' \ - 'inside the [CONTENT] tag:\n\n[CONTENT]\n{\n"Original Requirements": "Create a 2048 game",\n"Search ' \ - 'Information": "Search results for 2048 game",\n"Requirements": [\n"Create a game with the same rules ' \ - 'as the original 2048 game",\n"Implement a user interface that is easy to use and understand",\n"Add a ' \ - 'scoreboard to track the player progress",\n"Allow the player to undo and redo moves",\n"Implement a ' \ - 'game over screen to display the final score"\n],\n"Product Goals": [\n"Create a fun and engaging game ' \ - 'experience for the player",\n"Design a user interface that is visually appealing and easy to use",\n"' \ - 'Optimize the game for performance and responsiveness"\n],\n"User Stories": [\n"As a player, I want to ' \ - 'be able to move tiles around the board to combine numbers",\n"As a player, I want to be able to undo ' \ - 'and redo moves to correct mistakes",\n"As a player, I want to see the final score and game over screen' \ - ' when I win"\n],\n"Competitive Analysis": [\n"Competitor A: 2048 game with a simple user interface and' \ - ' basic graphics",\n"Competitor B: 2048 game with a more complex user interface and better graphics",' \ - '\n"Competitor C: 2048 game with a unique twist on the rules and a more challenging gameplay experience"' \ - '\n],\n"Competitive Quadrant Chart": "quadrantChart\\n\ttitle Reach and engagement of campaigns\\n\t\t' \ - 'x-axis Low Reach --> High Reach\\n\t\ty-axis Low Engagement --> High Engagement\\n\tquadrant-1 We ' \ - 'should expand\\n\tquadrant-2 Need to promote\\n\tquadrant-3 Re-evaluate\\n\tquadrant-4 May be ' \ - 'improved\\n\tCampaign A: [0.3, 0.6]\\n\tCampaign B: [0.45, 0.23]\\n\tCampaign C: [0.57, 0.69]\\n\t' \ - 'Campaign D: [0.78, 0.34]\\n\tCampaign E: [0.40, 0.34]\\n\tCampaign F: [0.35, 0.78]"\n],\n"Requirement ' \ - 'Analysis": "The requirements are clear and well-defined, but there may be some ambiguity around the ' \ - 'specific implementation details",\n"Requirement Pool": [\n["P0", "Implement a game with the same ' \ - 'rules as the original 2048 game"],\n["P1", "Add a scoreboard to track the player progress"],\n["P2", ' \ - '"Allow the player to undo and redo moves"]\n],\n"UI Design draft": "The UI should be simple and easy ' \ - 'to use, with a clean and visually appealing design. The game board should be the main focus of the ' \ - 'UI, with clear and concise buttons for the player to interact with.",\n"Anything UNCLEAR": ""\n}\n' \ - '[/CONTENT]\n\nI hope this helps! Let me know if you have any further questions or if there anything ' \ - 'else I can do to assist you.' + output = ( + "Sure, I would be happy to help! Here is the information you provided, formatted as a JSON object " + 'inside the [CONTENT] tag:\n\n[CONTENT]\n{\n"Original Requirements": "Create a 2048 game",\n"Search ' + 'Information": "Search results for 2048 game",\n"Requirements": [\n"Create a game with the same rules ' + 'as the original 2048 game",\n"Implement a user interface that is easy to use and understand",\n"Add a ' + 'scoreboard to track the player progress",\n"Allow the player to undo and redo moves",\n"Implement a ' + 'game over screen to display the final score"\n],\n"Product Goals": [\n"Create a fun and engaging game ' + 'experience for the player",\n"Design a user interface that is visually appealing and easy to use",\n"' + 'Optimize the game for performance and responsiveness"\n],\n"User Stories": [\n"As a player, I want to ' + 'be able to move tiles around the board to combine numbers",\n"As a player, I want to be able to undo ' + 'and redo moves to correct mistakes",\n"As a player, I want to see the final score and game over screen' + ' when I win"\n],\n"Competitive Analysis": [\n"Competitor A: 2048 game with a simple user interface and' + ' basic graphics",\n"Competitor B: 2048 game with a more complex user interface and better graphics",' + '\n"Competitor C: 2048 game with a unique twist on the rules and a more challenging gameplay experience"' + '\n],\n"Competitive Quadrant Chart": "quadrantChart\\n\ttitle Reach and engagement of campaigns\\n\t\t' + "x-axis Low Reach --> High Reach\\n\t\ty-axis Low Engagement --> High Engagement\\n\tquadrant-1 We " + "should expand\\n\tquadrant-2 Need to promote\\n\tquadrant-3 Re-evaluate\\n\tquadrant-4 May be " + "improved\\n\tCampaign A: [0.3, 0.6]\\n\tCampaign B: [0.45, 0.23]\\n\tCampaign C: [0.57, 0.69]\\n\t" + 'Campaign D: [0.78, 0.34]\\n\tCampaign E: [0.40, 0.34]\\n\tCampaign F: [0.35, 0.78]"\n],\n"Requirement ' + 'Analysis": "The requirements are clear and well-defined, but there may be some ambiguity around the ' + 'specific implementation details",\n"Requirement Pool": [\n["P0", "Implement a game with the same ' + 'rules as the original 2048 game"],\n["P1", "Add a scoreboard to track the player progress"],\n["P2", ' + '"Allow the player to undo and redo moves"]\n],\n"UI Design draft": "The UI should be simple and easy ' + "to use, with a clean and visually appealing design. The game board should be the main focus of the " + 'UI, with clear and concise buttons for the player to interact with.",\n"Anything UNCLEAR": ""\n}\n' + "[/CONTENT]\n\nI hope this helps! Let me know if you have any further questions or if there anything " + "else I can do to assist you." + ) output = extract_content_from_output(output) - assert output.startswith('{\n"Original Requirements"') and \ - output.endswith('"Anything UNCLEAR": ""\n}') + assert output.startswith('{\n"Original Requirements"') and output.endswith('"Anything UNCLEAR": ""\n}') output = """ Sure, I'd be happy to help! Here's the JSON output for the given context:\n\n[CONTENT]\n{ "Implementation approach": "We will use the open-source framework PyGame to create a 2D game engine, which will @@ -316,5 +312,6 @@ def test_extract_content_from_output(): information for a developer to understand the design and implementation of the 2048 game. """ output = extract_content_from_output(output) - assert output.startswith('{\n"Implementation approach"') and \ - output.endswith('"Anything UNCLEAR": "The requirement is clear to me."\n}') + assert output.startswith('{\n"Implementation approach"') and output.endswith( + '"Anything UNCLEAR": "The requirement is clear to me."\n}' + )