Merge branch 'feature/at_role' into 'mgx_ops'

feat: 支持直接给软件公司各个角色发用户需求

See merge request pub/MetaGPT!84
This commit is contained in:
林义章 2024-04-30 09:18:01 +00:00
commit c0875d6ae8
34 changed files with 409 additions and 495 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -139,3 +139,6 @@ LLM_API_TIMEOUT = 300
# Assistant alias
ASSISTANT_ALIAS = "response"
# Metadata defines
AGENT = "agent"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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*(.*=.*)?(\[.*\])"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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