diff --git a/examples/debate.py b/examples/debate.py index 0f5d1591b..e62a5aaa1 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -88,7 +88,7 @@ async def debate(idea: str, investment: float = 3.0, n_round: int = 5): team = Team() team.hire([Biden, Trump]) team.invest(investment) - team.start_project(idea, send_to="Biden") # send debate topic to Biden and let him speak first + team.run_project(idea, send_to="Biden") # send debate topic to Biden and let him speak first await team.run(n_round=n_round) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 790295d55..f8016b8a2 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -30,6 +30,10 @@ class Action(ABC): self.desc = "" self.content = "" self.instruct_content = None + self.env = None + + def set_env(self, env): + self.env = env def set_prefix(self, prefix, profile): """Set prefix for later usage""" diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index f58d49495..9e2bfc12c 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -14,7 +14,6 @@ from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.utils.common import CodeParser from metagpt.utils.get_template import get_template -from metagpt.utils.json_to_markdown import json_to_markdown from metagpt.utils.mermaid import mermaid_to_file templates = { @@ -27,11 +26,12 @@ templates = { {format_example} ----- Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. Requirement: Fill in the following missing information based on the context, each section name is a key in json ## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select appropriate open-source frameworks. -## Python package name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores +## project_name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores ## File list: Provided as Python list[str], the list of files needed (including HTML & CSS IF NEEDED) to write the program. Only need relative paths. ALWAYS write a main.py or app.py here @@ -48,7 +48,7 @@ and only output the json inside this tag, nothing else [CONTENT] { "Implementation approach": "We will ...", - "Python package name": "snake_game", + "project_name": "snake_game", "File list": ["main.py"], "Data structures and interfaces": ' classDiagram @@ -78,12 +78,13 @@ and only output the json inside this tag, nothing else {format_example} ----- Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately -Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. +ATTENTION: Output carefully referenced "Format example" in format. ## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. -## Python package name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores +## project_name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores ## File list: Provided as Python list[str], the list of code files (including HTML & CSS IF NEEDED) to write the program. Only need relative paths. ALWAYS write a main.py or app.py here @@ -99,7 +100,7 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Implementation approach We will ... -## Python package name +## project_name ```python "snake_game" ``` @@ -138,7 +139,7 @@ The requirement is clear to me. OUTPUT_MAPPING = { "Implementation approach": (str, ...), - "Python package name": (str, ...), + "project_name": (str, ...), "File list": (List[str], ...), "Data structures and interfaces": (str, ...), "Program call flow": (str, ...), @@ -170,7 +171,7 @@ class WriteDesign(Action): if context[-1].instruct_content: logger.info(f"Saving PRD to {prd_file}") - prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict())) + prd_file.write_text(context[-1].instruct_content.json(ensure_ascii=False), encoding='utf-8') async def _save_system_design(self, docs_path, resources_path, system_design): data_api_design = system_design.instruct_content.dict()[ @@ -183,14 +184,14 @@ class WriteDesign(Action): await mermaid_to_file(seq_flow, resources_path / "seq_flow") system_design_file = docs_path / "system_design.md" logger.info(f"Saving System Designs to {system_design_file}") - system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict()))) + system_design_file.write_text(system_design.instruct_content.json(ensure_ascii=False), encoding='utf-8') async def _save(self, context, system_design): if isinstance(system_design, ActionOutput): - ws_name = system_design.instruct_content.dict()["Python package name"] + project_name = system_design.instruct_content.dict()["project_name"] else: - ws_name = CodeParser.parse_str(block="Python package name", text=system_design) - workspace = CONFIG.workspace_path / ws_name + project_name = CodeParser.parse_str(block="project_name", text=system_design) + workspace = CONFIG.workspace_path / project_name self.recreate_workspace(workspace) docs_path = workspace / "docs" resources_path = workspace / "resources" @@ -204,11 +205,11 @@ class WriteDesign(Action): prompt = prompt_template.format(context=context, format_example=format_example) # system_design = await self._aask(prompt) system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format) - # fix Python package name, we can't system_design.instruct_content.python_package_name = "xxx" since "Python package name" contain space, have to use setattr - setattr( - system_design.instruct_content, - "Python package name", - system_design.instruct_content.dict()["Python package name"].strip().strip("'").strip('"'), - ) + # fix project_name, we can't system_design.instruct_content.python_package_name = "xxx" since "project_name" contain space, have to use setattr + # setattr( + # system_design.instruct_content, + # "project_name", + # system_design.instruct_content.dict()["project_name"].strip().strip("'").strip('"'), + # ) await self._save(context, system_design) return system_design diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 467cb4d83..805226a25 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -11,7 +11,6 @@ from metagpt.actions.action import Action from metagpt.config import CONFIG from metagpt.utils.common import CodeParser from metagpt.utils.get_template import get_template -from metagpt.utils.json_to_markdown import json_to_markdown templates = { "json": { @@ -23,19 +22,20 @@ templates = { {format_example} ----- Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them -Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. +ATTENTION: Output carefully referenced "Format example" in format. ## Required Python third-party packages: Provide Python list[str] in requirements.txt format ## Required Other language third-party packages: Provide Python list[str] in requirements.txt format -## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. - ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. ## Anything UNCLEAR: Provide as Plain text. Try to clarify it. For example, don't forget a main entry. don't forget to init 3rd party libs. @@ -52,17 +52,17 @@ and only output the json inside this tag, nothing else "Required Other language third-party packages": [ "No third-party ..." ], + "Logic Analysis": [ + ["game.py", "Contains..."] + ], + "Task list": [ + "game.py" + ], "Full API spec": """ openapi: 3.0.0 ... description: A JSON object ... """, - "Logic Analysis": [ - ["game.py","Contains..."] - ], - "Task list": [ - "game.py" - ], "Shared Knowledge": """ 'game.py' contains ... """, @@ -86,12 +86,12 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Required Other language third-party packages: Provided in requirements.txt format -## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. - ## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + ## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. ## Anything UNCLEAR: Provide as Plain text. Try to clarify it. For example, don't forget a main entry. don't forget to init 3rd party libs. @@ -126,14 +126,16 @@ description: A JSON object ... ## Logic Analysis ```python [ - ["game.py", "Contains ..."], + ["index.js", "Contains ..."], + ["main.py", "Contains ..."], ] ``` ## Task list ```python [ - "game.py", + "index.js", + "main.py", ] ``` @@ -167,11 +169,11 @@ class WriteTasks(Action): def _save(self, context, rsp): if context[-1].instruct_content: - ws_name = context[-1].instruct_content.dict()["Python package name"] + ws_name = context[-1].instruct_content.dict()["project_name"] else: - ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + ws_name = CodeParser.parse_str(block="project_name", text=context[-1].content) file_path = CONFIG.workspace_path / ws_name / "docs/api_spec_and_tasks.md" - file_path.write_text(json_to_markdown(rsp.instruct_content.dict())) + file_path.write_text(rsp.instruct_content.json(ensure_ascii=False)) # Write requirements.txt requirements_path = CONFIG.workspace_path / ws_name / "requirements.txt" diff --git a/metagpt/actions/SummarizeCode.py b/metagpt/actions/summarize_code.py similarity index 62% rename from metagpt/actions/SummarizeCode.py rename to metagpt/actions/summarize_code.py index 49a350b75..a85d3cdeb 100644 --- a/metagpt/actions/SummarizeCode.py +++ b/metagpt/actions/summarize_code.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ @Author : alexanderwu -@File : SummarizeCode.py +@File : summarize_code.py """ from tenacity import retry, stop_after_attempt, wait_fixed @@ -13,6 +13,7 @@ from metagpt.schema import Message PROMPT_TEMPLATE = """ NOTICE Role: You are a professional software engineer, and your main task is to review the code. +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". ----- @@ -20,13 +21,13 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc {context} ----- -## Code Review All: 请你对历史所有文件进行阅读,分析每个文件是否都完整实现了用户需求,找到可能的bug,如函数未实现、调用错误、未引用等 +## Code Review All: 请你对历史所有文件进行阅读,在文件中找到可能的bug,如函数未实现、调用错误、未引用等 -## Call flow: 根据实现的函数,使用mermaid绘制完整的调用链 +## Call flow: mermaid代码,根据实现的函数,使用mermaid绘制完整的调用链 ## Summary: 根据历史文件的实现情况进行总结 -## TODOs: 这里写出需要修改的文件列表,我们会在之后进行修改 +## TODOs: Python dict[str, str],这里写出需要修改的文件列表与理由,我们会在之后进行修改 """ @@ -67,15 +68,15 @@ flowchart TB - ... ## TODOs -1. ... -2. ... -3. ... +{ + "a.py": "implement requirement xxx...", +} """ class SummarizeCode(Action): - def __init__(self, name="SummaryCode", context: list[Message] = None, llm=None): + def __init__(self, name="SummarizeCode", context: list[Message] = None, llm=None): super().__init__(name, context, llm) @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) @@ -84,8 +85,8 @@ class SummarizeCode(Action): return code_rsp async def run(self, context): - format_example = FORMAT_EXAMPLE.format() + format_example = FORMAT_EXAMPLE prompt = PROMPT_TEMPLATE.format(context=context, format_example=format_example) - logger.info("Code review all..") + logger.info("Summarize code..") rsp = await self.summarize_code(prompt) return rsp diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 1f6d16b3b..2631ec138 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -16,6 +16,7 @@ from metagpt.utils.common import CodeParser PROMPT_TEMPLATE = """ NOTICE Role: You are a professional engineer; the main goal is to write PEP8 compliant, elegant, modular, easy to read and maintain Python 3.9 code (but you can also use other programming language) +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". ----- @@ -60,7 +61,7 @@ class WriteCode(Action): design = [i for i in context if i.cause_by == WriteDesign][0] - ws_name = CodeParser.parse_str(block="Python package name", text=design.content) + ws_name = CodeParser.parse_str(block="project_name", text=design.content) ws_path = CONFIG.workspace_path / ws_name if f"{ws_name}/" not in filename and all(i not in filename for i in ["requirements.txt", ".md"]): ws_path = ws_path / ws_name diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index c6538bf7b..aebe3f4fa 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -6,56 +6,84 @@ @File : write_code_review.py """ +from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions.action import Action from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_fixed +from metagpt.config import CONFIG PROMPT_TEMPLATE = """ NOTICE Role: You are a professional software engineer, and your main task is to review the code. You need to ensure that the code conforms to the PEP8 standards, is elegantly designed and modularized, easy to read and maintain, and is written in Python 3.9 (or in another programming language). +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example". -## Code Review: Based on the following context and code, follow the check list, Provide key, clear, concise, and specific code modification suggestions, up to 5. -1. Is the code implemented as per the requirements? If not, how to achieve it? Analyse it step by step. -2. Are there any issues with the code logic? If so, how to solve it? -3. Does the existing code follow the "Data structures and interfaces"? -4. Is there a function in the code that is not fully implemented? If so, how to implement it? -5. Does the code have unnecessary or lack dependencies? If so, how to solve it? - -## Rewrite Code: rewrite {filename} based on "Code Review" with triple quotes. Do your utmost to optimize THIS SINGLE FILE. Implement ALL TODO. ------ # Context {context} -## Code: {filename} +## Code to be Reviewed: {filename} ``` {code} ``` + ----- +## Code Review: Based on the "Code to be Reviewed", provide key, clear, concise, and specific code modification suggestions, up to 5. +1. Is the code implemented as per the requirements? If not, how to achieve it? Analyse it step by step. +2. Is the code logic completely correct? If there are errors, please indicate how to correct them. +3. Does the existing code follow the "Data structures and interfaces"? +4. Are all functions implemented? If there is no implementation, please indicate how to achieve it step by step. +5. Have all necessary pre-dependencies been imported? If not, indicate which ones need to be imported +6. Is the code implemented concisely enough? Are methods from other files being reused correctly? + +## Code Review Result: If the code doesn't have bugs, we don't need to rewrite it, so answer LGTM and stop. ONLY ANSWER LGTM/LBTM. +LGTM/LBTM + +## Rewrite Code: if it still has some bugs, rewrite {filename} based on "Code Review" with triple quotes, try to get LGTM. Do your utmost to optimize THIS SINGLE FILE. Implement ALL TODO. RETURN ALL CODE, NEVER OMIT ANYTHING. 以任何方式省略代码都是不允许的。 +``` +``` + ## Format example ------ {format_example} ------ """ FORMAT_EXAMPLE = """ - -## Code Review +----- +# EXAMPLE 1 +## Code Review: {filename} 1. No, we should add the logic of ... 2. ... 3. ... 4. ... 5. ... +6. ... + +## Code Review Result: {filename} +LBTM ## Rewrite Code: {filename} ```python ## {filename} ... ``` +----- +# EXAMPLE 2 +## Code Review: {filename} +1. Yes. +2. Yes. +3. Yes. +4. Yes. +5. Yes. +6. Yes. + +## Code Review Result: {filename} +LGTM + +## Rewrite Code: {filename} +pass +----- """ @@ -64,17 +92,27 @@ class WriteCodeReview(Action): super().__init__(name, context, llm) @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - async def write_code(self, prompt): + async def write_code_review_and_rewrite(self, prompt): code_rsp = await self._aask(prompt) + result = CodeParser.parse_block("Code Review Result", code_rsp) + if "LGTM" in result: + return result, None code = CodeParser.parse_code(block="", text=code_rsp) - return code + return result, code async def run(self, context, code, filename): - format_example = FORMAT_EXAMPLE.format(filename=filename) - prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example) - logger.info(f'Code review {filename}..') - code = await self.write_code(prompt) + iterative_code = code + k = CONFIG.code_review_k_times + for i in range(k): + format_example = FORMAT_EXAMPLE.format(filename=filename) + prompt = PROMPT_TEMPLATE.format(context=context, code=iterative_code, filename=filename, format_example=format_example) + logger.info(f'Code review and rewrite {filename}: {i+1}/{k} | {len(iterative_code)=}, {len(code)=}') + result, rewrited_code = await self.write_code_review_and_rewrite(prompt) + if "LBTM" in result: + iterative_code = rewrited_code + elif "LGTM" in result: + return iterative_code # code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING) # self._save(context, filename, code) - return code - \ No newline at end of file + # 如果rewrited_code是None(原code perfect),那么直接返回code + return iterative_code diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 584d31998..4780762ca 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -17,54 +17,50 @@ templates = { "json": { "PROMPT_TEMPLATE": """ # Context -## Original Requirements -{requirements} - -## Search Information -{search_information} - -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "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] -``` +{{ + "Original Requirements": "{requirements}", + "Search Information": "" +}} ## Format example {format_example} ----- Role: You are a professional product manager; the goal is to design a concise, usable, efficient product -Requirements: According to the context, fill in the following missing information, each section name is a key in json +Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. +Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. +ATTENTION: Output carefully referenced "Format example" in format. -## Original Requirements: Provide as Plain text, place the polished complete original requirements here +## YOU NEED TO FULFILL THE BELOW JSON DOC -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories - -## Competitive Analysis: Provided as Python list[str], up to 8 competitive product analyses - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. - -## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. - -## Anything UNCLEAR: Provide as Plain text. Try to clarify it. +{{ + "Language": "", # str, use the same language as the user requirement. en_us / zh_cn etc. + "Original Requirements": "", # str, place the polished complete original requirements here + "project_name": "", # str, name it like game_2048 / web_2048 / simple_crm etc. + "Search Information": "", + "Requirements": "", + "Product Goals": [], # Provided as Python list[str], up to 3 clear, orthogonal product goals. + "User Stories": [], # Provided as Python list[str], up to 5 scenario-based user stories + "Competitive Analysis": [], # Provided as Python list[str], up to 8 competitive product analyses + # Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + "Competitive Quadrant Chart": "quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78]", + "Requirement Analysis": "", # Provide as Plain text. + "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], # Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards + "UI Design draft": "", # Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. + "Anything UNCLEAR": "", # Provide as Plain text. Try to clarify it. +}} output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else @@ -72,6 +68,7 @@ and only output the json inside this tag, nothing else "FORMAT_EXAMPLE": """ [CONTENT] { + "Language": "", "Original Requirements": "", "Search Information": "", "Requirements": "", @@ -132,9 +129,12 @@ quadrantChart {format_example} ----- Role: You are a professional product manager; the goal is to design a concise, usable, efficient product +Language: Please use the same language as the user requirement to answer, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese. Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD WRITE BEFORE the code and triple quote. Output carefully referenced "Format example" in format. +## Language: Provide as Plain text, use the same language as the user requirement. + ## Original Requirements: Provide as Plain text, place the polished complete original requirements here ## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. @@ -207,6 +207,7 @@ There are no unclear points. } OUTPUT_MAPPING = { + "Language": (str, ...), "Original Requirements": (str, ...), "Product Goals": (List[str], ...), "User Stories": (List[str], ...), @@ -232,11 +233,14 @@ class WritePRD(Action): logger.info(sas.result) logger.info(rsp) + # logger.info(format) prompt_template, format_example = get_template(templates, format) + # logger.info(prompt_template) + # logger.info(format_example) prompt = prompt_template.format( requirements=requirements, search_information=info, format_example=format_example ) - logger.debug(prompt) + # logger.info(prompt) # prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format) return prd diff --git a/metagpt/actions/write_test.py b/metagpt/actions/write_test.py index 2f4988c09..9988fda16 100644 --- a/metagpt/actions/write_test.py +++ b/metagpt/actions/write_test.py @@ -3,7 +3,7 @@ """ @Time : 2023/5/11 22:12 @Author : alexanderwu -@File : environment.py +@File : write_test.py """ from metagpt.actions.action import Action from metagpt.logs import logger diff --git a/metagpt/config.py b/metagpt/config.py index 1a9cdb4d2..d30a337e3 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -54,10 +54,7 @@ class Config(metaclass=Singleton): (not self.zhipuai_api_key or "YOUR_API_KEY" == self.zhipuai_api_key): raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first") self.openai_api_base = self._get("OPENAI_API_BASE") - openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy - if openai_proxy: - openai.proxy = openai_proxy - openai.api_base = self.openai_api_base + self.openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy self.openai_api_type = self._get("OPENAI_API_TYPE") self.openai_api_version = self._get("OPENAI_API_VERSION") self.openai_api_rpm = self._get("RPM", 3) @@ -87,6 +84,7 @@ class Config(metaclass=Singleton): logger.warning("LONG_TERM_MEMORY is True") self.max_budget = self._get("MAX_BUDGET", 10.0) self.total_cost = 0.0 + self.code_review_k_times = 2 self.puppeteer_config = self._get("PUPPETEER_CONFIG", "") self.mmdc = self._get("MMDC", "mmdc") diff --git a/metagpt/document.py b/metagpt/document.py index 044210218..cf0821421 100644 --- a/metagpt/document.py +++ b/metagpt/document.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : document.py """ - +from enum import Enum from typing import Union, Optional from pathlib import Path from pydantic import BaseModel, Field @@ -18,7 +18,9 @@ from langchain.document_loaders import ( from langchain.text_splitter import CharacterTextSplitter from tqdm import tqdm +from metagpt.config import CONFIG from metagpt.logs import logger +from metagpt.repo_parser import RepoParser def validate_cols(content_col: str, df: pd.DataFrame): @@ -48,42 +50,56 @@ def read_data(data_path: Path): return data +class DocumentStatus(Enum): + """Indicates document status, a mechanism similar to RFC/PEP""" + DRAFT = "draft" + UNDERREVIEW = "underreview" + APPROVED = "approved" + DONE = "done" + + class Document(BaseModel): """ Document: Handles operations related to document files. """ - content: str = Field(default='') - file_path: Path = Field(default=None) + path: Path = Field(default=None) + name: str = Field(default="") + content: str = Field(default="") + + # metadata? in content perhaps. + author: str = Field(default="") + status: DocumentStatus = Field(default=DocumentStatus.DRAFT) + reviews: list = Field(default_factory=list) @classmethod - def from_path(cls, file_path: Path): + def from_path(cls, path: Path): """ Create a Document instance from a file path. """ - if not file_path.exists(): - raise FileNotFoundError(f"File {file_path} not found.") - content = file_path.read_text() - return cls(content=content, file_path=file_path) + if not path.exists(): + raise FileNotFoundError(f"File {path} not found.") + content = path.read_text() + return cls(content=content, path=path) @classmethod - def from_text(cls, text: str, file_path: Optional[Path] = None): + def from_text(cls, text: str, path: Optional[Path] = None): """ Create a Document from a text string. """ - return cls(content=text, file_path=file_path) + return cls(content=text, path=path) - def to_path(self, file_path: Optional[Path] = None): + def to_path(self, path: Optional[Path] = None): """ Save content to the specified file path. """ - if file_path is not None: - self.file_path = file_path + if path is not None: + self.path = path - if self.file_path is None: + if self.path is None: raise ValueError("File path is not set.") - self.file_path.parent.mkdir(parents=True, exist_ok=True) - self.file_path.write_text(self.content) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.path.write_text(self.content, encoding="utf-8") def persist(self): """ @@ -140,25 +156,35 @@ class IndexableDocument(Document): raise NotImplementedError("Data type not supported for metadata extraction.") +class RepoMetadata(BaseModel): + + name: str = Field(default="") + n_docs: int = Field(default=0) + n_chars: int = Field(default=0) + symbols: list = Field(default_factory=list) + + class Repo(BaseModel): # Name of this repo. name: str = Field(default="") + # metadata: RepoMetadata = Field(default=RepoMetadata) docs: dict[Path, Document] = Field(default_factory=dict) codes: dict[Path, Document] = Field(default_factory=dict) assets: dict[Path, Document] = Field(default_factory=dict) - repo_path: Path = Field(default_factory=Path) + path: Path = Field(default=None) def _path(self, filename): - return self.repo_path / filename + return self.path / filename @classmethod - def from_path(cls, repo_path: Path): + def from_path(cls, path: Path): """Load documents, code, and assets from a repository path.""" - repo_path.mkdir(parents=True, exist_ok=True) - repo = Repo(repo_path = repo_path) - for file_path in repo_path.rglob('*'): - if file_path.is_file(): + path.mkdir(parents=True, exist_ok=True) + repo = Repo(path=path, name=path.name) + for file_path in path.rglob('*'): + # FIXME: These judgments are difficult to support multiple programming languages and need to be more general + if file_path.is_file() and file_path.suffix in [".json", ".txt", ".md", ".py", ".js", ".css", ".html"]: repo._set(file_path.read_text(), file_path) return repo @@ -171,23 +197,24 @@ class Repo(BaseModel): for asset in self.assets.values(): asset.to_path() - def _set(self, content: str, file_path: Path): + def _set(self, content: str, path: Path): """Add a document to the appropriate category based on its file extension.""" - file_ext = file_path.suffix + suffix = path.suffix + doc = Document(content=content, path=path, name=str(path.relative_to(self.path))) - doc = Document(content=content, file_path=file_path) - if file_ext.lower() == '.md': - self.docs[file_path] = doc - elif file_ext.lower() in ['.py', '.js', '.css', '.html']: - self.codes[file_path] = doc + # FIXME: These judgments are difficult to support multiple programming languages and need to be more general + if suffix.lower() == '.md': + self.docs[path] = doc + elif suffix.lower() in ['.py', '.js', '.css', '.html']: + self.codes[path] = doc else: - self.assets[file_path] = doc + self.assets[path] = doc return doc def set(self, content: str, filename: str): """Set a document and persist it to disk.""" - file_path = self._path(filename) - doc = self._set(content, file_path) + path = self._path(filename) + doc = self._set(content, path) doc.to_path() def get(self, filename: str) -> Optional[Document]: @@ -195,13 +222,32 @@ class Repo(BaseModel): path = self._path(filename) return self.docs.get(path) or self.codes.get(path) or self.assets.get(path) + def get_text_documents(self) -> list[Document]: + return list(self.docs.values()) + list(self.codes.values()) -def main(): - repo1 = Repo.from_path(Path("/Users/alexanderwu/workspace/t1")) + def eda(self) -> RepoMetadata: + n_docs = sum(len(i) for i in [self.docs, self.codes, self.assets]) + n_chars = sum(sum(len(j.content) for j in i.values()) for i in [self.docs, self.codes, self.assets]) + symbols = RepoParser(base_directory=self.path).generate_symbols() + return RepoMetadata(name=self.name, n_docs=n_docs, n_chars=n_chars, symbols=symbols) + + +def set_existing_repo(path=CONFIG.workspace_path / "t1"): + repo1 = Repo.from_path(path) repo1.set("wtf content", "doc/wtf_file.md") repo1.set("wtf code", "code/wtf_file.py") logger.info(repo1) # check doc +def load_existing_repo(path=CONFIG.workspace_path / "web_tetris"): + repo = Repo.from_path(path) + logger.info(repo) + logger.info(repo.eda()) + + +def main(): + load_existing_repo() + + if __name__ == '__main__': main() diff --git a/metagpt/environment.py b/metagpt/environment.py index 38077c90d..44c9b1c67 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -7,10 +7,12 @@ """ import asyncio from typing import Iterable +from pathlib import Path from pydantic import BaseModel, Field # from metagpt.document import Document +from metagpt.logs import logger from metagpt.document import Repo from metagpt.memory import Memory from metagpt.roles import Role @@ -26,6 +28,7 @@ class Environment(BaseModel): memory: Memory = Field(default_factory=Memory) history: str = Field(default='') repo: Repo = Field(default_factory=Repo) + kv: dict = Field(default_factory=dict) class Config: arbitrary_types_allowed = True @@ -52,9 +55,32 @@ class Environment(BaseModel): self.memory.add(message) self.history += f"\n{message}" - def publish_doc(self, content: str, filename: str): + def set_doc(self, content: str, filename: str): """向当前环境发布文档(包括代码)""" - self.repo.set(content, filename) + return self.repo.set(content, filename) + + def get_doc(self, filename: str): + return self.repo.get(filename) + + def set(self, k: str, v: str): + self.kv[k] = v + + def get(self, k: str): + return self.kv.get(k, None) + + def load_existing_repo(self, path: Path, inc: bool): + self.repo = Repo.from_path(path) + logger.info(self.repo.eda()) + + # Incremental mode: publish all docs to messages. Then roles can read the docs. + if inc: + docs = self.repo.get_text_documents() + for doc in docs: + msg = Message(content=doc.content) + self.publish_message(msg) + logger.info(f"Message from existing doc {doc.path}: {msg}") + logger.info(f"Load {len(docs)} docs from existing repo.") + raise NotImplementedError async def run(self, k=1): """处理一次所有信息的运行 diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index f8abea5f3..b21f80b7d 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -28,7 +28,7 @@ class LongTermMemory(Memory): logger.warning(f"It may the first time to run Agent {role_id}, the long-term memory is empty") else: logger.warning( - f"Agent {role_id} has existed memory storage with {len(messages)} messages " f"and has recovered them." + f"Agent {role_id} has existing memory storage with {len(messages)} messages " f"and has recovered them." ) self.msg_from_recover = True self.add_batch(messages) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 34e5693f8..8ac0c4b21 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -157,6 +157,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): if config.openai_api_type: openai.api_type = config.openai_api_type openai.api_version = config.openai_api_version + if config.openai_proxy: + openai.proxy = config.openai_proxy self.rpm = int(config.get("RPM", 10)) async def _achat_completion_stream(self, messages: list[dict]) -> str: diff --git a/metagpt/document_store/repo_parser.py b/metagpt/repo_parser.py similarity index 67% rename from metagpt/document_store/repo_parser.py rename to metagpt/repo_parser.py index f7e2b0f4a..0020d47aa 100644 --- a/metagpt/document_store/repo_parser.py +++ b/metagpt/repo_parser.py @@ -6,15 +6,19 @@ @File : repo_parser.py """ import json -import pathlib +from pathlib import Path + import ast - import pandas as pd +from pydantic import BaseModel, Field +from pprint import pformat + +from metagpt.config import CONFIG +from metagpt.logs import logger -class RepoParser: - def __init__(self): - self.base_directory = None +class RepoParser(BaseModel): + base_directory: Path = Field(default=None) def parse_file(self, file_path): """Parse a Python file in the repository.""" @@ -38,43 +42,42 @@ class RepoParser: file_info["classes"].append({"name": node.name, "methods": class_methods}) elif is_func(node): file_info["functions"].append(node.name) - elif isinstance(node, ast.Assign) or isinstance(node, ast.AnnAssign): + elif isinstance(node, (ast.Assign, ast.AnnAssign)): for target in node.targets if isinstance(node, ast.Assign) else [node.target]: if isinstance(target, ast.Name): file_info["globals"].append(target.id) return file_info - def generate_json_structure(self, directory, output_path): - """Generate a JSON file documenting the repository structure.""" + def generate_symbols(self): files_classes = [] + directory = self.base_directory for path in directory.rglob('*.py'): tree = self.parse_file(path) file_info = self.extract_class_and_function_info(tree, path) files_classes.append(file_info) + return files_classes + + def generate_json_structure(self, output_path): + """Generate a JSON file documenting the repository structure.""" + files_classes = self.generate_symbols() output_path.write_text(json.dumps(files_classes, indent=4)) - def generate_dataframe_structure(self, directory, output_path): + def generate_dataframe_structure(self, output_path): """Generate a DataFrame documenting the repository structure and save as CSV.""" - files_classes = [] - for path in directory.rglob('*.py'): - tree = self.parse_file(path) - file_info = self.extract_class_and_function_info(tree, path) - files_classes.append(file_info) - + files_classes = self.generate_symbols() df = pd.DataFrame(files_classes) df.to_csv(output_path, index=False) - def generate_structure(self, directory_path, output_path=None, mode='json'): + def generate_structure(self, output_path=None, mode='json'): """Generate the structure of the repository as a specified format.""" - self.base_directory = pathlib.Path(directory_path) output_file = self.base_directory / f"{self.base_directory.name}-structure.{mode}" - output_path = pathlib.Path(output_path) if output_path else output_file + output_path = Path(output_path) if output_path else output_file if mode == 'json': - self.generate_json_structure(self.base_directory, output_path) + self.generate_json_structure(output_path) elif mode == 'csv': - self.generate_dataframe_structure(self.base_directory, output_path) + self.generate_dataframe_structure(output_path) def is_func(node): @@ -82,8 +85,9 @@ def is_func(node): def main(): - repo_parser = RepoParser() - repo_parser.generate_structure("/Users/alexanderwu/git/mg1/metagpt", "/Users/alexanderwu/git/mg1/mg1-structure.csv", mode='csv') + repo_parser = RepoParser(base_directory=CONFIG.workspace_path / "web_2048") + symbols = repo_parser.generate_symbols() + logger.info(pformat(symbols)) if __name__ == '__main__': diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 171af47f0..e3f36b50d 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -11,7 +11,7 @@ from collections import OrderedDict from pathlib import Path from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks -from metagpt.actions.SummarizeCode import SummarizeCode +from metagpt.actions.summarize_code import SummarizeCode from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.roles import Role @@ -74,8 +74,8 @@ class Engineer(Role): super().__init__(name, profile, goal, constraints) self._init_actions([WriteCode]) self.use_code_review = use_code_review - if self.use_code_review: - self._init_actions([WriteCode, WriteCodeReview]) + # if self.use_code_review: + # self._init_actions([WriteCode, WriteCodeReview]) self._watch([WriteTasks]) self.todos = [] self.n_borg = n_borg @@ -93,8 +93,8 @@ class Engineer(Role): @classmethod def parse_workspace(cls, system_design_msg: Message) -> str: if system_design_msg.instruct_content: - return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip('"') - return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) + return system_design_msg.instruct_content.dict().get("project_name").strip().strip("'").strip('"') + return CodeParser.parse_str(block="project_name", text=system_design_msg.content) def get_workspace(self) -> Path: msg = self._rc.memory.get_by_action(WriteDesign)[-1] @@ -182,16 +182,16 @@ class Engineer(Role): msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) for m in msg: context.append(m.content) - context_str = "\n".join(context) + context_str = "\n----------\n".join(context) # Write code code = await WriteCode().run(context=context_str, filename=todo) # Code review if self.use_code_review: - try: - rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo) - code = rewrite_code - except Exception as e: - logger.error("code review failed!", e) + # try: + rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo) + code = rewrite_code + # except Exception as e: + # logger.error("code review failed!", e) file_path = self.write_file(todo, code) msg = Message(content=code, role=self.profile, cause_by=WriteCode) self._rc.memory.add(msg) @@ -203,8 +203,8 @@ class Engineer(Role): msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) for m in msg: context.append(m.content) - context_str = "\n".join(context) - code_review_all = await SummarizeCode().run(context=context_str) + context_str = "\n----------\n".join(context) + summary = await SummarizeCode().run(context=context_str) logger.info(f"Done {self.get_workspace()} generating.") msg = Message( diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index f124646b3..313fe4aba 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -45,8 +45,8 @@ class QaEngineer(Role): @classmethod def parse_workspace(cls, system_design_msg: Message) -> str: if system_design_msg.instruct_content: - return system_design_msg.instruct_content.dict().get("Python package name") - return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) + return system_design_msg.instruct_content.dict().get("project_name") + return CodeParser.parse_str(block="project_name", text=system_design_msg.content) def get_workspace(self, return_proj_dir=True) -> Path: msg = self._rc.memory.get_by_action(WriteDesign)[-1] diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index d772c0748..5c5e7b76d 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -50,6 +50,7 @@ ROLE_TEMPLATE = """Your response should be based on the previous conversation hi {name}: {result} """ + class RoleReactMode(str, Enum): REACT = "react" BY_ORDER = "by_order" @@ -59,6 +60,7 @@ class RoleReactMode(str, Enum): def values(cls): return [item.value for item in cls] + class RoleSetting(BaseModel): """Role Settings""" name: str @@ -131,6 +133,7 @@ class Role: logger.warning(f"is_human attribute does not take effect," f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances") i = action + i.set_env(self._rc.env) i.set_prefix(self._get_prefix(), self.profile) self._actions.append(i) self._states.append(f"{idx}. {action}") @@ -172,6 +175,18 @@ class Role: """Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing.""" self._rc.env = env + def set_doc(self, content: str, filename: str): + return self._rc.env.set_doc(content, filename) + + def get_doc(self, filename: str): + return self._rc.env.get_doc(filename) + + def set(self, k, v): + return self._rc.env.set(k, v) + + def get(self, k): + return self._rc.env.get(k) + @property def profile(self): """Get the role description (position)""" diff --git a/metagpt/startup.py b/metagpt/startup.py index d8ca4072f..38f457fc2 100644 --- a/metagpt/startup.py +++ b/metagpt/startup.py @@ -15,7 +15,8 @@ def startup( code_review: bool = typer.Option(True, help="Whether to use code review."), run_tests: bool = typer.Option(False, help="Whether to enable QA for adding & running tests."), implement: bool = typer.Option(True, help="Enable or disable code implementation."), - project_name: str = typer.Option("", help="Unique project name, such as 'game_2048'"), + project_name: str = typer.Option("", help="Unique project name, such as 'game_2048'."), + inc: bool = typer.Option(False, help="Incremental mode. Use it to coop with existing repo."), ): """Run a startup. Be a boss.""" from metagpt.roles import ProductManager, Architect, ProjectManager, Engineer, QaEngineer @@ -37,9 +38,9 @@ def startup( company.hire([QaEngineer()]) company.invest(investment) - company.start_project(project_name, idea) + company.run_project(idea, project_name=project_name, inc=inc) asyncio.run(company.run(n_round=n_round)) if __name__ == "__main__": - app() + startup(idea="Make a 2048 game.") diff --git a/metagpt/team.py b/metagpt/team.py index 2332aaa46..a22a09fe4 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -42,15 +42,19 @@ class Team(BaseModel): if CONFIG.total_cost > CONFIG.max_budget: raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}') - def start_project(self, project_name, idea, send_to: str = ""): + def run_project(self, idea, send_to: str = "", project_name: str = "", inc: bool = False): """Start a project from publishing user requirement.""" self.idea = idea # If user set project_name, then use it. - self.env.repo.name = project_name + if project_name: + path = CONFIG.workspace_path / project_name + self.env.load_existing_repo(path, inc=inc) + + # Human requirement. self.env.publish_message(Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to)) def _save(self): - logger.info(self.json()) + logger.info(self.json(ensure_ascii=False)) async def run(self, n_round=3): """Run company until target round or no money""" diff --git a/tests/metagpt/actions/mock.py b/tests/metagpt/actions/mock.py index 5be1d8001..d367e253e 100644 --- a/tests/metagpt/actions/mock.py +++ b/tests/metagpt/actions/mock.py @@ -90,7 +90,7 @@ Python's in-built data structures like lists and dictionaries will be used exten For testing, we can use the PyTest framework. This is a mature full-featured Python testing tool that helps you write better programs. -## Python package name: +## project_name: ```python "adventure_game" ``` diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index fbad06acb..c06844389 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -71,7 +71,7 @@ PRD = '''## 原始需求 ``` ''' -SYSTEM_DESIGN = '''## Python package name +SYSTEM_DESIGN = '''## project_name ```python "smart_search_engine" ``` diff --git a/tests/metagpt/roles/test_ui.py b/tests/metagpt/roles/test_ui.py index d58d31bd9..ec507f75d 100644 --- a/tests/metagpt/roles/test_ui.py +++ b/tests/metagpt/roles/test_ui.py @@ -18,5 +18,5 @@ async def test_ui_role(idea: str, investment: float = 3.0, n_round: int = 5): company = Team() company.hire([ProductManager(), UI()]) company.invest(investment) - company.start_project(idea) + company.run_project(idea) await company.run(n_round=n_round) diff --git a/tests/metagpt/test_software_company.py b/tests/metagpt/test_startup.py similarity index 51% rename from tests/metagpt/test_software_company.py rename to tests/metagpt/test_startup.py index 4fc651f52..53a8d8735 100644 --- a/tests/metagpt/test_software_company.py +++ b/tests/metagpt/test_startup.py @@ -3,17 +3,26 @@ """ @Time : 2023/5/15 11:40 @Author : alexanderwu -@File : test_software_company.py +@File : test_startup.py """ import pytest +from typer.testing import CliRunner + +runner = CliRunner() from metagpt.logs import logger from metagpt.team import Team +from metagpt.startup import app @pytest.mark.asyncio async def test_team(): company = Team() - company.start_project("做一个基础搜索引擎,可以支持知识库") + company.run_project("做一个基础搜索引擎,可以支持知识库") history = await company.run(n_round=5) logger.info(history) + + +# def test_startup(): +# args = ["Make a 2048 game"] +# result = runner.invoke(app, args)