mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-05-15 11:02:36 +02:00
Merge branch 'feature/at_role' into 'mgx_ops'
feat: 支持直接给软件公司各个角色发用户需求 See merge request pub/MetaGPT!84
This commit is contained in:
commit
c0875d6ae8
34 changed files with 409 additions and 495 deletions
|
|
@ -70,6 +70,6 @@ class DebugError(Action):
|
|||
prompt = PROMPT_TEMPLATE.format(code=code_doc.content, test_code=test_doc.content, logs=output_detail.stderr)
|
||||
|
||||
rsp = await self._aask(prompt)
|
||||
code = CodeParser.parse_code(block="", text=rsp)
|
||||
code = CodeParser.parse_code(text=rsp)
|
||||
|
||||
return code
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class WriteAnalysisCode(Action):
|
|||
)
|
||||
|
||||
rsp = await self._aask(reflection_prompt, system_msgs=[REFLECTION_SYSTEM_MSG])
|
||||
reflection = json.loads(CodeParser.parse_code(block=None, text=rsp))
|
||||
reflection = json.loads(CodeParser.parse_code(text=rsp))
|
||||
|
||||
return reflection["improved_impl"]
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ class WriteAnalysisCode(Action):
|
|||
code = await self._debug_with_reflection(context=context, working_memory=working_memory)
|
||||
else:
|
||||
rsp = await self.llm.aask(context, system_msgs=[INTERPRETER_SYSTEM_MSG], **kwargs)
|
||||
code = CodeParser.parse_code(block=None, text=rsp)
|
||||
code = CodeParser.parse_code(text=rsp)
|
||||
|
||||
return code
|
||||
|
||||
|
|
@ -69,5 +69,5 @@ class CheckData(Action):
|
|||
code_written = "\n\n".join(code_written)
|
||||
prompt = CHECK_DATA_PROMPT.format(code_written=code_written)
|
||||
rsp = await self._aask(prompt)
|
||||
code = CodeParser.parse_code(block=None, text=rsp)
|
||||
code = CodeParser.parse_code(text=rsp)
|
||||
return code
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class WritePlan(Action):
|
|||
context="\n".join([str(ct) for ct in context]), max_tasks=max_tasks, task_type_desc=task_type_desc
|
||||
)
|
||||
rsp = await self._aask(prompt)
|
||||
rsp = CodeParser.parse_code(block=None, text=rsp)
|
||||
rsp = CodeParser.parse_code(text=rsp)
|
||||
return rsp
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@
|
|||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions import Action, UserRequirement
|
||||
from metagpt.const import REQUIREMENT_FILENAME
|
||||
from metagpt.schema import AIMessage
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
|
||||
|
|
@ -21,6 +23,13 @@ class PrepareDocuments(Action):
|
|||
|
||||
name: str = "PrepareDocuments"
|
||||
i_context: Optional[str] = None
|
||||
key_descriptions: Optional[Dict[str, str]] = None
|
||||
send_to: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if not self.key_descriptions:
|
||||
self.key_descriptions = {"project_path": 'the project path if exists in "Original Requirement"'}
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
|
|
@ -40,10 +49,26 @@ class PrepareDocuments(Action):
|
|||
|
||||
async def run(self, with_messages, **kwargs):
|
||||
"""Create and initialize the workspace folder, initialize the Git environment."""
|
||||
user_requirements = [i for i in with_messages if i.cause_by == any_to_str(UserRequirement)]
|
||||
if not self.config.project_path and user_requirements and self.key_descriptions:
|
||||
args = await user_requirements[0].parse_resources(llm=self.llm, key_descriptions=self.key_descriptions)
|
||||
for k, v in args.items():
|
||||
if not v or k in ["resources", "reason"]:
|
||||
continue
|
||||
self.context.kwargs.set(k, v)
|
||||
if self.context.kwargs.project_path:
|
||||
self.config.update_via_cli(
|
||||
project_path=self.context.kwargs.project_path,
|
||||
project_name="",
|
||||
inc=False,
|
||||
reqa_file=self.context.kwargs.reqa_file or "",
|
||||
max_auto_summarize_code=0,
|
||||
)
|
||||
|
||||
self._init_repo()
|
||||
|
||||
# Write the newly added requirements from the main parameter idea to `docs/requirement.txt`.
|
||||
doc = await self.repo.docs.save(filename=REQUIREMENT_FILENAME, content=with_messages[0].content)
|
||||
# Send a Message notification to the WritePRD action, instructing it to process requirements using
|
||||
# `docs/requirement.txt` and `docs/prd/`.
|
||||
return ActionOutput(content=doc.content, instruct_content=doc)
|
||||
return AIMessage(content="", instruct_content=doc, cause_by=self, send_to=self.send_to)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class WriteCode(Action):
|
|||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
async def write_code(self, prompt) -> str:
|
||||
code_rsp = await self._aask(prompt)
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
code = CodeParser.parse_code(text=code_rsp)
|
||||
return code
|
||||
|
||||
async def run(self, *args, **kwargs) -> CodingContext:
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class WriteCodeReview(Action):
|
|||
# if LBTM, rewrite code
|
||||
rewrite_prompt = f"{context_prompt}\n{cr_rsp}\n{REWRITE_CODE_TEMPLATE.format(filename=filename)}"
|
||||
code_rsp = await self._aask(rewrite_prompt)
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
code = CodeParser.parse_code(text=code_rsp)
|
||||
return result, code
|
||||
|
||||
async def run(self, *args, **kwargs) -> CodingContext:
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class WriteTest(Action):
|
|||
code_rsp = await self._aask(prompt)
|
||||
|
||||
try:
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
code = CodeParser.parse_code(text=code_rsp)
|
||||
except Exception:
|
||||
# Handle the exception if needed
|
||||
logger.error(f"Can't parse the code: {code_rsp}")
|
||||
|
|
|
|||
|
|
@ -139,3 +139,6 @@ LLM_API_TIMEOUT = 300
|
|||
|
||||
# Assistant alias
|
||||
ASSISTANT_ALIAS = "response"
|
||||
|
||||
# Metadata defines
|
||||
AGENT = "agent"
|
||||
|
|
|
|||
|
|
@ -181,13 +181,13 @@ class STRole(Role):
|
|||
|
||||
logger.info(f"Role: {self.name} saved role's memory into {str(self.role_storage_path)}")
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
async def _observe(self) -> int:
|
||||
if not self.rc.env:
|
||||
return 0
|
||||
news = []
|
||||
if not news:
|
||||
news = self.rc.msg_buffer.pop_all()
|
||||
old_messages = [] if ignore_memory else self.rc.memory.get()
|
||||
old_messages = [] if not self.enable_memory else self.rc.memory.get()
|
||||
# Filter out messages of interest.
|
||||
self.rc.news = [
|
||||
n for n in news if (n.cause_by in self.rc.watch or self.name in n.send_to) and n not in old_messages
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ class OpenAILLM(BaseLLM):
|
|||
# The response content is `code``, but it appears in the content instead of the arguments.
|
||||
code_formats = "```"
|
||||
if message.content.startswith(code_formats) and message.content.endswith(code_formats):
|
||||
code = CodeParser.parse_code(None, message.content)
|
||||
code = CodeParser.parse_code(text=message.content)
|
||||
return {"language": "python", "code": code}
|
||||
# reponse is message
|
||||
return {"language": "markdown", "code": self.get_choice_text(rsp)}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
@File : architect.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.utils.common import any_to_str
|
||||
|
||||
|
||||
class Architect(Role):
|
||||
|
|
@ -32,8 +34,24 @@ class Architect(Role):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.enable_memory = False
|
||||
# Initialize actions specific to the Architect role
|
||||
self.set_actions([WriteDesign])
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self), context=self.context), WriteDesign])
|
||||
|
||||
# Set events or actions the Architect should watch or be aware of
|
||||
self._watch({WritePRD})
|
||||
self._watch({UserRequirement, PrepareDocuments, WritePRD})
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
mappings = {
|
||||
any_to_str(UserRequirement): 0,
|
||||
any_to_str(PrepareDocuments): 1,
|
||||
any_to_str(WritePRD): 1,
|
||||
}
|
||||
for i in self.rc.news:
|
||||
idx = mappings.get(i.cause_by, -1)
|
||||
if idx < 0:
|
||||
continue
|
||||
self.rc.todo = self.actions[idx]
|
||||
return bool(self.rc.todo)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class DataInterpreter(Role):
|
|||
|
||||
prompt = REACT_THINK_PROMPT.format(user_requirement=self.user_requirement, context=context)
|
||||
rsp = await self.llm.aask(prompt)
|
||||
rsp_dict = json.loads(CodeParser.parse_code(block=None, text=rsp))
|
||||
rsp_dict = json.loads(CodeParser.parse_code(text=rsp))
|
||||
self.working_memory.add(Message(content=rsp_dict["thoughts"], role="assistant"))
|
||||
need_action = rsp_dict["state"]
|
||||
self._set_state(0) if need_action else self._set_state(-1)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class TeamLeader(Role):
|
|||
context = self.llm.format_msg(self.get_memories(k=10) + [Message(content=prompt, role="user")])
|
||||
|
||||
rsp = await self.llm.aask(context, system_msgs=[SYSTEM_PROMPT])
|
||||
self.commands = json.loads(CodeParser.parse_code(block=None, text=rsp))
|
||||
self.commands = json.loads(CodeParser.parse_code(text=rsp))
|
||||
self.rc.memory.add(Message(content=rsp, role="assistant"))
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ from collections import defaultdict
|
|||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks
|
||||
from metagpt.actions import (
|
||||
Action,
|
||||
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
|
||||
|
|
@ -39,6 +46,7 @@ from metagpt.const import (
|
|||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import (
|
||||
AIMessage,
|
||||
CodePlanAndChangeContext,
|
||||
CodeSummarizeContext,
|
||||
CodingContext,
|
||||
|
|
@ -53,6 +61,7 @@ from metagpt.utils.common import (
|
|||
get_project_srcs_path,
|
||||
init_python_folder,
|
||||
)
|
||||
from metagpt.utils.git_repository import ChangeType
|
||||
|
||||
IS_PASS_PROMPT = """
|
||||
{context}
|
||||
|
|
@ -93,9 +102,20 @@ class Engineer(Role):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.enable_memory = False
|
||||
self.set_actions([WriteCode])
|
||||
self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug, WriteCodePlanAndChange])
|
||||
self._watch(
|
||||
[
|
||||
UserRequirement,
|
||||
PrepareDocuments,
|
||||
WriteTasks,
|
||||
SummarizeCode,
|
||||
WriteCode,
|
||||
WriteCodeReview,
|
||||
FixBug,
|
||||
WriteCodePlanAndChange,
|
||||
]
|
||||
)
|
||||
self.code_todos = []
|
||||
self.summarize_todos = []
|
||||
self.next_todo_action = any_to_name(WriteCode)
|
||||
|
|
@ -130,13 +150,11 @@ class Engineer(Role):
|
|||
dependencies=list(dependencies),
|
||||
content=coding_context.code_doc.content,
|
||||
)
|
||||
msg = Message(
|
||||
AIMessage(
|
||||
content=coding_context.model_dump_json(),
|
||||
instruct_content=coding_context,
|
||||
role=self.profile,
|
||||
cause_by=WriteCode,
|
||||
)
|
||||
self.rc.memory.add(msg)
|
||||
|
||||
changed_files.add(coding_context.code_doc.filename)
|
||||
if not changed_files:
|
||||
|
|
@ -156,13 +174,12 @@ class Engineer(Role):
|
|||
if isinstance(self.rc.todo, SummarizeCode):
|
||||
self.next_todo_action = any_to_name(WriteCode)
|
||||
return await self._act_summarize()
|
||||
return None
|
||||
return await self.rc.todo.run(self.rc.history)
|
||||
|
||||
async def _act_write_code(self):
|
||||
changed_files = await self._act_sp_with_cr(review=self.use_code_review)
|
||||
return Message(
|
||||
return AIMessage(
|
||||
content="\n".join(changed_files),
|
||||
role=self.profile,
|
||||
cause_by=WriteCodeReview if self.use_code_review else WriteCode,
|
||||
send_to=self,
|
||||
sent_from=self,
|
||||
|
|
@ -195,9 +212,8 @@ class Engineer(Role):
|
|||
|
||||
logger.info(f"--max-auto-summarize-code={self.config.max_auto_summarize_code}")
|
||||
if not tasks or self.config.max_auto_summarize_code == 0:
|
||||
return Message(
|
||||
return AIMessage(
|
||||
content="",
|
||||
role=self.profile,
|
||||
cause_by=SummarizeCode,
|
||||
sent_from=self,
|
||||
send_to="Edward", # The name of QaEngineer
|
||||
|
|
@ -205,9 +221,7 @@ class Engineer(Role):
|
|||
# The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited.
|
||||
# This parameter is used for debugging the workflow.
|
||||
self.n_summarize += 1 if self.config.max_auto_summarize_code > self.n_summarize else 0
|
||||
return Message(
|
||||
content=json.dumps(tasks), role=self.profile, cause_by=SummarizeCode, send_to=self, sent_from=self
|
||||
)
|
||||
return AIMessage(content=json.dumps(tasks), cause_by=SummarizeCode, send_to=self, sent_from=self)
|
||||
|
||||
async def _act_code_plan_and_change(self):
|
||||
"""Write code plan and change that guides subsequent WriteCode and WriteCodeReview"""
|
||||
|
|
@ -229,9 +243,8 @@ class Engineer(Role):
|
|||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
return Message(
|
||||
return AIMessage(
|
||||
content=code_plan_and_change,
|
||||
role=self.profile,
|
||||
cause_by=WriteCodePlanAndChange,
|
||||
send_to=self,
|
||||
sent_from=self,
|
||||
|
|
@ -245,14 +258,25 @@ class Engineer(Role):
|
|||
return False, rsp
|
||||
|
||||
async def _think(self) -> Action | None:
|
||||
if not self.src_workspace:
|
||||
self.src_workspace = get_project_srcs_path(self.project_repo.workdir)
|
||||
write_plan_and_change_filters = any_to_str_set([WriteTasks, FixBug])
|
||||
write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode])
|
||||
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
|
||||
if not self.rc.news:
|
||||
return None
|
||||
msg = self.rc.news[0]
|
||||
if msg.cause_by == any_to_str(UserRequirement):
|
||||
self.rc.todo = PrepareDocuments(
|
||||
key_descriptions={
|
||||
"project_path": 'the project path if exists in "Original Requirement"',
|
||||
"src_filename": 'the file name of the source code file explicitly requested for modification if exists in "Original Requirement"',
|
||||
},
|
||||
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)
|
||||
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)
|
||||
|
|
@ -308,7 +332,11 @@ class Engineer(Role):
|
|||
async def _new_code_actions(self):
|
||||
bug_fix = await self._is_fixbug()
|
||||
# Prepare file repos
|
||||
changed_src_files = self.project_repo.srcs.all_files if bug_fix else self.project_repo.srcs.changed_files
|
||||
changed_src_files = self.project_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_files = Documents()
|
||||
# Recode caused by upstream changes.
|
||||
|
|
@ -319,6 +347,8 @@ class Engineer(Role):
|
|||
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)
|
||||
if not old_code_doc:
|
||||
old_code_doc = Document(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role, RoleReactMode
|
||||
from metagpt.utils.common import any_to_name
|
||||
from metagpt.utils.common import any_to_name, any_to_str
|
||||
|
||||
|
||||
class ProductManager(Role):
|
||||
|
|
@ -32,8 +32,8 @@ class ProductManager(Role):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.set_actions([PrepareDocuments, WritePRD])
|
||||
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)
|
||||
|
|
@ -47,6 +47,3 @@ class ProductManager(Role):
|
|||
self.config.git_reinit = False
|
||||
self.todo_action = any_to_name(WritePRD)
|
||||
return bool(self.rc.todo)
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
return await super()._observe(ignore_memory=True)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
@File : project_manager.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.actions import UserRequirement, WriteTasks
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.utils.common import any_to_str
|
||||
|
||||
|
||||
class ProjectManager(Role):
|
||||
|
|
@ -32,6 +34,21 @@ class ProjectManager(Role):
|
|||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.enable_memory = False
|
||||
self.set_actions([PrepareDocuments(send_to=any_to_str(self), context=self.context), WriteTasks])
|
||||
self._watch([UserRequirement, PrepareDocuments, WriteDesign])
|
||||
|
||||
self.set_actions([WriteTasks])
|
||||
self._watch([WriteDesign])
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
mappings = {
|
||||
any_to_str(UserRequirement): 0,
|
||||
any_to_str(PrepareDocuments): 1,
|
||||
any_to_str(WriteDesign): 1,
|
||||
}
|
||||
for i in self.rc.news:
|
||||
idx = mappings.get(i.cause_by, -1)
|
||||
if idx < 0:
|
||||
continue
|
||||
self.rc.todo = self.actions[idx]
|
||||
return bool(self.rc.todo)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -15,13 +15,19 @@
|
|||
of SummarizeCode.
|
||||
"""
|
||||
|
||||
from metagpt.actions import DebugError, RunCode, WriteTest
|
||||
from metagpt.actions import DebugError, RunCode, UserRequirement, WriteTest
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.const import MESSAGE_ROUTE_TO_NONE
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Document, Message, RunCodeContext, TestingContext
|
||||
from metagpt.utils.common import any_to_str_set, init_python_folder, parse_recipient
|
||||
from metagpt.schema import AIMessage, Document, Message, RunCodeContext, TestingContext
|
||||
from metagpt.utils.common import (
|
||||
any_to_str,
|
||||
any_to_str_set,
|
||||
init_python_folder,
|
||||
parse_recipient,
|
||||
)
|
||||
|
||||
|
||||
class QaEngineer(Role):
|
||||
|
|
@ -37,19 +43,22 @@ class QaEngineer(Role):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enable_memory = False
|
||||
|
||||
# FIXME: a bit hack here, only init one action to circumvent _think() logic,
|
||||
# will overwrite _think() in future updates
|
||||
self.set_actions([WriteTest])
|
||||
self._watch([SummarizeCode, WriteTest, RunCode, DebugError])
|
||||
self.set_actions(
|
||||
[
|
||||
WriteTest,
|
||||
]
|
||||
)
|
||||
self._watch([UserRequirement, PrepareDocuments, SummarizeCode, WriteTest, RunCode, DebugError])
|
||||
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
|
||||
changed_files = set(src_file_repo.changed_files.keys())
|
||||
# Unit tests only.
|
||||
if self.config.reqa_file and self.config.reqa_file not in changed_files:
|
||||
changed_files.add(self.config.reqa_file)
|
||||
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())
|
||||
for filename in changed_files:
|
||||
# write tests
|
||||
if not filename or "test" in filename:
|
||||
|
|
@ -80,9 +89,8 @@ class QaEngineer(Role):
|
|||
additional_python_paths=[str(self.context.src_workspace)],
|
||||
)
|
||||
self.publish_message(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
|
|
@ -116,9 +124,8 @@ class QaEngineer(Role):
|
|||
recipient = parse_recipient(result.summary)
|
||||
mappings = {"Engineer": "Alex", "QaEngineer": "Edward"}
|
||||
self.publish_message(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=RunCode,
|
||||
sent_from=self,
|
||||
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
|
||||
|
|
@ -131,9 +138,8 @@ class QaEngineer(Role):
|
|||
await self.project_repo.tests.save(filename=run_code_context.test_filename, content=code)
|
||||
run_code_context.output = None
|
||||
self.publish_message(
|
||||
Message(
|
||||
AIMessage(
|
||||
content=run_code_context.model_dump_json(),
|
||||
role=self.profile,
|
||||
cause_by=DebugError,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
|
|
@ -141,18 +147,18 @@ class QaEngineer(Role):
|
|||
)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
await init_python_folder(self.project_repo.tests.workdir)
|
||||
if self.project_path:
|
||||
await init_python_folder(self.project_repo.tests.workdir)
|
||||
if self.test_round > self.test_round_allowed:
|
||||
result_msg = Message(
|
||||
result_msg = AIMessage(
|
||||
content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)",
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
return result_msg
|
||||
|
||||
code_filters = any_to_str_set({SummarizeCode})
|
||||
code_filters = any_to_str_set({PrepareDocuments, SummarizeCode})
|
||||
test_filters = any_to_str_set({WriteTest, DebugError})
|
||||
run_filters = any_to_str_set({RunCode})
|
||||
for msg in self.rc.news:
|
||||
|
|
@ -167,16 +173,26 @@ class QaEngineer(Role):
|
|||
elif msg.cause_by in run_filters:
|
||||
# I ran my test code, time to fix bugs, if any
|
||||
await self._debug_error(msg)
|
||||
elif msg.cause_by == any_to_str(UserRequirement):
|
||||
return await self._parse_user_requirement(msg)
|
||||
self.test_round += 1
|
||||
return Message(
|
||||
return AIMessage(
|
||||
content=f"Round {self.test_round} of tests done",
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
# This role has events that trigger and execute themselves based on conditions, and cannot rely on the
|
||||
# content of memory to activate.
|
||||
return await super()._observe(ignore_memory=True)
|
||||
async def _parse_user_requirement(self, msg: Message) -> AIMessage:
|
||||
action = PrepareDocuments(
|
||||
send_to=any_to_str(self),
|
||||
key_descriptions={
|
||||
"project_path": 'the project path if exists in "Original Requirement"',
|
||||
"reqa_file": 'the file name to rewrite unit test if exists in "Original Requirement"',
|
||||
},
|
||||
context=self.context,
|
||||
)
|
||||
rsp = await action.run([msg])
|
||||
if not self.src_workspace:
|
||||
self.src_workspace = self.git_repo.workdir / self.git_repo.workdir.name
|
||||
return rsp
|
||||
|
|
|
|||
|
|
@ -34,7 +34,14 @@ from metagpt.context_mixin import ContextMixin
|
|||
from metagpt.logs import logger
|
||||
from metagpt.memory import Memory
|
||||
from metagpt.provider import HumanProvider
|
||||
from metagpt.schema import Message, MessageQueue, SerializationMixin
|
||||
from metagpt.schema import (
|
||||
AIMessage,
|
||||
Message,
|
||||
MessageQueue,
|
||||
SerializationMixin,
|
||||
Task,
|
||||
TaskResult,
|
||||
)
|
||||
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
|
||||
|
|
@ -134,6 +141,9 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
constraints: str = ""
|
||||
desc: str = ""
|
||||
is_human: bool = False
|
||||
enable_memory: bool = (
|
||||
True # Stateless, atomic roles, or roles that use external storage can disable this to save memory.
|
||||
)
|
||||
|
||||
role_id: str = ""
|
||||
states: list[str] = []
|
||||
|
|
@ -245,10 +255,9 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
return self
|
||||
|
||||
def _init_action(self, action: Action):
|
||||
if not action.private_config:
|
||||
action.set_llm(self.llm, override=True)
|
||||
else:
|
||||
action.set_llm(self.llm, override=False)
|
||||
action.set_context(self.context)
|
||||
override = not action.private_config
|
||||
action.set_llm(self.llm, override=override)
|
||||
action.set_prefix(self._get_prefix())
|
||||
|
||||
def set_action(self, action: Action):
|
||||
|
|
@ -390,22 +399,22 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
|
||||
response = await self.rc.todo.run(self.rc.history)
|
||||
if isinstance(response, (ActionOutput, ActionNode)):
|
||||
msg = Message(
|
||||
msg = AIMessage(
|
||||
content=response.content,
|
||||
instruct_content=response.instruct_content,
|
||||
role=self._setting,
|
||||
cause_by=self.rc.todo,
|
||||
sent_from=self,
|
||||
)
|
||||
elif isinstance(response, Message):
|
||||
msg = response
|
||||
else:
|
||||
msg = Message(content=response, role=self.profile, cause_by=self.rc.todo, sent_from=self)
|
||||
self.rc.memory.add(msg)
|
||||
msg = AIMessage(content=response, cause_by=self.rc.todo, sent_from=self)
|
||||
if self.enable_memory:
|
||||
self.rc.memory.add(msg)
|
||||
|
||||
return msg
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
async def _observe(self) -> int:
|
||||
"""Prepare new messages for processing from the message buffer and other sources."""
|
||||
# Read unprocessed messages from the msg buffer.
|
||||
news = []
|
||||
|
|
@ -414,7 +423,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
if not news:
|
||||
news = self.rc.msg_buffer.pop_all()
|
||||
# Store the read messages in your own memory to prevent duplicate processing.
|
||||
old_messages = [] if ignore_memory else self.rc.memory.get()
|
||||
old_messages = [] if not self.enable_memory else self.rc.memory.get()
|
||||
# Filter in messages of interest.
|
||||
self.rc.news = [
|
||||
n for n in news if (n.cause_by in self.rc.watch or self.name in n.send_to) and n not in old_messages
|
||||
|
|
@ -451,7 +460,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
Use llm to select actions in _think dynamically
|
||||
"""
|
||||
actions_taken = 0
|
||||
rsp = Message(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
|
||||
rsp = AIMessage(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
|
||||
while actions_taken < self.rc.max_react_loop:
|
||||
# think
|
||||
has_todo = await self._think()
|
||||
|
|
@ -466,7 +475,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
async def _act_by_order(self) -> Message:
|
||||
"""switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ..."""
|
||||
start_idx = self.rc.state if self.rc.state >= 0 else 0 # action to run from recovered state
|
||||
rsp = Message(content="No actions taken yet") # return default message if actions=[]
|
||||
rsp = AIMessage(content="No actions taken yet") # return default message if actions=[]
|
||||
for i in range(start_idx, len(self.states)):
|
||||
self._set_state(i)
|
||||
rsp = await self._act()
|
||||
|
|
@ -523,6 +532,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
else:
|
||||
raise ValueError(f"Unsupported react mode: {self.rc.react_mode}")
|
||||
self._set_state(state=-1) # current reaction is complete, reset state to -1 and todo back to None
|
||||
if isinstance(rsp, AIMessage):
|
||||
rsp.with_agent(self._setting)
|
||||
return rsp
|
||||
|
||||
def get_memories(self, k=0) -> list[Message]:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ from pydantic import (
|
|||
)
|
||||
|
||||
from metagpt.const import (
|
||||
AGENT,
|
||||
MESSAGE_ROUTE_CAUSE_BY,
|
||||
MESSAGE_ROUTE_FROM,
|
||||
MESSAGE_ROUTE_TO,
|
||||
|
|
@ -48,7 +49,7 @@ from metagpt.const import (
|
|||
)
|
||||
from metagpt.logs import ToolLogItem, log_tool_output, logger
|
||||
from metagpt.repo_parser import DotClassInfo
|
||||
from metagpt.utils.common import any_to_str, any_to_str_set, import_class
|
||||
from metagpt.utils.common import CodeParser, any_to_str, any_to_str_set, import_class
|
||||
from metagpt.utils.exceptions import handle_exception
|
||||
from metagpt.utils.serialize import (
|
||||
actionoutout_schema_to_mapping,
|
||||
|
|
@ -186,6 +187,14 @@ class Documents(BaseModel):
|
|||
return ActionOutput(content=self.model_dump_json(), instruct_content=self)
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
"""Used by `Message`.`parse_resources`"""
|
||||
|
||||
resource_type: str # the type of resource
|
||||
value: str # a string type of resource content
|
||||
description: str # explanation
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""list[<role>: <content>]"""
|
||||
|
||||
|
|
@ -196,6 +205,7 @@ class Message(BaseModel):
|
|||
cause_by: str = Field(default="", validate_default=True)
|
||||
sent_from: str = Field(default="", validate_default=True)
|
||||
send_to: set[str] = Field(default={MESSAGE_ROUTE_TO_ALL}, validate_default=True)
|
||||
metadata: Dict[str, str] = Field(default_factory=dict) # metadata for `content` and `instruct_content`
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -311,14 +321,53 @@ class Message(BaseModel):
|
|||
logger.error(f"parse json failed: {val}, error:{err}")
|
||||
return None
|
||||
|
||||
async def parse_resources(self, llm: "BaseLLM", key_descriptions: Dict[str, str] = None) -> Dict:
|
||||
"""
|
||||
`parse_resources` corresponds to the in-context adaptation capability of the input of the atomic action,
|
||||
which will be migrated to the context builder later.
|
||||
|
||||
Args:
|
||||
llm (BaseLLM): The instance of the BaseLLM class.
|
||||
key_descriptions (Dict[str, str], optional): A dictionary containing descriptions for each key,
|
||||
if provided. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Dict: A dictionary containing parsed resources.
|
||||
|
||||
"""
|
||||
if not self.content:
|
||||
return {}
|
||||
content = f"## Original Requirement\n```text\n{self.content}\n```\n"
|
||||
return_format = (
|
||||
"Return a markdown JSON object with:\n"
|
||||
'- a "resources" key contain a list of objects. Each object with:\n'
|
||||
' - a "resource_type" key explain the type of resource;\n'
|
||||
' - a "value" key containing a string type of resource content;\n'
|
||||
' - a "description" key explaining why;\n'
|
||||
)
|
||||
key_descriptions = key_descriptions or {}
|
||||
for k, v in key_descriptions.items():
|
||||
return_format += f'- a "{k}" key containing {v};\n'
|
||||
return_format += '- a "reason" key explaining why;\n'
|
||||
instructions = ['Lists all the resources contained in the "Original Requirement".', return_format]
|
||||
rsp = await llm.aask(msg=content, system_msgs=instructions)
|
||||
json_data = CodeParser.parse_code(text=rsp, lang="json")
|
||||
m = json.loads(json_data)
|
||||
m["resources"] = [Resource(**i) for i in m.get("resources", [])]
|
||||
return m
|
||||
|
||||
def add_metadata(self, key: str, value: str):
|
||||
self.metadata[key] = value
|
||||
|
||||
|
||||
class UserMessage(Message):
|
||||
"""便于支持OpenAI的消息
|
||||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="user")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="user", **kwargs)
|
||||
|
||||
|
||||
class SystemMessage(Message):
|
||||
|
|
@ -326,8 +375,9 @@ class SystemMessage(Message):
|
|||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="system")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="system", **kwargs)
|
||||
|
||||
|
||||
class AIMessage(Message):
|
||||
|
|
@ -335,8 +385,17 @@ class AIMessage(Message):
|
|||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content=content, role="assistant")
|
||||
def __init__(self, content: str, **kwargs):
|
||||
kwargs.pop("role", None)
|
||||
super().__init__(content=content, role="assistant", **kwargs)
|
||||
|
||||
def with_agent(self, name: str):
|
||||
self.add_metadata(key=AGENT, value=name)
|
||||
return self
|
||||
|
||||
@property
|
||||
def agent(self) -> str:
|
||||
return self.metadata.get(AGENT, "")
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||
import typer
|
||||
|
||||
from metagpt.const import CONFIG_ROOT
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
||||
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
||||
|
|
@ -65,7 +66,7 @@ def generate_repo(
|
|||
idea = company.idea
|
||||
|
||||
company.invest(investment)
|
||||
company.run_project(idea)
|
||||
company.run_project(idea, send_to=any_to_str(ProductManager))
|
||||
asyncio.run(company.run(n_round=n_round))
|
||||
|
||||
return ctx.repo
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class ThoughtSolverBase(BaseModel):
|
|||
current_state=current_state, **{"n_generate_sample": self.config.n_generate_sample}
|
||||
)
|
||||
rsp = await self.llm.aask(msg=state_prompt + "\n" + OUTPUT_FORMAT)
|
||||
thoughts = CodeParser.parse_code(block="", text=rsp)
|
||||
thoughts = CodeParser.parse_code(text=rsp)
|
||||
thoughts = eval(thoughts)
|
||||
# fixme 避免不跟随,生成过多nodes
|
||||
# valid_thoughts = [_node for idx, _node in enumerate(thoughts) if idx < self.n_generate_sample]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from metagpt.context import Context
|
|||
from metagpt.environment import Environment
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.schema import UserMessage
|
||||
from metagpt.utils.common import (
|
||||
NoMoneyException,
|
||||
read_json_file,
|
||||
|
|
@ -102,7 +102,7 @@ class Team(BaseModel):
|
|||
|
||||
# Human requirement.
|
||||
self.env.publish_message(
|
||||
Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL),
|
||||
UserMessage(content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL),
|
||||
peekable=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ from metagpt.tools.libs import (
|
|||
deployer,
|
||||
)
|
||||
from metagpt.tools.libs.env import get_env, set_get_env_entry, default_get_env, get_env_description
|
||||
from metagpt.tools.libs.software_development import (
|
||||
git_archive,
|
||||
)
|
||||
|
||||
_ = (
|
||||
data_preprocess,
|
||||
|
|
@ -28,7 +25,6 @@ _ = (
|
|||
gpt_v_generator,
|
||||
web_scraping,
|
||||
email_login,
|
||||
git_archive,
|
||||
terminal,
|
||||
file_manager,
|
||||
browser,
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class GPTvGenerator:
|
|||
webpages_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
index_path = webpages_path / "index.html"
|
||||
index_path.write_text(CodeParser.parse_code(block=None, text=webpages, lang="html"))
|
||||
index_path.write_text(CodeParser.parse_code(text=webpages, lang="html"))
|
||||
|
||||
extract_and_save_code(folder=webpages_path, text=webpages, pattern="styles?.css", language="css")
|
||||
|
||||
|
|
@ -102,5 +102,5 @@ def extract_and_save_code(folder, text, pattern, language):
|
|||
word = re.search(pattern, text)
|
||||
if word:
|
||||
path = folder / word.group(0)
|
||||
code = CodeParser.parse_code(block=None, text=text, lang=language)
|
||||
code = CodeParser.parse_code(text=text, lang=language)
|
||||
path.write_text(code, encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -3,384 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from metagpt.const import ASSISTANT_ALIAS, BUGFIX_FILENAME, REQUIREMENT_FILENAME
|
||||
from metagpt.const import ASSISTANT_ALIAS
|
||||
from metagpt.logs import ToolLogItem, log_tool_output
|
||||
from metagpt.schema import BugFixContext, Message
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
from metagpt.utils.common import any_to_str
|
||||
from metagpt.utils.project_repo import ProjectRepo
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "ProductManager"])
|
||||
async def write_prd(idea: str, project_path: Optional[str | Path] = None) -> Path:
|
||||
"""Writes a PRD based on user requirements.
|
||||
|
||||
Args:
|
||||
idea (str): The idea or concept for the PRD.
|
||||
project_path (Optional[str|Path], optional): The path to an existing project directory.
|
||||
If it's None, a new project path will be created. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Path: The path to the PRD files under the project directory
|
||||
|
||||
Example:
|
||||
>>> # Create a new project:
|
||||
>>> from metagpt.tools.libs.software_development import write_prd
|
||||
>>> prd_path = await write_prd("Create a new feature for the application")
|
||||
>>> print(prd_path)
|
||||
'/path/to/project_path/docs/prd/'
|
||||
|
||||
>>> # Add user requirements to the exists project:
|
||||
>>> from metagpt.tools.libs.software_development import write_prd
|
||||
>>> project_path = '/path/to/exists_project_path'
|
||||
>>> prd_path = await write_prd("Create a new feature for the application", project_path=project_path)
|
||||
>>> print(prd_path = )
|
||||
'/path/to/project_path/docs/prd/'
|
||||
"""
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import ProductManager
|
||||
|
||||
log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_prd.__name__)], tool_name=write_prd.__name__)
|
||||
|
||||
ctx = Context()
|
||||
if project_path and Path(project_path).exists():
|
||||
ctx.config.project_path = Path(project_path)
|
||||
ctx.config.inc = True
|
||||
|
||||
role = ProductManager(context=ctx)
|
||||
msg = await role.run(with_message=Message(content=idea, cause_by=UserRequirement))
|
||||
await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia PRD File", value=str(ctx.repo.docs.prd.workdir / i))
|
||||
for i in ctx.repo.docs.prd.changed_files.keys()
|
||||
]
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="PRD File", value=str(ctx.repo.resources.prd.workdir / i))
|
||||
for i in ctx.repo.resources.prd.changed_files.keys()
|
||||
]
|
||||
)
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="Competitive Analysis", value=str(ctx.repo.resources.competitive_analysis.workdir / i))
|
||||
for i in ctx.repo.resources.competitive_analysis.changed_files.keys()
|
||||
]
|
||||
)
|
||||
log_tool_output(output=outputs, tool_name=write_prd.__name__)
|
||||
|
||||
return ctx.repo.docs.prd.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["Design", "software development", "Architect"])
|
||||
async def write_design(prd_path: str | Path) -> Path:
|
||||
"""Writes a system design to the project repository, based on the PRD of the project.
|
||||
|
||||
Args:
|
||||
prd_path (str|Path): The path to the PRD files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to the system design files under the project directory.
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import write_design
|
||||
>>> prd_path = '/path/to/project_path/docs/prd' # Returned by `write_prd`
|
||||
>>> system_design_path = await write_desgin(prd_path)
|
||||
>>> print(system_design_path)
|
||||
'/path/to/project_path/docs/system_design/'
|
||||
|
||||
"""
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Architect
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_design.__name__)], tool_name=write_design.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
prd_path = Path(prd_path)
|
||||
project_path = (Path(prd_path) if not prd_path.is_file() else prd_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = Architect(context=ctx)
|
||||
await role.run(with_message=Message(content="", cause_by=WritePRD))
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia Design File", value=str(ctx.repo.docs.system_design.workdir / i))
|
||||
for i in ctx.repo.docs.system_design.changed_files.keys()
|
||||
]
|
||||
for i in ctx.repo.resources.system_design.changed_files.keys():
|
||||
outputs.append(ToolLogItem(name="Design File", value=str(ctx.repo.resources.system_design.workdir / i)))
|
||||
for i in ctx.repo.resources.data_api_design.changed_files.keys():
|
||||
outputs.append(
|
||||
ToolLogItem(name="Class Diagram File", value=str(ctx.repo.resources.data_api_design.workdir / i))
|
||||
)
|
||||
for i in ctx.repo.resources.seq_flow.changed_files.keys():
|
||||
outputs.append(ToolLogItem(name="Sequence Diagram File", value=str(ctx.repo.resources.seq_flow.workdir / i)))
|
||||
log_tool_output(output=outputs, tool_name=write_design.__name__)
|
||||
|
||||
return ctx.repo.docs.system_design.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Architect"])
|
||||
async def write_project_plan(system_design_path: str | Path) -> Path:
|
||||
"""Writes a project plan to the project repository, based on the design of the project.
|
||||
|
||||
Args:
|
||||
system_design_path (str|Path): The path to the system design files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to task files under the project directory.
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import write_project_plan
|
||||
>>> system_design_path = '/path/to/project_path/docs/system_design/' # Returned by `write_design`
|
||||
>>> task_path = await write_project_plan(system_design_path)
|
||||
>>> print(task_path)
|
||||
'/path/to/project_path/docs/task'
|
||||
|
||||
"""
|
||||
from metagpt.actions import WriteDesign
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import ProjectManager
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_project_plan.__name__)],
|
||||
tool_name=write_project_plan.__name__,
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
system_design_path = Path(system_design_path)
|
||||
project_path = (system_design_path if not system_design_path.is_file() else system_design_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = ProjectManager(context=ctx)
|
||||
await role.run(with_message=Message(content="", cause_by=WriteDesign))
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Intermedia Project Plan", value=str(ctx.repo.docs.task.workdir / i))
|
||||
for i in ctx.repo.docs.task.changed_files.keys()
|
||||
]
|
||||
outputs.extend(
|
||||
[
|
||||
ToolLogItem(name="Project Plan", value=str(ctx.repo.resources.api_spec_and_task.workdir / i))
|
||||
for i in ctx.repo.resources.api_spec_and_task.changed_files.keys()
|
||||
]
|
||||
)
|
||||
log_tool_output(output=outputs, tool_name=write_project_plan.__name__)
|
||||
|
||||
return ctx.repo.docs.task.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Engineer"])
|
||||
async def write_codes(task_path: str | Path, inc: bool = False) -> Path:
|
||||
"""Writes code to implement designed features according to the project plan and adds them to the project repository.
|
||||
In code writing tasks, prioritize calling this tool against writing code from scratch directly.
|
||||
|
||||
Args:
|
||||
task_path (str|Path): The path to task files under the project directory.
|
||||
inc (bool, optional): Whether to write incremental codes. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Path: The path to the source code files under the project directory.
|
||||
|
||||
Example:
|
||||
# Write codes to a new project
|
||||
>>> from metagpt.tools.libs.software_development import write_codes
|
||||
>>> task_path = '/path/to/project_path/docs/task' # Returned by `write_project_plan`
|
||||
>>> src_path = await write_codes(task_path)
|
||||
>>> print(src_path)
|
||||
'/path/to/project_path/src/'
|
||||
|
||||
# Write increment codes to the exists project
|
||||
>>> from metagpt.tools.libs.software_development import write_codes
|
||||
>>> task_path = '/path/to/project_path/docs/task' # Returned by `write_prd`
|
||||
>>> src_path = await write_codes(task_path, inc=True)
|
||||
>>> print(src_path)
|
||||
'/path/to/project_path/src/'
|
||||
"""
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Engineer
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_codes.__name__)], tool_name=write_codes.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
ctx.config.inc = inc
|
||||
task_path = Path(task_path)
|
||||
project_path = (task_path if not task_path.is_file() else task_path.parent) / "../.."
|
||||
ctx.set_repo_dir(project_path)
|
||||
|
||||
role = Engineer(context=ctx)
|
||||
msg = Message(content="", cause_by=WriteTasks, send_to=role)
|
||||
me = {any_to_str(role), role.name}
|
||||
while me.intersection(msg.send_to):
|
||||
msg = await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Source File", value=str(ctx.repo.srcs.workdir / i))
|
||||
for i in ctx.repo.srcs.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=write_codes.__name__)
|
||||
|
||||
return ctx.repo.srcs.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "QaEngineer"])
|
||||
async def run_qa_test(src_path: str | Path) -> Path:
|
||||
"""Run QA test on the project repository.
|
||||
|
||||
Args:
|
||||
src_path (str|Path): The path to the source code files under the project directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to the unit tests under the project directory
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import run_qa_test
|
||||
>>> src_path = '/path/to/project_path/src/' # Returned by `write_codes`
|
||||
>>> test_path = await run_qa_test(src_path)
|
||||
>>> print(test_path)
|
||||
'/path/to/project_path/tests'
|
||||
"""
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.context import Context
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.roles import QaEngineer
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=run_qa_test.__name__)], tool_name=run_qa_test.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
src_path = Path(src_path)
|
||||
project_path = (src_path if not src_path.is_file() else src_path.parent) / ".."
|
||||
ctx.set_repo_dir(project_path)
|
||||
ctx.src_workspace = ctx.git_repo.workdir / ctx.git_repo.workdir.name
|
||||
|
||||
env = Environment(context=ctx)
|
||||
role = QaEngineer(context=ctx)
|
||||
env.add_role(role)
|
||||
|
||||
msg = Message(content="", cause_by=SummarizeCode, send_to=role)
|
||||
env.publish_message(msg)
|
||||
|
||||
while not env.is_idle:
|
||||
await env.run()
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Unit Test File", value=str(ctx.repo.tests.workdir / i))
|
||||
for i in ctx.repo.tests.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=run_qa_test.__name__)
|
||||
|
||||
return ctx.repo.tests.workdir
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "Engineer"])
|
||||
async def fix_bug(project_path: str | Path, issue: str) -> Path:
|
||||
"""Fix bugs in the project repository.
|
||||
|
||||
Args:
|
||||
project_path (str|Path): The path to the project repository.
|
||||
issue (str): Description of the bug or issue.
|
||||
|
||||
Returns:
|
||||
Path: The path to the project directory
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import fix_bug
|
||||
>>> project_path = '/path/to/project_path' # Returned by `write_codes`
|
||||
>>> issue = 'Exception: exception about ...; Bug: bug about ...; Issue: issue about ...'
|
||||
>>> project_path = await fix_bug(project_path=project_path, issue=issue)
|
||||
>>> print(project_path)
|
||||
'/path/to/project_path'
|
||||
"""
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.context import Context
|
||||
from metagpt.roles import Engineer
|
||||
|
||||
log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=fix_bug.__name__)], tool_name=fix_bug.__name__)
|
||||
|
||||
ctx = Context()
|
||||
ctx.set_repo_dir(project_path)
|
||||
ctx.src_workspace = ctx.git_repo.workdir / ctx.git_repo.workdir.name
|
||||
await ctx.repo.docs.save(filename=BUGFIX_FILENAME, content=issue)
|
||||
await ctx.repo.docs.save(filename=REQUIREMENT_FILENAME, content="")
|
||||
|
||||
role = Engineer(context=ctx)
|
||||
bug_fix = BugFixContext(filename=BUGFIX_FILENAME)
|
||||
msg = Message(
|
||||
content=bug_fix.model_dump_json(),
|
||||
instruct_content=bug_fix,
|
||||
role="",
|
||||
cause_by=FixBug,
|
||||
sent_from=role,
|
||||
send_to=role,
|
||||
)
|
||||
me = {any_to_str(role), role.name}
|
||||
while me.intersection(msg.send_to):
|
||||
msg = await role.run(with_message=msg)
|
||||
|
||||
outputs = [
|
||||
ToolLogItem(name="Changed File", value=str(ctx.repo.srcs.workdir / i))
|
||||
for i in ctx.repo.srcs.changed_files.keys()
|
||||
]
|
||||
log_tool_output(output=outputs, tool_name=fix_bug.__name__)
|
||||
|
||||
return project_path
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "git"])
|
||||
async def git_archive(project_path: str | Path) -> str:
|
||||
"""Stage and commit changes for the project repository using Git.
|
||||
|
||||
Args:
|
||||
project_path (str|Path): The path to the project repository.
|
||||
|
||||
|
||||
Returns:
|
||||
git log
|
||||
|
||||
Example:
|
||||
>>> from metagpt.tools.libs.software_development import git_archive
|
||||
>>> project_path = '/path/to/project_path' # Returned by `write_prd`
|
||||
>>> git_log = await git_archive(project_path=project_path)
|
||||
>>> print(git_log)
|
||||
commit a221d1c418c07f2b4fc07001e486285ead1a520a (HEAD -> feature/toollib/software_company, geekan/main)
|
||||
Merge: e01afd09 4a72f398
|
||||
Author: Sirui Hong <x@xx.github.com>
|
||||
Date: Tue Mar 19 15:16:03 2024 +0800
|
||||
Merge pull request #1037 from iorisa/fixbug/issues/1018
|
||||
fixbug: #1018
|
||||
|
||||
"""
|
||||
from metagpt.context import Context
|
||||
|
||||
log_tool_output(
|
||||
output=[ToolLogItem(name=ASSISTANT_ALIAS, value=git_archive.__name__)], tool_name=git_archive.__name__
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
project_dir = ProjectRepo.search_project_path(project_path)
|
||||
if not project_dir:
|
||||
ValueError(f"{project_path} is not a valid git repository.")
|
||||
ctx.set_repo_dir(project_dir)
|
||||
files = " ".join(ctx.git_repo.changed_files.keys())
|
||||
outputs = [ToolLogItem(name="cmd", value=f"git add {files}")]
|
||||
log_tool_output(output=outputs, tool_name=git_archive.__name__)
|
||||
ctx.git_repo.archive()
|
||||
|
||||
outputs = [ToolLogItem(name="cmd", value="git commit -m 'Archive'")]
|
||||
log_tool_output(output=outputs, tool_name=git_archive.__name__)
|
||||
|
||||
return ctx.git_repo.log()
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "import git repo"])
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class ToolRecommender(BaseModel):
|
|||
topk=topk,
|
||||
)
|
||||
rsp = await LLM().aask(prompt, stream=False)
|
||||
rsp = CodeParser.parse_code(block=None, text=rsp)
|
||||
rsp = CodeParser.parse_code(text=rsp)
|
||||
ranked_tools = json.loads(rsp)
|
||||
|
||||
valid_tools = validate_tool_names(ranked_tools)
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ class CodeParser:
|
|||
return block_dict
|
||||
|
||||
@classmethod
|
||||
def parse_code(cls, block: Optional[str], text: str, lang: str = "") -> str:
|
||||
def parse_code(cls, text: str, lang: str = "", block: Optional[str] = None) -> str:
|
||||
if block:
|
||||
text = cls.parse_block(block, text)
|
||||
pattern = rf"```{lang}.*?\s+(.*?)```"
|
||||
|
|
@ -287,7 +287,7 @@ class CodeParser:
|
|||
|
||||
@classmethod
|
||||
def parse_str(cls, block: str, text: str, lang: str = ""):
|
||||
code = cls.parse_code(block, text, lang)
|
||||
code = cls.parse_code(block=block, text=text, lang=lang)
|
||||
code = code.split("=")[-1]
|
||||
code = code.strip().strip("'").strip('"')
|
||||
return code
|
||||
|
|
@ -295,7 +295,7 @@ class CodeParser:
|
|||
@classmethod
|
||||
def parse_file_list(cls, block: str, text: str, lang: str = "") -> list[str]:
|
||||
# Regular expression pattern to find the tasks list.
|
||||
code = cls.parse_code(block, text, lang)
|
||||
code = cls.parse_code(block=block, text=text, lang=lang)
|
||||
# print(code)
|
||||
pattern = r"\s*(.*=.*)?(\[.*\])"
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ async def test_write_refined_code(context, git_dir):
|
|||
|
||||
# old_workspace contains the legacy code
|
||||
await context.repo.with_src_path(context.repo.old_workspace).srcs.save(
|
||||
filename="game.py", content=CodeParser.parse_code(block="", text=REFINED_CODE_INPUT_SAMPLE)
|
||||
filename="game.py", content=CodeParser.parse_code(text=REFINED_CODE_INPUT_SAMPLE)
|
||||
)
|
||||
|
||||
ccontext = CodingContext(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ async def test_write_code_plan_and_change_an(mocker, context, git_dir):
|
|||
await context.repo.docs.task.save(filename="2.json", content=json.dumps(REFINED_TASK_JSON))
|
||||
|
||||
await context.repo.with_src_path(context.repo.old_workspace).srcs.save(
|
||||
filename="game.py", content=CodeParser.parse_code(block="", text=REFINED_CODE_INPUT_SAMPLE)
|
||||
filename="game.py", content=CodeParser.parse_code(text=REFINED_CODE_INPUT_SAMPLE)
|
||||
)
|
||||
|
||||
root = ActionNode.from_children(
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ target_code = """task_list = [
|
|||
|
||||
|
||||
def test_parse_code():
|
||||
code = CodeParser.parse_code("Task list", TASKS, lang="python")
|
||||
code = CodeParser.parse_code(block="Task list", text=TASKS, lang="python")
|
||||
logger.info(code)
|
||||
assert isinstance(code, str)
|
||||
assert target_code == code
|
||||
|
|
|
|||
|
|
@ -11,17 +11,44 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.context import Context
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Architect, ProductManager, Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.roles import (
|
||||
Architect,
|
||||
Engineer,
|
||||
ProductManager,
|
||||
ProjectManager,
|
||||
QaEngineer,
|
||||
Role,
|
||||
)
|
||||
from metagpt.schema import Message, UserMessage
|
||||
from metagpt.utils.common import any_to_str, is_send_to
|
||||
|
||||
serdeser_path = Path(__file__).absolute().parent.joinpath("../data/serdeser_storage")
|
||||
|
||||
|
||||
class MockEnv(Environment):
|
||||
def publish_message(self, message: Message, peekable: bool = True) -> bool:
|
||||
consumers = []
|
||||
for role, addrs in self.member_addrs.items():
|
||||
if is_send_to(message, addrs):
|
||||
role.put_message(message)
|
||||
consumers.append(role)
|
||||
if not consumers:
|
||||
logger.warning(f"Message no recipients: {message.dump()}")
|
||||
if message.cause_by in [any_to_str(UserRequirement), any_to_str(PrepareDocuments)]:
|
||||
assert len(consumers) == 1
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
return Environment()
|
||||
context = Context()
|
||||
context.kwargs.tag = __file__
|
||||
return MockEnv(context=context)
|
||||
|
||||
|
||||
def test_add_role(env: Environment):
|
||||
|
|
@ -54,10 +81,56 @@ async def test_publish_and_process_message(env: Environment):
|
|||
|
||||
env.add_roles([product_manager, architect])
|
||||
|
||||
env.publish_message(Message(role="User", content="需要一个基于LLM做总结的搜索引擎", cause_by=UserRequirement))
|
||||
env.publish_message(UserMessage(content="需要一个基于LLM做总结的搜索引擎", cause_by=UserRequirement, send_to=product_manager))
|
||||
await env.run(k=2)
|
||||
logger.info(f"{env.history=}")
|
||||
assert len(env.history) > 10
|
||||
logger.info(f"{env.history}")
|
||||
assert len(env.history.storage) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("content", "send_to"),
|
||||
[
|
||||
("snake game", any_to_str(ProductManager)),
|
||||
(
|
||||
"Rewrite the PRD file of the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game', add 'moving enemy' to the original requirement",
|
||||
any_to_str(ProductManager),
|
||||
),
|
||||
(
|
||||
"Add 'random moving enemy, and dispears after 10 seconds' design to the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
any_to_str(Architect),
|
||||
),
|
||||
(
|
||||
'Rewrite the tasks file of the project at "/Users/iorishinier/github/MetaGPT/workspace/snake_game"',
|
||||
any_to_str(ProjectManager),
|
||||
),
|
||||
(
|
||||
"Rewrite 'main.py' of the project at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
any_to_str(Engineer),
|
||||
),
|
||||
(
|
||||
"Rewrite the unit test of 'main.py' at '/Users/iorishinier/github/MetaGPT/workspace/snake_game'",
|
||||
any_to_str(QaEngineer),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_env(content, send_to):
|
||||
context = Context()
|
||||
env = MockEnv(context=context)
|
||||
env.add_roles(
|
||||
[
|
||||
ProductManager(context=context),
|
||||
Architect(context=context),
|
||||
ProjectManager(context=context),
|
||||
Engineer(n_borg=5, use_code_review=True, context=context),
|
||||
QaEngineer(context=context, test_round_allowed=2),
|
||||
]
|
||||
)
|
||||
msg = UserMessage(content=content, send_to=send_to)
|
||||
env.publish_message(msg)
|
||||
while not env.is_idle:
|
||||
await env.run()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -350,5 +350,47 @@ class TestPlan:
|
|||
assert plan.current_task_id == "2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "key_descriptions"),
|
||||
[
|
||||
(
|
||||
"""
|
||||
Traceback (most recent call last):
|
||||
File "/Users/iorishinier/github/MetaGPT/workspace/game_2048_1/game_2048/main.py", line 38, in <module>
|
||||
Main().main()
|
||||
File "/Users/iorishinier/github/MetaGPT/workspace/game_2048_1/game_2048/main.py", line 28, in main
|
||||
self.user_interface.draw()
|
||||
File "/Users/iorishinier/github/MetaGPT/workspace/game_2048_1/game_2048/user_interface.py", line 16, in draw
|
||||
if grid[i][j] != 0:
|
||||
TypeError: 'Grid' object is not subscriptable
|
||||
""",
|
||||
{
|
||||
"filename": "the string type of the path name of the source code where the bug resides",
|
||||
"line": "the integer type of the line error occurs",
|
||||
"function_name": "the string type of the function name the error occurs in",
|
||||
"code": "the string type of the codes where the error occurs at",
|
||||
"info": "the string type of the error information",
|
||||
},
|
||||
),
|
||||
(
|
||||
"将代码提交到github上的iorisa/repo1的branch1分支,发起pull request ,合并到master分支。",
|
||||
{
|
||||
"repo_name": "the string type of github repo to create pull",
|
||||
"head": "the string type of github branch to be pushed",
|
||||
"base": "the string type of github branch to merge the changes into",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_parse_resources(context, content: str, key_descriptions):
|
||||
msg = Message(content=content)
|
||||
llm = context.llm_with_cost_manager_from_llm_config(context.config.llm)
|
||||
result = await msg.parse_resources(llm=llm, key_descriptions=key_descriptions)
|
||||
assert result
|
||||
assert result.get("resources")
|
||||
for k in key_descriptions.keys():
|
||||
assert k in result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-s"])
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class TestCodeParser:
|
|||
assert "game.py" in result
|
||||
|
||||
def test_parse_code(self, parser, text):
|
||||
result = parser.parse_code("Task list", text, "python")
|
||||
result = parser.parse_code(block="Task list", text=text, lang="python")
|
||||
print(result)
|
||||
assert "game.py" in result
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class MockLLM(OriginalLLM):
|
|||
format_msgs: Optional[list[dict[str, str]]] = None,
|
||||
images: Optional[Union[str, list[str]]] = None,
|
||||
timeout=LLM_API_TIMEOUT,
|
||||
stream=True,
|
||||
stream=False,
|
||||
) -> str:
|
||||
# used to identify it a message has been called before
|
||||
if isinstance(msg, list):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue