From ed8777db998e27dc7d58e53058449ea527e992f4 Mon Sep 17 00:00:00 2001 From: yzlin Date: Sat, 13 Apr 2024 11:44:31 +0800 Subject: [PATCH 01/14] get_human_input interface --- metagpt/actions/di/ask_review.py | 4 ++-- metagpt/logs.py | 17 +++++++++++++++++ tests/metagpt/actions/di/test_ask_review.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/di/ask_review.py b/metagpt/actions/di/ask_review.py index 041011e80..ecbbd992e 100644 --- a/metagpt/actions/di/ask_review.py +++ b/metagpt/actions/di/ask_review.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Tuple from metagpt.actions import Action -from metagpt.logs import logger +from metagpt.logs import get_human_input, logger from metagpt.schema import Message, Plan @@ -50,7 +50,7 @@ class AskReview(Action): "Please type your review below:\n" ) - rsp = input(prompt) + rsp = await get_human_input(prompt) if rsp.lower() in ReviewConst.EXIT_WORDS: exit() diff --git a/metagpt/logs.py b/metagpt/logs.py index b208e0868..d6b7cc419 100644 --- a/metagpt/logs.py +++ b/metagpt/logs.py @@ -8,6 +8,7 @@ from __future__ import annotations +import inspect import sys from datetime import datetime from functools import partial @@ -59,6 +60,14 @@ async def log_tool_output_async(output: ToolLogItem | list[ToolLogItem], tool_na await _tool_output_log_async(output=output, tool_name=tool_name) +async def get_human_input(prompt: str = ""): + """interface for getting human input, can be set to get input from different sources with set_human_input_func""" + if inspect.iscoroutinefunction(_get_human_input): + return await _get_human_input(prompt) + else: + return _get_human_input(prompt) + + def set_llm_stream_logfunc(func): global _llm_stream_log _llm_stream_log = func @@ -75,6 +84,11 @@ async def set_tool_output_logfunc_async(func): _tool_output_log_async = func +def set_human_input_func(func): + global _get_human_input + _get_human_input = func + + _llm_stream_log = partial(print, end="") @@ -86,3 +100,6 @@ _tool_output_log = ( async def _tool_output_log_async(*args, **kwargs): # async version pass + + +_get_human_input = input # get human input from console by default diff --git a/tests/metagpt/actions/di/test_ask_review.py b/tests/metagpt/actions/di/test_ask_review.py index 6bb1accf5..d49ad176a 100644 --- a/tests/metagpt/actions/di/test_ask_review.py +++ b/tests/metagpt/actions/di/test_ask_review.py @@ -6,7 +6,7 @@ from metagpt.actions.di.ask_review import AskReview @pytest.mark.asyncio async def test_ask_review(mocker): mock_review_input = "confirm" - mocker.patch("builtins.input", return_value=mock_review_input) + mocker.patch("metagpt.actions.di.ask_review.get_human_input", return_value=mock_review_input) rsp, confirmed = await AskReview().run() assert rsp == mock_review_input assert confirmed From 1b57395d0ee4d407e7cb5b0c4e890b119add086a Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 01:10:30 +0800 Subject: [PATCH 02/14] rough scratch for tl and thinking command --- metagpt/environment/mgx/__init__.py | 3 + metagpt/environment/mgx/mgx_env.py | 47 ++++++ metagpt/prompts/di/team_leader.py | 182 +++++++++++++++++++++ metagpt/roles/di/team_leader.py | 151 +++++++++++++++++ metagpt/strategy/thinking_command.py | 66 ++++++++ tests/metagpt/roles/di/test_team_leader.py | 167 +++++++++++++++++++ 6 files changed, 616 insertions(+) create mode 100644 metagpt/environment/mgx/__init__.py create mode 100644 metagpt/environment/mgx/mgx_env.py create mode 100644 metagpt/prompts/di/team_leader.py create mode 100644 metagpt/roles/di/team_leader.py create mode 100644 metagpt/strategy/thinking_command.py create mode 100644 tests/metagpt/roles/di/test_team_leader.py diff --git a/metagpt/environment/mgx/__init__.py b/metagpt/environment/mgx/__init__.py new file mode 100644 index 000000000..2bcf8efd0 --- /dev/null +++ b/metagpt/environment/mgx/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py new file mode 100644 index 000000000..9a148f24c --- /dev/null +++ b/metagpt/environment/mgx/mgx_env.py @@ -0,0 +1,47 @@ +from metagpt.environment.base_env import Environment +from metagpt.logs import get_human_input +from metagpt.schema import Message + + +class MGXEnv(Environment): + """MGX Environment""" + + history: dict[str, Message] = {} # redefine message history + + def _publish_message(self, message: Message, peekable: bool = True) -> bool: + return super().publish_message(message, peekable) + + def publish_message(self, message: Message, user_defined_recipient: str = "", publicer: str = "") -> bool: + """let the team leader take over message publishing""" + tl = self.get_role("Team Leader") + + if user_defined_recipient: + self._publish_message(message) + # bypass team leader, team leader only needs to know but not to react + tl.rc.memory.add(message) + + elif publicer == tl.profile: + # message processed by team leader can be published now + self._publish_message(message) + + else: + # every regular message goes through team leader + tl.put_message(message) + + self.history[message.id] = message + + return True + + def forward_message(self, message_id: str) -> str: + if message_id not in self.history: + return f"invalid message_id {message_id}, not found in history." + msg = self.history[message_id] + return self._publish_message(msg) + + async def ask_human(self, question: str) -> str: + # NOTE: Can be overwritten in remote setting + return get_human_input(question) + + async def reply_to_human(self, message: str) -> str: + # NOTE: Can be overwritten in remote setting + return message diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py new file mode 100644 index 000000000..8b843a47d --- /dev/null +++ b/metagpt/prompts/di/team_leader.py @@ -0,0 +1,182 @@ +from metagpt.strategy.thinking_command import Command + + +def prepare_command_prompt(commands: list[Command]) -> str: + command_prompt = "" + for i, command in enumerate(commands): + command_prompt += f"{i+1}. {command.value.signature}:\n{command.value.desc}\n\n" + print(command_prompt) + return command_prompt + + +PLANNING_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 +You are a team leader, and you are responsible for drafting tasks and routing tasks to your team members. +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. +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. + +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 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 -> QaEngine, each assigned ONE task. +3. If the requirement contains both DATA-RELATED part and software development part, 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: +Some text indicating your thoughts, including how you categorize the requirement based on Note or how you should update the plan status. Then a json array of commands. +```json +[ + {{ + "command_name": str, + "args": {{"arg_name": arg_value, ...}} + }}, + ... +] +``` +""" + +ROUTING_CMD_PROMPT = """ +# Team Member Info +{team_info} + +# Available Commands +{available_commands} + +# Current Plan +{plan_status} + +# Example +{example} + +# Instructions +You are a team leader, you can use publish_message or forward_message to team members, asking them to start their task. +Note: +1. You should carefully review the recent conversation, if there are messages from team members, forward them or withold them properly. +2. Prioritize forward_message if messages exist, only public_message if you want to instruct the team member yourself. +3. Pay attention to task dependency, don't assign a task to a team member if the dependent tasks are not finished yet. +4. Review plan status, think about what you should do next, you can use any of the Available Commands. + +# Your commands in a json array, in the following output format: +```json +[ + {{ + "command_name": str, + "args": {{"arg_name": arg_value, ...}} + }}, + ... +] +""" + +PLANNING_EXAMPLE = """ +## example 1 +User Requirement: Create a cli snake game +Explanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. +```json +[ + { + "command_name": "append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI snake game.", + "assignee": "Alice" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.", + "assignee": "Bob" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.", + "assignee": "Eve" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "4", + "dependent_task_ids": ["3"], + "instruction": "Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.", + "assignee": "Alex" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "5", + "dependent_task_ids": ["4"], + "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", + "assignee": "Edward" + } + } +] +``` + +## example 2 +User requirement: 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. +Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. +```json +[ + { + "command_name": "append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "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.", + "assignee": "David" + } + } +] +``` +""" + +ROUTING_EXAMPLE = """ +## example 1: Forward a message from one team member to another +Conversation History: +[ + ..., + {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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",...], ...}}}}}, + {'role': 'assistant', 'content': 'Based on the feedback from Alice, the Product Manager, it seems that the PRD for the snake game has been successfully created. Since the PRD is complete and there are no indications of issues with the task, we can mark the current task as finished and move on to the next task in the plan. The next task is assigned to Bob, the Architect, who will be responsible for designing the software architecture for the game based on the PRD provided by Alice.\n\nHere are the commands to update the plan status:\n\n```json\n[\n {\n "command_name": "finish_current_task"\n }\n]\n```'} +] +Command: +```json +[ + { + "command_name": "forward_message", + "args": { + "message_id": "739d9b4983fd4e97a0f78fde5e9ef158" + } + } +] +""" diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py new file mode 100644 index 000000000..ec293f776 --- /dev/null +++ b/metagpt/roles/di/team_leader.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import json + +from pydantic import model_validator + +from metagpt.environment.mgx.mgx_env import MGXEnv +from metagpt.prompts.di.team_leader import ( + PLANNING_CMD_PROMPT, + PLANNING_EXAMPLE, + ROUTING_CMD_PROMPT, + ROUTING_EXAMPLE, + prepare_command_prompt, +) +from metagpt.roles import Role +from metagpt.schema import Message, Task, TaskResult +from metagpt.strategy.planner import Planner +from metagpt.strategy.thinking_command import Command +from metagpt.utils.common import CodeParser + + +class TeamLeader(Role): + name: str = "Tim" + profile: str = "Team Leader" + task_result: TaskResult = None + planning_commands: list[Command] = [ + Command.APPEND_TASK, + Command.RESET_TASK, + Command.REPLACE_TASK, + Command.FINISH_CURRENT_TASK, + Command.REPLY_TO_HUMAN, + Command.PASS, + ] + env_commands: list[Command] = [ + Command.PUBLISH_MESSAGE, + Command.FORWARD_MESSAGE, + Command.ASK_HUMAN, + Command.REPLY_TO_HUMAN, + Command.PASS, + ] + + @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 + + def _run_env_command(self, cmd): + assert isinstance(self.rc.env, MGXEnv), "TeamLeader should only be used in an MGXEnv" + if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: + self.rc.env.publish_message(Message(**cmd["args"]), publicer=self.profile) + elif cmd["command_name"] in Command.FORWARD_MESSAGE.cmd_name: + self.rc.env.forward_message(**cmd["args"]) + elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: + self.rc.env.ask_human(**cmd["args"]) + elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: + self.rc.env.reply_to_human(**cmd["args"]) + + def _run_internal_command(self, cmd): + if cmd["command_name"] == Command.APPEND_TASK.cmd_name: + self.planner.plan.append_task(Task(**cmd["args"])) + elif cmd["command_name"] == Command.RESET_TASK.cmd_name: + self.planner.plan.reset_task(**cmd["args"]) + elif cmd["command_name"] == Command.REPLACE_TASK.cmd_name: + self.planner.plan.replace_task(Task(**cmd["args"])) + elif cmd["command_name"] == Command.FINISH_CURRENT_TASK.cmd_name: + self.planner.plan.current_task.update_task_result(task_result=self.task_result) + self.planner.plan.finish_current_task() + self.rc.working_memory.clear() + + def run_commands(self, cmds): + print(*cmds, sep="\n") + for cmd in cmds: + self._run_env_command(cmd) + self._run_internal_command(cmd) + + if self.planner.plan.is_plan_finished(): + self._set_state(-1) + + def get_memory(self) -> list[Message]: + mem = self.rc.memory.get() + for m in mem: + if m.role not in ["system", "user", "assistant"]: + m.content = f"id: {m.id[:10]}, from {m.role} to {m.send_to}: {m.content}" + m.role = "assistant" + return mem + + async def _think(self) -> bool: + """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" + self.commands = [] + + example = "" + if not self.planner.plan.goal: + user_requirement = self.get_memories()[-1].content + self.planner.plan.goal = user_requirement + example = PLANNING_EXAMPLE + + # common info + team_info = "" + for role in self.rc.env.roles.values(): + if role.profile == "TeamLeader": + continue + team_info += f"{role.name}: {role.profile}, {role.goal}\n" + # print(team_info) + + # plan commands + plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) + for task in plan_status["tasks"]: + task.pop("code") + task.pop("result") + plan_prompt = PLANNING_CMD_PROMPT.format( + plan_status=plan_status, + team_info=team_info, + example=example, + available_commands=prepare_command_prompt(self.planning_commands), + ) + context = self.llm.format_msg(self.get_memory() + [Message(content=plan_prompt, role="user")]) + print(*context, sep="*" * 10 + "\n\n") + # breakpoint() + + plan_rsp = await self.llm.aask(context) + plan_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=plan_rsp)) + self.commands.extend(plan_rsp_dict) + self.rc.memory.add(Message(content=plan_rsp, role="assistant")) + + # routing commands + route_prompt = ROUTING_CMD_PROMPT.format( + plan_status=plan_status, + team_info=team_info, + example=ROUTING_EXAMPLE, + available_commands=prepare_command_prompt(self.env_commands), + ) + context = self.llm.format_msg(self.get_memory() + [Message(content=route_prompt, role="user")]) + print(*context, sep="*" * 10 + "\n\n") + # breakpoint() + + route_rsp = await self.llm.aask(context) + route_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=route_rsp)) + self.commands.extend(route_rsp_dict) + self.rc.memory.add(Message(content=route_rsp, role="assistant")) + + return True + + async def _act(self) -> Message: + """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" + self.run_commands(self.commands) + self.task_result = TaskResult(result="Success", is_success=True) + + async def run(self, with_message=None) -> Message | None: + if await self._observe(): + await self._think() + await self._act() diff --git a/metagpt/strategy/thinking_command.py b/metagpt/strategy/thinking_command.py new file mode 100644 index 000000000..cc4626c6d --- /dev/null +++ b/metagpt/strategy/thinking_command.py @@ -0,0 +1,66 @@ +from enum import Enum + +from pydantic import BaseModel + + +class CommandDef(BaseModel): + name: str + signature: str = "" + desc: str = "" + + +class Command(Enum): + # commands for planning + APPEND_TASK = CommandDef( + name="append_task", + signature="append_task(task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str)", + desc="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.", + ) + RESET_TASK = CommandDef( + name="reset_task", + signature="reset_task(task_id: str)", + desc="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.", + ) + REPLACE_TASK = CommandDef( + name="replace_task", + signature="replace_task(task_id: str, new_dependent_task_ids: list[str], new_instruction: str, new_assignee: str)", + desc="Replace an existing task (can be current task) based on task_id, and reset all tasks depending on it.", + ) + FINISH_CURRENT_TASK = CommandDef( + name="finish_current_task", + signature="finish_current_task()", + desc="Finishes current task, set Task.is_finished=True, set current task to next task", + ) + + # commands for env interaction + PUBLISH_MESSAGE = CommandDef( + name="publish_message", + signature="publish_message(content: str, send_to: str)", + desc="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 from original content to team members because you are their sole info source. However, if the original content is long or contains concrete info, you should forward the message using forward_message instead of publishing it.", + ) + FORWARD_MESSAGE = CommandDef( + name="forward_message", + signature="forward_message(message_id: str)", + desc="Forward a message from one team member to another or all without any modification. This will make the recipient start their work, too.", + ) + REPLY_TO_HUMAN = CommandDef( + name="reply_to_human", + signature="reply_to_human(content: str)", + desc="Reply to human user with the content provided. Use this when you have a clear answer or solution to the user's question.", + ) + ASK_HUMAN = CommandDef( + name="ask_human", + signature="ask_human(question: str)", + desc="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.", + ) + + # common commands + PASS = CommandDef( + name="pass", + signature="pass", + desc="Pass and do nothing, if you don't think the plan needs to be updated nor a message to be published or forwarded. The reasons can be the latest message is unnecessary or obsolete, or you want to wait for more information before making a move.", + ) + + @property + def cmd_name(self): + return self.value.name diff --git a/tests/metagpt/roles/di/test_team_leader.py b/tests/metagpt/roles/di/test_team_leader.py new file mode 100644 index 000000000..6dd9ef4a6 --- /dev/null +++ b/tests/metagpt/roles/di/test_team_leader.py @@ -0,0 +1,167 @@ +import pytest + +from metagpt.environment.mgx.mgx_env import MGXEnv +from metagpt.roles import ( + Architect, + Engineer, + ProductManager, + ProjectManager, + QaEngineer, +) +from metagpt.roles.di.data_interpreter import DataInterpreter +from metagpt.roles.di.team_leader import TeamLeader +from metagpt.schema import Message + + +@pytest.fixture +def env(): + test_env = MGXEnv() + tl = TeamLeader() + da = DataInterpreter( + name="David", + profile="Data Analyst", + goal="Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web deployment, terminal operation, git operation, etc.", + react_mode="react", + ) + test_env.add_roles( + [ + tl, + ProductManager(), + Architect(), + ProjectManager(), + Engineer(n_borg=5, use_code_review=True), + QaEngineer(), + da, + ] + ) + return test_env + + +@pytest.mark.asyncio +async def test_plan_for_software_requirement(env): + requirement = "create a 2048 game" + + tl = env.get_role("Team Leader") + env.publish_message(Message(content=requirement, send_to=tl.name)) + await tl.run() + + # TL should assign tasks to 5 members first, then send message to the first assignee, 6 commands in total + assert len(tl.commands) == 6 + plan_cmd = tl.commands[:5] + route_cmd = tl.commands[5] + + task_assignment = [task["args"]["assignee"] for task in plan_cmd] + assert task_assignment == [ + ProductManager().name, + Architect().name, + ProjectManager().name, + Engineer().name, + QaEngineer().name, + ] + + assert route_cmd["command_name"] == "publish_message" + assert route_cmd["args"]["send_to"] == ProductManager().name + + +@pytest.mark.asyncio +async def test_plan_for_data_related_requirement(env): + requirement = "I want to use yolov5 for target detection, yolov5 all the information from the following link, please help me according to the content of the link (https://github.com/ultralytics/yolov5), set up the environment and download the model parameters, and finally provide a few pictures for inference, the inference results will be saved!" + + tl = env.get_role("Team Leader") + env.publish_message(Message(content=requirement, send_to=tl.name)) + await tl.run() + + # TL should assign 1 task to Data Analyst and send message to it + assert len(tl.commands) == 2 + plan_cmd = tl.commands[0] + route_cmd = tl.commands[-1] + + da = env.get_role("Data Analyst") + assert plan_cmd["command_name"] == "append_task" + assert plan_cmd["args"]["assignee"] == da.name + + assert route_cmd["command_name"] == "publish_message" + assert "https://github.com" in route_cmd["args"]["content"] # necessary info must be in the message + assert route_cmd["args"]["send_to"] == da.name + + +@pytest.mark.asyncio +async def test_plan_for_mixed_requirement(env): + requirement = "Search the web for the new game 2048X, then replicate it" + + tl = env.get_role("Team Leader") + env.publish_message(Message(content=requirement, send_to=tl.name)) + await tl.run() + + # TL should assign 6 tasks, first to Data Analyst to search the web, following by the software team sequence + # TL should send message to Data Analyst after task assignment + assert len(tl.commands) == 7 + plan_cmd = tl.commands[:6] + route_cmd = tl.commands[-1] + + task_assignment = [task["args"]["assignee"] for task in plan_cmd] + da = env.get_role("Data Analyst") + assert task_assignment == [ + da.name, + ProductManager().name, + Architect().name, + ProjectManager().name, + Engineer().name, + QaEngineer().name, + ] + + assert route_cmd["command_name"] == "publish_message" + assert route_cmd["args"]["send_to"] == da.name + + +PRD_MSG_CONTENT = """{'docs': {'20240424153821.json': {'root_path': 'docs/prd', 'filename': '20240424153821.json', 'content': '{"Language":"en_us","Programming Language":"Python","Original Requirements":"create a 2048 game","Project Name":"game_2048","Product Goals":["Develop an intuitive and addictive 2048 game variant","Ensure the game is accessible and performs well on various devices","Design a visually appealing and modern user interface"],"User Stories":["As a player, I want to be able to undo my last move so I can correct mistakes","As a player, I want to see my high scores to track my progress over time","As a player, I want to be able to play the game without any internet connection"],"Competitive Analysis":["2048 Original: Classic gameplay, minimalistic design, lacks social sharing features","2048 Hex: Unique hexagon board, but not mobile-friendly","2048 Multiplayer: Offers real-time competition, but overwhelming ads","2048 Bricks: Innovative gameplay with bricks, but poor performance on older devices","2048.io: Multiplayer battle royale mode, but complicated UI for new players","2048 Animated: Animated tiles add fun, but the game consumes a lot of battery","2048 3D: 3D version of the game, but has a steep learning curve"],"Competitive Quadrant Chart":"quadrantChart\\n title \\"User Experience and Feature Set of 2048 Games\\"\\n x-axis \\"Basic Features\\" --> \\"Rich Features\\"\\n y-axis \\"Poor Experience\\" --> \\"Great Experience\\"\\n quadrant-1 \\"Need Improvement\\"\\n quadrant-2 \\"Feature-Rich but Complex\\"\\n quadrant-3 \\"Simplicity with Poor UX\\"\\n quadrant-4 \\"Balanced\\"\\n \\"2048 Original\\": [0.2, 0.7]\\n \\"2048 Hex\\": [0.3, 0.4]\\n \\"2048 Multiplayer\\": [0.6, 0.5]\\n \\"2048 Bricks\\": [0.4, 0.3]\\n \\"2048.io\\": [0.7, 0.4]\\n \\"2048 Animated\\": [0.5, 0.6]\\n \\"2048 3D\\": [0.6, 0.3]\\n \\"Our Target Product\\": [0.8, 0.9]","Requirement Analysis":"The game must be engaging and retain players, which requires a balance of simplicity and challenge. Accessibility on various devices is crucial for a wider reach. A modern UI is needed to attract and retain the modern user. The ability to play offline is important for users on the go. High score tracking and the ability to undo moves are features that will enhance user experience.","Requirement Pool":[["P0","Implement core 2048 gameplay mechanics"],["P0","Design responsive UI for multiple devices"],["P1","Develop undo move feature"],["P1","Integrate high score tracking system"],["P2","Enable offline gameplay capability"]],"UI Design draft":"The UI will feature a clean and modern design with a minimalist color scheme. The game board will be center-aligned with smooth tile animations. Score and high score will be displayed at the top. Undo and restart buttons will be easily accessible. The design will be responsive to fit various screen sizes.","Anything UNCLEAR":"The monetization strategy for the game is not specified. Further clarification is needed on whether the game should include advertisements, in-app purchases, or be completely free."}'}}}""" +DESIGN_CONTENT = """{"docs":{"20240424214432.json":{"root_path":"docs/system_design","filename":"20240424214432.json","content":"{\\"Implementation approach\\":\\"We will develop the 2048 game using Python, leveraging the pygame library for rendering the game interface and handling user input. This library is suitable for creating games and is widely used in the open-source community. We will ensure that the game logic is separated from the UI code to maintain a clean architecture. The game will be designed to be responsive and accessible on both desktop and mobile devices using scalable dimensions and touch-friendly controls.\\",\\"File list\\":[\\"main.py\\",\\"game.py\\",\\"ui.py\\",\\"constants.py\\",\\"logic.py\\"],\\"Data structures and interfaces\\":\\"\\\\nclassDiagram\\\\n class Main {\\\\n +main() void\\\\n }\\\\n class Game {\\\\n -UI ui\\\\n -Logic logic\\\\n +start_game() void\\\\n +restart_game() void\\\\n }\\\\n class UI {\\\\n -current_score int\\\\n -high_score int\\\\n +draw_board(board: list) void\\\\n +update_score(score: int) void\\\\n +show_game_over() void\\\\n }\\\\n class Logic {\\\\n -board list\\\\n -score int\\\\n +move(direction: str) bool\\\\n +check_game_over() bool\\\\n +get_current_score() int\\\\n +get_high_score() int\\\\n +reset_game() void\\\\n }\\\\n class Constants {\\\\n +BOARD_SIZE int\\\\n +INITIAL_TILES int\\\\n }\\\\n Main --> Game\\\\n Game --> UI\\\\n Game --> Logic\\\\n\\",\\"Program call flow\\":\\"\\\\nsequenceDiagram\\\\n participant M as Main\\\\n participant G as Game\\\\n participant UI as UI\\\\n participant L as Logic\\\\n M->>G: start_game()\\\\n loop Game Loop\\\\n G->>UI: draw_board(board)\\\\n G->>L: move(direction)\\\\n alt if move successful\\\\n L-->>G: return true\\\\n G->>UI: update_score(score)\\\\n else if move not successful\\\\n L-->>G: return false\\\\n end\\\\n G->>L: check_game_over()\\\\n alt if game over\\\\n L-->>G: return true\\\\n G->>UI: show_game_over()\\\\n G->>G: restart_game()\\\\n else\\\\n L-->>G: return false\\\\n end\\\\n end\\\\n\\",\\"Anything UNCLEAR\\":\\"Clarification needed on the specific touch-friendly controls for mobile devices and how they will be implemented using pygame.\\"}"}}}""" + + +@pytest.mark.asyncio +async def test_plan_update_and_routing(env): + requirement = "create a 2048 game" + + tl = env.get_role("Team Leader") + env.publish_message(Message(content=requirement, send_to=tl.name)) + await tl.run() + + # Assuming Product Manager finishes its task + env.publish_message(Message(content=PRD_MSG_CONTENT, role="Alice(Product Manager)", sent_from="Alice")) + await tl.run() + + # TL should mark current task as finished, and forward Product Manager's message to Architect + plan_cmd = tl.commands[0] + route_cmd = tl.commands[-1] + assert plan_cmd["command_name"] == "finish_current_task" + assert route_cmd["command_name"] == "forward_message" or route_cmd["command_name"] == "publish_message" + + +async def main(): + requirement = [ + # "Create a cli snake game", + # "I want to use yolov5 for target detection, yolov5 all the information from the following link, please help me according to the content of the link(https://github.com/ultralytics/yolov5), set up the environment and download the model parameters, and finally provide a few pictures for inference, the inference results will be saved!", + # "Create a website widget for TODO list management. Users should be able to add, mark as complete, and delete tasks. Include features like prioritization, due dates, and categories. Make it visually appealing, responsive, and user-friendly. Use HTML, CSS, and JavaScript. Consider additional features like notifications or task export. Keep it simple and enjoyable for users.dont use vue or react.dont use third party library, use localstorage to save data", + # "Search the web for the new game 2048X, then replicate it", + # """从36kr创投平台https://pitchhub.36kr.com/financing-flash 所有初创企业融资的信息, **注意: 这是一个中文网站**; + # 下面是一个大致流程, 你会根据每一步的运行结果对当前计划中的任务做出适当调整: + # 1. 爬取并本地保存html结构; + # 2. 直接打印第7个*`快讯`*关键词后2000个字符的html内容, 作为*快讯的html内容示例*; + # 3. 反思*快讯的html内容示例*中的规律, 设计正则匹配表达式来获取*`快讯`*的标题、链接、时间; + # 4. 筛选最近3天的初创企业融资*`快讯`*, 以list[dict]形式打印前5个。 + # 5. 将全部结果存在本地csv中 + # """, + """ + I would like to imitate the website available at https://news.youth.cn/gn/202404/t20240406_15178916.htm. Could you please browse through it? + Note: + - don't ignore the image, use https://source.unsplash.com/random to get random images + - use the same text, the same layout, the same color as the original website + if you can not do it, please try to get as close as possible. + """, + ] + tl.put_message(Message(requirement[0])) + await tl._observe() + await tl._think() + + +# asyncio.run(main()) From f621186c2ef3bf1231584329b22376a89a6084f3 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 10:34:46 +0800 Subject: [PATCH 03/14] reserve exp retriever & some formatting --- metagpt/environment/base_env.py | 2 +- metagpt/environment/mgx/mgx_env.py | 4 +- metagpt/prompts/di/team_leader.py | 95 +-------------- metagpt/roles/di/team_leader.py | 20 ++-- metagpt/strategy/experience_retriever.py | 130 +++++++++++++++++++++ tests/metagpt/roles/di/test_team_leader.py | 38 +----- 6 files changed, 146 insertions(+), 143 deletions(-) create mode 100644 metagpt/strategy/experience_retriever.py diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 024c46877..4a2d0c114 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -190,7 +190,7 @@ class Environment(ExtEnv): found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") - self.history += f"\n{message}" # For debug + # self.history += f"\n{message}" # For debug return True diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 9a148f24c..6626679ce 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -42,6 +42,6 @@ class MGXEnv(Environment): # NOTE: Can be overwritten in remote setting return get_human_input(question) - async def reply_to_human(self, message: str) -> str: + async def reply_to_human(self, content: str) -> str: # NOTE: Can be overwritten in remote setting - return message + return content diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 8b843a47d..5a5dce37f 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -5,7 +5,6 @@ def prepare_command_prompt(commands: list[Command]) -> str: command_prompt = "" for i, command in enumerate(commands): command_prompt += f"{i+1}. {command.value.signature}:\n{command.value.desc}\n\n" - print(command_prompt) return command_prompt @@ -36,7 +35,7 @@ You should NOT assign consecutive tasks to the same team member, instead, assign 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. 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 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. +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 -> QaEngine, each assigned ONE task. 3. If the requirement contains both DATA-RELATED part and software development part, 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 @@ -88,95 +87,3 @@ Note: ... ] """ - -PLANNING_EXAMPLE = """ -## example 1 -User Requirement: Create a cli snake game -Explanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. -```json -[ - { - "command_name": "append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI snake game.", - "assignee": "Alice" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.", - "assignee": "Bob" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "3", - "dependent_task_ids": ["2"], - "instruction": "Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.", - "assignee": "Eve" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "4", - "dependent_task_ids": ["3"], - "instruction": "Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.", - "assignee": "Alex" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "5", - "dependent_task_ids": ["4"], - "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", - "assignee": "Edward" - } - } -] -``` - -## example 2 -User requirement: 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. -Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. -```json -[ - { - "command_name": "append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "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.", - "assignee": "David" - } - } -] -``` -""" - -ROUTING_EXAMPLE = """ -## example 1: Forward a message from one team member to another -Conversation History: -[ - ..., - {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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",...], ...}}}}}, - {'role': 'assistant', 'content': 'Based on the feedback from Alice, the Product Manager, it seems that the PRD for the snake game has been successfully created. Since the PRD is complete and there are no indications of issues with the task, we can mark the current task as finished and move on to the next task in the plan. The next task is assigned to Bob, the Architect, who will be responsible for designing the software architecture for the game based on the PRD provided by Alice.\n\nHere are the commands to update the plan status:\n\n```json\n[\n {\n "command_name": "finish_current_task"\n }\n]\n```'} -] -Command: -```json -[ - { - "command_name": "forward_message", - "args": { - "message_id": "739d9b4983fd4e97a0f78fde5e9ef158" - } - } -] -""" diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index ec293f776..e8cb6570d 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -7,13 +7,15 @@ from pydantic import model_validator from metagpt.environment.mgx.mgx_env import MGXEnv from metagpt.prompts.di.team_leader import ( PLANNING_CMD_PROMPT, - PLANNING_EXAMPLE, ROUTING_CMD_PROMPT, - ROUTING_EXAMPLE, prepare_command_prompt, ) from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult +from metagpt.strategy.experience_retriever import ( + SimplePlanningExpRetriever, + SimpleRoutingExpRetriever, +) from metagpt.strategy.planner import Planner from metagpt.strategy.thinking_command import Command from metagpt.utils.common import CodeParser @@ -92,7 +94,7 @@ class TeamLeader(Role): if not self.planner.plan.goal: user_requirement = self.get_memories()[-1].content self.planner.plan.goal = user_requirement - example = PLANNING_EXAMPLE + example = SimplePlanningExpRetriever().retrieve() # common info team_info = "" @@ -114,8 +116,6 @@ class TeamLeader(Role): available_commands=prepare_command_prompt(self.planning_commands), ) context = self.llm.format_msg(self.get_memory() + [Message(content=plan_prompt, role="user")]) - print(*context, sep="*" * 10 + "\n\n") - # breakpoint() plan_rsp = await self.llm.aask(context) plan_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=plan_rsp)) @@ -126,12 +126,10 @@ class TeamLeader(Role): route_prompt = ROUTING_CMD_PROMPT.format( plan_status=plan_status, team_info=team_info, - example=ROUTING_EXAMPLE, + example=SimpleRoutingExpRetriever().retrieve(), available_commands=prepare_command_prompt(self.env_commands), ) context = self.llm.format_msg(self.get_memory() + [Message(content=route_prompt, role="user")]) - print(*context, sep="*" * 10 + "\n\n") - # breakpoint() route_rsp = await self.llm.aask(context) route_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=route_rsp)) @@ -144,8 +142,4 @@ class TeamLeader(Role): """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" self.run_commands(self.commands) self.task_result = TaskResult(result="Success", is_success=True) - - async def run(self, with_message=None) -> Message | None: - if await self._observe(): - await self._think() - await self._act() + return "\n".join(self.commands) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py new file mode 100644 index 000000000..af7229b6e --- /dev/null +++ b/metagpt/strategy/experience_retriever.py @@ -0,0 +1,130 @@ +from pydantic import BaseModel + + +class ExpRetriever(BaseModel): + """interface for experience retriever""" + + def retrieve(self, context: str) -> str: + raise NotImplementedError + + +class SimplePlanningExpRetriever(ExpRetriever): + """A simple experience retriever that returns manually crafted planning examples.""" + + EXAMPLE: str = """ + ## example 1 + User Requirement: Create a cli snake game + Explanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. + ```json + [ + { + "command_name": "append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI snake game.", + "assignee": "Alice" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.", + "assignee": "Bob" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.", + "assignee": "Eve" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "4", + "dependent_task_ids": ["3"], + "instruction": "Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.", + "assignee": "Alex" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "5", + "dependent_task_ids": ["4"], + "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", + "assignee": "Edward" + } + } + ] + ``` + + ## example 2 + User requirement: 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. + Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. + ```json + [ + { + "command_name": "append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "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.", + "assignee": "David" + } + } + ] + ``` + + ## example 3 + Conversation History: + [ + ..., + {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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, this marks her task as finished and moves the plan to the next task. + ```json + [ + { + "command_name": "finish_current_task", + "args": {} + } + ] + ``` + """ + + def retrieve(self, context: str = "") -> str: + return self.EXAMPLE + + +class SimpleRoutingExpRetriever(ExpRetriever): + """A simple experience retriever that returns manually crafted routing examples.""" + + EXAMPLE: str = """ + ## example 1: Forward a message from one team member to another + Conversation History: + [ + ..., + {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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",...], ...}}}}}, + {'role': 'assistant', 'content': 'Based on the feedback from Alice, the Product Manager, it seems that the PRD for the snake game has been successfully created. Since the PRD is complete and there are no indications of issues with the task, we can mark the current task as finished and move on to the next task in the plan. The next task is assigned to Bob, the Architect, who will be responsible for designing the software architecture for the game based on the PRD provided by Alice.\n\nHere are the commands to update the plan status:\n\n```json\n[\n {\n "command_name": "finish_current_task"\n }\n]\n```'} + ] + Command: + ```json + [ + { + "command_name": "forward_message", + "args": { + "message_id": "739d9b4983fd4e97a0f78fde5e9ef158" + } + } + ] + """ + + def retrieve(self, context: str = "") -> str: + return self.EXAMPLE diff --git a/tests/metagpt/roles/di/test_team_leader.py b/tests/metagpt/roles/di/test_team_leader.py index 6dd9ef4a6..1ac3abffe 100644 --- a/tests/metagpt/roles/di/test_team_leader.py +++ b/tests/metagpt/roles/di/test_team_leader.py @@ -123,7 +123,7 @@ async def test_plan_update_and_routing(env): requirement = "create a 2048 game" tl = env.get_role("Team Leader") - env.publish_message(Message(content=requirement, send_to=tl.name)) + env.publish_message(Message(content=requirement)) await tl.run() # Assuming Product Manager finishes its task @@ -131,37 +131,9 @@ async def test_plan_update_and_routing(env): await tl.run() # TL should mark current task as finished, and forward Product Manager's message to Architect - plan_cmd = tl.commands[0] + # Current task should be updated to the second task + plan_cmd = tl.commands[:-1] route_cmd = tl.commands[-1] - assert plan_cmd["command_name"] == "finish_current_task" + assert "finish_current_task" in [cmd["command_name"] for cmd in plan_cmd] assert route_cmd["command_name"] == "forward_message" or route_cmd["command_name"] == "publish_message" - - -async def main(): - requirement = [ - # "Create a cli snake game", - # "I want to use yolov5 for target detection, yolov5 all the information from the following link, please help me according to the content of the link(https://github.com/ultralytics/yolov5), set up the environment and download the model parameters, and finally provide a few pictures for inference, the inference results will be saved!", - # "Create a website widget for TODO list management. Users should be able to add, mark as complete, and delete tasks. Include features like prioritization, due dates, and categories. Make it visually appealing, responsive, and user-friendly. Use HTML, CSS, and JavaScript. Consider additional features like notifications or task export. Keep it simple and enjoyable for users.dont use vue or react.dont use third party library, use localstorage to save data", - # "Search the web for the new game 2048X, then replicate it", - # """从36kr创投平台https://pitchhub.36kr.com/financing-flash 所有初创企业融资的信息, **注意: 这是一个中文网站**; - # 下面是一个大致流程, 你会根据每一步的运行结果对当前计划中的任务做出适当调整: - # 1. 爬取并本地保存html结构; - # 2. 直接打印第7个*`快讯`*关键词后2000个字符的html内容, 作为*快讯的html内容示例*; - # 3. 反思*快讯的html内容示例*中的规律, 设计正则匹配表达式来获取*`快讯`*的标题、链接、时间; - # 4. 筛选最近3天的初创企业融资*`快讯`*, 以list[dict]形式打印前5个。 - # 5. 将全部结果存在本地csv中 - # """, - """ - I would like to imitate the website available at https://news.youth.cn/gn/202404/t20240406_15178916.htm. Could you please browse through it? - Note: - - don't ignore the image, use https://source.unsplash.com/random to get random images - - use the same text, the same layout, the same color as the original website - if you can not do it, please try to get as close as possible. - """, - ] - tl.put_message(Message(requirement[0])) - await tl._observe() - await tl._think() - - -# asyncio.run(main()) + assert tl.planner.plan.current_task_id == "2" From e39f7ff0b150d9344de38799f2c846a62d6bc36a Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 12:00:32 +0800 Subject: [PATCH 04/14] rm forward_message --- metagpt/environment/base_env.py | 2 +- metagpt/environment/mgx/mgx_env.py | 10 -------- metagpt/prompts/di/team_leader.py | 12 +++------ metagpt/roles/di/team_leader.py | 14 +++-------- metagpt/schema.py | 15 ++++++++++- metagpt/strategy/experience_retriever.py | 29 +--------------------- metagpt/strategy/thinking_command.py | 5 ---- tests/metagpt/roles/di/test_team_leader.py | 4 +-- 8 files changed, 26 insertions(+), 65 deletions(-) diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 4a2d0c114..024c46877 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -190,7 +190,7 @@ class Environment(ExtEnv): found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") - # self.history += f"\n{message}" # For debug + self.history += f"\n{message}" # For debug return True diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 6626679ce..b15d57e0f 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -6,8 +6,6 @@ from metagpt.schema import Message class MGXEnv(Environment): """MGX Environment""" - history: dict[str, Message] = {} # redefine message history - def _publish_message(self, message: Message, peekable: bool = True) -> bool: return super().publish_message(message, peekable) @@ -28,16 +26,8 @@ class MGXEnv(Environment): # every regular message goes through team leader tl.put_message(message) - self.history[message.id] = message - return True - def forward_message(self, message_id: str) -> str: - if message_id not in self.history: - return f"invalid message_id {message_id}, not found in history." - msg = self.history[message_id] - return self._publish_message(msg) - async def ask_human(self, question: str) -> str: # NOTE: Can be overwritten in remote setting return get_human_input(question) diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 5a5dce37f..2ad0c4b54 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -37,14 +37,14 @@ If plan is created, you should track the progress based on team member feedback 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 -> QaEngine, each assigned ONE task. -3. If the requirement contains both DATA-RELATED part and software development part, 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. +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: -Some text indicating your thoughts, including how you categorize the requirement based on Note or how you should update the plan status. Then a json array of commands. +Some text indicating your thoughts, including 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 [ {{ @@ -70,12 +70,8 @@ ROUTING_CMD_PROMPT = """ {example} # Instructions -You are a team leader, you can use publish_message or forward_message to team members, asking them to start their task. -Note: -1. You should carefully review the recent conversation, if there are messages from team members, forward them or withold them properly. -2. Prioritize forward_message if messages exist, only public_message if you want to instruct the team member yourself. -3. Pay attention to task dependency, don't assign a task to a team member if the dependent tasks are not finished yet. -4. Review plan status, think about what you should do next, you can use any of the Available Commands. +You are a team leader, you can publish_message to team members, asking them to start their task. +If there are new user message, review the conversation history and think about what you should do next, you can use any of the Available Commands. # Your commands in a json array, in the following output format: ```json diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index e8cb6570d..3b1ac9831 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -12,10 +12,7 @@ from metagpt.prompts.di.team_leader import ( ) from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult -from metagpt.strategy.experience_retriever import ( - SimplePlanningExpRetriever, - SimpleRoutingExpRetriever, -) +from metagpt.strategy.experience_retriever import SimplePlanningExpRetriever from metagpt.strategy.planner import Planner from metagpt.strategy.thinking_command import Command from metagpt.utils.common import CodeParser @@ -35,7 +32,6 @@ class TeamLeader(Role): ] env_commands: list[Command] = [ Command.PUBLISH_MESSAGE, - Command.FORWARD_MESSAGE, Command.ASK_HUMAN, Command.REPLY_TO_HUMAN, Command.PASS, @@ -50,8 +46,6 @@ class TeamLeader(Role): assert isinstance(self.rc.env, MGXEnv), "TeamLeader should only be used in an MGXEnv" if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: self.rc.env.publish_message(Message(**cmd["args"]), publicer=self.profile) - elif cmd["command_name"] in Command.FORWARD_MESSAGE.cmd_name: - self.rc.env.forward_message(**cmd["args"]) elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: self.rc.env.ask_human(**cmd["args"]) elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: @@ -82,7 +76,7 @@ class TeamLeader(Role): mem = self.rc.memory.get() for m in mem: if m.role not in ["system", "user", "assistant"]: - m.content = f"id: {m.id[:10]}, from {m.role} to {m.send_to}: {m.content}" + m.content = f"from {m.role} to {m.send_to}: {m.content}" m.role = "assistant" return mem @@ -126,7 +120,7 @@ class TeamLeader(Role): route_prompt = ROUTING_CMD_PROMPT.format( plan_status=plan_status, team_info=team_info, - example=SimpleRoutingExpRetriever().retrieve(), + example="", available_commands=prepare_command_prompt(self.env_commands), ) context = self.llm.format_msg(self.get_memory() + [Message(content=route_prompt, role="user")]) @@ -142,4 +136,4 @@ class TeamLeader(Role): """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" self.run_commands(self.commands) self.task_result = TaskResult(result="Success", is_success=True) - return "\n".join(self.commands) + return Message(content="Commands executed", role="assistant") diff --git a/metagpt/schema.py b/metagpt/schema.py index b24d18d09..d0396ec26 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -348,6 +348,7 @@ class Task(BaseModel): result: str = "" is_success: bool = False is_finished: bool = False + assignee: str = "" def reset(self): self.code = "" @@ -489,7 +490,11 @@ class Plan(BaseModel): Returns: None """ - assert not self.has_task_id(new_task.task_id), "Task already in current plan, use replace_task instead" + # assert not self.has_task_id(new_task.task_id), "Task already in current plan, use replace_task instead" + if self.has_task_id(new_task.task_id): + logger.warning( + "Task already in current plan, should use replace_task instead. Overwriting the existing task." + ) assert all( [self.has_task_id(dep_id) for dep_id in new_task.dependent_task_ids] @@ -504,6 +509,10 @@ class Plan(BaseModel): return task_id in self.task_map def _update_current_task(self): + self.tasks = self._topological_sort(self.tasks) + # Update the task map for quick access to tasks by ID + self.task_map = {task.task_id: task for task in self.tasks} + current_task_id = "" for task in self.tasks: if not task.is_finished: @@ -534,6 +543,10 @@ class Plan(BaseModel): self.current_task.is_finished = True self._update_current_task() # set to next task + def is_plan_finished(self) -> bool: + """Check if all tasks are finished""" + return all(task.is_finished for task in self.tasks) + def get_finished_tasks(self) -> list[Task]: """return all finished tasks in correct linearized order diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index af7229b6e..aea354645 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -86,7 +86,7 @@ class SimplePlanningExpRetriever(ExpRetriever): Conversation History: [ ..., - {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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",...], ...}}}}}, + {'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, this marks her task as finished and moves the plan to the next task. ```json @@ -101,30 +101,3 @@ class SimplePlanningExpRetriever(ExpRetriever): def retrieve(self, context: str = "") -> str: return self.EXAMPLE - - -class SimpleRoutingExpRetriever(ExpRetriever): - """A simple experience retriever that returns manually crafted routing examples.""" - - EXAMPLE: str = """ - ## example 1: Forward a message from one team member to another - Conversation History: - [ - ..., - {'role': 'assistant', 'content': 'id: 739d9b4983fd4e97a0f78fde5e9ef158, 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",...], ...}}}}}, - {'role': 'assistant', 'content': 'Based on the feedback from Alice, the Product Manager, it seems that the PRD for the snake game has been successfully created. Since the PRD is complete and there are no indications of issues with the task, we can mark the current task as finished and move on to the next task in the plan. The next task is assigned to Bob, the Architect, who will be responsible for designing the software architecture for the game based on the PRD provided by Alice.\n\nHere are the commands to update the plan status:\n\n```json\n[\n {\n "command_name": "finish_current_task"\n }\n]\n```'} - ] - Command: - ```json - [ - { - "command_name": "forward_message", - "args": { - "message_id": "739d9b4983fd4e97a0f78fde5e9ef158" - } - } - ] - """ - - def retrieve(self, context: str = "") -> str: - return self.EXAMPLE diff --git a/metagpt/strategy/thinking_command.py b/metagpt/strategy/thinking_command.py index cc4626c6d..19604ad39 100644 --- a/metagpt/strategy/thinking_command.py +++ b/metagpt/strategy/thinking_command.py @@ -38,11 +38,6 @@ class Command(Enum): signature="publish_message(content: str, send_to: str)", desc="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 from original content to team members because you are their sole info source. However, if the original content is long or contains concrete info, you should forward the message using forward_message instead of publishing it.", ) - FORWARD_MESSAGE = CommandDef( - name="forward_message", - signature="forward_message(message_id: str)", - desc="Forward a message from one team member to another or all without any modification. This will make the recipient start their work, too.", - ) REPLY_TO_HUMAN = CommandDef( name="reply_to_human", signature="reply_to_human(content: str)", diff --git a/tests/metagpt/roles/di/test_team_leader.py b/tests/metagpt/roles/di/test_team_leader.py index 1ac3abffe..3c0b5ef92 100644 --- a/tests/metagpt/roles/di/test_team_leader.py +++ b/tests/metagpt/roles/di/test_team_leader.py @@ -20,7 +20,7 @@ def env(): da = DataInterpreter( name="David", profile="Data Analyst", - goal="Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web deployment, terminal operation, git operation, etc.", + goal="Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git operation, etc.", react_mode="react", ) test_env.add_roles( @@ -135,5 +135,5 @@ async def test_plan_update_and_routing(env): plan_cmd = tl.commands[:-1] route_cmd = tl.commands[-1] assert "finish_current_task" in [cmd["command_name"] for cmd in plan_cmd] - assert route_cmd["command_name"] == "forward_message" or route_cmd["command_name"] == "publish_message" + assert route_cmd["command_name"] == "publish_message" assert tl.planner.plan.current_task_id == "2" From dee1d6fe6066d2631af612a0e455e61f5bd60089 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 16:06:27 +0800 Subject: [PATCH 05/14] modify Role._think --- metagpt/roles/role.py | 4 ++-- metagpt/strategy/thinking_command.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index e0f8a7ea6..b1773b739 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -455,8 +455,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel): rsp = Message(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act while actions_taken < self.rc.max_react_loop: # think - await self._think() - if self.rc.todo is None: + has_todo = await self._think() + if not has_todo: break # act logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}") diff --git a/metagpt/strategy/thinking_command.py b/metagpt/strategy/thinking_command.py index 19604ad39..7b48cae51 100644 --- a/metagpt/strategy/thinking_command.py +++ b/metagpt/strategy/thinking_command.py @@ -36,7 +36,7 @@ class Command(Enum): PUBLISH_MESSAGE = CommandDef( name="publish_message", signature="publish_message(content: str, send_to: str)", - desc="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 from original content to team members because you are their sole info source. However, if the original content is long or contains concrete info, you should forward the message using forward_message instead of publishing it.", + desc="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 from original content to team members because you are their sole info source.", ) REPLY_TO_HUMAN = CommandDef( name="reply_to_human", From 12b02d5725b79c15edbec6b9f6dbd7bff97eb09f Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 17:40:24 +0800 Subject: [PATCH 06/14] merge commands for better maintenance --- metagpt/prompts/di/team_leader.py | 33 ++---------- metagpt/roles/di/team_leader.py | 60 ++++++---------------- metagpt/strategy/experience_retriever.py | 39 +++++++++++++- tests/metagpt/roles/di/test_team_leader.py | 34 +++++++++++- 4 files changed, 90 insertions(+), 76 deletions(-) diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 2ad0c4b54..21ec86f5e 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -8,7 +8,7 @@ def prepare_command_prompt(commands: list[Command]) -> str: return command_prompt -PLANNING_CMD_PROMPT = """ +CMD_PROMPT = """ # Data Structure class Task(BaseModel): task_id: str = "" @@ -32,7 +32,10 @@ class Task(BaseModel): # Instructions You are a team leader, and you are responsible for drafting tasks and routing tasks to your team members. 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. 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. @@ -55,31 +58,3 @@ Some text indicating your thoughts, including how you categorize the requirement ] ``` """ - -ROUTING_CMD_PROMPT = """ -# Team Member Info -{team_info} - -# Available Commands -{available_commands} - -# Current Plan -{plan_status} - -# Example -{example} - -# Instructions -You are a team leader, you can publish_message to team members, asking them to start their task. -If there are new user message, review the conversation history and think about what you should do next, you can use any of the Available Commands. - -# Your commands in a json array, in the following output format: -```json -[ - {{ - "command_name": str, - "args": {{"arg_name": arg_value, ...}} - }}, - ... -] -""" diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 3b1ac9831..3f3a86f76 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -5,11 +5,7 @@ import json from pydantic import model_validator from metagpt.environment.mgx.mgx_env import MGXEnv -from metagpt.prompts.di.team_leader import ( - PLANNING_CMD_PROMPT, - ROUTING_CMD_PROMPT, - prepare_command_prompt, -) +from metagpt.prompts.di.team_leader import CMD_PROMPT, prepare_command_prompt from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult from metagpt.strategy.experience_retriever import SimplePlanningExpRetriever @@ -22,15 +18,11 @@ class TeamLeader(Role): name: str = "Tim" profile: str = "Team Leader" task_result: TaskResult = None - planning_commands: list[Command] = [ + commands: list[Command] = [ Command.APPEND_TASK, Command.RESET_TASK, Command.REPLACE_TASK, Command.FINISH_CURRENT_TASK, - Command.REPLY_TO_HUMAN, - Command.PASS, - ] - env_commands: list[Command] = [ Command.PUBLISH_MESSAGE, Command.ASK_HUMAN, Command.REPLY_TO_HUMAN, @@ -45,7 +37,7 @@ class TeamLeader(Role): def _run_env_command(self, cmd): assert isinstance(self.rc.env, MGXEnv), "TeamLeader should only be used in an MGXEnv" if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: - self.rc.env.publish_message(Message(**cmd["args"]), publicer=self.profile) + self.rc.env.publish_message(Message(sent_from=self.profile, **cmd["args"]), publicer=self.profile) elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: self.rc.env.ask_human(**cmd["args"]) elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: @@ -72,8 +64,8 @@ class TeamLeader(Role): if self.planner.plan.is_plan_finished(): self._set_state(-1) - def get_memory(self) -> list[Message]: - mem = self.rc.memory.get() + def get_memory(self, k=10) -> list[Message]: + mem = self.rc.memory.get(k=k) for m in mem: if m.role not in ["system", "user", "assistant"]: m.content = f"from {m.role} to {m.send_to}: {m.content}" @@ -84,51 +76,33 @@ class TeamLeader(Role): """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" self.commands = [] - example = "" if not self.planner.plan.goal: user_requirement = self.get_memories()[-1].content self.planner.plan.goal = user_requirement - example = SimplePlanningExpRetriever().retrieve() - # common info + plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) + for task in plan_status["tasks"]: + task.pop("code") + task.pop("result") team_info = "" for role in self.rc.env.roles.values(): if role.profile == "TeamLeader": continue team_info += f"{role.name}: {role.profile}, {role.goal}\n" - # print(team_info) + example = SimplePlanningExpRetriever().retrieve() - # plan commands - plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) - for task in plan_status["tasks"]: - task.pop("code") - task.pop("result") - plan_prompt = PLANNING_CMD_PROMPT.format( + prompt = CMD_PROMPT.format( plan_status=plan_status, team_info=team_info, example=example, - available_commands=prepare_command_prompt(self.planning_commands), + available_commands=prepare_command_prompt(self.commands), ) - context = self.llm.format_msg(self.get_memory() + [Message(content=plan_prompt, role="user")]) + context = self.llm.format_msg(self.get_memory() + [Message(content=prompt, role="user")]) - plan_rsp = await self.llm.aask(context) - plan_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=plan_rsp)) - self.commands.extend(plan_rsp_dict) - self.rc.memory.add(Message(content=plan_rsp, role="assistant")) - - # routing commands - route_prompt = ROUTING_CMD_PROMPT.format( - plan_status=plan_status, - team_info=team_info, - example="", - available_commands=prepare_command_prompt(self.env_commands), - ) - context = self.llm.format_msg(self.get_memory() + [Message(content=route_prompt, role="user")]) - - route_rsp = await self.llm.aask(context) - route_rsp_dict = json.loads(CodeParser.parse_code(block=None, text=route_rsp)) - self.commands.extend(route_rsp_dict) - self.rc.memory.add(Message(content=route_rsp, role="assistant")) + rsp = await self.llm.aask(context) + rsp_dict = json.loads(CodeParser.parse_code(block=None, text=rsp)) + self.commands.extend(rsp_dict) + self.rc.memory.add(Message(content=rsp, role="assistant")) return True diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index aea354645..56a7c304e 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -61,12 +61,19 @@ class SimplePlanningExpRetriever(ExpRetriever): "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", "assignee": "Edward" } + }, + { + "command_name": "publish_message", + "args": { + "content": "User request to create a cli snake game. Please create a product requirement document (PRD) outlining the features, user interface, and user experience of the snake game.", + "send_to": "Alice" + } } ] ``` ## example 2 - User requirement: 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. + User Requirement: 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. Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. ```json [ @@ -78,6 +85,13 @@ class SimplePlanningExpRetriever(ExpRetriever): "instruction": "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.", "assignee": "David" } + }, + { + "command_name": "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" + } } ] ``` @@ -88,12 +102,33 @@ class SimplePlanningExpRetriever(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, this marks her task as finished and moves the plan to the next task. + 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. ```json [ { "command_name": "finish_current_task", "args": {} + }, + { + "command_name": "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" + } + } + ] + ``` + + ## example 4 + User Question: how does the project go? + Explanation: The user is asking for a general update on the project status. Give a straight answer about the current task the team is working on and provide a summary of the completed tasks. + ```json + [ + { + "command_name": "reply_to_human", + "args": { + "content": "The team is currently working on ... We have completed ...", + } } ] ``` diff --git a/tests/metagpt/roles/di/test_team_leader.py b/tests/metagpt/roles/di/test_team_leader.py index 3c0b5ef92..1b33a6edc 100644 --- a/tests/metagpt/roles/di/test_team_leader.py +++ b/tests/metagpt/roles/di/test_team_leader.py @@ -132,8 +132,38 @@ async def test_plan_update_and_routing(env): # TL should mark current task as finished, and forward Product Manager's message to Architect # Current task should be updated to the second task - plan_cmd = tl.commands[:-1] + plan_cmd = tl.commands[0] route_cmd = tl.commands[-1] - assert "finish_current_task" in [cmd["command_name"] for cmd in plan_cmd] + assert plan_cmd["command_name"] == "finish_current_task" assert route_cmd["command_name"] == "publish_message" + assert route_cmd["args"]["send_to"] == Architect().name assert tl.planner.plan.current_task_id == "2" + + # Next step, assuming Architect finishes its task + env.publish_message(Message(content=DESIGN_CONTENT, role="Bob(Architect)", sent_from="Bob")) + await tl.run() + plan_cmd = tl.commands[0] + route_cmd = tl.commands[-1] + assert plan_cmd["command_name"] == "finish_current_task" + assert route_cmd["command_name"] == "publish_message" + assert route_cmd["args"]["send_to"] == ProjectManager().name + assert tl.planner.plan.current_task_id == "3" + + +@pytest.mark.asyncio +async def test_reply_to_human(env): + requirement = "create a 2048 game" + + tl = env.get_role("Team Leader") + env.publish_message(Message(content=requirement)) + await tl.run() + + # Assuming Product Manager finishes its task + env.publish_message(Message(content=PRD_MSG_CONTENT, role="Alice(Product Manager)", sent_from="Alice")) + await tl.run() + + # Human inquires about the progress + env.publish_message(Message(content="Who is working? How does the project go?")) + await tl.run() + + assert tl.commands[0]["command_name"] == "reply_to_human" From d8ede20235dab0bb0e67bac27ba83acabb5b4f31 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 18:42:44 +0800 Subject: [PATCH 07/14] fix publish msg bug --- metagpt/roles/di/team_leader.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 3f3a86f76..a24d85dc1 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -8,7 +8,7 @@ from metagpt.environment.mgx.mgx_env import MGXEnv from metagpt.prompts.di.team_leader import CMD_PROMPT, prepare_command_prompt from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult -from metagpt.strategy.experience_retriever import SimplePlanningExpRetriever +from metagpt.strategy.experience_retriever import SimpleExpRetriever from metagpt.strategy.planner import Planner from metagpt.strategy.thinking_command import Command from metagpt.utils.common import CodeParser @@ -37,7 +37,7 @@ class TeamLeader(Role): def _run_env_command(self, cmd): assert isinstance(self.rc.env, MGXEnv), "TeamLeader should only be used in an MGXEnv" if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: - self.rc.env.publish_message(Message(sent_from=self.profile, **cmd["args"]), publicer=self.profile) + self.publish_message(Message(sent_from=self.profile, **cmd["args"])) elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: self.rc.env.ask_human(**cmd["args"]) elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: @@ -89,7 +89,7 @@ class TeamLeader(Role): if role.profile == "TeamLeader": continue team_info += f"{role.name}: {role.profile}, {role.goal}\n" - example = SimplePlanningExpRetriever().retrieve() + example = SimpleExpRetriever().retrieve() prompt = CMD_PROMPT.format( plan_status=plan_status, @@ -111,3 +111,12 @@ class TeamLeader(Role): self.run_commands(self.commands) self.task_result = TaskResult(result="Success", is_success=True) return Message(content="Commands executed", role="assistant") + + def publish_message(self, msg): + """If the role belongs to env, then the role's messages will be broadcast to env""" + if not msg: + return + if not self.rc.env: + # If env does not exist, do not publish the message + return + self.rc.env.publish_message(msg, publicer=self.profile) From 872ac85de52a18b00716dac4b48178aa79450aa7 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 18:43:10 +0800 Subject: [PATCH 08/14] rename --- metagpt/strategy/experience_retriever.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 56a7c304e..e0d2601e3 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -8,8 +8,8 @@ class ExpRetriever(BaseModel): raise NotImplementedError -class SimplePlanningExpRetriever(ExpRetriever): - """A simple experience retriever that returns manually crafted planning examples.""" +class SimpleExpRetriever(ExpRetriever): + """A simple experience retriever that returns manually crafted examples.""" EXAMPLE: str = """ ## example 1 From c63a7ecc18ea8377ef25f74567d216b0ad072d25 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 25 Apr 2024 21:41:02 +0800 Subject: [PATCH 09/14] bugfix --- metagpt/environment/mgx/mgx_env.py | 1 + metagpt/roles/di/team_leader.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index b15d57e0f..a6e15ffa5 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -24,6 +24,7 @@ class MGXEnv(Environment): else: # every regular message goes through team leader + message.send_to.add(tl.name) tl.put_message(message) return True diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index a24d85dc1..48302d00b 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -110,7 +110,9 @@ class TeamLeader(Role): """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" self.run_commands(self.commands) self.task_result = TaskResult(result="Success", is_success=True) - return Message(content="Commands executed", role="assistant") + msg = Message(content="Commands executed", role="user", send_to=self) + 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""" From 3590cd77b6a234e39a0d77ff2bfc6e86b3891006 Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 26 Apr 2024 00:45:32 +0800 Subject: [PATCH 10/14] add quick routing rule --- metagpt/environment/base_env.py | 5 ++-- metagpt/environment/mgx/mgx_env.py | 42 ++++++++++++++++++++++++++++++ metagpt/prompts/di/team_leader.py | 14 ++++++++-- metagpt/roles/di/team_leader.py | 15 +++++++++-- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 024c46877..f6d2e431d 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -19,6 +19,7 @@ from metagpt.environment.api.env_api import ( ) from metagpt.environment.base_env_space import BaseEnvAction, BaseEnvObsParams from metagpt.logs import logger +from metagpt.memory import Memory from metagpt.schema import Message from metagpt.utils.common import get_function_schema, is_coroutine_func, is_send_to @@ -131,7 +132,7 @@ class Environment(ExtEnv): desc: str = Field(default="") # 环境描述 roles: dict[str, SerializeAsAny["Role"]] = Field(default_factory=dict, validate_default=True) member_addrs: Dict["Role", Set] = Field(default_factory=dict, exclude=True) - history: str = "" # For debug + history: Memory = Field(default_factory=Memory) # For debug context: Context = Field(default_factory=Context, exclude=True) def reset( @@ -190,7 +191,7 @@ class Environment(ExtEnv): found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") - self.history += f"\n{message}" # For debug + self.history.add(message) # For debug return True diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index a6e15ffa5..17b479679 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -1,6 +1,22 @@ +from metagpt.actions import ( + UserRequirement, + WriteDesign, + WritePRD, + WriteTasks, + WriteTest, +) +from metagpt.actions.summarize_code import SummarizeCode from metagpt.environment.base_env import Environment from metagpt.logs import get_human_input +from metagpt.roles import ( + Architect, + Engineer, + ProductManager, + ProjectManager, + QaEngineer, +) from metagpt.schema import Message +from metagpt.utils.common import any_to_str, any_to_str_set class MGXEnv(Environment): @@ -18,6 +34,17 @@ class MGXEnv(Environment): # bypass team leader, team leader only needs to know but not to react tl.rc.memory.add(message) + elif self.message_within_software_sop(message) and not self.has_user_requirement(): + # Quick routing for messages within software SOP, bypassing TL. + # Use rules to check for user intervention and to finish task. + # NOTE: This escapes TL's supervision and has pitfalls such as routing obsolete messages when TL has acquired a new user requirement. + # In addition, we should not determine the status of a task based on message cause_by. + # Consider replacing this in the future. + self._publish_message(message) + if self.is_software_task_finished(message): + tl.rc.memory.add(message) + tl.finish_current_task() + elif publicer == tl.profile: # message processed by team leader can be published now self._publish_message(message) @@ -27,6 +54,8 @@ class MGXEnv(Environment): message.send_to.add(tl.name) tl.put_message(message) + self.history.add(message) + return True async def ask_human(self, question: str) -> str: @@ -36,3 +65,16 @@ class MGXEnv(Environment): async def reply_to_human(self, content: str) -> str: # NOTE: Can be overwritten in remote setting return content + + def message_within_software_sop(self, message: Message) -> bool: + return message.sent_from in any_to_str_set([ProductManager, Architect, ProjectManager, Engineer, QaEngineer]) + + def has_user_requirement(self, k=3) -> bool: + """A heuristics to check if there is a recent user intervention""" + return any_to_str(UserRequirement) in [msg.cause_by for msg in self.history.get(k)] + + def is_software_task_finished(self, message: Message) -> bool: + """Use a hard-coded rule to check if one software task is finished""" + return message.cause_by in any_to_str_set([WritePRD, WriteDesign, WriteTasks, SummarizeCode]) or ( + message.cause_by == any_to_str(WriteTest) and "Exceeding" in message.content + ) diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 21ec86f5e..87e8a1336 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -46,8 +46,8 @@ 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: -Some text indicating your thoughts, including 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. +# 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 [ {{ @@ -58,3 +58,13 @@ Some text indicating your thoughts, including how you categorize the requirement ] ``` """ + +FINISH_CURRENT_TASK_CMD = """ +```json +[ + { + "command_name": "finish_current_task", + "args": {{}} + } +``` +""" diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 48302d00b..0034a9eef 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -4,8 +4,13 @@ import json from pydantic import model_validator +from metagpt.actions.di.run_command import RunCommand from metagpt.environment.mgx.mgx_env import MGXEnv -from metagpt.prompts.di.team_leader import CMD_PROMPT, prepare_command_prompt +from metagpt.prompts.di.team_leader import ( + CMD_PROMPT, + FINISH_CURRENT_TASK_CMD, + prepare_command_prompt, +) from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult from metagpt.strategy.experience_retriever import SimpleExpRetriever @@ -37,7 +42,7 @@ class TeamLeader(Role): def _run_env_command(self, cmd): assert isinstance(self.rc.env, MGXEnv), "TeamLeader should only be used in an MGXEnv" if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: - self.publish_message(Message(sent_from=self.profile, **cmd["args"])) + self.publish_message(Message(**cmd["args"])) elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: self.rc.env.ask_human(**cmd["args"]) elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: @@ -121,4 +126,10 @@ class TeamLeader(Role): 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 self.rc.env.publish_message(msg, publicer=self.profile) + + def finish_current_task(self): + self.planner.plan.finish_current_task() + self.rc.memory.add(Message(content=FINISH_CURRENT_TASK_CMD, role="assistant")) From 2479f75bcb7b170aa54fd36911e5fba89596ce64 Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 26 Apr 2024 00:53:06 +0800 Subject: [PATCH 11/14] add args for ask_human and reply_to_human --- metagpt/environment/mgx/mgx_env.py | 5 +++-- metagpt/roles/di/team_leader.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 17b479679..ae97d72f6 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -14,6 +14,7 @@ from metagpt.roles import ( ProductManager, ProjectManager, QaEngineer, + Role, ) from metagpt.schema import Message from metagpt.utils.common import any_to_str, any_to_str_set @@ -58,11 +59,11 @@ class MGXEnv(Environment): return True - async def ask_human(self, question: str) -> str: + async def ask_human(self, question: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting return get_human_input(question) - async def reply_to_human(self, content: str) -> str: + async def reply_to_human(self, content: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting return content diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 0034a9eef..346535308 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -44,9 +44,9 @@ class TeamLeader(Role): if cmd["command_name"] == Command.PUBLISH_MESSAGE.cmd_name: self.publish_message(Message(**cmd["args"])) elif cmd["command_name"] == Command.ASK_HUMAN.cmd_name: - self.rc.env.ask_human(**cmd["args"]) + self.rc.env.ask_human(sent_from=self, **cmd["args"]) elif cmd["command_name"] == Command.REPLY_TO_HUMAN.cmd_name: - self.rc.env.reply_to_human(**cmd["args"]) + self.rc.env.reply_to_human(sent_from=self, **cmd["args"]) def _run_internal_command(self, cmd): if cmd["command_name"] == Command.APPEND_TASK.cmd_name: From cd6654d013ea7ca51c2831586673015dbf17d3ce Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 26 Apr 2024 10:24:58 +0800 Subject: [PATCH 12/14] minor update --- metagpt/environment/mgx/mgx_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index ae97d72f6..ada638f8e 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -38,7 +38,7 @@ class MGXEnv(Environment): elif self.message_within_software_sop(message) and not self.has_user_requirement(): # Quick routing for messages within software SOP, bypassing TL. # Use rules to check for user intervention and to finish task. - # NOTE: This escapes TL's supervision and has pitfalls such as routing obsolete messages when TL has acquired a new user requirement. + # NOTE: This escapes TL's supervision and has pitfalls such as routing obsolete messages even if TL has acquired a new user requirement. # In addition, we should not determine the status of a task based on message cause_by. # Consider replacing this in the future. self._publish_message(message) @@ -61,7 +61,7 @@ class MGXEnv(Environment): async def ask_human(self, question: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting - return get_human_input(question) + return await get_human_input(question) async def reply_to_human(self, content: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting From 531fe12e3ff242656ff7fd166b2833fdf5dbec83 Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 26 Apr 2024 10:41:25 +0800 Subject: [PATCH 13/14] add dummy action --- metagpt/actions/di/run_command.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 metagpt/actions/di/run_command.py diff --git a/metagpt/actions/di/run_command.py b/metagpt/actions/di/run_command.py new file mode 100644 index 000000000..510bb5d92 --- /dev/null +++ b/metagpt/actions/di/run_command.py @@ -0,0 +1,5 @@ +from metagpt.actions import Action + + +class RunCommand(Action): + """A dummy RunCommand action used as a symbol only""" From ede3e36944c8acef06092f6a2cbe3ab57208f1cf Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 26 Apr 2024 15:42:52 +0800 Subject: [PATCH 14/14] fix di bug, small enhancement for tl --- metagpt/environment/mgx/mgx_env.py | 20 +++++++++++++++++--- metagpt/roles/di/data_interpreter.py | 15 ++++----------- metagpt/roles/di/team_leader.py | 10 +++------- metagpt/roles/role.py | 2 ++ 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index ada638f8e..6c7069b56 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -33,7 +33,7 @@ class MGXEnv(Environment): if user_defined_recipient: self._publish_message(message) # bypass team leader, team leader only needs to know but not to react - tl.rc.memory.add(message) + tl.rc.memory.add(self.move_message_info_to_content(message)) elif self.message_within_software_sop(message) and not self.has_user_requirement(): # Quick routing for messages within software SOP, bypassing TL. @@ -43,7 +43,7 @@ class MGXEnv(Environment): # Consider replacing this in the future. self._publish_message(message) if self.is_software_task_finished(message): - tl.rc.memory.add(message) + tl.rc.memory.add(self.move_message_info_to_content(message)) tl.finish_current_task() elif publicer == tl.profile: @@ -52,6 +52,7 @@ class MGXEnv(Environment): else: # every regular message goes through team leader + message = self.move_message_info_to_content(message) message.send_to.add(tl.name) tl.put_message(message) @@ -70,7 +71,7 @@ class MGXEnv(Environment): def message_within_software_sop(self, message: Message) -> bool: return message.sent_from in any_to_str_set([ProductManager, Architect, ProjectManager, Engineer, QaEngineer]) - def has_user_requirement(self, k=3) -> bool: + def has_user_requirement(self, k=2) -> bool: """A heuristics to check if there is a recent user intervention""" return any_to_str(UserRequirement) in [msg.cause_by for msg in self.history.get(k)] @@ -79,3 +80,16 @@ class MGXEnv(Environment): return message.cause_by in any_to_str_set([WritePRD, WriteDesign, WriteTasks, SummarizeCode]) or ( message.cause_by == any_to_str(WriteTest) and "Exceeding" in message.content ) + + def move_message_info_to_content(self, message: Message) -> Message: + """Two things here: + 1. Convert role, since role field must be reserved for LLM API, and is limited to, for example, one of ["user", "assistant", "system"] + 2. Add sender and recipient info to content, making TL aware, since LLM API only takes content as input + """ + if message.role in ["system", "user", "assistant"]: + sent_from = message.sent_from + else: + sent_from = message.role + message.role = "assistant" + message.content = f"from {sent_from} to {message.send_to}: {message.content}" + return message diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index 08a6b7f2b..2e1e0a2da 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -14,7 +14,7 @@ from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult from metagpt.strategy.task_type import TaskType from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender -from metagpt.utils.common import CodeParser, role_raise_decorator +from metagpt.utils.common import CodeParser REACT_THINK_PROMPT = """ # User Requirement @@ -62,7 +62,7 @@ class DataInterpreter(Role): async def _think(self) -> bool: """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" - user_requirement = self.get_memories()[0].content + user_requirement = self.get_memories()[-1].content context = self.working_memory.get() if not context: @@ -86,6 +86,7 @@ class DataInterpreter(Role): return Message(content=code, role="assistant", cause_by=WriteAnalysisCode) async def _plan_and_act(self) -> Message: + self._set_state(0) try: rsp = await super()._plan_and_act() await self.execute_code.terminate() @@ -153,7 +154,7 @@ class DataInterpreter(Role): logger.info(f"ready to {todo.name}") use_reflection = counter > 0 and self.use_reflection # only use reflection after the first trial - user_requirement = self.get_memories()[0].content # issue: 1)多次用户交互时,永远只读用户的第1次request;2)prerequisite没处理 + user_requirement = self.get_memories()[-1].content code = await todo.run( user_requirement=user_requirement, @@ -186,11 +187,3 @@ class DataInterpreter(Role): print(result) data_info = DATA_INFO.format(info=result) self.working_memory.add(Message(content=data_info, role="user", cause_by=CheckData)) - - @role_raise_decorator - async def run(self, with_message=None) -> Message | None: - if not self.rc.todo: - self.set_actions([WriteAnalysisCode]) - self._set_state(0) - - return await super().run(with_message) diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 346535308..9f951799a 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -70,12 +70,8 @@ class TeamLeader(Role): self._set_state(-1) def get_memory(self, k=10) -> list[Message]: - mem = self.rc.memory.get(k=k) - for m in mem: - if m.role not in ["system", "user", "assistant"]: - m.content = f"from {m.role} to {m.send_to}: {m.content}" - m.role = "assistant" - return mem + """A wrapper with default value""" + return self.rc.memory.get(k=k) async def _think(self) -> bool: """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" @@ -115,7 +111,7 @@ class TeamLeader(Role): """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" self.run_commands(self.commands) self.task_result = TaskResult(result="Success", is_success=True) - msg = Message(content="Commands executed", role="user", send_to=self) + msg = Message(content="Commands executed", send_to="no one") # a dummy message to conform to the interface self.rc.memory.add(msg) return msg diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b1773b739..9b15ab9a5 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -492,6 +492,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel): await self.planner.process_task_result(task_result) rsp = self.planner.get_useful_memories()[0] # return the completed plan as a response + rsp.role = "assistant" + rsp.sent_from = self._setting self.rc.memory.add(rsp) # add to persistent memory