refactor: format

This commit is contained in:
莘权 马 2023-12-14 20:44:27 +08:00
parent 829dfd8997
commit 290fb8b8d0
21 changed files with 262 additions and 284 deletions

1
.gitignore vendored
View file

@ -59,6 +59,7 @@ cover/
# Django stuff:
*.log
logs
local_settings.py
db.sqlite3
db.sqlite3-journal

View file

@ -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()

View file

@ -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()

View file

@ -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):

View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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:
"""

View file

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

View file

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

View file

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

View file

@ -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}")

View file

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

View file

@ -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 = "<unknown>"
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

View file

@ -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"])

View file

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

View file

@ -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}'
)