mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-02 14:45:17 +02:00
feat: new/inc/patch pass
This commit is contained in:
parent
5c416a1f31
commit
ee0b9d2039
21 changed files with 512 additions and 237 deletions
|
|
@ -24,20 +24,15 @@ from collections import defaultdict
|
|||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from metagpt.actions import (
|
||||
Action,
|
||||
UserRequirement,
|
||||
WriteCode,
|
||||
WriteCodeReview,
|
||||
WriteTasks,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.actions import UserRequirement, WriteCode, WriteCodeReview, WriteTasks
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange
|
||||
from metagpt.const import (
|
||||
BUGFIX_FILENAME,
|
||||
CODE_PLAN_AND_CHANGE_FILE_REPO,
|
||||
MESSAGE_ROUTE_TO_SELF,
|
||||
REQUIREMENT_FILENAME,
|
||||
|
|
@ -63,6 +58,7 @@ from metagpt.utils.common import (
|
|||
init_python_folder,
|
||||
)
|
||||
from metagpt.utils.git_repository import ChangeType
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
||||
IS_PASS_PROMPT = """
|
||||
{context}
|
||||
|
|
@ -100,6 +96,8 @@ class Engineer(Role):
|
|||
summarize_todos: list = []
|
||||
next_todo_action: str = ""
|
||||
n_summarize: int = 0
|
||||
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
|
||||
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -139,14 +137,20 @@ class Engineer(Role):
|
|||
coding_context = await todo.run()
|
||||
# Code review
|
||||
if review:
|
||||
action = WriteCodeReview(i_context=coding_context, context=self.context, llm=self.llm)
|
||||
action = WriteCodeReview(
|
||||
i_context=coding_context,
|
||||
repo=self.repo,
|
||||
input_args=self.input_args,
|
||||
context=self.context,
|
||||
llm=self.llm,
|
||||
)
|
||||
self._init_action(action)
|
||||
coding_context = await action.run()
|
||||
|
||||
dependencies = {coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path}
|
||||
if self.config.inc:
|
||||
dependencies.add(coding_context.code_plan_and_change_doc.root_relative_path)
|
||||
await self.project_repo.srcs.save(
|
||||
await self.repo.srcs.save(
|
||||
filename=coding_context.filename,
|
||||
dependencies=list(dependencies),
|
||||
content=coding_context.code_doc.content,
|
||||
|
|
@ -186,9 +190,9 @@ class Engineer(Role):
|
|||
summary_filename = Path(todo.i_context.design_filename).with_suffix(".md").name
|
||||
dependencies = {todo.i_context.design_filename, todo.i_context.task_filename}
|
||||
for filename in todo.i_context.codes_filenames:
|
||||
rpath = self.project_repo.src_relative_path / filename
|
||||
rpath = self.repo.src_relative_path / filename
|
||||
dependencies.add(str(rpath))
|
||||
await self.project_repo.resources.code_summary.save(
|
||||
await self.repo.resources.code_summary.save(
|
||||
filename=summary_filename, content=summary, dependencies=dependencies
|
||||
)
|
||||
is_pass, reason = await self._is_pass(summary)
|
||||
|
|
@ -196,23 +200,39 @@ class Engineer(Role):
|
|||
todo.i_context.reason = reason
|
||||
tasks.append(todo.i_context.model_dump())
|
||||
|
||||
await self.project_repo.docs.code_summary.save(
|
||||
await self.repo.docs.code_summary.save(
|
||||
filename=Path(todo.i_context.design_filename).name,
|
||||
content=todo.i_context.model_dump_json(),
|
||||
dependencies=dependencies,
|
||||
)
|
||||
else:
|
||||
await self.project_repo.docs.code_summary.delete(filename=Path(todo.i_context.design_filename).name)
|
||||
await self.repo.docs.code_summary.delete(filename=Path(todo.i_context.design_filename).name)
|
||||
self.summarize_todos = []
|
||||
logger.info(f"--max-auto-summarize-code={self.config.max_auto_summarize_code}")
|
||||
if not tasks or self.config.max_auto_summarize_code == 0:
|
||||
self.n_summarize = 0
|
||||
kvs = self.input_args.model_dump()
|
||||
kvs["changed_src_filenames"] = [
|
||||
str(self.repo.srcs.workdir / i) for i in list(self.repo.srcs.changed_files.keys())
|
||||
]
|
||||
if self.repo.docs.code_plan_and_change.changed_files:
|
||||
kvs["changed_code_plan_and_change_filenames"] = [
|
||||
str(self.repo.docs.code_plan_and_change.workdir / i)
|
||||
for i in list(self.repo.docs.code_plan_and_change.changed_files.keys())
|
||||
]
|
||||
if self.repo.docs.code_summary.changed_files:
|
||||
kvs["changed_code_summary_filenames"] = [
|
||||
str(self.repo.docs.code_summary.workdir / i)
|
||||
for i in list(self.repo.docs.code_summary.changed_files.keys())
|
||||
]
|
||||
return AIMessage(
|
||||
content=f"Coding is complete. The source code is at {self.project_repo.workdir.name}/{self.project_repo.srcs.root_path}, containing: "
|
||||
content=f"Coding is complete. The source code is at {self.repo.workdir.name}/{self.repo.srcs.root_path}, containing: "
|
||||
+ "\n".join(
|
||||
list(self.project_repo.resources.code_summary.changed_files.keys())
|
||||
+ list(self.project_repo.srcs.changed_files.keys())
|
||||
list(self.repo.resources.code_summary.changed_files.keys())
|
||||
+ list(self.repo.srcs.changed_files.keys())
|
||||
+ list(self.repo.resources.code_plan_and_change.changed_files.keys())
|
||||
),
|
||||
instruct_content=AIMessage.create_instruct_value(kvs=kvs, class_name="SummarizeCodeOutput"),
|
||||
cause_by=SummarizeCode,
|
||||
send_to="Edward", # The name of QaEngineer
|
||||
)
|
||||
|
|
@ -227,15 +247,15 @@ class Engineer(Role):
|
|||
code_plan_and_change = node.instruct_content.model_dump_json()
|
||||
dependencies = {
|
||||
REQUIREMENT_FILENAME,
|
||||
str(self.project_repo.docs.prd.root_path / self.rc.todo.i_context.prd_filename),
|
||||
str(self.project_repo.docs.system_design.root_path / self.rc.todo.i_context.design_filename),
|
||||
str(self.project_repo.docs.task.root_path / self.rc.todo.i_context.task_filename),
|
||||
str(Path(self.rc.todo.i_context.prd_filename).relative_to(self.repo.workdir)),
|
||||
str(Path(self.rc.todo.i_context.design_filename).relative_to(self.repo.workdir)),
|
||||
str(Path(self.rc.todo.i_context.task_filename).relative_to(self.repo.workdir)),
|
||||
}
|
||||
code_plan_and_change_filepath = Path(self.rc.todo.i_context.design_filename)
|
||||
await self.project_repo.docs.code_plan_and_change.save(
|
||||
await self.repo.docs.code_plan_and_change.save(
|
||||
filename=code_plan_and_change_filepath.name, content=code_plan_and_change, dependencies=dependencies
|
||||
)
|
||||
await self.project_repo.resources.code_plan_and_change.save(
|
||||
await self.repo.resources.code_plan_and_change.save(
|
||||
filename=code_plan_and_change_filepath.with_suffix(".md").name,
|
||||
content=node.content,
|
||||
dependencies=dependencies,
|
||||
|
|
@ -250,10 +270,11 @@ class Engineer(Role):
|
|||
return True, rsp
|
||||
return False, rsp
|
||||
|
||||
async def _think(self) -> Action | None:
|
||||
async def _think(self) -> bool:
|
||||
if not self.rc.news:
|
||||
return None
|
||||
return False
|
||||
msg = self.rc.news[0]
|
||||
input_args = msg.instruct_content
|
||||
if msg.cause_by == any_to_str(UserRequirement):
|
||||
self.rc.todo = PrepareDocuments(
|
||||
key_descriptions={
|
||||
|
|
@ -263,42 +284,47 @@ class Engineer(Role):
|
|||
context=self.context,
|
||||
send_to=any_to_str(self),
|
||||
)
|
||||
return self.rc.todo
|
||||
|
||||
if not self.src_workspace:
|
||||
self.src_workspace = get_project_srcs_path(self.project_repo.workdir)
|
||||
self.repo = ProjectRepo(input_args.project_path)
|
||||
self.input_args = input_args
|
||||
return bool(self.rc.todo)
|
||||
elif msg.cause_by in {any_to_str(WriteTasks), any_to_str(FixBug)}:
|
||||
self.input_args = input_args
|
||||
self.repo = ProjectRepo(input_args.project_path)
|
||||
if self.repo.src_relative_path is None:
|
||||
path = get_project_srcs_path(self.repo.workdir)
|
||||
self.repo.with_src_path(path)
|
||||
write_plan_and_change_filters = any_to_str_set([PrepareDocuments, WriteTasks, FixBug])
|
||||
write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode])
|
||||
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
|
||||
if self.config.inc and msg.cause_by in write_plan_and_change_filters:
|
||||
logger.debug(f"TODO WriteCodePlanAndChange:{msg.model_dump_json()}")
|
||||
await self._new_code_plan_and_change_action(cause_by=msg.cause_by)
|
||||
return self.rc.todo
|
||||
return bool(self.rc.todo)
|
||||
if msg.cause_by in write_code_filters:
|
||||
logger.debug(f"TODO WriteCode:{msg.model_dump_json()}")
|
||||
await self._new_code_actions()
|
||||
return self.rc.todo
|
||||
return bool(self.rc.todo)
|
||||
if msg.cause_by in summarize_code_filters and msg.sent_from == any_to_str(self):
|
||||
logger.debug(f"TODO SummarizeCode:{msg.model_dump_json()}")
|
||||
await self._new_summarize_actions()
|
||||
return self.rc.todo
|
||||
return None
|
||||
return bool(self.rc.todo)
|
||||
return False
|
||||
|
||||
async def _new_coding_context(self, filename, dependency) -> Optional[CodingContext]:
|
||||
old_code_doc = await self.project_repo.srcs.get(filename)
|
||||
old_code_doc = await self.repo.srcs.get(filename)
|
||||
if not old_code_doc:
|
||||
old_code_doc = Document(root_path=str(self.project_repo.src_relative_path), filename=filename, content="")
|
||||
old_code_doc = Document(root_path=str(self.repo.src_relative_path), filename=filename, content="")
|
||||
dependencies = {Path(i) for i in await dependency.get(old_code_doc.root_relative_path)}
|
||||
task_doc = None
|
||||
design_doc = None
|
||||
code_plan_and_change_doc = await self._get_any_code_plan_and_change() if await self._is_fixbug() else None
|
||||
for i in dependencies:
|
||||
if str(i.parent) == TASK_FILE_REPO:
|
||||
task_doc = await self.project_repo.docs.task.get(i.name)
|
||||
task_doc = await self.repo.docs.task.get(i.name)
|
||||
elif str(i.parent) == SYSTEM_DESIGN_FILE_REPO:
|
||||
design_doc = await self.project_repo.docs.system_design.get(i.name)
|
||||
design_doc = await self.repo.docs.system_design.get(i.name)
|
||||
elif str(i.parent) == CODE_PLAN_AND_CHANGE_FILE_REPO:
|
||||
code_plan_and_change_doc = await self.project_repo.docs.code_plan_and_change.get(i.name)
|
||||
code_plan_and_change_doc = await self.repo.docs.code_plan_and_change.get(i.name)
|
||||
if not task_doc or not design_doc:
|
||||
if filename == "__init__.py": # `__init__.py` created by `init_python_folder`
|
||||
return None
|
||||
|
|
@ -318,34 +344,66 @@ class Engineer(Role):
|
|||
if not context:
|
||||
return None # `__init__.py` created by `init_python_folder`
|
||||
coding_doc = Document(
|
||||
root_path=str(self.project_repo.src_relative_path), filename=filename, content=context.model_dump_json()
|
||||
root_path=str(self.repo.src_relative_path), filename=filename, content=context.model_dump_json()
|
||||
)
|
||||
return coding_doc
|
||||
|
||||
async def _new_code_actions(self):
|
||||
bug_fix = await self._is_fixbug()
|
||||
# Prepare file repos
|
||||
changed_src_files = self.project_repo.srcs.changed_files
|
||||
changed_src_files = self.repo.srcs.changed_files
|
||||
if self.context.kwargs.src_filename:
|
||||
changed_src_files = {self.context.kwargs.src_filename: ChangeType.UNTRACTED}
|
||||
if bug_fix:
|
||||
changed_src_files = self.project_repo.srcs.all_files
|
||||
changed_task_files = self.project_repo.docs.task.changed_files
|
||||
changed_src_files = self.repo.srcs.all_files
|
||||
changed_files = Documents()
|
||||
# Recode caused by upstream changes.
|
||||
for filename in changed_task_files:
|
||||
design_doc = await self.project_repo.docs.system_design.get(filename)
|
||||
task_doc = await self.project_repo.docs.task.get(filename)
|
||||
code_plan_and_change_doc = await self.project_repo.docs.code_plan_and_change.get(filename)
|
||||
if hasattr(self.input_args, "changed_task_filenames"):
|
||||
changed_task_filenames = self.input_args.changed_task_filenames
|
||||
else:
|
||||
changed_task_filenames = [
|
||||
str(self.repo.docs.task.workdir / i) for i in list(self.repo.docs.task.changed_files.keys())
|
||||
]
|
||||
for filename in changed_task_filenames:
|
||||
task_filename = Path(filename)
|
||||
design_filename = None
|
||||
if hasattr(self.input_args, "changed_system_design_filenames"):
|
||||
changed_system_design_filenames = self.input_args.changed_system_design_filenames
|
||||
else:
|
||||
changed_system_design_filenames = [
|
||||
str(self.repo.docs.system_design.workdir / i)
|
||||
for i in list(self.repo.docs.system_design.changed_files.keys())
|
||||
]
|
||||
for i in changed_system_design_filenames:
|
||||
if task_filename.name == Path(i).name:
|
||||
design_filename = Path(i)
|
||||
break
|
||||
code_plan_and_change_filename = None
|
||||
if hasattr(self.input_args, "changed_code_plan_and_change_filenames"):
|
||||
changed_code_plan_and_change_filenames = self.input_args.changed_code_plan_and_change_filenames
|
||||
else:
|
||||
changed_code_plan_and_change_filenames = [
|
||||
str(self.repo.docs.code_plan_and_change.workdir / i)
|
||||
for i in list(self.repo.docs.code_plan_and_change.changed_files.keys())
|
||||
]
|
||||
for i in changed_code_plan_and_change_filenames:
|
||||
if task_filename.name == Path(i).name:
|
||||
code_plan_and_change_filename = Path(i)
|
||||
break
|
||||
design_doc = await Document.load(filename=design_filename, project_path=self.repo.workdir)
|
||||
task_doc = await Document.load(filename=task_filename, project_path=self.repo.workdir)
|
||||
code_plan_and_change_doc = await Document.load(
|
||||
filename=code_plan_and_change_filename, project_path=self.repo.workdir
|
||||
)
|
||||
task_list = self._parse_tasks(task_doc)
|
||||
await self._init_python_folder(task_list)
|
||||
for task_filename in task_list:
|
||||
if self.context.kwargs.src_filename and task_filename != self.context.kwargs.src_filename:
|
||||
continue
|
||||
old_code_doc = await self.project_repo.srcs.get(task_filename)
|
||||
old_code_doc = await self.repo.srcs.get(task_filename)
|
||||
if not old_code_doc:
|
||||
old_code_doc = Document(
|
||||
root_path=str(self.project_repo.src_relative_path), filename=task_filename, content=""
|
||||
root_path=str(self.repo.src_relative_path), filename=task_filename, content=""
|
||||
)
|
||||
if not code_plan_and_change_doc:
|
||||
context = CodingContext(
|
||||
|
|
@ -360,7 +418,7 @@ class Engineer(Role):
|
|||
code_plan_and_change_doc=code_plan_and_change_doc,
|
||||
)
|
||||
coding_doc = Document(
|
||||
root_path=str(self.project_repo.src_relative_path),
|
||||
root_path=str(self.repo.src_relative_path),
|
||||
filename=task_filename,
|
||||
content=context.model_dump_json(),
|
||||
)
|
||||
|
|
@ -371,10 +429,11 @@ class Engineer(Role):
|
|||
)
|
||||
changed_files.docs[task_filename] = coding_doc
|
||||
self.code_todos = [
|
||||
WriteCode(i_context=i, context=self.context, llm=self.llm) for i in changed_files.docs.values()
|
||||
WriteCode(i_context=i, repo=self.repo, input_args=self.input_args, context=self.context, llm=self.llm)
|
||||
for i in changed_files.docs.values()
|
||||
]
|
||||
# Code directly modified by the user.
|
||||
dependency = await self.git_repo.get_dependency()
|
||||
dependency = await self.repo.git_repo.get_dependency()
|
||||
for filename in changed_src_files:
|
||||
if filename in changed_files.docs:
|
||||
continue
|
||||
|
|
@ -382,24 +441,30 @@ class Engineer(Role):
|
|||
if not coding_doc:
|
||||
continue # `__init__.py` created by `init_python_folder`
|
||||
changed_files.docs[filename] = coding_doc
|
||||
self.code_todos.append(WriteCode(i_context=coding_doc, context=self.context, llm=self.llm))
|
||||
self.code_todos.append(
|
||||
WriteCode(
|
||||
i_context=coding_doc, repo=self.repo, input_args=self.input_args, context=self.context, llm=self.llm
|
||||
)
|
||||
)
|
||||
|
||||
if self.code_todos:
|
||||
self.set_todo(self.code_todos[0])
|
||||
|
||||
async def _new_summarize_actions(self):
|
||||
src_files = self.project_repo.srcs.all_files
|
||||
src_files = self.repo.srcs.all_files
|
||||
# Generate a SummarizeCode action for each pair of (system_design_doc, task_doc).
|
||||
summarizations = defaultdict(list)
|
||||
for filename in src_files:
|
||||
dependencies = await self.project_repo.srcs.get_dependency(filename=filename)
|
||||
dependencies = await self.repo.srcs.get_dependency(filename=filename)
|
||||
ctx = CodeSummarizeContext.loads(filenames=list(dependencies))
|
||||
summarizations[ctx].append(filename)
|
||||
for ctx, filenames in summarizations.items():
|
||||
if not ctx.design_filename or not ctx.task_filename:
|
||||
continue # cause by `__init__.py` which is created by `init_python_folder`
|
||||
ctx.codes_filenames = filenames
|
||||
new_summarize = SummarizeCode(i_context=ctx, context=self.context, llm=self.llm)
|
||||
new_summarize = SummarizeCode(
|
||||
i_context=ctx, repo=self.repo, input_args=self.input_args, context=self.context, llm=self.llm
|
||||
)
|
||||
for i, act in enumerate(self.summarize_todos):
|
||||
if act.i_context.task_filename == new_summarize.i_context.task_filename:
|
||||
self.summarize_todos[i] = new_summarize
|
||||
|
|
@ -412,16 +477,37 @@ class Engineer(Role):
|
|||
|
||||
async def _new_code_plan_and_change_action(self, cause_by: str):
|
||||
"""Create a WriteCodePlanAndChange action for subsequent to-do actions."""
|
||||
files = self.project_repo.all_files
|
||||
options = {}
|
||||
if cause_by != any_to_str(FixBug):
|
||||
requirement_doc = await self.project_repo.docs.get(REQUIREMENT_FILENAME)
|
||||
requirement_doc = await Document.load(filename=self.input_args.requirements_filename)
|
||||
options["requirement"] = requirement_doc.content
|
||||
else:
|
||||
fixbug_doc = await self.project_repo.docs.get(BUGFIX_FILENAME)
|
||||
fixbug_doc = await Document.load(filename=self.input_args.issue_filename)
|
||||
options["issue"] = fixbug_doc.content
|
||||
code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, **options)
|
||||
self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_ctx, context=self.context, llm=self.llm)
|
||||
# The code here is flawed: if there are multiple unrelated requirements, this piece of logic will break
|
||||
if hasattr(self.input_args, "changed_prd_filenames"):
|
||||
code_plan_and_change_ctx = CodePlanAndChangeContext(
|
||||
requirement=options.get("requirement", ""),
|
||||
issue=options.get("issue", ""),
|
||||
prd_filename=self.input_args.changed_prd_filenames[0],
|
||||
design_filename=self.input_args.changed_system_design_filenames[0],
|
||||
task_filename=self.input_args.changed_task_filenames[0],
|
||||
)
|
||||
else:
|
||||
code_plan_and_change_ctx = CodePlanAndChangeContext(
|
||||
requirement=options.get("requirement", ""),
|
||||
issue=options.get("issue", ""),
|
||||
prd_filename=str(self.repo.docs.prd.workdir / self.repo.docs.prd.all_files[0]),
|
||||
design_filename=str(self.repo.docs.system_design.workdir / self.repo.docs.system_design.all_files[0]),
|
||||
task_filename=str(self.repo.docs.task.workdir / self.repo.docs.task.all_files[0]),
|
||||
)
|
||||
self.rc.todo = WriteCodePlanAndChange(
|
||||
i_context=code_plan_and_change_ctx,
|
||||
repo=self.repo,
|
||||
input_args=self.input_args,
|
||||
context=self.context,
|
||||
llm=self.llm,
|
||||
)
|
||||
|
||||
@property
|
||||
def action_description(self) -> str:
|
||||
|
|
@ -433,17 +519,16 @@ class Engineer(Role):
|
|||
filename = Path(i)
|
||||
if filename.suffix != ".py":
|
||||
continue
|
||||
workdir = self.src_workspace / filename.parent
|
||||
workdir = self.repo.srcs.workdir / filename.parent
|
||||
await init_python_folder(workdir)
|
||||
|
||||
async def _is_fixbug(self) -> bool:
|
||||
fixbug_doc = await self.project_repo.docs.get(BUGFIX_FILENAME)
|
||||
return bool(fixbug_doc and fixbug_doc.content)
|
||||
return bool(self.input_args and hasattr(self.input_args, "issue_filename"))
|
||||
|
||||
async def _get_any_code_plan_and_change(self) -> Optional[Document]:
|
||||
changed_files = self.project_repo.docs.code_plan_and_change.changed_files
|
||||
changed_files = self.repo.docs.code_plan_and_change.changed_files
|
||||
for filename in changed_files.keys():
|
||||
doc = await self.project_repo.docs.code_plan_and_change.get(filename)
|
||||
doc = await self.repo.docs.code_plan_and_change.get(filename)
|
||||
if doc and doc.content:
|
||||
return doc
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
@Modified By: mashenquan, 2023/11/27. Add `PrepareDocuments` action according to Section 2.2.3.5.1 of RFC 135.
|
||||
"""
|
||||
|
||||
|
||||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role, RoleReactMode
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.utils.common import any_to_name, any_to_str
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class ProductManager(Role):
|
||||
|
|
@ -35,12 +37,11 @@ class ProductManager(Role):
|
|||
self.enable_memory = False
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self)), WritePRD])
|
||||
self._watch([UserRequirement, PrepareDocuments])
|
||||
self.rc.react_mode = RoleReactMode.BY_ORDER
|
||||
self.todo_action = any_to_name(WritePRD)
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
if self.git_repo and not self.config.git_reinit:
|
||||
if GitRepository.is_git_dir(self.config.project_path) and not self.config.git_reinit:
|
||||
self._set_state(1)
|
||||
else:
|
||||
self._set_state(0)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
|
||||
of SummarizeCode.
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.actions import DebugError, RunCode, UserRequirement, WriteTest
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
|
|
@ -25,9 +28,11 @@ from metagpt.schema import AIMessage, Document, Message, RunCodeContext, Testing
|
|||
from metagpt.utils.common import (
|
||||
any_to_str,
|
||||
any_to_str_set,
|
||||
get_project_srcs_path,
|
||||
init_python_folder,
|
||||
parse_recipient,
|
||||
)
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
from metagpt.utils.report import EditorReporter
|
||||
|
||||
|
||||
|
|
@ -41,6 +46,8 @@ class QaEngineer(Role):
|
|||
)
|
||||
test_round_allowed: int = 5
|
||||
test_round: int = 0
|
||||
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
|
||||
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -57,22 +64,21 @@ class QaEngineer(Role):
|
|||
self.test_round = 0
|
||||
|
||||
async def _write_test(self, message: Message) -> None:
|
||||
src_file_repo = self.project_repo.with_src_path(self.context.src_workspace).srcs
|
||||
reqa_file = self.context.kwargs.reqa_file or self.config.reqa_file
|
||||
changed_files = {reqa_file} if reqa_file else set(src_file_repo.changed_files.keys())
|
||||
changed_files = {reqa_file} if reqa_file else set(self.repo.srcs.changed_files.keys())
|
||||
for filename in changed_files:
|
||||
# write tests
|
||||
if not filename or "test" in filename:
|
||||
continue
|
||||
code_doc = await src_file_repo.get(filename)
|
||||
if not code_doc:
|
||||
code_doc = await self.repo.srcs.get(filename)
|
||||
if not code_doc or not code_doc.content:
|
||||
continue
|
||||
if not code_doc.filename.endswith(".py"):
|
||||
continue
|
||||
test_doc = await self.project_repo.tests.get("test_" + code_doc.filename)
|
||||
test_doc = await self.repo.tests.get("test_" + code_doc.filename)
|
||||
if not test_doc:
|
||||
test_doc = Document(
|
||||
root_path=str(self.project_repo.tests.root_path), filename="test_" + code_doc.filename, content=""
|
||||
root_path=str(self.repo.tests.root_path), filename="test_" + code_doc.filename, content=""
|
||||
)
|
||||
logger.info(f"Writing {test_doc.filename}..")
|
||||
context = TestingContext(filename=test_doc.filename, test_doc=test_doc, code_doc=code_doc)
|
||||
|
|
@ -81,40 +87,38 @@ class QaEngineer(Role):
|
|||
async with EditorReporter(enable_llm_stream=True) as reporter:
|
||||
await reporter.async_report({"type": "test", "filename": test_doc.filename}, "meta")
|
||||
|
||||
doc = await self.project_repo.tests.save_doc(
|
||||
doc = await self.repo.tests.save_doc(
|
||||
doc=context.test_doc, dependencies={context.code_doc.root_relative_path}
|
||||
)
|
||||
await reporter.async_report(self.project_repo.workdir / doc.root_relative_path, "path")
|
||||
await reporter.async_report(self.repo.workdir / doc.root_relative_path, "path")
|
||||
|
||||
# prepare context for run tests in next round
|
||||
run_code_context = RunCodeContext(
|
||||
command=["python", context.test_doc.root_relative_path],
|
||||
code_filename=context.code_doc.filename,
|
||||
test_filename=context.test_doc.filename,
|
||||
working_directory=str(self.project_repo.workdir),
|
||||
working_directory=str(self.repo.workdir),
|
||||
additional_python_paths=[str(self.context.src_workspace)],
|
||||
)
|
||||
self.publish_message(
|
||||
AIMessage(content=run_code_context.model_dump_json(), cause_by=WriteTest, send_to=MESSAGE_ROUTE_TO_SELF)
|
||||
)
|
||||
|
||||
logger.info(f"Done {str(self.project_repo.tests.workdir)} generating.")
|
||||
logger.info(f"Done {str(self.repo.tests.workdir)} generating.")
|
||||
|
||||
async def _run_code(self, msg):
|
||||
run_code_context = RunCodeContext.loads(msg.content)
|
||||
src_doc = await self.project_repo.with_src_path(self.context.src_workspace).srcs.get(
|
||||
run_code_context.code_filename
|
||||
)
|
||||
src_doc = await self.repo.srcs.get(run_code_context.code_filename)
|
||||
if not src_doc:
|
||||
return
|
||||
test_doc = await self.project_repo.tests.get(run_code_context.test_filename)
|
||||
test_doc = await self.repo.tests.get(run_code_context.test_filename)
|
||||
if not test_doc:
|
||||
return
|
||||
run_code_context.code = src_doc.content
|
||||
run_code_context.test_code = test_doc.content
|
||||
result = await RunCode(i_context=run_code_context, context=self.context, llm=self.llm).run()
|
||||
run_code_context.output_filename = run_code_context.test_filename + ".json"
|
||||
await self.project_repo.test_outputs.save(
|
||||
await self.repo.test_outputs.save(
|
||||
filename=run_code_context.output_filename,
|
||||
content=result.model_dump_json(),
|
||||
dependencies={src_doc.root_relative_path, test_doc.root_relative_path},
|
||||
|
|
@ -124,31 +128,53 @@ class QaEngineer(Role):
|
|||
# the recipient might be Engineer or myself
|
||||
recipient = parse_recipient(result.summary)
|
||||
mappings = {"Engineer": "Alex", "QaEngineer": "Edward"}
|
||||
self.publish_message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
cause_by=RunCode,
|
||||
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
|
||||
if recipient != "Engineer":
|
||||
self.publish_message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
cause_by=RunCode,
|
||||
instruct_content=self.input_args,
|
||||
send_to=MESSAGE_ROUTE_TO_SELF,
|
||||
)
|
||||
)
|
||||
else:
|
||||
kvs = self.input_args.model_dump()
|
||||
kvs["changed_test_filenames"] = [
|
||||
str(self.repo.tests.workdir / i) for i in list(self.repo.tests.changed_files.keys())
|
||||
]
|
||||
self.publish_message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
cause_by=RunCode,
|
||||
instruct_content=self.input_args,
|
||||
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _debug_error(self, msg):
|
||||
run_code_context = RunCodeContext.loads(msg.content)
|
||||
code = await DebugError(i_context=run_code_context, context=self.context, llm=self.llm).run()
|
||||
await self.project_repo.tests.save(filename=run_code_context.test_filename, content=code)
|
||||
code = await DebugError(
|
||||
i_context=run_code_context, repo=self.repo, input_args=self.input_args, context=self.context, llm=self.llm
|
||||
).run()
|
||||
await self.repo.tests.save(filename=run_code_context.test_filename, content=code)
|
||||
run_code_context.output = None
|
||||
self.publish_message(
|
||||
AIMessage(content=run_code_context.model_dump_json(), cause_by=DebugError, send_to=MESSAGE_ROUTE_TO_SELF)
|
||||
)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
if self.project_path:
|
||||
await init_python_folder(self.project_repo.tests.workdir)
|
||||
if self.input_args.project_path:
|
||||
await init_python_folder(self.repo.tests.workdir)
|
||||
if self.test_round > self.test_round_allowed:
|
||||
kvs = self.input_args.model_dump()
|
||||
kvs["changed_test_filenames"] = [
|
||||
str(self.repo.tests.workdir / i) for i in list(self.repo.tests.changed_files.keys())
|
||||
]
|
||||
result_msg = AIMessage(
|
||||
content=f"Exceeding {self.test_round_allowed} rounds of tests, stop. "
|
||||
+ "\n".join(list(self.project_repo.tests.changed_files.keys())),
|
||||
+ "\n".join(list(self.repo.tests.changed_files.keys())),
|
||||
cause_by=WriteTest,
|
||||
instruct_content=AIMessage.create_instruct_value(kvs=kvs, class_name="WriteTestOutput"),
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
return result_msg
|
||||
|
|
@ -171,8 +197,13 @@ class QaEngineer(Role):
|
|||
elif msg.cause_by == any_to_str(UserRequirement):
|
||||
return await self._parse_user_requirement(msg)
|
||||
self.test_round += 1
|
||||
kvs = self.input_args.model_dump()
|
||||
kvs["changed_test_filenames"] = [
|
||||
str(self.repo.tests.workdir / i) for i in list(self.repo.tests.changed_files.keys())
|
||||
]
|
||||
return AIMessage(
|
||||
content=f"Round {self.test_round} of tests done",
|
||||
instruct_content=AIMessage.create_instruct_value(kvs=kvs, class_name="WriteTestOutput"),
|
||||
cause_by=WriteTest,
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
|
|
@ -190,3 +221,15 @@ class QaEngineer(Role):
|
|||
if not self.src_workspace:
|
||||
self.src_workspace = self.git_repo.workdir / self.git_repo.workdir.name
|
||||
return rsp
|
||||
|
||||
async def _think(self) -> bool:
|
||||
if not self.rc.news:
|
||||
return False
|
||||
msg = self.rc.news[0]
|
||||
if msg.cause_by == any_to_str(SummarizeCode):
|
||||
self.input_args = msg.instruct_content
|
||||
self.repo = ProjectRepo(self.input_args.project_path)
|
||||
if self.repo.src_relative_path is None:
|
||||
path = get_project_srcs_path(self.repo.workdir)
|
||||
self.repo.with_src_path(path)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ from metagpt.schema import (
|
|||
)
|
||||
from metagpt.strategy.planner import Planner
|
||||
from metagpt.utils.common import any_to_name, any_to_str, role_raise_decorator
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -196,29 +195,6 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
value.context = self.context
|
||||
self.rc.todo = value
|
||||
|
||||
@property
|
||||
def git_repo(self):
|
||||
"""Git repo"""
|
||||
return self.context.git_repo
|
||||
|
||||
@git_repo.setter
|
||||
def git_repo(self, value):
|
||||
self.context.git_repo = value
|
||||
|
||||
@property
|
||||
def src_workspace(self):
|
||||
"""Source workspace under git repo"""
|
||||
return self.context.src_workspace
|
||||
|
||||
@src_workspace.setter
|
||||
def src_workspace(self, value):
|
||||
self.context.src_workspace = value
|
||||
|
||||
@property
|
||||
def project_repo(self) -> ProjectRepo:
|
||||
project_repo = ProjectRepo(self.context.git_repo)
|
||||
return project_repo.with_src_path(self.context.src_workspace) if self.context.src_workspace else project_repo
|
||||
|
||||
@property
|
||||
def prompt_schema(self):
|
||||
"""Prompt schema: json/markdown"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue