feat: +SummarizeCode, refactor project_name

This commit is contained in:
莘权 马 2023-12-04 23:04:07 +08:00
parent 838b3cfcc8
commit 9d84c8f047
31 changed files with 671 additions and 245 deletions

View file

@ -7,6 +7,7 @@
@Modified By: mashenquan, 2023/11/27.
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
2. According to the design in Section 2.2.3.5.3 of RFC 135, add incremental iteration functionality.
@Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD.
"""
import json
from pathlib import Path
@ -23,7 +24,6 @@ from metagpt.const import (
)
from metagpt.logs import logger
from metagpt.schema import Document, Documents
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.get_template import get_template
from metagpt.utils.mermaid import mermaid_to_file
@ -43,7 +43,7 @@ Requirement: Fill in the following missing information based on the context, eac
## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select appropriate open-source frameworks.
## project_name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores
## Project name: Constant text.
## 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
@ -58,15 +58,15 @@ and only output the json inside this tag, nothing else
""",
"FORMAT_EXAMPLE": """
[CONTENT]
{
{{
"Implementation approach": "We will ...",
"project_name": "snake_game",
"Project name": "{project_name}",
"File list": ["main.py"],
"Data structures and interfaces": '
classDiagram
class Game{
class Game{{
+int score
}
}}
...
Game "1" -- "1" Food: has
',
@ -77,7 +77,7 @@ and only output the json inside this tag, nothing else
G->>M: end game
',
"Anything UNCLEAR": "The requirement is clear to me."
}
}}
[/CONTENT]
""",
},
@ -96,7 +96,7 @@ 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.
## project_name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores
## Project name: Constant text.
## 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
@ -112,9 +112,9 @@ ATTENTION: Output carefully referenced "Format example" in format.
## Implementation approach
We will ...
## project_name
## Project name
```python
"snake_game"
"{project_name}"
```
## File list
@ -151,7 +151,7 @@ The requirement is clear to me.
OUTPUT_MAPPING = {
"Implementation approach": (str, ...),
"project_name": (str, ...),
"Project name": (str, ...),
"File list": (List[str], ...),
"Data structures and interfaces": (str, ...),
"Program call flow": (str, ...),
@ -173,7 +173,7 @@ ATTENTION: Output carefully referenced "Old Design" in format.
## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework.
## project_name: Provide as Plain text, concise and clear, characters only use a combination of all lowercase and underscores
## Project name: Constant text "{project_name}".
## 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
@ -229,50 +229,21 @@ class WriteDesign(Action):
async def _new_system_design(self, context, format=CONFIG.prompt_format):
prompt_template, format_example = get_template(templates, format)
format_example = format_example.format(project_name=CONFIG.project_name)
prompt = prompt_template.format(context=context, format_example=format_example)
system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format)
self._rename_project_name(system_design=system_design)
await self._rename_workspace(system_design)
return system_design
async def _merge(self, prd_doc, system_design_doc, format=CONFIG.prompt_format):
prompt = MERGE_PROMPT.format(old_design=system_design_doc.content, context=prd_doc.content)
prompt = MERGE_PROMPT.format(
old_design=system_design_doc.content, context=prd_doc.content, project_name=CONFIG.project_name
)
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
self._rename_project_name(system_design=system_design)
system_design_doc.content = system_design.instruct_content.json(ensure_ascii=False)
return system_design_doc
@staticmethod
def _rename_project_name(system_design):
# fix project_name, we can't system_design.instruct_content.python_package_name = "xxx" since "project_name"
# contain space, have to use setattr
if CONFIG.project_name:
setattr(
system_design.instruct_content,
"project_name",
CONFIG.project_name,
)
return
setattr(
system_design.instruct_content,
"project_name",
system_design.instruct_content.dict()["project_name"].strip().strip("'").strip('"'),
)
@staticmethod
async def _rename_workspace(system_design):
if CONFIG.project_path: # Updating on the old version has already been specified if it's valid. According to
# Section 2.2.3.10 of RFC 135
return
if isinstance(system_design, ActionOutput):
ws_name = system_design.instruct_content.dict()["project_name"]
else:
ws_name = CodeParser.parse_str(block="project_name", text=system_design)
CONFIG.git_repo.rename_root(ws_name)
async def _update_system_design(self, filename, prds_file_repo, system_design_file_repo) -> Document:
prd = await prds_file_repo.get(filename)
old_system_design_doc = await system_design_file_repo.get(filename)
@ -296,10 +267,10 @@ class WriteDesign(Action):
@staticmethod
async def _save_data_api_design(design_doc):
m = json.loads(design_doc.content)
data_api_design = m.get("Data structures and interface definitions")
data_api_design = m.get("Data structures and interfaces")
if not data_api_design:
return
pathname = CONFIG.git_repo.workdir / Path(DATA_API_DESIGN_FILE_REPO) / Path(design_doc.filename).with_suffix("")
pathname = CONFIG.git_repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("")
await WriteDesign._save_mermaid_file(data_api_design, pathname)
logger.info(f"Save class view to {str(pathname)}")

View file

@ -3,7 +3,7 @@
"""
@Time : 2023/11/20
@Author : mashenquan
@File : git_repository.py
@File : prepare_documents.py
@Desc: PrepareDocuments Action: initialize project folder and add new requirements to docs/requirements.txt.
RFC 135 2.2.3.5.1.
"""
@ -26,7 +26,10 @@ class PrepareDocuments(Action):
if not CONFIG.git_repo:
# Create and initialize the workspace folder, initialize the Git environment.
project_name = CONFIG.project_name or FileRepository.new_filename()
workdir = Path(CONFIG.project_path or DEFAULT_WORKSPACE_ROOT / project_name)
workdir = CONFIG.project_path
if not workdir and CONFIG.workspace:
workdir = Path(CONFIG.workspace) / project_name
workdir = Path(workdir or DEFAULT_WORKSPACE_ROOT / project_name)
if not CONFIG.inc and workdir.exists():
shutil.rmtree(workdir)
CONFIG.git_repo = GitRepository()

View file

@ -183,6 +183,10 @@ MERGE_PROMPT = """
## Old Tasks
{old_tasks}
-----
## Format example
{format_example}
-----
Role: You are a project manager; The goal is to merge the new PRD/technical design content from 'Context' into 'Old Tasks.' Based on this merged result, break down tasks, give a task list, and analyze task dependencies to start with the prerequisite modules.
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 '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote.
@ -201,7 +205,7 @@ Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD W
## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs.
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like "Old Tasks" format,
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like "Format example" format,
and only output the json inside this tag, nothing else
"""
@ -264,7 +268,9 @@ class WriteTasks(Action):
return rsp
async def _merge(self, system_design_doc, task_doc, format=CONFIG.prompt_format) -> Document:
prompt = MERGE_PROMPT.format(context=system_design_doc.content, old_tasks=task_doc.content)
_, format_example = get_template(templates, format)
prompt = MERGE_PROMPT.format(context=system_design_doc.content, old_tasks=task_doc.content,
format_example=format_example)
rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format)
task_doc.content = rsp.instruct_content.json(ensure_ascii=False)
return task_doc

View file

