mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-05 16:02:14 +02:00
disentangle planner and tool module, optimize tool module, add react mode
This commit is contained in:
parent
0a2273c7a0
commit
0116de01b9
20 changed files with 554 additions and 354 deletions
|
|
@ -1,6 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pydantic import Field
|
||||
import json
|
||||
from typing import Literal, Union
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from metagpt.actions.mi.ask_review import ReviewConst
|
||||
from metagpt.actions.mi.execute_nb_code import ExecuteNbCode
|
||||
|
|
@ -9,40 +12,80 @@ from metagpt.logs import logger
|
|||
from metagpt.prompts.mi.write_analysis_code import DATA_INFO
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message, Task, TaskResult
|
||||
from metagpt.tools.tool_type import ToolType
|
||||
from metagpt.strategy.task_type import TaskType
|
||||
from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender
|
||||
from metagpt.utils.common import CodeParser
|
||||
|
||||
REACT_THINK_PROMPT = """
|
||||
# User Requirement
|
||||
{user_requirement}
|
||||
# Context
|
||||
{context}
|
||||
|
||||
Output a json following the format:
|
||||
```json
|
||||
{{
|
||||
"thoughts": str = "Thoughts on current situation, reflect on how you should proceed to fulfill the user requirement",
|
||||
"state": bool = "Decide whether you need to take more actions to complete the user requirement. Return true if you think so. Return false if you think the requirement has been completely fulfilled."
|
||||
}}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class Interpreter(Role):
|
||||
name: str = "Ivy"
|
||||
profile: str = "Interpreter"
|
||||
auto_run: bool = True
|
||||
use_tools: bool = False
|
||||
use_plan: bool = True
|
||||
use_reflection: bool = False
|
||||
execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True)
|
||||
tools: list[str] = []
|
||||
tools: Union[str, list[str]] = []
|
||||
tool_recommender: ToolRecommender = None
|
||||
react_mode: Literal["plan_and_act", "react"] = "plan_and_act"
|
||||
max_react_loop: int = 10 # used for react mode
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auto_run=True,
|
||||
use_tools=False,
|
||||
tools=[],
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(auto_run=auto_run, use_tools=use_tools, tools=tools, **kwargs)
|
||||
self._set_react_mode(react_mode="plan_and_act", auto_run=auto_run, use_tools=use_tools)
|
||||
if use_tools and tools:
|
||||
from metagpt.tools.tool_registry import (
|
||||
validate_tool_names, # import upon use
|
||||
)
|
||||
|
||||
self.tools = validate_tool_names(tools)
|
||||
logger.info(f"will only use {self.tools} as tools")
|
||||
@model_validator(mode="after")
|
||||
def set_plan_and_tool(self) -> "Interpreter":
|
||||
self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop, auto_run=self.auto_run)
|
||||
self.use_plan = (
|
||||
self.react_mode == "plan_and_act"
|
||||
) # create a flag for convenience, overwrite any passed-in value
|
||||
if self.tools:
|
||||
self.tool_recommender = BM25ToolRecommender(tools=self.tools)
|
||||
self.set_actions([WriteCodeWithTools])
|
||||
return self
|
||||
|
||||
@property
|
||||
def working_memory(self):
|
||||
return self.rc.working_memory
|
||||
|
||||
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
|
||||
context = self.working_memory.get()
|
||||
|
||||
if not context:
|
||||
# just started the run, we need action certainly
|
||||
self.working_memory.add(self.get_memories()[0]) # add user requirement to working memory
|
||||
self._set_state(0)
|
||||
return True
|
||||
|
||||
prompt = REACT_THINK_PROMPT.format(user_requirement=user_requirement, context=context)
|
||||
rsp = await self.llm.aask(prompt)
|
||||
rsp_dict = json.loads(CodeParser.parse_code(block=None, text=rsp))
|
||||
self.working_memory.add(Message(content=rsp_dict["thoughts"], role="assistant"))
|
||||
need_action = rsp_dict["state"]
|
||||
self._set_state(0) if need_action else self._set_state(-1)
|
||||
|
||||
return need_action
|
||||
|
||||
async def _act(self) -> Message:
|
||||
"""Useful in 'react' mode. Return a Message conforming to Role._act interface."""
|
||||
code, _, _ = await self._write_and_exec_code()
|
||||
return Message(content=code, role="assistant", cause_by=WriteCodeWithTools)
|
||||
|
||||
async def _act_on_task(self, current_task: Task) -> TaskResult:
|
||||
"""Useful in 'plan_and_act' mode. Wrap the output in a TaskResult for review and confirmation."""
|
||||
code, result, is_success = await self._write_and_exec_code()
|
||||
task_result = TaskResult(code=code, result=result, is_success=is_success)
|
||||
return task_result
|
||||
|
|
@ -51,11 +94,25 @@ class Interpreter(Role):
|
|||
counter = 0
|
||||
success = False
|
||||
|
||||
# plan info
|
||||
plan_status = self.planner.get_plan_status() if self.use_plan else ""
|
||||
|
||||
# tool info
|
||||
if self.tools:
|
||||
context = (
|
||||
self.working_memory.get()[-1].content if self.working_memory.get() else ""
|
||||
) # thoughts from _think stage in 'react' mode
|
||||
plan = self.planner.plan if self.use_plan else None
|
||||
tool_info = await self.tool_recommender.get_recommended_tool_info(context=context, plan=plan)
|
||||
else:
|
||||
tool_info = ""
|
||||
|
||||
# data info
|
||||
await self._check_data()
|
||||
|
||||
while not success and counter < max_retry:
|
||||
### write code ###
|
||||
code, cause_by = await self._write_code(counter)
|
||||
code, cause_by = await self._write_code(counter, plan_status, tool_info)
|
||||
|
||||
self.working_memory.add(Message(content=code, role="assistant", cause_by=cause_by))
|
||||
|
||||
|
|
@ -76,22 +133,33 @@ class Interpreter(Role):
|
|||
|
||||
return code, result, success
|
||||
|
||||
async def _write_code(self, counter):
|
||||
todo = WriteCodeWithTools(use_tools=self.use_tools, selected_tools=self.tools)
|
||||
async def _write_code(
|
||||
self,
|
||||
counter,
|
||||
plan_status="",
|
||||
tool_info="",
|
||||
):
|
||||
todo = WriteCodeWithTools()
|
||||
logger.info(f"ready to {todo.name}")
|
||||
use_reflection = counter > 0 and self.use_reflection
|
||||
|
||||
user_requirement = self.get_memories()[0].content
|
||||
|
||||
code = await todo.run(
|
||||
plan=self.planner.plan, working_memory=self.working_memory.get(), use_reflection=use_reflection
|
||||
user_requirement=user_requirement,
|
||||
plan_status=plan_status,
|
||||
tool_info=tool_info,
|
||||
working_memory=self.working_memory.get(),
|
||||
use_reflection=use_reflection,
|
||||
)
|
||||
|
||||
return code, todo
|
||||
|
||||
async def _check_data(self):
|
||||
current_task = self.planner.plan.current_task
|
||||
if current_task.task_type not in [
|
||||
ToolType.DATA_PREPROCESS.type_name,
|
||||
ToolType.FEATURE_ENGINEERING.type_name,
|
||||
ToolType.MODEL_TRAIN.type_name,
|
||||
if not self.use_plan or self.planner.plan.current_task.task_type not in [
|
||||
TaskType.DATA_PREPROCESS.type_name,
|
||||
TaskType.FEATURE_ENGINEERING.type_name,
|
||||
TaskType.MODEL_TRAIN.type_name,
|
||||
]:
|
||||
return
|
||||
logger.info("Check updated data")
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
self.actions.append(i)
|
||||
self.states.append(f"{len(self.actions)}. {action}")
|
||||
|
||||
def _set_react_mode(self, react_mode: str, max_react_loop: int = 1, auto_run: bool = True, use_tools: bool = False):
|
||||
def _set_react_mode(self, react_mode: str, max_react_loop: int = 1, auto_run: bool = True):
|
||||
"""Set strategy of the Role reacting to observed Message. Variation lies in how
|
||||
this Role elects action to perform during the _think stage, especially if it is capable of multiple Actions.
|
||||
|
||||
|
|
@ -304,9 +304,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
|
|||
if react_mode == RoleReactMode.REACT:
|
||||
self.rc.max_react_loop = max_react_loop
|
||||
elif react_mode == RoleReactMode.PLAN_AND_ACT:
|
||||
self.planner = Planner(
|
||||
goal=self.goal, working_memory=self.rc.working_memory, auto_run=auto_run, use_tools=use_tools
|
||||
)
|
||||
self.planner = Planner(goal=self.goal, working_memory=self.rc.working_memory, auto_run=auto_run)
|
||||
|
||||
def _watch(self, actions: Iterable[Type[Action]] | Iterable[Action]):
|
||||
"""Watch Actions of interest. Role will select Messages caused by these Actions from its personal message
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue