mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-05-04 21:32:38 +02:00
Merge branch 'role_zero_draft' into 'mgx_ops'
Role Zero See merge request pub/MetaGPT!141
This commit is contained in:
commit
cbea1bedec
20 changed files with 688 additions and 182 deletions
|
|
@ -48,7 +48,7 @@ NEW_REQ_TEMPLATE = """
|
|||
"""
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "write system design"])
|
||||
@register_tool(include_functions=["run"])
|
||||
class WriteDesign(Action):
|
||||
name: str = ""
|
||||
i_context: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ NEW_REQ_TEMPLATE = """
|
|||
"""
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "write a project schedule given a project system design file"])
|
||||
@register_tool(include_functions=["run"])
|
||||
class WriteTasks(Action):
|
||||
name: str = "CreateTasks"
|
||||
i_context: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ NEW_REQ_TEMPLATE = """
|
|||
"""
|
||||
|
||||
|
||||
@register_tool(tags=["software development", "write product requirement documents"])
|
||||
@register_tool(include_functions=["run"])
|
||||
class WritePRD(Action):
|
||||
"""WritePRD deal with the following situations:
|
||||
1. Bugfix: If the requirement is a bugfix, the bugfix document will be generated.
|
||||
|
|
|
|||
6
metagpt/prompts/di/engineer2.py
Normal file
6
metagpt/prompts/di/engineer2.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from metagpt.prompts.di.role_zero import ROLE_INSTRUCTION
|
||||
|
||||
ENGINEER2_INSTRUCTION = (
|
||||
ROLE_INSTRUCTION
|
||||
+ "4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand."
|
||||
)
|
||||
52
metagpt/prompts/di/role_zero.py
Normal file
52
metagpt/prompts/di/role_zero.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
ROLE_INSTRUCTION = """
|
||||
Based on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.
|
||||
If plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.
|
||||
When presented a current task, tackle the task using the available commands.
|
||||
Pay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.
|
||||
Note:
|
||||
1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.
|
||||
2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.
|
||||
3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.
|
||||
"""
|
||||
|
||||
CMD_PROMPT = """
|
||||
# Data Structure
|
||||
class Task(BaseModel):
|
||||
task_id: str = ""
|
||||
dependent_task_ids: list[str] = []
|
||||
instruction: str = ""
|
||||
task_type: str = ""
|
||||
assignee: str = ""
|
||||
|
||||
# Available Commands
|
||||
{available_commands}
|
||||
Special Command: Use {{"command_name": "pass"}} to do nothing and {{"command_name": "end"}} to indicate completion of all requirements and the end of actions.
|
||||
|
||||
# Current Plan
|
||||
{plan_status}
|
||||
|
||||
# Current Task
|
||||
{current_task}
|
||||
|
||||
# Example
|
||||
{example}
|
||||
|
||||
# Instruction
|
||||
{instruction}
|
||||
|
||||
Pay close attention to the Example provided, you can reuse the example for your current situation if it fits.
|
||||
You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.
|
||||
If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.
|
||||
|
||||
# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:
|
||||
Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.
|
||||
```json
|
||||
[
|
||||
{{
|
||||
"command_name": str,
|
||||
"args": {{"arg_name": arg_value, ...}}
|
||||
}},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
|
|
@ -4,62 +4,27 @@ When drafting and routing tasks, ALWAYS include necessary or important info insi
|
|||
Each time you do something, reply to human letting them know what you did.
|
||||
"""
|
||||
|
||||
CMD_PROMPT = """
|
||||
# Data Structure
|
||||
class Task(BaseModel):
|
||||
task_id: str = ""
|
||||
dependent_task_ids: list[str] = []
|
||||
instruction: str = ""
|
||||
task_type: str = ""
|
||||
assignee: str = ""
|
||||
|
||||
# Team Member Info
|
||||
{team_info}
|
||||
|
||||
# Available Commands
|
||||
{available_commands}
|
||||
|
||||
# Current Plan
|
||||
{plan_status}
|
||||
|
||||
# Example
|
||||
{example}
|
||||
|
||||
# Instructions
|
||||
TL_INSTRUCTION = """
|
||||
You are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.
|
||||
Your team member:
|
||||
{team_info}
|
||||
You should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.
|
||||
When creating a new plan involving multiple members, create all tasks at once.
|
||||
If plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as finish_current_task, reset_task, replace_task, etc.
|
||||
You should publish_message to team members, asking them to start their task.
|
||||
Pay close attention to new user message, review the conversation history, use reply_to_human to respond to the user directly, DON'T ask your team members.
|
||||
If plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.
|
||||
You should use TeamLeader.publish_team_message to team members, asking them to start their task.
|
||||
Pay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.
|
||||
|
||||
Note:
|
||||
1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.
|
||||
2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.
|
||||
3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.
|
||||
Pay close attention to the Example provided
|
||||
|
||||
You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.
|
||||
If you finish current task, you will automatically take the next task in the existing plan, use finish_task, DON'T append a new task.
|
||||
|
||||
# Your commands in a json array, in the following output format, always output a json array, if there is nothing to do, use the pass command:
|
||||
Some text indicating your thoughts, such as how you categorize the requirement based on Note (is it 1., 2., or 3.?) or how you should update the plan status. Then a json array of commands.
|
||||
```json
|
||||
[
|
||||
{{
|
||||
"command_name": str,
|
||||
"args": {{"arg_name": arg_value, ...}}
|
||||
}},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
|
||||
FINISH_CURRENT_TASK_CMD = """
|
||||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "finish_current_task",
|
||||
"command_name": "Plan.finish_current_task",
|
||||
"args": {{}}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@
|
|||
@Author : alexanderwu
|
||||
@File : architect.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.roles.di.role_zero import RoleZero
|
||||
|
||||
|
||||
class Architect(Role):
|
||||
class Architect(RoleZero):
|
||||
"""
|
||||
Represents an Architect role in a software development process.
|
||||
|
||||
|
|
@ -30,11 +29,26 @@ class Architect(Role):
|
|||
"libraries. Use same language as user requirement"
|
||||
)
|
||||
|
||||
instruction: str = """Use WriteDesign tool to write a system design document"""
|
||||
max_react_loop: int = 1 # FIXME: Read and edit files requires more steps, consider later
|
||||
tools: list[str] = ["Editor:write,read,write_content", "RoleZero", "WriteDesign"]
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# NOTE: The following init setting will only be effective when self.use_fixed_sop is changed to True
|
||||
self.enable_memory = False
|
||||
# Initialize actions specific to the Architect role
|
||||
self.set_actions([WriteDesign])
|
||||
|
||||
# Set events or actions the Architect should watch or be aware of
|
||||
self._watch({WritePRD})
|
||||
|
||||
def _update_tool_execution(self):
|
||||
wd = WriteDesign()
|
||||
self.tool_execution_map.update(
|
||||
{
|
||||
"WriteDesign.run": wd.run,
|
||||
"WriteDesign": wd.run, # alias
|
||||
}
|
||||
)
|
||||
|
|
|
|||
13
metagpt/roles/di/engineer2.py
Normal file
13
metagpt/roles/di/engineer2.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from metagpt.prompts.di.engineer2 import ENGINEER2_INSTRUCTION
|
||||
from metagpt.roles.di.role_zero import RoleZero
|
||||
|
||||
|
||||
class Engineer2(RoleZero):
|
||||
name: str = "Alex"
|
||||
profile: str = "Engineer"
|
||||
goal: str = "Take on game, app, and web development"
|
||||
instruction: str = ENGINEER2_INSTRUCTION
|
||||
|
||||
tools: str = ["Plan", "Editor:write,read,write_content", "RoleZero"]
|
||||
259
metagpt/roles/di/role_zero.py
Normal file
259
metagpt/roles/di/role_zero.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import traceback
|
||||
from typing import Literal, Tuple
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.actions.di.run_command import RunCommand
|
||||
from metagpt.logs import logger
|
||||
from metagpt.prompts.di.role_zero import CMD_PROMPT, ROLE_INSTRUCTION
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import AIMessage, Message, UserMessage
|
||||
from metagpt.strategy.experience_retriever import DummyExpRetriever, ExpRetriever
|
||||
from metagpt.strategy.planner import Planner
|
||||
from metagpt.tools.libs.browser import Browser
|
||||
from metagpt.tools.libs.editor import Editor
|
||||
from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.report import ThoughtReporter
|
||||
|
||||
|
||||
@register_tool(include_functions=["ask_human", "reply_to_human"])
|
||||
class RoleZero(Role):
|
||||
"""A role who can think and act dynamically"""
|
||||
|
||||
# Basic Info
|
||||
name: str = "Zero"
|
||||
profile: str = "RoleZero"
|
||||
goal: str = ""
|
||||
system_msg: list[str] = None # Use None to conform to the default value at llm.aask
|
||||
cmd_prompt: str = CMD_PROMPT
|
||||
instruction: str = ROLE_INSTRUCTION
|
||||
|
||||
# React Mode
|
||||
react_mode: Literal["react"] = "react"
|
||||
max_react_loop: int = 20 # used for react mode
|
||||
|
||||
# Tools
|
||||
tools: list[str] = [] # Use special symbol ["<all>"] to indicate use of all registered tools
|
||||
tool_recommender: ToolRecommender = None
|
||||
tool_execution_map: dict[str, callable] = {}
|
||||
special_tool_commands: list[str] = ["Plan.finish_current_task", "end"]
|
||||
# Equipped with three basic tools by default for optional use
|
||||
editor: Editor = Editor()
|
||||
browser: Browser = Browser()
|
||||
# terminal: Terminal = Terminal() # FIXME: TypeError: cannot pickle '_thread.lock' object
|
||||
|
||||
# Experience
|
||||
experience_retriever: ExpRetriever = DummyExpRetriever()
|
||||
|
||||
# Others
|
||||
user_requirement: str = ""
|
||||
command_rsp: str = "" # the raw string containing the commands
|
||||
commands: list[dict] = [] # commands to be executed
|
||||
memory_k: int = 20 # number of memories (messages) to use as historical context
|
||||
use_fixed_sop: bool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_plan_and_tool(self) -> "RoleZero":
|
||||
# We force using this parameter for DataAnalyst
|
||||
assert self.react_mode == "react"
|
||||
|
||||
# Roughly the same part as DataInterpreter.set_plan_and_tool
|
||||
self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop)
|
||||
if self.tools and not self.tool_recommender:
|
||||
self.tool_recommender = BM25ToolRecommender(tools=self.tools, force=True)
|
||||
self.set_actions([RunCommand])
|
||||
|
||||
# HACK: Init Planner, control it through dynamic thinking; Consider formalizing as a react mode
|
||||
self.planner = Planner(goal="", working_memory=self.rc.working_memory, auto_run=True)
|
||||
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_tool_execution(self) -> "RoleZero":
|
||||
# default map
|
||||
self.tool_execution_map = {
|
||||
"Plan.append_task": self.planner.plan.append_task,
|
||||
"Plan.reset_task": self.planner.plan.reset_task,
|
||||
"Plan.replace_task": self.planner.plan.replace_task,
|
||||
"Editor.write": self.editor.write,
|
||||
"Editor.write_content": self.editor.write_content,
|
||||
"Editor.read": self.editor.read,
|
||||
"RoleZero.ask_human": self.ask_human,
|
||||
"RoleZero.reply_to_human": self.reply_to_human,
|
||||
}
|
||||
# can be updated by subclass
|
||||
self._update_tool_execution()
|
||||
return self
|
||||
|
||||
def _update_tool_execution(self):
|
||||
pass
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Useful in 'react' mode. Use LLM to decide whether and what to do next."""
|
||||
# Compatibility
|
||||
if self.use_fixed_sop:
|
||||
return await super()._think()
|
||||
|
||||
### 0. Preparation ###
|
||||
if not self.rc.todo:
|
||||
return False
|
||||
|
||||
if not self.planner.plan.goal:
|
||||
self.user_requirement = self.get_memories()[-1].content
|
||||
self.planner.plan.goal = self.user_requirement
|
||||
|
||||
### 1. Experience ###
|
||||
example = self._retrieve_experience()
|
||||
|
||||
### 2. Plan Status ###
|
||||
plan_status, current_task = self._get_plan_status()
|
||||
|
||||
### 3. Tool/Command Info ###
|
||||
tools = await self.tool_recommender.recommend_tools()
|
||||
tool_info = json.dumps({tool.name: tool.schemas for tool in tools})
|
||||
|
||||
### Make Decision Dynamically ###
|
||||
prompt = self.cmd_prompt.format(
|
||||
plan_status=plan_status,
|
||||
current_task=current_task,
|
||||
example=example,
|
||||
available_commands=tool_info,
|
||||
instruction=self.instruction.strip(),
|
||||
)
|
||||
context = self.llm.format_msg(self.rc.memory.get(self.memory_k) + [UserMessage(content=prompt)])
|
||||
# print(*context, sep="\n" + "*" * 5 + "\n")
|
||||
async with ThoughtReporter():
|
||||
self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg)
|
||||
self.rc.memory.add(AIMessage(content=self.command_rsp))
|
||||
|
||||
return True
|
||||
|
||||
async def _act(self) -> Message:
|
||||
if self.use_fixed_sop:
|
||||
return await super()._act()
|
||||
|
||||
try:
|
||||
commands = json.loads(CodeParser.parse_code(block=None, lang="json", text=self.command_rsp))
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
print(tb)
|
||||
error_msg = UserMessage(content=str(e))
|
||||
self.rc.memory.add(error_msg)
|
||||
return error_msg
|
||||
outputs = await self._run_commands(commands)
|
||||
self.rc.memory.add(UserMessage(content=outputs))
|
||||
return AIMessage(
|
||||
content=f"Complete run with outputs: {outputs}",
|
||||
sent_from=self.name,
|
||||
cause_by=RunCommand,
|
||||
)
|
||||
|
||||
async def _react(self) -> Message:
|
||||
# NOTE: Diff 1: Each time landing here means observing news, set todo to allow news processing in _think
|
||||
self._set_state(0)
|
||||
|
||||
actions_taken = 0
|
||||
rsp = AIMessage(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act
|
||||
while actions_taken < self.rc.max_react_loop:
|
||||
# NOTE: Diff 2: Keep observing within _react, news will go into memory, allowing adapting to new info
|
||||
await self._observe()
|
||||
|
||||
# think
|
||||
has_todo = await self._think()
|
||||
if not has_todo:
|
||||
break
|
||||
# act
|
||||
logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}")
|
||||
rsp = await self._act()
|
||||
actions_taken += 1
|
||||
return rsp # return output from the last action
|
||||
|
||||
async def _run_commands(self, commands) -> list:
|
||||
outputs = []
|
||||
for cmd in commands:
|
||||
# handle special command first
|
||||
if await self._run_special_command(cmd):
|
||||
continue
|
||||
# run command as specified by tool_execute_map
|
||||
if cmd["command_name"] in self.tool_execution_map:
|
||||
tool_obj = self.tool_execution_map[cmd["command_name"]]
|
||||
output = f"Command {cmd['command_name']} executed"
|
||||
try:
|
||||
if inspect.iscoroutinefunction(tool_obj):
|
||||
tool_output = await tool_obj(**cmd["args"])
|
||||
else:
|
||||
tool_output = tool_obj(**cmd["args"])
|
||||
if tool_output:
|
||||
output += f": {str(tool_output)}"
|
||||
outputs.append(output)
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
logger.exception(e + tb)
|
||||
outputs.append(output + f": {tb}")
|
||||
break # Stop executing if any command fails
|
||||
else:
|
||||
outputs.append(f"Command {cmd['command_name']} not found.")
|
||||
break
|
||||
outputs = "\n\n".join(outputs)
|
||||
|
||||
return outputs
|
||||
|
||||
async def _run_special_command(self, cmd) -> bool:
|
||||
"""command requiring special check or parsing"""
|
||||
is_special_cmd = cmd["command_name"] in self.special_tool_commands
|
||||
|
||||
if cmd["command_name"] == "Plan.finish_current_task" and not self.planner.plan.is_plan_finished():
|
||||
# task_result = TaskResult(code=str(commands), result=outputs, is_success=is_success)
|
||||
# self.planner.plan.current_task.update_task_result(task_result=task_result)
|
||||
self.planner.plan.finish_current_task()
|
||||
|
||||
elif cmd["command_name"] == "end":
|
||||
self._set_state(-1)
|
||||
|
||||
return is_special_cmd
|
||||
|
||||
def _get_plan_status(self) -> Tuple[str, str]:
|
||||
plan_status = self.planner.plan.model_dump(include=["goal", "tasks"])
|
||||
for task in plan_status["tasks"]:
|
||||
task.pop("code")
|
||||
task.pop("result")
|
||||
task.pop("is_success")
|
||||
# print(plan_status)
|
||||
current_task = (
|
||||
self.planner.plan.current_task.model_dump(exclude=["code", "result", "is_success"])
|
||||
if self.planner.plan.current_task
|
||||
else ""
|
||||
)
|
||||
return plan_status, current_task
|
||||
|
||||
def _retrieve_experience(self) -> str:
|
||||
"""Default implementation of experience retrieval. Can be overwritten in subclasses."""
|
||||
context = [str(msg) for msg in self.rc.memory.get(self.memory_k)]
|
||||
context = "\n\n".join(context)
|
||||
example = self.experience_retriever.retrieve(context=context)
|
||||
return example
|
||||
|
||||
async def ask_human(self, question: str) -> str:
|
||||
"""Use this when you fail the current task or if you are unsure of the situation encountered. Your response should contain a brief summary of your situation, ended with a clear and concise question."""
|
||||
# NOTE: Can be overwritten in remote setting
|
||||
from metagpt.environment.mgx.mgx_env import MGXEnv # avoid circular import
|
||||
|
||||
if not isinstance(self.rc.env, MGXEnv):
|
||||
return "Not in MGXEnv, command will not be executed."
|
||||
return await self.rc.env.get_human_input(question, sent_from=self)
|
||||
|
||||
async def reply_to_human(self, content: str) -> str:
|
||||
"""Reply to human user with the content provided. Use this when you have a clear answer or solution to the user's question."""
|
||||
# NOTE: Can be overwritten in remote setting
|
||||
from metagpt.environment.mgx.mgx_env import MGXEnv # avoid circular import
|
||||
|
||||
if not isinstance(self.rc.env, MGXEnv):
|
||||
return "Not in MGXEnv, command will not be executed."
|
||||
return await self.rc.env.reply_to_human(content, sent_from=self)
|
||||
|
|
@ -1,101 +1,69 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from metagpt.actions.di.run_command import RunCommand
|
||||
from metagpt.prompts.di.team_leader import (
|
||||
CMD_PROMPT,
|
||||
FINISH_CURRENT_TASK_CMD,
|
||||
SYSTEM_PROMPT,
|
||||
TL_INSTRUCTION,
|
||||
)
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message, TaskResult
|
||||
from metagpt.strategy.experience_retriever import SimpleExpRetriever
|
||||
from metagpt.strategy.planner import Planner
|
||||
from metagpt.strategy.thinking_command import (
|
||||
Command,
|
||||
prepare_command_prompt,
|
||||
run_commands,
|
||||
)
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.report import ThoughtReporter
|
||||
from metagpt.roles.di.role_zero import RoleZero
|
||||
from metagpt.schema import AIMessage, Message, UserMessage
|
||||
from metagpt.strategy.experience_retriever import ExpRetriever, SimpleExpRetriever
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
|
||||
|
||||
class TeamLeader(Role):
|
||||
@register_tool(include_functions=["publish_team_message"])
|
||||
class TeamLeader(RoleZero):
|
||||
name: str = "Tim"
|
||||
profile: str = "Team Leader"
|
||||
task_result: TaskResult = None
|
||||
available_commands: list[Command] = [
|
||||
Command.APPEND_TASK,
|
||||
Command.RESET_TASK,
|
||||
Command.REPLACE_TASK,
|
||||
Command.FINISH_CURRENT_TASK,
|
||||
Command.PUBLISH_MESSAGE,
|
||||
Command.ASK_HUMAN,
|
||||
Command.REPLY_TO_HUMAN,
|
||||
Command.PASS,
|
||||
]
|
||||
commands: list[dict] = [] # issued commands to be executed
|
||||
system_msg: list[str] = [SYSTEM_PROMPT]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_plan(self) -> "TeamLeader":
|
||||
self.planner = Planner(goal=self.goal, working_memory=self.rc.working_memory, auto_run=True)
|
||||
return self
|
||||
max_react_loop: int = 1 # TeamLeader only reacts once each time
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Useful in 'react' mode. Use LLM to decide whether and what to do next."""
|
||||
tools: list[str] = ["Plan", "RoleZero", "TeamLeader"]
|
||||
|
||||
if not self.planner.plan.goal:
|
||||
user_requirement = self.get_memories()[-1].content
|
||||
self.planner.plan.goal = user_requirement
|
||||
experience_retriever: ExpRetriever = SimpleExpRetriever()
|
||||
|
||||
plan_status = self.planner.plan.model_dump(include=["goal", "tasks"])
|
||||
for task in plan_status["tasks"]:
|
||||
task.pop("code")
|
||||
task.pop("result")
|
||||
def _update_tool_execution(self):
|
||||
self.tool_execution_map.update(
|
||||
{
|
||||
"TeamLeader.publish_team_message": self.publish_team_message,
|
||||
"TeamLeader.publish_message": self.publish_team_message, # alias
|
||||
}
|
||||
)
|
||||
|
||||
def set_instruction(self):
|
||||
team_info = ""
|
||||
for role in self.rc.env.roles.values():
|
||||
if role.profile == "TeamLeader":
|
||||
continue
|
||||
# if role.profile == "Team Leader":
|
||||
# continue
|
||||
team_info += f"{role.name}: {role.profile}, {role.goal}\n"
|
||||
example = SimpleExpRetriever().retrieve()
|
||||
self.instruction = TL_INSTRUCTION.format(team_info=team_info)
|
||||
|
||||
prompt = CMD_PROMPT.format(
|
||||
plan_status=plan_status,
|
||||
team_info=team_info,
|
||||
example=example,
|
||||
available_commands=prepare_command_prompt(self.available_commands),
|
||||
)
|
||||
context = self.llm.format_msg(self.get_memories(k=10) + [Message(content=prompt, role="user")])
|
||||
async def _think(self) -> bool:
|
||||
self.set_instruction()
|
||||
return await super()._think()
|
||||
|
||||
async with ThoughtReporter():
|
||||
rsp = await self.llm.aask(context, system_msgs=[SYSTEM_PROMPT])
|
||||
self.commands = json.loads(CodeParser.parse_code(text=rsp))
|
||||
self.rc.memory.add(Message(content=rsp, role="assistant"))
|
||||
|
||||
return True
|
||||
|
||||
async def _act(self) -> Message:
|
||||
"""Useful in 'react' mode. Return a Message conforming to Role._act interface."""
|
||||
await run_commands(self, self.commands, self.rc.memory)
|
||||
self.task_result = TaskResult(result="Success", is_success=True)
|
||||
msg = Message(content="Commands executed", send_to="no one") # a dummy message to conform to the interface
|
||||
self.rc.memory.add(msg)
|
||||
return msg
|
||||
|
||||
def publish_message(self, msg):
|
||||
"""If the role belongs to env, then the role's messages will be broadcast to env"""
|
||||
def publish_message(self, msg: Message, send_to="no one"):
|
||||
"""Overwrite Role.publish_message, send to no one if called within Role.run, send to the specified role if called dynamically."""
|
||||
if not msg:
|
||||
return
|
||||
if not self.rc.env:
|
||||
# If env does not exist, do not publish the message
|
||||
return
|
||||
msg.sent_from = self.profile
|
||||
msg.cause_by = RunCommand
|
||||
msg.send_to = send_to
|
||||
self.rc.env.publish_message(msg, publicer=self.profile)
|
||||
|
||||
def publish_team_message(self, content: str, send_to: str):
|
||||
"""
|
||||
Publish a message to a team member, use member name to fill send_to args. You may copy the full original content or add additional information from upstream. This will make team members start their work.
|
||||
DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.
|
||||
"""
|
||||
# Specify the outer send_to to overwrite the default "no one" value. Use UserMessage because message from self is like a user request for others.
|
||||
self.publish_message(
|
||||
UserMessage(content=content, sent_from=self.name, send_to=send_to, cause_by=RunCommand), send_to=send_to
|
||||
)
|
||||
|
||||
def finish_current_task(self):
|
||||
self.planner.plan.finish_current_task()
|
||||
self.rc.memory.add(Message(content=FINISH_CURRENT_TASK_CMD, role="assistant"))
|
||||
self.rc.memory.add(AIMessage(content=FINISH_CURRENT_TASK_CMD))
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@
|
|||
@Modified By: mashenquan, 2023/11/27. Add `PrepareDocuments` action according to Section 2.2.3.5.1 of RFC 135.
|
||||
"""
|
||||
|
||||
|
||||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.roles.role import Role, RoleReactMode
|
||||
from metagpt.roles.di.role_zero import RoleZero
|
||||
from metagpt.roles.role import RoleReactMode
|
||||
from metagpt.utils.common import any_to_name, any_to_str
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class ProductManager(Role):
|
||||
class ProductManager(RoleZero):
|
||||
"""
|
||||
Represents a Product Manager role responsible for product development and management.
|
||||
|
||||
|
|
@ -30,18 +30,35 @@ class ProductManager(Role):
|
|||
profile: str = "Product Manager"
|
||||
goal: str = "efficiently create a successful product that meets market demands and user expectations"
|
||||
constraints: str = "utilize the same language as the user requirements for seamless communication"
|
||||
todo_action: str = ""
|
||||
todo_action: str = any_to_name(WritePRD)
|
||||
|
||||
instruction: str = """Use WritePRD tool to write PRD"""
|
||||
max_react_loop: int = 1 # FIXME: Read and edit files requires more steps, consider later
|
||||
tools: list[str] = ["Editor:write,read,write_content", "RoleZero", "WritePRD"]
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# NOTE: The following init setting will only be effective when self.use_fixed_sop is changed to True
|
||||
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)
|
||||
if self.use_fixed_sop:
|
||||
self.rc.react_mode = RoleReactMode.BY_ORDER
|
||||
|
||||
def _update_tool_execution(self):
|
||||
wp = WritePRD()
|
||||
self.tool_execution_map.update(
|
||||
{
|
||||
"WritePRD.run": wp.run,
|
||||
"WritePRD": wp.run, # alias
|
||||
}
|
||||
)
|
||||
|
||||
async def _think(self) -> bool:
|
||||
"""Decide what to do"""
|
||||
if not self.use_fixed_sop:
|
||||
return await super()._think()
|
||||
|
||||
if GitRepository.is_git_dir(self.config.project_path) and not self.config.git_reinit:
|
||||
self._set_state(1)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@
|
|||
@Author : alexanderwu
|
||||
@File : project_manager.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.roles.role import Role
|
||||
from metagpt.roles.di.role_zero import RoleZero
|
||||
|
||||
|
||||
class ProjectManager(Role):
|
||||
class ProjectManager(RoleZero):
|
||||
"""
|
||||
Represents a Project Manager role responsible for overseeing project execution and team efficiency.
|
||||
|
||||
|
|
@ -30,8 +29,22 @@ class ProjectManager(Role):
|
|||
)
|
||||
constraints: str = "use same language as user requirement"
|
||||
|
||||
instruction: str = """Use WriteTasks tool to write a project task list"""
|
||||
max_react_loop: int = 1 # FIXME: Read and edit files requires more steps, consider later
|
||||
tools: list[str] = ["Editor:write,read,write_content", "RoleZero", "WriteTasks"]
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# NOTE: The following init setting will only be effective when self.use_fixed_sop is changed to True
|
||||
self.enable_memory = False
|
||||
self.set_actions([WriteTasks])
|
||||
self._watch([WriteDesign])
|
||||
|
||||
def _update_tool_execution(self):
|
||||
wt = WriteTasks()
|
||||
self.tool_execution_map.update(
|
||||
{
|
||||
"WriteTasks.run": wt.run,
|
||||
"WriteTasks": wt.run, # alias
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from metagpt.const import (
|
|||
)
|
||||
from metagpt.logs import logger
|
||||
from metagpt.repo_parser import DotClassInfo
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
from metagpt.utils.common import (
|
||||
CodeParser,
|
||||
any_to_str,
|
||||
|
|
@ -476,7 +477,17 @@ class TaskResult(BaseModel):
|
|||
is_success: bool
|
||||
|
||||
|
||||
@register_tool(
|
||||
include_functions=[
|
||||
"append_task",
|
||||
"reset_task",
|
||||
"replace_task",
|
||||
"finish_current_task",
|
||||
]
|
||||
)
|
||||
class Plan(BaseModel):
|
||||
"""Plan is a sequence of tasks towards a goal."""
|
||||
|
||||
goal: str
|
||||
context: str = ""
|
||||
tasks: list[Task] = []
|
||||
|
|
@ -549,13 +560,10 @@ class Plan(BaseModel):
|
|||
|
||||
def reset_task(self, task_id: str):
|
||||
"""
|
||||
Clear code and result of the task based on task_id, and set the task as unfinished.
|
||||
Reset a task based on task_id, i.e. set Task.is_finished=False and request redo. This also resets all tasks depending on it.
|
||||
|
||||
Args:
|
||||
task_id (str): The ID of the task to be reset.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if task_id in self.task_map:
|
||||
task = self.task_map[task_id]
|
||||
|
|
@ -568,7 +576,7 @@ class Plan(BaseModel):
|
|||
|
||||
self._update_current_task()
|
||||
|
||||
def replace_task(self, new_task: Task):
|
||||
def _replace_task(self, new_task: Task):
|
||||
"""
|
||||
Replace an existing task with the new input task based on task_id, and reset all tasks depending on it.
|
||||
|
||||
|
|
@ -593,7 +601,7 @@ class Plan(BaseModel):
|
|||
|
||||
self._update_current_task()
|
||||
|
||||
def append_task(self, new_task: Task):
|
||||
def _append_task(self, new_task: Task):
|
||||
"""
|
||||
Append a new task to the end of existing task sequences
|
||||
|
||||
|
|
@ -661,6 +669,23 @@ class Plan(BaseModel):
|
|||
"""
|
||||
return [task for task in self.tasks if task.is_finished]
|
||||
|
||||
def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str):
|
||||
"""Append a new task with task_id (number) to the end of existing task sequences. If dependent_task_ids is not empty, the task will depend on the tasks with the ids in the list."""
|
||||
new_task = Task(
|
||||
task_id=task_id, dependent_task_ids=dependent_task_ids, instruction=instruction, assignee=assignee
|
||||
)
|
||||
return self._append_task(new_task)
|
||||
|
||||
def replace_task(self, task_id: str, new_dependent_task_ids: list[str], new_instruction: str, new_assignee: str):
|
||||
"""Replace an existing task (can be current task) based on task_id, and reset all tasks depending on it."""
|
||||
new_task = Task(
|
||||
task_id=task_id,
|
||||
dependent_task_ids=new_dependent_task_ids,
|
||||
instruction=new_instruction,
|
||||
assignee=new_assignee,
|
||||
)
|
||||
return self._replace_task(new_task)
|
||||
|
||||
|
||||
class MessageQueue(BaseModel):
|
||||
"""Message queue which supports asynchronous updates."""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ class ExpRetriever(BaseModel):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class DummyExpRetriever(ExpRetriever):
|
||||
"""A dummy experience retriever that returns empty string."""
|
||||
|
||||
def retrieve(self, context: str = "") -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class SimpleExpRetriever(ExpRetriever):
|
||||
"""A simple experience retriever that returns manually crafted examples."""
|
||||
|
||||
|
|
@ -20,7 +27,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "1",
|
||||
"dependent_task_ids": [],
|
||||
|
|
@ -29,7 +36,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "2",
|
||||
"dependent_task_ids": ["1"],
|
||||
|
|
@ -38,7 +45,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "3",
|
||||
"dependent_task_ids": ["2"],
|
||||
|
|
@ -47,7 +54,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "4",
|
||||
"dependent_task_ids": ["3"],
|
||||
|
|
@ -56,7 +63,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "5",
|
||||
"dependent_task_ids": ["4"],
|
||||
|
|
@ -65,14 +72,14 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "publish_message",
|
||||
"command_name": "TeamLeader.publish_message",
|
||||
"args": {
|
||||
"content": "Create a cli snake game using Python",
|
||||
"send_to": "Alice"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command_name": "reply_to_human",
|
||||
"command_name": "RoleZero.reply_to_human",
|
||||
"args": {
|
||||
"content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly",
|
||||
}
|
||||
|
|
@ -86,7 +93,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "1",
|
||||
"dependent_task_ids": [],
|
||||
|
|
@ -95,14 +102,14 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "publish_message",
|
||||
"command_name": "TeamLeader.publish_message",
|
||||
"args": {
|
||||
"content": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.",
|
||||
"send_to": "David"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command_name": "reply_to_human",
|
||||
"command_name": "RoleZero.reply_to_human",
|
||||
"args": {
|
||||
"content": "I have assigned the task to David. He will break down the task further by himself and starts solving it.",
|
||||
}
|
||||
|
|
@ -116,22 +123,22 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
...,
|
||||
{'role': 'assistant', 'content': 'from Alice(Product Manager) to {'Bob'}: {'docs': {'20240424153821.json': {'root_path': 'docs/prd', 'filename': '20240424153821.json', 'content': '{"Language":"en_us","Programming Language":"Python","Original Requirements":"create a cli snake game","Project Name":"snake_game","Product Goals":["Develop an intuitive and addictive snake game",...], ...}}}}},
|
||||
]
|
||||
Explanation: You received a message from Alice, the Product Manager, that she has completed the PRD, use finish_current_task to mark her task as finished and moves the plan to the next task. Based on plan status, next task is for Bob (Architect), publish a message asking him to start. The message content should contain important path info.
|
||||
Explanation: You received a message from Alice, the Product Manager, that she has completed the PRD, use Plan.finish_current_task to mark her task as finished and moves the plan to the next task. Based on plan status, next task is for Bob (Architect), publish a message asking him to start. The message content should contain important path info.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "finish_current_task",
|
||||
"command_name": "Plan.finish_current_task",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"command_name": "publish_message",
|
||||
"command_name": "TeamLeader.publish_message",
|
||||
"args": {
|
||||
"content": "Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.",
|
||||
"send_to": "Bob"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command_name": "reply_to_human",
|
||||
"command_name": "RoleZero.reply_to_human",
|
||||
"args": {
|
||||
"content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture.",
|
||||
}
|
||||
|
|
@ -145,7 +152,7 @@ class SimpleExpRetriever(ExpRetriever):
|
|||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "reply_to_human",
|
||||
"command_name": "RoleZero.reply_to_human",
|
||||
"args": {
|
||||
"content": "The team is currently working on ... We have completed ...",
|
||||
}
|
||||
|
|
@ -180,7 +187,7 @@ Explanation: Launching a service requires Terminal tool with daemon mode, write
|
|||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "1",
|
||||
"dependent_task_ids": [],
|
||||
|
|
@ -189,7 +196,7 @@ Explanation: Launching a service requires Terminal tool with daemon mode, write
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "2",
|
||||
"dependent_task_ids": ["1"],
|
||||
|
|
@ -198,7 +205,7 @@ Explanation: Launching a service requires Terminal tool with daemon mode, write
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "3",
|
||||
"dependent_task_ids": ["2"],
|
||||
|
|
@ -216,7 +223,7 @@ Explanation: The requirement is to fix an issue in an existing repository. The p
|
|||
```json
|
||||
[
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "1",
|
||||
"dependent_task_ids": [],
|
||||
|
|
@ -225,7 +232,7 @@ Explanation: The requirement is to fix an issue in an existing repository. The p
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "2",
|
||||
"dependent_task_ids": ["1"],
|
||||
|
|
@ -234,7 +241,7 @@ Explanation: The requirement is to fix an issue in an existing repository. The p
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "3",
|
||||
"dependent_task_ids": ["2"],
|
||||
|
|
@ -243,7 +250,7 @@ Explanation: The requirement is to fix an issue in an existing repository. The p
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "4",
|
||||
"dependent_task_ids": ["3"],
|
||||
|
|
@ -252,7 +259,7 @@ Explanation: The requirement is to fix an issue in an existing repository. The p
|
|||
}
|
||||
},
|
||||
{
|
||||
"command_name": "append_task",
|
||||
"command_name": "Plan.append_task",
|
||||
"args": {
|
||||
"task_id": "5",
|
||||
"dependent_task_ids": ["4"],
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from pydantic import BaseModel
|
|||
from metagpt.environment.mgx.mgx_env import MGXEnv
|
||||
from metagpt.memory import Memory
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message, Task
|
||||
from metagpt.schema import Message
|
||||
|
||||
|
||||
class CommandDef(BaseModel):
|
||||
|
|
@ -92,17 +92,11 @@ async def run_env_command(role: Role, cmd: list[dict], role_memory: Memory = Non
|
|||
|
||||
def run_plan_command(role: Role, cmd: list[dict]):
|
||||
if cmd["command_name"] == Command.APPEND_TASK.cmd_name:
|
||||
role.planner.plan.append_task(Task(**cmd["args"]))
|
||||
role.planner.plan.append_task(**cmd["args"])
|
||||
elif cmd["command_name"] == Command.RESET_TASK.cmd_name:
|
||||
role.planner.plan.reset_task(**cmd["args"])
|
||||
elif cmd["command_name"] == Command.REPLACE_TASK.cmd_name:
|
||||
new_task = Task(
|
||||
task_id=cmd["args"]["task_id"],
|
||||
dependent_task_ids=cmd["args"]["new_dependent_task_ids"],
|
||||
instruction=cmd["args"]["new_instruction"],
|
||||
assignee=cmd["args"]["new_assignee"],
|
||||
)
|
||||
role.planner.plan.replace_task(new_task)
|
||||
role.planner.plan.replace_task(**cmd["args"])
|
||||
elif cmd["command_name"] == Command.FINISH_CURRENT_TASK.cmd_name:
|
||||
if role.planner.plan.is_plan_finished():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -25,8 +25,11 @@ class Editor:
|
|||
self.resource = EditorReporter()
|
||||
|
||||
def write(self, path: str, content: str):
|
||||
"""Write the whole content to a file."""
|
||||
with open(path, "w") as f:
|
||||
"""Write the whole content to a file. When used, make sure content arg contains the full content of the file."""
|
||||
directory = os.path.dirname(path)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
self.resource.report(path, "path")
|
||||
|
||||
|
|
@ -117,7 +120,7 @@ class Editor:
|
|||
file_path (str): The file path to write the new block content.
|
||||
start_line (int): start line of the original block to be updated (inclusive).
|
||||
end_line (int): end line of the original block to be updated (inclusive).
|
||||
new_block_content (str): The new block content to write.
|
||||
new_block_content (str): The new block content to write. Don't include row number in the content.
|
||||
|
||||
Returns:
|
||||
str: A message indicating the status of the write operation.
|
||||
|
|
@ -161,7 +164,9 @@ class Editor:
|
|||
|
||||
if new_block_content:
|
||||
# Split the new_block_content by newline and ensure each line ends with a newline character
|
||||
new_content_lines = [line + "\n" for line in new_block_content.split("\n")]
|
||||
new_content_lines = new_block_content.splitlines(
|
||||
keepends=True
|
||||
) # FIXME: This will split \n within a line, such as ab\ncd
|
||||
if end_line >= start_line:
|
||||
# This replaces the block between start_line and end_line with new_block_content
|
||||
# irrespective of the length difference between the original and new content.
|
||||
|
|
|
|||
|
|
@ -137,8 +137,26 @@ def validate_tool_names(tools: list[str]) -> dict[str, Tool]:
|
|||
# if tool paths are provided, they will be registered on the fly
|
||||
if os.path.isdir(key) or os.path.isfile(key):
|
||||
valid_tools.update(register_tools_from_path(key))
|
||||
elif TOOL_REGISTRY.has_tool(key):
|
||||
valid_tools.update({key: TOOL_REGISTRY.get_tool(key)})
|
||||
elif TOOL_REGISTRY.has_tool(key.split(":")[0]):
|
||||
if ":" in key:
|
||||
# handle class tools with methods specified, such as Editor:read,write
|
||||
class_tool_name = key.split(":")[0]
|
||||
method_names = key.split(":")[1].split(",")
|
||||
class_tool = TOOL_REGISTRY.get_tool(class_tool_name)
|
||||
|
||||
methods_filtered = {}
|
||||
for method_name in method_names:
|
||||
if method_name in class_tool.schemas["methods"]:
|
||||
methods_filtered[method_name] = class_tool.schemas["methods"][method_name]
|
||||
else:
|
||||
logger.warning(f"invalid method {method_name} under tool {class_tool_name}, skipped")
|
||||
class_tool_filtered = class_tool.model_copy(deep=True)
|
||||
class_tool_filtered.schemas["methods"] = methods_filtered
|
||||
|
||||
valid_tools.update({class_tool_name: class_tool_filtered})
|
||||
|
||||
else:
|
||||
valid_tools.update({key: TOOL_REGISTRY.get_tool(key)})
|
||||
elif TOOL_REGISTRY.has_tool_tag(key):
|
||||
valid_tools.update(TOOL_REGISTRY.get_tools_by_tag(key))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,19 +5,25 @@ import threading
|
|||
from metagpt.environment.mgx.mgx_env import MGXEnv
|
||||
from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager
|
||||
from metagpt.roles.di.data_analyst import DataAnalyst
|
||||
from metagpt.roles.di.engineer2 import Engineer2
|
||||
from metagpt.roles.di.team_leader import TeamLeader
|
||||
from metagpt.schema import Message
|
||||
|
||||
|
||||
async def main(requirement="", enable_human_input=False):
|
||||
env = MGXEnv()
|
||||
async def main(requirement="", enable_human_input=False, use_fixed_sop=False):
|
||||
if use_fixed_sop:
|
||||
engineer = Engineer(n_borg=5, use_code_review=False)
|
||||
else:
|
||||
engineer = Engineer2()
|
||||
|
||||
env = MGXEnv(allow_bypass_team_leader=use_fixed_sop)
|
||||
env.add_roles(
|
||||
[
|
||||
TeamLeader(),
|
||||
ProductManager(),
|
||||
Architect(),
|
||||
ProjectManager(),
|
||||
Engineer(n_borg=5, use_code_review=False),
|
||||
ProductManager(use_fixed_sop=use_fixed_sop),
|
||||
Architect(use_fixed_sop=use_fixed_sop),
|
||||
ProjectManager(use_fixed_sop=use_fixed_sop),
|
||||
engineer,
|
||||
# QaEngineer(),
|
||||
DataAnalyst(tools=["<all>"]),
|
||||
]
|
||||
|
|
@ -52,8 +58,10 @@ def send_human_input(env):
|
|||
|
||||
|
||||
GAME_REQ = "create a 2048 game"
|
||||
WEB_GAME_REQ = "Write a 2048 game using JavaScript without using any frameworks, user can play with keyboard."
|
||||
WEB_GAME_REQ_DEPLOY = "Write a 2048 game using JavaScript without using any frameworks, user can play with keyboard. When finished, deploy the game to public at port 8090."
|
||||
SIMPLE_REQ = "print statistic summary of sklearn iris dataset"
|
||||
WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy."
|
||||
WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, and train a model to predict wine class (20% as validation), and show validation accuracy."
|
||||
PAPER_LIST_REQ = """
|
||||
Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/,
|
||||
and save it to a csv file. paper title must include `multiagent` or `large language model`. *notice: print key variables*
|
||||
|
|
@ -67,7 +75,7 @@ data_path = "data/titanic"
|
|||
train_path = f"{data_path}/split_train.csv"
|
||||
eval_path = f"{data_path}/split_eval.csv"
|
||||
TITANIC_REQ = f"This is a titanic passenger survival dataset, your goal is to predict passenger survival outcome. The target column is Survived. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report accuracy on the eval data. Train data path: '{train_path}', eval data path: '{eval_path}'."
|
||||
FIX_ISSUE = """
|
||||
FIX_ISSUE1 = """
|
||||
Write a fix for this issue: https://github.com/langchain-ai/langchain/issues/20453,
|
||||
you can fix it on this repo https://github.com/garylin2099/langchain,
|
||||
checkout a branch named test-fix, commit your changes, push, and create a PR to the master branch of https://github.com/iorisa/langchain
|
||||
|
|
@ -98,4 +106,4 @@ if __name__ == "__main__":
|
|||
os.environ["access_token"] = "ghp_xxx"
|
||||
# NOTE: Change the requirement to the one you want to test
|
||||
# Set enable_human_input to True if you want to simulate sending messages in chatbox
|
||||
asyncio.run(main(requirement=FIX_ISSUE, enable_human_input=False))
|
||||
asyncio.run(main(requirement=GAME_REQ, enable_human_input=False, use_fixed_sop=False))
|
||||
|
|
|
|||
38
tests/metagpt/roles/di/run_architect.py
Normal file
38
tests/metagpt/roles/di/run_architect.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
from metagpt.roles.architect import Architect
|
||||
|
||||
DESIGN_DOC_SNAKE = """
|
||||
{
|
||||
"Implementation approach": "We will use the Pygame library to create the CLI-based snake game. Pygame is a set of Python modules designed for writing video games, which will help us handle graphics, sound, and input. The game will be structured into different modules to handle the main game loop, snake movement, food generation, collision detection, and user interface. We will ensure the game is engaging and responsive by optimizing the game loop and input handling. The score display and different speed levels will be implemented to enhance the user experience.",
|
||||
"File list": [
|
||||
"main.py",
|
||||
"game.py",
|
||||
"snake.py",
|
||||
"food.py",
|
||||
"ui.py"
|
||||
],
|
||||
"Data structures and interfaces": "\nclassDiagram\n class Main {\n +main() void\n }\n class Game {\n -Snake snake\n -Food food\n -int score\n -int speed\n +__init__(speed: int)\n +run() void\n +restart() void\n +update_score() void\n }\n class Snake {\n -list body\n -str direction\n +__init__()\n +move() void\n +change_direction(new_direction: str) void\n +check_collision() bool\n +grow() void\n }\n class Food {\n -tuple position\n +__init__()\n +generate_new_position() void\n }\n class UI {\n +display_score(score: int) void\n +display_game_over() void\n +display_game(snake: Snake, food: Food) void\n }\n Main --> Game\n Game --> Snake\n Game --> Food\n Game --> UI\n",
|
||||
"Program call flow": "\nsequenceDiagram\n participant M as Main\n participant G as Game\n participant S as Snake\n participant F as Food\n participant U as UI\n M->>G: __init__(speed)\n M->>G: run()\n G->>S: __init__()\n G->>F: __init__()\n loop Game Loop\n G->>S: move()\n G->>S: check_collision()\n alt Collision Detected\n G->>G: restart()\n G->>U: display_game_over()\n else No Collision\n G->>F: generate_new_position()\n G->>S: grow()\n G->>G: update_score()\n G->>U: display_score(score)\n end\n G->>U: display_game(snake, food)\n end\n",
|
||||
"Anything UNCLEAR": "Currently, all aspects of the project are clear."
|
||||
}
|
||||
"""
|
||||
|
||||
WRITE_SNAKE = """Write a system design for a cli snake game with pygame"""
|
||||
|
||||
REWRITE_SNAKE = """Rewrite the system design at temp_design.json, add a web UI"""
|
||||
|
||||
CASUAL_CHAT = """What's your name?"""
|
||||
|
||||
|
||||
async def main(requirement):
|
||||
with open("temp_design.json", "w") as f:
|
||||
f.write(DESIGN_DOC_SNAKE)
|
||||
architect = Architect()
|
||||
await architect.run(requirement)
|
||||
os.remove("temp_design.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main(WRITE_SNAKE))
|
||||
104
tests/metagpt/roles/di/run_engineer2.py
Normal file
104
tests/metagpt/roles/di/run_engineer2.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import asyncio
|
||||
|
||||
from metagpt.roles.di.engineer2 import Engineer2
|
||||
|
||||
DESIGN_DOC_2048 = '{"Implementation approach":"We will use the Pygame library to implement the 2048 game logic and user interface. Pygame is a set of Python modules designed for writing video games, which will help us create a responsive and visually appealing UI. For the mobile responsiveness, we will ensure that the game scales appropriately on different screen sizes. We will also use the Pygame GUI library to create buttons for restarting the game and choosing difficulty levels.","File list":["main.py","game.py","ui.py"],"Data structures and interfaces":"\\nclassDiagram\\n class Game {\\n -grid: list[list[int]]\\n -score: int\\n +__init__()\\n +move(direction: str) bool\\n +merge() bool\\n +spawn_tile() None\\n +is_game_over() bool\\n +reset() None\\n }\\n class UI {\\n -game: Game\\n +__init__(game: Game)\\n +draw_grid() None\\n +draw_score() None\\n +draw_buttons() None\\n +handle_input() None\\n }\\n class Main {\\n -ui: UI\\n +main() None\\n }\\n Main --> UI\\n UI --> Game\\n","Program call flow":"\\nsequenceDiagram\\n participant M as Main\\n participant U as UI\\n participant G as Game\\n M->>U: __init__(game)\\n U->>G: __init__()\\n M->>U: draw_grid()\\n U->>G: move(direction)\\n G-->>U: return bool\\n U->>G: merge()\\n G-->>U: return bool\\n U->>G: spawn_tile()\\n G-->>U: return None\\n U->>G: is_game_over()\\n G-->>U: return bool\\n U->>G: reset()\\n G-->>U: return None\\n M->>U: draw_score()\\n M->>U: draw_buttons()\\n M->>U: handle_input()\\n","Anything UNCLEAR":"Clarification needed on the specific design elements for the UI to ensure it meets the \'beautiful\' requirement. Additionally, we need to confirm the exact difficulty levels and how they should affect the game mechanics."}'
|
||||
TASK_DOC_2048 = '{"Required Python packages":["pygame==2.0.1","pygame_gui==0.5.7"],"Required Other language third-party packages":["No third-party dependencies required"],"Logic Analysis":[["game.py","Contains Game class with methods: __init__, move, merge, spawn_tile, is_game_over, reset"],["ui.py","Contains UI class with methods: __init__, draw_grid, draw_score, draw_buttons, handle_input"],["main.py","Contains Main class with method: main, initializes UI and Game"]],"Task list":["game.py","ui.py","main.py"],"Full API spec":"","Shared Knowledge":"`game.py` contains core game logic and state management. `ui.py` handles all user interface elements and interactions. `main.py` serves as the entry point to initialize and run the game.","Anything UNCLEAR":"Clarification needed on the specific design elements for the UI to ensure it meets the \'beautiful\' requirement. Additionally, we need to confirm the exact difficulty levels and how they should affect the game mechanics."}'
|
||||
DESIGN_DOC_SNAKE = """
|
||||
{
|
||||
"Implementation approach": "We will use the Pygame library to create the CLI-based snake game. Pygame is a set of Python modules designed for writing video games, which will help us handle graphics, sound, and input. The game will be structured into different modules to handle the main game loop, snake movement, food generation, collision detection, and user interface. We will ensure the game is engaging and responsive by optimizing the game loop and input handling. The score display and different speed levels will be implemented to enhance the user experience.",
|
||||
"File list": [
|
||||
"main.py",
|
||||
"game.py",
|
||||
"snake.py",
|
||||
"food.py",
|
||||
"ui.py"
|
||||
],
|
||||
"Data structures and interfaces": "\nclassDiagram\n class Main {\n +main() void\n }\n class Game {\n -Snake snake\n -Food food\n -int score\n -int speed\n +__init__(speed: int)\n +run() void\n +restart() void\n +update_score() void\n }\n class Snake {\n -list body\n -str direction\n +__init__()\n +move() void\n +change_direction(new_direction: str) void\n +check_collision() bool\n +grow() void\n }\n class Food {\n -tuple position\n +__init__()\n +generate_new_position() void\n }\n class UI {\n +display_score(score: int) void\n +display_game_over() void\n +display_game(snake: Snake, food: Food) void\n }\n Main --> Game\n Game --> Snake\n Game --> Food\n Game --> UI\n",
|
||||
"Program call flow": "\nsequenceDiagram\n participant M as Main\n participant G as Game\n participant S as Snake\n participant F as Food\n participant U as UI\n M->>G: __init__(speed)\n M->>G: run()\n G->>S: __init__()\n G->>F: __init__()\n loop Game Loop\n G->>S: move()\n G->>S: check_collision()\n alt Collision Detected\n G->>G: restart()\n G->>U: display_game_over()\n else No Collision\n G->>F: generate_new_position()\n G->>S: grow()\n G->>G: update_score()\n G->>U: display_score(score)\n end\n G->>U: display_game(snake, food)\n end\n",
|
||||
"Anything UNCLEAR": "Currently, all aspects of the project are clear."
|
||||
}
|
||||
"""
|
||||
TASK_DOC_SNAKE = """
|
||||
{
|
||||
"Required Python packages": [
|
||||
"pygame==2.0.1"
|
||||
],
|
||||
"Required Other language third-party packages": [
|
||||
"No third-party dependencies required"
|
||||
],
|
||||
"Logic Analysis": [
|
||||
[
|
||||
"main.py",
|
||||
"Contains the main function to initialize and start the game. Imports Game from game.py."
|
||||
],
|
||||
[
|
||||
"game.py",
|
||||
"Contains the Game class which manages the game loop, score, and speed. Imports Snake from snake.py, Food from food.py, and UI from ui.py."
|
||||
],
|
||||
[
|
||||
"snake.py",
|
||||
"Contains the Snake class which handles snake movement, direction changes, collision detection, and growth."
|
||||
],
|
||||
[
|
||||
"food.py",
|
||||
"Contains the Food class which handles food position generation."
|
||||
],
|
||||
[
|
||||
"ui.py",
|
||||
"Contains the UI class which handles displaying the score, game over screen, and the game state."
|
||||
]
|
||||
],
|
||||
"Task list": [
|
||||
"snake.py",
|
||||
"food.py",
|
||||
"ui.py",
|
||||
"game.py",
|
||||
"main.py"
|
||||
],
|
||||
"Full API spec": "",
|
||||
"Shared Knowledge": "`game.py` contains the main game loop and integrates all other modules (snake, food, UI).",
|
||||
"Anything UNCLEAR": "Currently, all aspects of the project are clear."
|
||||
}
|
||||
"""
|
||||
|
||||
GAME_REQ_2048 = f"""
|
||||
Create a 2048 game, follow the design doc and task doc. Write your code under /Users/gary/Files/temp/workspace/2048_game/src.
|
||||
After writing all codes, write a code review for the codes, make improvement or adjustment based on the review.
|
||||
Notice: You MUST implement the full code, don't leave comment without implementation!
|
||||
Design doc:
|
||||
{TASK_DOC_2048}
|
||||
Task doc:
|
||||
{DESIGN_DOC_2048}
|
||||
"""
|
||||
GAME_REQ_SNAKE = f"""
|
||||
Create a snake game, follow the design doc and task doc. Write your code under /Users/gary/Files/temp/workspace/snake_game/src.
|
||||
After writing all codes, write a code review for the codes, make improvement or adjustment based on the review.
|
||||
Notice: You MUST implement the full code, don't leave comment without implementation!
|
||||
Design doc:
|
||||
{TASK_DOC_SNAKE}
|
||||
Task doc:
|
||||
{DESIGN_DOC_SNAKE}
|
||||
"""
|
||||
GAME_REQ_2048_NO_DOC = """
|
||||
Create a 2048 game with pygame. Write your code under /Users/gary/Files/temp/workspace/2048_game/src.
|
||||
Consider what files you will write, break down the requests to multiple tasks and write one file in each task.
|
||||
After writing all codes, write a code review for the codes, make improvement or adjustment based on the review.
|
||||
Notice: You MUST implement the full code, don't leave comment without implementation!
|
||||
"""
|
||||
GAME_INC_REQ_2048 = """
|
||||
I found an issue with the 2048 code: when tiles are merged, no new tiles pop up.
|
||||
Write code review for the codes (game.py, main.py, ui.py) under under /Users/gary/Files/temp/workspace/2048_game_bugs/src.
|
||||
Then correct any issues you find. You can review all code in one time, and solve issues in one time.
|
||||
"""
|
||||
GAME_INC_REQ_SNAKE = """
|
||||
Found this issue, TypeError: generate_new_position() missing 1 required positional argument: 'snake_body'
|
||||
Write code review for the codes (food.py, game.py, main.py, snake.py, ui.py) under under /Users/gary/Files/temp/workspace/snake_game_bugs/src.
|
||||
Then correct any issues you find. You can review all code in one time, and solve issues in one time.
|
||||
"""
|
||||
CASUAL_CHAT = """what's your name?"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
engineer2 = Engineer2()
|
||||
asyncio.run(engineer2.run(GAME_REQ_2048_NO_DOC))
|
||||
Loading…
Add table
Add a link
Reference in a new issue