@ -3,12 +3,15 @@
"""
@Author : alexanderwu
@File : summarize_code.py
@Modified By: mashenquan, 2023/12/5. Archive the summarization content of issue discovery for use in WriteCode.
"""
from pathlib import Path
from tenacity import retry, stop_after_attempt, wait_fixed
from metagpt.actions.action import Action
from metagpt.config import CONFIG
from metagpt.const import SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO
from metagpt.logs import logger
from metagpt.utils.file_repository import FileRepository
@ -95,8 +98,10 @@ class SummarizeCode(Action):
return code_rsp
async def run(self):
design_doc = await FileRepository.get_file(self.context.design_filename)
task_doc = await FileRepository.get_file(self.context.task_filename)
design_pathname = Path(self.context.design_filename)
design_doc = await FileRepository.get_file(filename=design_pathname.name, relative_path=SYSTEM_DESIGN_FILE_REPO)
task_pathname = Path(self.context.task_filename)
task_doc = await FileRepository.get_file(filename=task_pathname.name, relative_path=TASK_FILE_REPO)
src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace)
code_blocks = []
for filename in self.context.codes_filenames:

View file

@ -15,13 +15,13 @@
RunCodeResult to standardize and unify parameter passing between WriteCode, RunCode, and DebugError.
"""
from tenacity import retry, stop_after_attempt, wait_fixed
from metagpt.actions.action import Action
from metagpt.const import TEST_OUTPUTS_FILE_REPO
from metagpt.config import CONFIG
from metagpt.const import CODE_SUMMARIES_FILE_REPO, TEST_OUTPUTS_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import CodingContext, RunCodeResult
from metagpt.schema import CodingContext, Document, RunCodeResult
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
@ -50,6 +50,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
# Debug logs
```text
{logs}
{summary_log}
```
-----
@ -90,18 +92,26 @@ class WriteCode(Action):
test_doc = await FileRepository.get_file(
filename="test_" + coding_context.filename + ".json", relative_path=TEST_OUTPUTS_FILE_REPO
)
summary_doc = None
if coding_context.design_doc.filename:
summary_doc = await FileRepository.get_file(
filename=coding_context.design_doc.filename, relative_path=CODE_SUMMARIES_FILE_REPO
)
logs = ""
if test_doc:
test_detail = RunCodeResult.loads(test_doc.content)
logs = test_detail.stderr
prompt = PROMPT_TEMPLATE.format(
design=coding_context.design_doc.content,
tasks=coding_context.task_doc.content,
code=coding_context.code_doc.content,
tasks=coding_context.task_doc.content if coding_context.task_doc else "",
code=coding_context.code_doc.content if coding_context.code_doc else "",
logs=logs,
filename=self.context.filename,
summary_log=summary_doc.content if summary_doc else "",
)
logger.info(f"Writing {coding_context.filename}..")
code = await self.write_code(prompt)
if not coding_context.code_doc:
coding_context.code_doc = Document(filename=coding_context.filename, root_path=CONFIG.src_workspace)
coding_context.code_doc.content = code
return coding_context

View file

@ -108,10 +108,11 @@ class WriteCodeReview(Action):
k = CONFIG.code_review_k_times or 1
for i in range(k):
format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename)
task_content = self.context.task_doc.content if self.context.task_doc else ""
context = "\n----------\n".join(
[
"```text\n" + self.context.design_doc.content + "```\n",
"```text\n" + self.context.task_doc.content + "```\n",
"```text\n" + task_content + "```\n",
"```python\n" + self.context.code_doc.content + "```\n",
]
)

View file

@ -8,6 +8,7 @@
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
2. According to the design in Section 2.2.3.5.2 of RFC 135, add incremental iteration functionality.
3. Move the document storage operations related to WritePRD from the save operation of WriteDesign.
@Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD.
"""
from __future__ import annotations
@ -27,6 +28,7 @@ from metagpt.const import (
)
from metagpt.logs import logger
from metagpt.schema import Document, Documents
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.get_template import get_template
from metagpt.utils.mermaid import mermaid_to_file
@ -53,7 +55,7 @@ ATTENTION: Output carefully referenced "Format example" in format.
{{
"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.
"Project Name": "{project_name}", # str, if it's empty, name it with snake case style, 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.
@ -85,9 +87,10 @@ and only output the json inside this tag, nothing else
""",
"FORMAT_EXAMPLE": """
[CONTENT]
{
{{
"Language": "",
"Original Requirements": "",
"Project Name": "{project_name}",
"Search Information": "",
"Requirements": "",
"Product Goals": [],
@ -111,7 +114,7 @@ and only output the json inside this tag, nothing else
"Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]],
"UI Design draft": "",
"Anything UNCLEAR": "",
}
}}
[/CONTENT]
""",
},
@ -228,6 +231,7 @@ There are no unclear points.
OUTPUT_MAPPING = {
"Language": (str, ...),
"Original Requirements": (str, ...),
"Project Name": (str, ...),
"Product Goals": (List[str], ...),
"User Stories": (List[str], ...),
"Competitive Analysis": (List[str], ...),
@ -270,7 +274,7 @@ ATTENTION: Output carefully referenced "Old PRD" in format.
{{
"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.
"Project Name": "{project_name}", # str, if it's empty, name it with snake case style, 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.
@ -320,6 +324,7 @@ class WritePRD(Action):
if not prd_doc:
continue
change_files.docs[prd_doc.filename] = prd_doc
logger.info(f"REWRITE PRD:{prd_doc.filename}")
# If there is no existing PRD, generate one using 'docs/requirement.txt'.
if not change_files.docs:
prd_doc = await self._update_prd(
@ -327,6 +332,7 @@ class WritePRD(Action):
)
if prd_doc:
change_files.docs[prd_doc.filename] = prd_doc
logger.info(f"NEW PRD:{prd_doc.filename}")
# Once all files under 'docs/prds/' have been compared with the newly added requirements, trigger the
# 'publish' message to transition the workflow to the next stage. This design allows room for global
# optimization in subsequent steps.
@ -343,32 +349,36 @@ class WritePRD(Action):
# logger.info(format)
prompt_template, format_example = get_template(templates, format)
project_name = CONFIG.project_name if CONFIG.project_name else ""
format_example = format_example.format(project_name=project_name)
# logger.info(prompt_template)
# logger.info(format_example)
prompt = prompt_template.format(
requirements=requirements, search_information=info, format_example=format_example
requirements=requirements, search_information=info, format_example=format_example, project_name=project_name
)
# logger.info(prompt)
# prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
await self._rename_workspace(prd)
return prd
async def _is_relative_to(self, new_requirement_doc, old_prd_doc) -> bool:
m = json.loads(old_prd_doc.content)
if m.get("Original Requirements") == new_requirement_doc.content:
# There have been no changes in the requirements, so they are considered unrelated.
return False
prompt = IS_RELATIVE_PROMPT.format(old_prd=old_prd_doc.content, requirements=new_requirement_doc.content)
res = await self._aask(prompt=prompt)
logger.info(f"[{new_requirement_doc.root_relative_path}, {old_prd_doc.root_relative_path}]: {res}")
logger.info(f"REQ-RELATIVE:[{new_requirement_doc.root_relative_path}, {old_prd_doc.root_relative_path}]: {res}")
if "YES" in res:
return True
return False
async def _merge(self, new_requirement_doc, prd_doc, format=CONFIG.prompt_format) -> Document:
prompt = MERGE_PROMPT.format(requirements=new_requirement_doc.content, old_prd=prd_doc.content)
if not CONFIG.project_name:
CONFIG.project_name = Path(CONFIG.project_path).name
prompt = MERGE_PROMPT.format(
requirements=new_requirement_doc.content, old_prd=prd_doc.content, project_name=CONFIG.project_name
)
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
prd_doc.content = prd.instruct_content.json(ensure_ascii=False)
await self._rename_workspace(prd)
return prd_doc
async def _update_prd(self, requirement_doc, prd_doc, prds_file_repo, *args, **kwargs) -> Document | None:
@ -404,3 +414,19 @@ class WritePRD(Action):
@staticmethod
async def _save_pdf(prd_doc):
await FileRepository.save_as(doc=prd_doc, with_suffix=".md", relative_path=PRD_PDF_FILE_REPO)
@staticmethod
async def _rename_workspace(prd):
if CONFIG.project_path: # Updating on the old version has already been specified if it's valid. According to
# Section 2.2.3.10 of RFC 135
if not CONFIG.project_name:
CONFIG.project_name = Path(CONFIG.project_path).name
return
if not CONFIG.project_name:
if isinstance(prd, ActionOutput):
ws_name = prd.instruct_content.dict()["Project Name"]
else:
ws_name = CodeParser.parse_str(block="Project Name", text=prd)
CONFIG.project_name = ws_name
CONFIG.git_repo.rename_root(CONFIG.project_name)

View file

@ -9,8 +9,9 @@
"""
from metagpt.actions.action import Action
from metagpt.config import CONFIG
from metagpt.const import TEST_CODES_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import TestingContext
from metagpt.schema import Document, TestingContext
from metagpt.utils.common import CodeParser
PROMPT_TEMPLATE = """
@ -52,6 +53,10 @@ class WriteTest(Action):
return code
async def run(self, *args, **kwargs) -> TestingContext:
if not self.context.test_doc:
self.context.test_doc = Document(
filename="test_" + self.context.code_doc.filename, root_path=TEST_CODES_FILE_REPO
)
prompt = PROMPT_TEMPLATE.format(
code_to_test=self.context.code_doc.content,
test_file_name=self.context.test_doc.filename,

View file

@ -7,6 +7,7 @@
@Modified By: mashenquan, 2023-11-1. According to Section 2.2.1 and 2.2.2 of RFC 116, added key definitions for
common properties in the Message.
@Modified By: mashenquan, 2023-11-27. Defines file repository paths according to Section 2.2.3.4 of RFC 135.
@Modified By: mashenquan, 2023/12/5. Add directories for code summarization..
"""
import contextvars
import os
@ -87,5 +88,7 @@ PRD_PDF_FILE_REPO = "resources/prd"
TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks"
TEST_CODES_FILE_REPO = "tests"
TEST_OUTPUTS_FILE_REPO = "test_outputs"
CODE_SUMMARIES_FILE_REPO = "docs/code_summaries"
CODE_SUMMARIES_PDF_FILE_REPO = "resources/code_summaries"
YAPI_URL = "http://yapi.deepwisdomai.com/"

View file

@ -38,7 +38,7 @@ class BaseGPTAPI(BaseChatbot):
rsp = self.completion(message)
return self.get_choice_text(rsp)
async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str:
async def aask(self, msg: str, system_msgs: Optional[list[str]] = None, stream=True) -> str:
if system_msgs:
message = (
self._system_msgs(system_msgs) + [self._user_msg(msg)]
@ -49,7 +49,7 @@ class BaseGPTAPI(BaseChatbot):
message = (
[self._default_system_msg(), self._user_msg(msg)] if self.use_system_prompt else [self._user_msg(msg)]
)
rsp = await self.acompletion_text(message, stream=True)
rsp = await self.acompletion_text(message, stream=stream)
logger.debug(message)
# logger.debug(rsp)
return rsp

View file

@ -13,17 +13,25 @@
@Modified By: mashenquan, 2023-11-27.
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
2. According to the design in Section 2.2.3.5.5 of RFC 135, add incremental iteration functionality.
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
of SummarizeCode.
"""
from __future__ import annotations
import json
from collections import defaultdict
from pathlib import Path
from typing import Set
from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks
from metagpt.actions.summarize_code import SummarizeCode
from metagpt.config import CONFIG
from metagpt.const import MESSAGE_ROUTE_TO_NONE, SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO
from metagpt.const import (
CODE_SUMMARIES_FILE_REPO,
CODE_SUMMARIES_PDF_FILE_REPO,
SYSTEM_DESIGN_FILE_REPO,
TASK_FILE_REPO,
)
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import (
@ -33,6 +41,16 @@ from metagpt.schema import (
Documents,
Message,
)
from metagpt.utils.common import any_to_str, any_to_str_set
IS_PASS_PROMPT = """
{context}
----
Does the above log indicate anything that needs to be done?
If there are any tasks to be completed, please answer 'NO' along with the to-do list in JSON format;
otherwise, answer 'YES' in JSON format.
"""
class Engineer(Role):
@ -60,7 +78,7 @@ class Engineer(Role):
"""Initializes the Engineer role with given attributes."""
super().__init__(name, profile, goal, constraints)
self.use_code_review = use_code_review
self._watch([WriteTasks])
self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview])
self.code_todos = []
self.summarize_todos = []
self.n_borg = n_borg
@ -105,39 +123,88 @@ class Engineer(Role):
if self._rc.todo is None:
return None
if isinstance(self._rc.todo, WriteCode):
changed_files = await self._act_sp_with_cr(review=self.use_code_review)
# Unit tests only.
if CONFIG.REQA_FILENAME and CONFIG.REQA_FILENAME not in changed_files:
changed_files.add(CONFIG.REQA_FILENAME)
return Message(
content="\n".join(changed_files),
role=self.profile,
cause_by=WriteCodeReview if self.use_code_review else WriteCode,
send_to="Edward", # The name of QaEngineer
)
return await self._act_write_code()
if isinstance(self._rc.todo, SummarizeCode):
summaries = []
for todo in self.summarize_todos:
summary = await todo.run()
summaries.append(summary.json(ensure_ascii=False))
return await self._act_summarize()
return None
async def _act_write_code(self):
changed_files = await self._act_sp_with_cr(review=self.use_code_review)
return Message(
content="\n".join(changed_files),
role=self.profile,
cause_by=WriteCodeReview if self.use_code_review else WriteCode,
send_to=self,
sent_from=self,
)
async def _act_summarize(self):
code_summaries_file_repo = CONFIG.git_repo.new_file_repository(CODE_SUMMARIES_FILE_REPO)
code_summaries_pdf_file_repo = CONFIG.git_repo.new_file_repository(CODE_SUMMARIES_PDF_FILE_REPO)
tasks = []
src_relative_path = CONFIG.src_workspace.relative_to(CONFIG.git_repo.workdir)
for todo in self.summarize_todos:
summary = await todo.run()
summary_filename = Path(todo.context.design_filename).with_suffix(".md").name
dependencies = {todo.context.design_filename, todo.context.task_filename}
for filename in todo.context.codes_filenames:
rpath = src_relative_path / filename
dependencies.add(str(rpath))
await code_summaries_pdf_file_repo.save(
filename=summary_filename, content=summary, dependencies=dependencies
)
is_pass, reason = await self._is_pass(summary)
if not is_pass:
todo.context.reason = reason
tasks.append(todo.context.dict())
await code_summaries_file_repo.save(
filename=Path(todo.context.design_filename).name,
content=todo.context.json(),
dependencies=dependencies,
)
else:
await code_summaries_file_repo.delete(filename=Path(todo.context.design_filename).name)
logger.info(f"--max-auto-summarize-code={CONFIG.max_auto_summarize_code}")
if not tasks or CONFIG.max_auto_summarize_code == 0:
return Message(
content="\n".join(summaries),
content="",
role=self.profile,
cause_by=SummarizeCode,
send_to=MESSAGE_ROUTE_TO_NONE,
sent_from=self,
send_to="Edward", # The name of QaEngineer
)
return None
# The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited.
# This parameter is used for debugging the workflow.
CONFIG.max_auto_summarize_code -= 1 if CONFIG.max_auto_summarize_code > 0 else 0
return Message(
content=json.dumps(tasks), role=self.profile, cause_by=SummarizeCode, send_to=self, sent_from=self
)
async def _is_pass(self, summary) -> (str, str):
rsp = await self._llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False)
logger.info(rsp)
if "YES" in rsp:
return True, rsp
return False, rsp
async def _think(self) -> Action | None:
if not CONFIG.src_workspace:
CONFIG.src_workspace = CONFIG.git_repo.workdir / CONFIG.git_repo.workdir.name
if not self.code_todos:
await self._new_code_actions()
elif not self.summarize_todos:
await self._new_summarize_actions()
else:
write_code_filters = any_to_str_set([WriteTasks, SummarizeCode])
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
if not self._rc.news:
return None
return self._rc.todo # For agent store
msg = self._rc.news[0]
if msg.cause_by in write_code_filters:
logger.info(f"TODO WriteCode:{msg.json()}")
await self._new_code_actions()
return self._rc.todo
if msg.cause_by in summarize_code_filters and msg.sent_from == any_to_str(self):
logger.info(f"TODO SummarizeCode:{msg.json()}")
await self._new_summarize_actions()
return self._rc.todo
return None
@staticmethod
async def _new_coding_context(
@ -151,9 +218,9 @@ class Engineer(Role):
design_doc = None
for i in dependencies:
if str(i.parent) == TASK_FILE_REPO:
task_doc = task_file_repo.get(i.filename)
task_doc = await task_file_repo.get(i.name)
elif str(i.parent) == SYSTEM_DESIGN_FILE_REPO:
design_doc = design_file_repo.get(i.filename)
design_doc = await design_file_repo.get(i.name)
context = CodingContext(filename=filename, design_doc=design_doc, task_doc=task_doc, code_doc=old_code_doc)
return context
@ -216,16 +283,13 @@ class Engineer(Role):
async def _new_summarize_actions(self):
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
changed_src_files = src_file_repo.changed_files
src_files = src_file_repo.all_files
# Generate a SummarizeCode action for each pair of (system_design_doc, task_doc).
summarizations = {}
for filename in changed_src_files:
dependencies = src_file_repo.get_dependency(filename=filename)
summarizations = defaultdict(list)
for filename in src_files:
dependencies = await src_file_repo.get_dependency(filename=filename)
ctx = CodeSummarizeContext.loads(filenames=dependencies)
if ctx not in summarizations:
summarizations[ctx] = set()
srcs = summarizations.get(ctx)
srcs.add(filename)
summarizations[ctx].append(filename)
for ctx, filenames in summarizations.items():
ctx.codes_filenames = filenames
self.summarize_todos.append(SummarizeCode(context=ctx, llm=self._llm))

View file

@ -11,10 +11,13 @@
WriteTest/RunCode/DebugError object, rather than passing them in when calling the run function.
2. According to Section 2.2.3.5.7 of RFC 135, change the method of transferring files from using the Message
to using file references.
@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.summarize_code import SummarizeCode
from metagpt.config import CONFIG
from metagpt.const import (
MESSAGE_ROUTE_TO_NONE,
@ -40,13 +43,16 @@ class QaEngineer(Role):
self._init_actions(
[WriteTest]
) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates
self._watch([WriteCode, WriteCodeReview, WriteTest, RunCode, DebugError])
self._watch([SummarizeCode, WriteTest, RunCode, DebugError])
self.test_round = 0
self.test_round_allowed = test_round_allowed
async def _write_test(self, message: Message) -> None:
changed_files = message.content.splitlines()
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
changed_files = set(src_file_repo.changed_files.keys())
# Unit tests only.
if CONFIG.reqa_file and CONFIG.reqa_file not in changed_files:
changed_files.add(CONFIG.reqa_file)
tests_file_repo = CONFIG.git_repo.new_file_repository(TEST_CODES_FILE_REPO)
for filename in changed_files:
# write tests
@ -146,7 +152,7 @@ class QaEngineer(Role):
)
return result_msg
code_filters = any_to_str_set({WriteCode, WriteCodeReview})
code_filters = any_to_str_set({SummarizeCode})
test_filters = any_to_str_set({WriteTest, DebugError})
run_filters = any_to_str_set({RunCode})
for msg in self._rc.news:

View file

@ -284,9 +284,10 @@ class Role:
instruct_content=response.instruct_content,
role=self.profile,
cause_by=self._rc.todo,
sent_from=self,
)
else:
msg = Message(content=response, role=self.profile, cause_by=self._rc.todo)
msg = Message(content=response, role=self.profile, cause_by=self._rc.todo, sent_from=self)
self._rc.memory.add(msg)
return msg

View file

@ -48,9 +48,9 @@ class Document(BaseModel):
Represents a document.
"""
root_path: str
filename: str
content: Optional[str] = None
root_path: str = ""
filename: str = ""
content: str = ""
def get_meta(self) -> Document:
"""Get metadata of the document.
@ -260,8 +260,8 @@ class MessageQueue:
class CodingContext(BaseModel):
filename: str
design_doc: Document
task_doc: Document
code_doc: Document
task_doc: Optional[Document]
code_doc: Optional[Document]
@staticmethod
def loads(val: str) -> CodingContext | None:
@ -275,7 +275,7 @@ class CodingContext(BaseModel):
class TestingContext(BaseModel):
filename: str
code_doc: Document
test_doc: Document
test_doc: Optional[Document]
@staticmethod
def loads(val: str) -> TestingContext | None:
@ -324,10 +324,11 @@ class RunCodeResult(BaseModel):
class CodeSummarizeContext(BaseModel):
design_filename: str = ""
task_filename: str = ""
codes_filenames: Set[str] = Field(default_factory=set)
codes_filenames: List[str] = Field(default_factory=list)
reason: str = ""
@staticmethod
def loads(filenames: Set) -> CodeSummarizeContext:
def loads(filenames: List) -> CodeSummarizeContext:
ctx = CodeSummarizeContext()
for filename in filenames:
if Path(filename).is_relative_to(SYSTEM_DESIGN_FILE_REPO):
@ -337,3 +338,6 @@ class CodeSummarizeContext(BaseModel):
ctx.task_filename = str(filename)
continue
return ctx
def __hash__(self):
return hash((self.design_filename, self.task_filename))

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import asyncio
from pathlib import Path
import typer
@ -24,6 +25,10 @@ def startup(
help="Specify the directory path of the old version project to fulfill the " "incremental requirements.",
),
reqa_file: str = typer.Option(default="", help="Specify the source file name for rewriting the quality test code."),
max_auto_summarize_code: int = typer.Option(
default=-1,
help="The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited. This parameter is used for debugging the workflow.",
),
):
"""Run a startup. Be a boss."""
from metagpt.roles import (
@ -36,10 +41,14 @@ def startup(
from metagpt.team import Team
# Use in the PrepareDocuments action according to Section 2.2.3.5.1 of RFC 135.
CONFIG.project_path = project_path
if project_path:
inc = True
project_name = project_name or Path(project_path).name
CONFIG.project_name = project_name
CONFIG.inc = inc
CONFIG.project_path = project_path
CONFIG.reqa_file = reqa_file
CONFIG.max_auto_summarize_code = max_auto_summarize_code
company = Team()
company.hire(

View file

@ -14,6 +14,7 @@ from typing import Set
import aiofiles
from metagpt.config import CONFIG
from metagpt.logs import logger
@ -81,7 +82,7 @@ class DependencyFile:
if persist:
await self.save()
async def get(self, filename: Path | str, persist=False):
async def get(self, filename: Path | str, persist=True):
"""Get dependencies for a file asynchronously.
:param filename: The filename or path.
@ -91,7 +92,7 @@ class DependencyFile:
if persist:
await self.load()
root = self._filename.parent
root = CONFIG.git_repo.workdir
try:
key = Path(filename).relative_to(root)
except ValueError:

View file

@ -151,6 +151,17 @@ class FileRepository:
relative_files[str(rf)] = ct
return relative_files
@property
def all_files(self) -> List:
"""Get a dictionary of all files in the repository.
The dictionary includes file paths relative to the current FileRepository.
:return: A dictionary where keys are file paths and values are file information.
:rtype: List
"""
return self._git_repo.get_files(relative_path=self._relative_path)
def get_change_dir_files(self, dir: Path | str) -> List:
"""Get the files in a directory that have changed.
@ -259,3 +270,25 @@ class FileRepository:
"""
file_repo = CONFIG.git_repo.new_file_repository(relative_path=relative_path)
return await file_repo.save_doc(doc=doc, with_suffix=with_suffix, dependencies=dependencies)
async def delete(self, filename: Path | str):
"""Delete a file from the file repository.
This method deletes a file from the file repository based on the provided filename.
:param filename: The name or path of the file to be deleted.
:type filename: Path or str
"""
pathname = self.workdir / filename
if not pathname.exists():
return
pathname.unlink(missing_ok=True)
dependency_file = await self._git_repo.get_dependency()
await dependency_file.update(filename=pathname, dependencies=None)
logger.info(f"remove dependency key: {str(pathname)}")
@staticmethod
async def delete_file(filename: Path | str, relative_path: Path | str = "."):
file_repo = CONFIG.git_repo.new_file_repository(relative_path=relative_path)
await file_repo.delete(filename=filename)

View file

@ -8,10 +8,11 @@
"""
from __future__ import annotations
import os
import shutil
from enum import Enum
from pathlib import Path
from typing import Dict
from typing import Dict, List
from git.repo import Repo
from git.repo.fun import is_git_dir
@ -196,10 +197,46 @@ class GitRepository:
if new_path.exists():
logger.info(f"Delete directory {str(new_path)}")
shutil.rmtree(new_path)
self.workdir.rename(new_path)
try:
shutil.move(src=str(self.workdir), dst=str(new_path))
except Exception as e:
logger.warning(f"Move {str(self.workdir)} to {str(new_path)} error: {e}")
logger.info(f"Rename directory {str(self.workdir)} to {str(new_path)}")
self._repository = Repo(new_path)
def get_files(self, relative_path: Path | str, root_relative_path: Path | str = None) -> List:
"""Retrieve a list of files in the specified relative path.
The method returns a list of file paths relative to the current FileRepository.
:param relative_path: The relative path within the repository.
:type relative_path: Path or str
:param root_relative_path: The root relative path within the repository.
:type root_relative_path: Path or str
:return: A list of file paths in the specified directory.
:rtype: List[str]
"""
try:
relative_path = Path(relative_path).relative_to(self.workdir)
except ValueError:
relative_path = Path(relative_path)
if not root_relative_path:
root_relative_path = Path(self.workdir) / relative_path
files = []
try:
directory_path = Path(self.workdir) / relative_path
for file_path in directory_path.iterdir():
if file_path.is_file():
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)
files.extend(subfolder_files)
except Exception as e:
logger.error(f"Error: {e}")
return files
if __name__ == "__main__":
path = DEFAULT_WORKSPACE_ROOT / "git"

View file

@ -12,8 +12,11 @@ from unittest.mock import Mock
import pytest
from metagpt.config import CONFIG
from metagpt.const import DEFAULT_WORKSPACE_ROOT
from metagpt.logs import logger
from metagpt.provider.openai_api import OpenAIGPTAPI as GPTAPI
from metagpt.utils.git_repository import GitRepository
class Context:
@ -68,3 +71,16 @@ def proxy():
server = asyncio.get_event_loop().run_until_complete(asyncio.start_server(handle_client, "127.0.0.1", 0))
return "http://{}:{}".format(*server.sockets[0].getsockname())
# init & dispose git repo
@pytest.fixture(scope="session", autouse=True)
def setup_and_teardown_git_repo(request):
CONFIG.git_repo = GitRepository(local_path=DEFAULT_WORKSPACE_ROOT / "unittest")
# Destroy git repo at the end of the test session.
def fin():
CONFIG.git_repo.delete_repository()
# Register the function for destroying the environment.
request.addfinalizer(fin)

View file

@ -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.
## project_name:
## Project Name:
```python
"adventure_game"
```

View file

@ -4,17 +4,19 @@
@Time : 2023/5/11 17:46
@Author : alexanderwu
@File : test_debug_error.py
@Modifiled By: mashenquan, 2023-12-6. According to RFC 135
"""
import uuid
import pytest
from metagpt.actions.debug_error import DebugError
from metagpt.config import CONFIG
from metagpt.const import TEST_CODES_FILE_REPO, TEST_OUTPUTS_FILE_REPO
from metagpt.schema import RunCodeContext, RunCodeResult
from metagpt.utils.file_repository import FileRepository
EXAMPLE_MSG_CONTENT = '''
---
## Development Code File Name
player.py
## Development Code
```python
CODE_CONTENT = '''
from typing import List
from deck import Deck
from card import Card
@ -58,12 +60,9 @@ class Player:
if self.score > 21 and any(card.rank == 'A' for card in self.hand):
self.score -= 10
return self.score
'''
```
## Test File Name
test_player.py
## Test Code
```python
TEST_CONTENT = """
import unittest
from blackjack_game.player import Player
from blackjack_game.deck import Deck
@ -114,42 +113,41 @@ class TestPlayer(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
```
## Running Command
python tests/test_player.py
## Running Output
standard output: ;
standard errors: ..F..
======================================================================
FAIL: test_player_calculate_score_with_multiple_aces (__main__.TestPlayer)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests/test_player.py", line 46, in test_player_calculate_score_with_multiple_aces
self.assertEqual(player.score, 12)
AssertionError: 22 != 12
----------------------------------------------------------------------
Ran 5 tests in 0.007s
FAILED (failures=1)
;
## instruction:
The error is in the development code, specifically in the calculate_score method of the Player class. The method is not correctly handling the case where there are multiple Aces in the player's hand. The current implementation only subtracts 10 from the score once if the score is over 21 and there's an Ace in the hand. However, in the case of multiple Aces, it should subtract 10 for each Ace until the score is 21 or less.
## File To Rewrite:
player.py
## Status:
FAIL
## Send To:
Engineer
---
'''
"""
@pytest.mark.asyncio
async def test_debug_error():
debug_error = DebugError("debug_error")
CONFIG.src_workspace = CONFIG.git_repo.workdir / uuid.uuid4().hex
ctx = RunCodeContext(
code_filename="player.py",
test_filename="test_player.py",
command=["python", "tests/test_player.py"],
output_filename="output.log",
)
file_name, rewritten_code = await debug_error.run(context=EXAMPLE_MSG_CONTENT)
await FileRepository.save_file(filename=ctx.code_filename, content=CODE_CONTENT, relative_path=CONFIG.src_workspace)
await FileRepository.save_file(filename=ctx.test_filename, content=TEST_CONTENT, relative_path=TEST_CODES_FILE_REPO)
output_data = RunCodeResult(
stdout=";",
stderr="",
summary="======================================================================\n"
"FAIL: test_player_calculate_score_with_multiple_aces (__main__.TestPlayer)\n"
"----------------------------------------------------------------------\n"
"Traceback (most recent call last):\n"
' File "tests/test_player.py", line 46, in test_player_calculate_score_'
"with_multiple_aces\n"
" self.assertEqual(player.score, 12)\nAssertionError: 22 != 12\n\n"
"----------------------------------------------------------------------\n"
"Ran 5 tests in 0.007s\n\nFAILED (failures=1)\n;\n",
)
await FileRepository.save_file(
filename=ctx.output_filename, content=output_data.json(), relative_path=TEST_OUTPUTS_FILE_REPO
)
debug_error = DebugError(context=ctx)
assert "class Player" in rewritten_code # rewrite the same class
assert "while self.score > 21" in rewritten_code # a key logic to rewrite to (original one is "if self.score > 12")
rsp = await debug_error.run()
assert "class Player" in rsp # rewrite the same class
# a key logic to rewrite to (original one is "if self.score > 12")
assert "while self.score > 21" in rsp

View file

@ -4,33 +4,27 @@
@Time : 2023/5/11 19:26
@Author : alexanderwu
@File : test_design_api.py
@Modifiled By: mashenquan, 2023-12-6. According to RFC 135
"""
import pytest
from metagpt.actions.design_api import WriteDesign
from metagpt.const import PRDS_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import Message
from metagpt.utils.file_repository import FileRepository
from tests.metagpt.actions.mock import PRD_SAMPLE
@pytest.mark.asyncio
async def test_design_api():
prd = "我们需要一个音乐播放器,它应该有播放、暂停、上一曲、下一曲等功能。"
inputs = ["我们需要一个音乐播放器,它应该有播放、暂停、上一曲、下一曲等功能。", PRD_SAMPLE]
for prd in inputs:
await FileRepository.save_file("new_prd.txt", content=prd, relative_path=PRDS_FILE_REPO)
design_api = WriteDesign("design_api")
design_api = WriteDesign("design_api")
result = await design_api.run([Message(content=prd, instruct_content=None)])
logger.info(result)
result = await design_api.run([Message(content=prd, instruct_content=None)])
logger.info(result)
assert result
@pytest.mark.asyncio
async def test_design_api_calculator():
prd = PRD_SAMPLE
design_api = WriteDesign("design_api")
result = await design_api.run([Message(content=prd, instruct_content=None)])
logger.info(result)
assert result
assert result

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/12/6
@Author : mashenquan
@File : test_prepare_documents.py
@Desc: Unit test for prepare_documents.py
"""
import pytest
from metagpt.actions.prepare_documents import PrepareDocuments
from metagpt.config import CONFIG
from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME
from metagpt.schema import Message
from metagpt.utils.file_repository import FileRepository
@pytest.mark.asyncio
async def test_prepare_documents():
msg = Message(content="New user requirements balabala...")
if CONFIG.git_repo:
CONFIG.git_repo.delete_repository()
CONFIG.git_repo = None
await PrepareDocuments().run(with_messages=[msg])
assert CONFIG.git_repo
doc = await FileRepository.get_file(filename=REQUIREMENT_FILENAME, relative_path=DOCS_FILE_REPO)
assert doc
assert doc.content == msg.content

View file

@ -4,10 +4,12 @@
@Time : 2023/5/11 17:46
@Author : alexanderwu
@File : test_run_code.py
@Modifiled By: mashenquan, 2023-12-6. According to RFC 135
"""
import pytest
from metagpt.actions.run_code import RunCode
from metagpt.schema import RunCodeContext
@pytest.mark.asyncio
@ -35,37 +37,29 @@ async def test_run_script():
@pytest.mark.asyncio
async def test_run():
action = RunCode()
result = await action.run(mode="text", code="print('Hello, World')")
assert "PASS" in result
result = await action.run(
mode="script",
code="echo 'Hello World'",
code_file_name="",
test_code="",
test_file_name="",
command=["echo", "Hello World"],
working_directory=".",
additional_python_paths=[],
)
assert "PASS" in result
@pytest.mark.asyncio
async def test_run_failure():
action = RunCode()
result = await action.run(mode="text", code="result = 1 / 0")
assert "FAIL" in result
result = await action.run(
mode="script",
code='python -c "print(1/0)"',
code_file_name="",
test_code="",
test_file_name="",
command=["python", "-c", "print(1/0)"],
working_directory=".",
additional_python_paths=[],
)
assert "FAIL" in result
inputs = [
(RunCodeContext(mode="text", code_filename="a.txt", code="print('Hello, World')"), "PASS"),
(
RunCodeContext(
mode="script",
code_filename="a.sh",
code="echo 'Hello World'",
command=["echo", "Hello World"],
working_directory=".",
),
"PASS",
),
(
RunCodeContext(
mode="script",
code_filename="a.py",
code='python -c "print(1/0)"',
command=["python", "-c", "print(1/0)"],
working_directory=".",
),
"FAIL",
),
]
for ctx, result in inputs:
rsp = await RunCode(context=ctx).run()
assert result in rsp.summary

View file

@ -0,0 +1,195 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/11 17:46
@Author : mashenquan
@File : test_summarize_code.py
@Modifiled By: mashenquan, 2023-12-6. Unit test for summarize_code.py
"""
import pytest
from metagpt.actions.summarize_code import SummarizeCode
from metagpt.config import CONFIG
from metagpt.const import SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO
from metagpt.logs import logger
from metagpt.schema import CodeSummarizeContext
from metagpt.utils.file_repository import FileRepository
DESIGN_CONTENT = """
{"Implementation approach": "To develop this snake game, we will use the Python language and choose the Pygame library. Pygame is an open-source Python module collection specifically designed for writing video games. It provides functionalities such as displaying images and playing sounds, making it suitable for creating intuitive and responsive user interfaces. We will ensure efficient game logic to prevent any delays during gameplay. The scoring system will be simple, with the snake gaining points for each food it eats. We will use Pygame's event handling system to implement pause and resume functionality, as well as high-score tracking. The difficulty will increase by speeding up the snake's movement. In the initial version, we will focus on single-player mode and consider adding multiplayer mode and customizable skins in future updates. Based on the new requirement, we will also add a moving obstacle that appears randomly. If the snake eats this obstacle, the game will end. If the snake does not eat the obstacle, it will disappear after 5 seconds. For this, we need to add mechanisms for obstacle generation, movement, and disappearance in the game logic.", "Project_name": "snake_game", "File list": ["main.py", "game.py", "snake.py", "food.py", "obstacle.py", "scoreboard.py", "constants.py", "assets/styles.css", "assets/index.html"], "Data structures and interfaces": "```mermaid\n classDiagram\n class Game{\n +int score\n +int speed\n +bool game_over\n +bool paused\n +Snake snake\n +Food food\n +Obstacle obstacle\n +Scoreboard scoreboard\n +start_game() void\n +pause_game() void\n +resume_game() void\n +end_game() void\n +increase_difficulty() void\n +update() void\n +render() void\n Game()\n }\n class Snake{\n +list body_parts\n +str direction\n +bool grow\n +move() void\n +grow() void\n +check_collision() bool\n Snake()\n }\n class Food{\n +tuple position\n +spawn() void\n Food()\n }\n class Obstacle{\n +tuple position\n +int lifetime\n +bool active\n +spawn() void\n +move() void\n +check_collision() bool\n +disappear() void\n Obstacle()\n }\n class Scoreboard{\n +int high_score\n +update_score(int) void\n +reset_score() void\n +load_high_score() void\n +save_high_score() void\n Scoreboard()\n }\n class Constants{\n }\n Game \"1\" -- \"1\" Snake: has\n Game \"1\" -- \"1\" Food: has\n Game \"1\" -- \"1\" Obstacle: has\n Game \"1\" -- \"1\" Scoreboard: has\n ```", "Program call flow": "```sequenceDiagram\n participant M as Main\n participant G as Game\n participant S as Snake\n participant F as Food\n participant O as Obstacle\n participant SB as Scoreboard\n M->>G: start_game()\n loop game loop\n G->>S: move()\n G->>S: check_collision()\n G->>F: spawn()\n G->>O: spawn()\n G->>O: move()\n G->>O: check_collision()\n G->>O: disappear()\n G->>SB: update_score(score)\n G->>G: update()\n G->>G: render()\n alt if paused\n M->>G: pause_game()\n M->>G: resume_game()\n end\n alt if game_over\n G->>M: end_game()\n end\n end\n```", "Anything UNCLEAR": "There is no need for further clarification as the requirements are already clear."}
"""
TASK_CONTENT = """
{"Required Python third-party packages": ["pygame==2.0.1"], "Required Other language third-party packages": ["No third-party packages required for other languages."], "Full API spec": "\n openapi: 3.0.0\n info:\n title: Snake Game API\n version: \"1.0.0\"\n paths:\n /start:\n get:\n summary: Start the game\n responses:\n '200':\n description: Game started successfully\n /pause:\n get:\n summary: Pause the game\n responses:\n '200':\n description: Game paused successfully\n /resume:\n get:\n summary: Resume the game\n responses:\n '200':\n description: Game resumed successfully\n /end:\n get:\n summary: End the game\n responses:\n '200':\n description: Game ended successfully\n /score:\n get:\n summary: Get the current score\n responses:\n '200':\n description: Current score retrieved successfully\n /highscore:\n get:\n summary: Get the high score\n responses:\n '200':\n description: High score retrieved successfully\n components: {}\n ", "Logic Analysis": [["constants.py", "Contains all the constant values like screen size, colors, game speeds, etc. This should be implemented first as it provides the base values for other components."], ["snake.py", "Contains the Snake class with methods for movement, growth, and collision detection. It is dependent on constants.py for configuration values."], ["food.py", "Contains the Food class responsible for spawning food items on the screen. It is dependent on constants.py for configuration values."], ["obstacle.py", "Contains the Obstacle class with methods for spawning, moving, and disappearing of obstacles, as well as collision detection with the snake. It is dependent on constants.py for configuration values."], ["scoreboard.py", "Contains the Scoreboard class for updating, resetting, loading, and saving high scores. It may use constants.py for configuration values and depends on the game's scoring logic."], ["game.py", "Contains the main Game class which includes the game loop and methods for starting, pausing, resuming, and ending the game. It is dependent on snake.py, food.py, obstacle.py, and scoreboard.py."], ["main.py", "The entry point of the game that initializes the game and starts the game loop. It is dependent on game.py."]], "Task list": ["constants.py", "snake.py", "food.py", "obstacle.py", "scoreboard.py", "game.py", "main.py"], "Shared Knowledge": "\n 'constants.py' should contain all the necessary configurations for the game, such as screen dimensions, color definitions, and speed settings. These constants will be used across multiple files, ensuring consistency and ease of updates. Ensure that the Pygame library is initialized correctly in 'main.py' before starting the game loop. Also, make sure that the game's state is managed properly when pausing and resuming the game.\n ", "Anything UNCLEAR": "The interaction between the 'obstacle.py' and the game loop needs to be clearly defined to ensure obstacles appear and disappear correctly. The lifetime of the obstacle and its random movement should be implemented in a way that does not interfere with the game's performance."}
"""
FOOD_PY = """
## food.py
import random
class Food:
def __init__(self):
self.position = (0, 0)
def generate(self):
x = random.randint(0, 9)
y = random.randint(0, 9)
self.position = (x, y)
def get_position(self):
return self.position
"""
GAME_PY = """
## game.py
import pygame
from snake import Snake
from food import Food
class Game:
def __init__(self):
self.score = 0
self.level = 1
self.snake = Snake()
self.food = Food()
def start_game(self):
pygame.init()
self.initialize_game()
self.game_loop()
def initialize_game(self):
self.score = 0
self.level = 1
self.snake.reset()
self.food.generate()
def game_loop(self):
game_over = False
while not game_over:
self.update()
self.draw()
self.handle_events()
self.check_collision()
self.increase_score()
self.increase_level()
if self.snake.is_collision():
game_over = True
self.game_over()
def update(self):
self.snake.move()
def draw(self):
self.snake.draw()
self.food.draw()
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.snake.change_direction("UP")
elif event.key == pygame.K_DOWN:
self.snake.change_direction("DOWN")
elif event.key == pygame.K_LEFT:
self.snake.change_direction("LEFT")
elif event.key == pygame.K_RIGHT:
self.snake.change_direction("RIGHT")
def check_collision(self):
if self.snake.get_head() == self.food.get_position():
self.snake.grow()
self.food.generate()
def increase_score(self):
self.score += 1
def increase_level(self):
if self.score % 10 == 0:
self.level += 1
def game_over(self):
print("Game Over")
self.initialize_game()
"""
MAIN_PY = """
## main.py
import pygame
from game import Game
def main():
pygame.init()
game = Game()
game.start_game()
if __name__ == "__main__":
main()
"""
SNAKE_PY = """
## snake.py
import pygame
class Snake:
def __init__(self):
self.body = [(0, 0)]
self.direction = (1, 0)
def move(self):
head = self.body[0]
dx, dy = self.direction
new_head = (head[0] + dx, head[1] + dy)
self.body.insert(0, new_head)
self.body.pop()
def change_direction(self, direction):
if direction == "UP":
self.direction = (0, -1)
elif direction == "DOWN":
self.direction = (0, 1)
elif direction == "LEFT":
self.direction = (-1, 0)
elif direction == "RIGHT":
self.direction = (1, 0)
def grow(self):
tail = self.body[-1]
dx, dy = self.direction
new_tail = (tail[0] - dx, tail[1] - dy)
self.body.append(new_tail)
def get_head(self):
return self.body[0]
def get_body(self):
return self.body[1:]
"""
@pytest.mark.asyncio
async def test_summarize_code():
CONFIG.src_workspace = CONFIG.git_repo.workdir / "src"
await FileRepository.save_file(filename="1.json", relative_path=SYSTEM_DESIGN_FILE_REPO, content=DESIGN_CONTENT)
await FileRepository.save_file(filename="1.json", relative_path=TASK_FILE_REPO, content=TASK_CONTENT)
await FileRepository.save_file(filename="food.py", relative_path=CONFIG.src_workspace, content=FOOD_PY)
await FileRepository.save_file(filename="game.py", relative_path=CONFIG.src_workspace, content=GAME_PY)
await FileRepository.save_file(filename="main.py", relative_path=CONFIG.src_workspace, content=MAIN_PY)
await FileRepository.save_file(filename="snake.py", relative_path=CONFIG.src_workspace, content=SNAKE_PY)
src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace)
all_files = src_file_repo.all_files
ctx = CodeSummarizeContext(design_filename="1.json", task_filename="1.json", codes_filenames=all_files)
action = SummarizeCode(context=ctx)
rsp = await action.run()
assert rsp
logger.info(rsp)

View file

@ -4,26 +4,31 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : test_write_code.py
@Modifiled By: mashenquan, 2023-12-6. According to RFC 135
"""
import pytest
from metagpt.actions.write_code import WriteCode
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.schema import CodingContext, Document
from tests.metagpt.actions.mock import TASKS_2, WRITE_CODE_PROMPT_SAMPLE
@pytest.mark.asyncio
async def test_write_code():
api_design = "设计一个名为'add'的函数,该函数接受两个整数作为输入,并返回它们的和。"
write_code = WriteCode("write_code")
context = CodingContext(
filename="task_filename.py", design_doc=Document(content="设计一个名为'add'的函数,该函数接受两个整数作为输入,并返回它们的和。")
)
doc = Document(content=context.json())
write_code = WriteCode(context=doc)
code = await write_code.run(api_design)
logger.info(code)
code = await write_code.run()
logger.info(code.json())
# 我们不能精确地预测生成的代码,但我们可以检查某些关键字
assert "def add" in code
assert "return" in code
assert "def add" in code.code_doc.content
assert "return" in code.code_doc.content
@pytest.mark.asyncio

View file

@ -8,6 +8,8 @@
import pytest
from metagpt.actions.write_code_review import WriteCodeReview
from metagpt.document import Document
from metagpt.schema import CodingContext
@pytest.mark.asyncio
@ -16,13 +18,15 @@ async def test_write_code_review(capfd):
def add(a, b):
return a +
"""
# write_code_review = WriteCodeReview("write_code_review")
context = CodingContext(
filename="math.py", design_doc=Document(content="编写一个从a加b的函数返回a+b"), code_doc=Document(content=code)
)
code = await WriteCodeReview().run(context="编写一个从a加b的函数返回a+b", code=code, filename="math.py")
context = await WriteCodeReview(context=context).run()
# 我们不能精确地预测生成的代码评审,但我们可以检查返回的是否为字符串
assert isinstance(code, str)
assert len(code) > 0
assert isinstance(context.code_doc.content, str)
assert len(context.code_doc.content) > 0
captured = capfd.readouterr()
print(f"输出内容: {captured.out}")

View file

@ -9,19 +9,24 @@
import pytest
from metagpt.actions import UserRequirement
from metagpt.config import CONFIG
from metagpt.const import DOCS_FILE_REPO, PRDS_FILE_REPO, REQUIREMENT_FILENAME
from metagpt.logs import logger
from metagpt.roles.product_manager import ProductManager
from metagpt.schema import Message
from metagpt.utils.file_repository import FileRepository
@pytest.mark.asyncio
async def test_write_prd():
product_manager = ProductManager()
requirements = "开发一个基于大语言模型与私有知识库的搜索引擎,希望可以基于大语言模型进行搜索总结"
await FileRepository.save_file(filename=REQUIREMENT_FILENAME, content=requirements, relative_path=DOCS_FILE_REPO)
prd = await product_manager.run(Message(content=requirements, cause_by=UserRequirement))
logger.info(requirements)
logger.info(prd)
# Assert the prd is not None or empty
assert prd is not None
assert prd != ""
assert prd.content != ""
assert CONFIG.git_repo.new_file_repository(relative_path=PRDS_FILE_REPO).changed_files

View file

@ -9,6 +9,7 @@ import pytest
from metagpt.actions.write_test import WriteTest
from metagpt.logs import logger
from metagpt.schema import Document, TestingContext
@pytest.mark.asyncio
@ -24,22 +25,17 @@ async def test_write_test():
def generate(self, max_y: int, max_x: int):
self.position = (random.randint(1, max_y - 1), random.randint(1, max_x - 1))
"""
context = TestingContext(filename="food.py", code_doc=Document(filename="food.py", content=code))
write_test = WriteTest(context=context)
write_test = WriteTest()
test_code = await write_test.run(
code_to_test=code,
test_file_name="test_food.py",
source_file_path="/some/dummy/path/cli_snake_game/cli_snake_game/food.py",
workspace="/some/dummy/path/cli_snake_game",
)
logger.info(test_code)
context = await write_test.run()
logger.info(context.json())
# We cannot exactly predict the generated test cases, but we can check if it is a string and if it is not empty
assert isinstance(test_code, str)
assert "from cli_snake_game.food import Food" in test_code
assert "class TestFood(unittest.TestCase)" in test_code
assert "def test_generate" in test_code
assert isinstance(context.test_doc.content, str)
assert "from food import Food" in context.test_doc.content
assert "class TestFood(unittest.TestCase)" in context.test_doc.content
assert "def test_generate" in context.test_doc.content
@pytest.mark.asyncio

View file

@ -71,7 +71,7 @@ PRD = '''## 原始需求
```
'''
SYSTEM_DESIGN = """## project_name
SYSTEM_DESIGN = """## Project name
```python
"smart_search_engine"
```

View file

@ -43,6 +43,10 @@ async def test_file_repo():
assert {"a.txt"} == await file_repo.get_changed_dependency("b.txt")
await file_repo.save("d/e.txt", "EEE")
assert ["d/e.txt"] == file_repo.get_change_dir_files("d")
assert set(file_repo.all_files) == {"a.txt", "b.txt", "d/e.txt"}
await file_repo.delete("d/e.txt")
await file_repo.delete("d/e.txt") # delete twice
assert set(file_repo.all_files) == {"a.txt", "b.txt"}
git_repo.delete_repository()