init
This commit is contained in:
24
functionality/meta_planner_agent/_planning_tools/__init__.py
Normal file
24
functionality/meta_planner_agent/_planning_tools/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""planning tools"""
|
||||
from ._planning_notebook import (
|
||||
PlannerNoteBook,
|
||||
RoadMap,
|
||||
SubTaskStatus,
|
||||
Update,
|
||||
WorkerInfo,
|
||||
WorkerResponse,
|
||||
)
|
||||
from ._roadmap_manager import RoadmapManager
|
||||
from ._worker_manager import WorkerManager, share_tools
|
||||
|
||||
__all__ = [
|
||||
"PlannerNoteBook",
|
||||
"RoadmapManager",
|
||||
"WorkerManager",
|
||||
"WorkerResponse",
|
||||
"RoadMap",
|
||||
"SubTaskStatus",
|
||||
"WorkerInfo",
|
||||
"Update",
|
||||
"share_tools",
|
||||
]
|
||||
@@ -0,0 +1,325 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=E0213
|
||||
"""
|
||||
Data structures about the roadmap for complicated tasks
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
def get_current_time_message() -> str:
|
||||
"""
|
||||
Returns the current time as a formatted string.
|
||||
|
||||
Returns:
|
||||
str: The current time formatted as 'YYYY-MM-DD HH:MM:SS'.
|
||||
"""
|
||||
return f"Current time is {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
|
||||
WORKER_PROGRESS_SUMMARY = (
|
||||
"## Instruction\n"
|
||||
"Review the execution trace above and generate a comprehensive summary "
|
||||
"report in Markdown format that addresses the original task/query. "
|
||||
"Your report must include:\n\n"
|
||||
"1. **Task Overview**\n"
|
||||
" - Include the original query/task verbatim;\n"
|
||||
" - Briefly state the main objective.\n"
|
||||
"2. **Comprehensive Analysis**"
|
||||
" - Provide a detailed, structured answer to the original query/task;\n"
|
||||
" - Include all relevant information requested in the original task;\n"
|
||||
" - Support your findings with specific references from your execution "
|
||||
"trace;\n"
|
||||
" - Organize content into logical sections with appropriate headings;\n"
|
||||
" - Include data visualizations, tables, or formatted lists when "
|
||||
"applicable.\n\n"
|
||||
"3. **Completion Checklist**\n"
|
||||
" - Reproduce the original 'Expected Output' checklist of required "
|
||||
"tasks/information; **NEVER** makeup additional expected output items "
|
||||
"in the checklist\n"
|
||||
" - Mark each item as [x] Completed or [ ] Incomplete;\n"
|
||||
" - For each completed item, reference where in your report this "
|
||||
"information appears;\n"
|
||||
" - For incomplete items, explain briefly why they remain unaddressed;\n"
|
||||
"4. **Conclusion**\n"
|
||||
" - If the task is fully complete, provide a brief conclusion "
|
||||
"summarizing key findings;\n"
|
||||
" - If the task remains incomplete, outline a specific meta_planner_agent to "
|
||||
"address remaining items, including:\n"
|
||||
" - Which tools would be used;\n"
|
||||
" - What information is still needed;\n"
|
||||
" - Sequence of planned actions.\n\n"
|
||||
"Format your report professionally with consistent heading levels, "
|
||||
"proper spacing, and appropriate emphasis for key information."
|
||||
)
|
||||
|
||||
|
||||
WORKER_NEXT_STEP_INSTRUCTION = """
|
||||
If the subtask remains incomplete, outline a specific meta_planner_agent to address remaining
|
||||
items, including:
|
||||
- Which tools would be used
|
||||
- What information is still needed
|
||||
- Sequence of planned actions
|
||||
Leave it as an empty string is the subtask has been done successfully.
|
||||
"""
|
||||
|
||||
WORKER_FILE_COLLECTION_INSTRUCTION = (
|
||||
"Collect all files generated in the execution process, "
|
||||
"such as the files generated by `write_file` and `edit_file`."
|
||||
"This field MUST be in dictionary, where"
|
||||
"the keys are the paths of generated files "
|
||||
"(e.g. '/FULL/PATH/OF/FILE_1.md') and the values are short "
|
||||
"descriptions about the generated files."
|
||||
)
|
||||
|
||||
|
||||
class WorkerResponse(BaseModel):
|
||||
"""
|
||||
Represents the response structure from a worker agent after task execution.
|
||||
|
||||
This class defines the expected format for worker responses, including
|
||||
progress summaries, next steps, tool usage information, and task
|
||||
completion status.
|
||||
|
||||
Attributes:
|
||||
subtask_progress_summary (str):
|
||||
Comprehensive summary report of task execution.
|
||||
next_step (str):
|
||||
Description of planned next actions if task is incomplete.
|
||||
generated_files (dict):
|
||||
Dictionary mapping file paths to descriptions of generated files.
|
||||
task_done (bool):
|
||||
Flag indicating whether the task has been completed.
|
||||
"""
|
||||
|
||||
subtask_progress_summary: str = Field(
|
||||
...,
|
||||
description=WORKER_PROGRESS_SUMMARY,
|
||||
)
|
||||
next_step: str = Field(
|
||||
...,
|
||||
description=WORKER_NEXT_STEP_INSTRUCTION,
|
||||
)
|
||||
generated_files: dict = Field(
|
||||
...,
|
||||
description=WORKER_FILE_COLLECTION_INSTRUCTION,
|
||||
)
|
||||
task_done: bool = Field(
|
||||
...,
|
||||
description="Whether task is done or it require addition effort",
|
||||
)
|
||||
|
||||
|
||||
class Update(BaseModel):
|
||||
"""Represents an update record from a worker during task execution.
|
||||
|
||||
This class tracks progress updates from workers as they work on subtasks,
|
||||
including status changes, progress summaries, and execution details.
|
||||
|
||||
Attributes:
|
||||
reason_for_status (str): Explanation for the current status.
|
||||
task_done (bool): Whether the task has been completed.
|
||||
subtask_progress_summary (str): Summary of progress made.
|
||||
next_step (str): Description of planned next actions.
|
||||
worker (str): Identifier of the worker providing the update.
|
||||
attempt_idx (int): Index of the current attempt.
|
||||
"""
|
||||
|
||||
reason_for_status: str
|
||||
task_done: bool
|
||||
subtask_progress_summary: str
|
||||
next_step: str
|
||||
worker: str
|
||||
attempt_idx: int
|
||||
|
||||
@field_validator(
|
||||
"subtask_progress_summary",
|
||||
"reason_for_status",
|
||||
"next_step",
|
||||
"worker",
|
||||
mode="before",
|
||||
)
|
||||
def _stringify(cls, v: Any) -> str:
|
||||
"""ensure the attributes are string"""
|
||||
if v is None:
|
||||
return ""
|
||||
return str(v)
|
||||
|
||||
|
||||
class WorkerInfo(BaseModel):
|
||||
"""Contains information about a worker agent assigned to a subtask.
|
||||
|
||||
This class stores metadata about worker agents, including their
|
||||
capabilities, creation type, and configuration details.
|
||||
|
||||
Attributes:
|
||||
worker_name (str):
|
||||
Name identifier of the worker.
|
||||
status (str):
|
||||
Current status of the worker.
|
||||
create_type (Literal["built-in", "dynamic-built"]):
|
||||
How the worker was created.
|
||||
description (str):
|
||||
Description of the worker's purpose and capabilities.
|
||||
tool_lists (List[str]):
|
||||
List of tools available to this worker.
|
||||
sys_prompt (str):
|
||||
System prompt used to configure the worker.
|
||||
"""
|
||||
|
||||
worker_name: str = ""
|
||||
status: str = ""
|
||||
create_type: Literal["built-in", "dynamic-built"] = "dynamic-built"
|
||||
description: str = ""
|
||||
# for dynamically create worker agents
|
||||
tool_lists: List[str] = Field(default_factory=list)
|
||||
sys_prompt: str = ""
|
||||
|
||||
@field_validator(
|
||||
"worker_name",
|
||||
"status",
|
||||
mode="before",
|
||||
)
|
||||
def _stringify(cls, v: Any) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
return str(v)
|
||||
|
||||
|
||||
class SubTaskSpecification(BaseModel):
|
||||
"""
|
||||
Details of a subtask within a larger task decomposition.
|
||||
"""
|
||||
|
||||
subtask_description: str = Field(description="Description of the subtask.")
|
||||
input_intro: str = Field(
|
||||
...,
|
||||
description="Introduction or context for the subtask input.",
|
||||
)
|
||||
exact_input: str = Field(
|
||||
...,
|
||||
description="The exact input data or parameters for the subtask.",
|
||||
)
|
||||
expected_output: str = Field(
|
||||
...,
|
||||
description="The expected output data or parameters for the subtask.",
|
||||
)
|
||||
desired_auxiliary_tools: str = Field(
|
||||
...,
|
||||
description="Tools that would be helpful for this subtask.",
|
||||
)
|
||||
|
||||
@field_validator(
|
||||
"subtask_description",
|
||||
"input_intro",
|
||||
"exact_input",
|
||||
"expected_output",
|
||||
"desired_auxiliary_tools",
|
||||
mode="before",
|
||||
)
|
||||
def _stringify(cls, v: Any) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
return str(v)
|
||||
|
||||
|
||||
class SubTaskStatus(BaseModel):
|
||||
"""
|
||||
Represents the status and details of a subtask within a
|
||||
larger task decomposition.
|
||||
|
||||
This class tracks individual subtasks, their execution status,
|
||||
assigned workers, and progress updates throughout the execution lifecycle.
|
||||
|
||||
Attributes:
|
||||
status (Literal["Planned", "In-process", "Done"]):
|
||||
Current execution status.
|
||||
updates (List[Update]):
|
||||
List of progress updates from workers.
|
||||
attempt (int):
|
||||
Number of execution attempts for this subtask.
|
||||
workers (List[WorkerInfo]):
|
||||
List of workers assigned to this subtask.
|
||||
"""
|
||||
|
||||
subtask_specification: SubTaskSpecification = Field(
|
||||
default_factory=SubTaskSpecification,
|
||||
)
|
||||
status: Literal["Planned", "In-process", "Done"] = "Planned"
|
||||
updates: List[Update] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"List of updates from workers. "
|
||||
"MUST be empty list when initialized."
|
||||
),
|
||||
)
|
||||
attempt: int = 0
|
||||
workers: List[WorkerInfo] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"List of workers that have been assigned to this subtask."
|
||||
"MUST be EMPTY when initialize the subtask."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RoadMap(BaseModel):
|
||||
"""Represents a roadmap for task decomposition and execution tracking.
|
||||
|
||||
This class manages the overall task breakdown, containing the original task
|
||||
description and a list of decomposed subtasks with their execution status.
|
||||
|
||||
Attributes:
|
||||
original_task (str):
|
||||
The original task description before decomposition.
|
||||
decomposed_tasks (List[SubTaskStatus]):
|
||||
List of subtasks created from the original task.
|
||||
"""
|
||||
|
||||
original_task: str = ""
|
||||
decomposed_tasks: List[SubTaskStatus] = Field(default_factory=list)
|
||||
|
||||
def next_unfinished_subtask(
|
||||
self,
|
||||
) -> Tuple[Optional[int], Optional[SubTaskStatus]]:
|
||||
"""Find the next subtask that is not yet completed.
|
||||
|
||||
Iterates through the decomposed tasks to find the first subtask
|
||||
with status "Planned" or "In-process".
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[int], Optional[SubTaskStatus]]: A tuple containing:
|
||||
- The index of the next unfinished subtask
|
||||
(None if all tasks are done)
|
||||
- The SubTaskStatus object of the next unfinished subtask
|
||||
(None if all tasks are done)
|
||||
"""
|
||||
for i, subtask in enumerate(self.decomposed_tasks):
|
||||
if subtask.status in ["Planned", "In-process"]:
|
||||
return i, subtask
|
||||
return None, None
|
||||
|
||||
|
||||
class PlannerNoteBook(BaseModel):
|
||||
"""
|
||||
Represents a planner notebook.
|
||||
|
||||
Attributes:
|
||||
time (str): The current time message.
|
||||
user_input (List[str]): List of user inputs.
|
||||
detail_analysis_for_plan (str): Detailed analysis for the meta_planner_agent.
|
||||
roadmap (RoadMap): The roadmap associated with the planner.
|
||||
files (Dict[str, str]): Dictionary of files related to the planner.
|
||||
full_tool_list (dict[str, dict]): Full schema of tools.
|
||||
"""
|
||||
|
||||
time: str = Field(default_factory=get_current_time_message)
|
||||
user_input: List[str] = Field(default_factory=list)
|
||||
detail_analysis_for_plan: str = (
|
||||
"Unknown. Please call `build_roadmap_and_decompose_task` to analyze."
|
||||
)
|
||||
roadmap: RoadMap = Field(default_factory=RoadMap)
|
||||
files: Dict[str, str] = Field(default_factory=dict)
|
||||
full_tool_list: list[dict] = Field(default_factory=list)
|
||||
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Planning handler module for meta planner
|
||||
"""
|
||||
from typing import Literal, Optional
|
||||
|
||||
from agentscope.message import TextBlock
|
||||
from agentscope.module import StateModule
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
from ._planning_notebook import (
|
||||
PlannerNoteBook,
|
||||
SubTaskSpecification,
|
||||
SubTaskStatus,
|
||||
Update,
|
||||
)
|
||||
|
||||
|
||||
class RoadmapManager(StateModule):
|
||||
"""Handles planning operations for meta planner agent.
|
||||
|
||||
This class provides functionality for task decomposition, roadmap creation,
|
||||
and roadmap revision.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
planner_notebook: PlannerNoteBook,
|
||||
):
|
||||
"""Initialize the PlanningHandler.
|
||||
|
||||
Args:
|
||||
planner_notebook (PlannerNoteBook):
|
||||
Data structure containing planning state.
|
||||
"""
|
||||
super().__init__()
|
||||
self.planner_notebook = planner_notebook
|
||||
|
||||
async def decompose_task_and_build_roadmap(
|
||||
self,
|
||||
user_latest_input: str,
|
||||
given_task_conclusion: str,
|
||||
detail_analysis_for_plan: str,
|
||||
decomposed_subtasks: list[SubTaskSpecification],
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Analysis the user subtask, generate a comprehensive reasoning for how
|
||||
to decompose the task into multiple subtasks.
|
||||
|
||||
Args:
|
||||
user_latest_input (str):
|
||||
The latest user input. If there are multiple rounds
|
||||
of user input, faithfully record the latest user input.
|
||||
given_task_conclusion (str):
|
||||
The user's task to decompose. If there are multiple rounds
|
||||
of user input, analysis and give the key idea of the task that
|
||||
the user really you to solve.
|
||||
detail_analysis_for_plan (str):
|
||||
A detailed analysis of how a task should be decomposed.
|
||||
decomposed_subtasks (list[SubTaskSpecification]):
|
||||
List of subtasks that was decomposed.
|
||||
"""
|
||||
self.planner_notebook.detail_analysis_for_plan = (
|
||||
detail_analysis_for_plan
|
||||
)
|
||||
self.planner_notebook.roadmap.original_task = given_task_conclusion
|
||||
for subtask in decomposed_subtasks:
|
||||
if isinstance(subtask, dict):
|
||||
subtask_status = SubTaskStatus(
|
||||
subtask_specification=SubTaskSpecification(
|
||||
**subtask,
|
||||
),
|
||||
)
|
||||
elif isinstance(subtask, SubTaskSpecification):
|
||||
subtask_status = SubTaskStatus(
|
||||
subtask_specification=subtask,
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Unexpected type of `decomposed_subtasks`,"
|
||||
"which is expected to strictly follow List of "
|
||||
"SubTaskSpecification.",
|
||||
)
|
||||
self.planner_notebook.roadmap.decomposed_tasks.append(
|
||||
subtask_status,
|
||||
)
|
||||
self.planner_notebook.user_input.append(user_latest_input)
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text="Successfully decomposed the task into subtasks",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def get_next_unfinished_subtask_from_roadmap(self) -> ToolResponse:
|
||||
"""
|
||||
Obtains the next unfinished subtask from the roadmap.
|
||||
"""
|
||||
idx, subtask = self.planner_notebook.roadmap.next_unfinished_subtask()
|
||||
if idx is None or subtask is None:
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
"No unfinished subtask was found. "
|
||||
"Either all subtasks have been done, or the task"
|
||||
" has not been decomposed."
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
return ToolResponse(
|
||||
metadata={"success": True, "subtask": subtask},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=f"Next unfinished subtask idx: {idx}",
|
||||
),
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=subtask.model_dump_json(indent=2),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def revise_roadmap(
|
||||
self,
|
||||
action: Literal["add_subtask", "revise_subtask", "remove_subtask"],
|
||||
subtask_idx: int,
|
||||
subtask_specification: Optional[SubTaskSpecification] = None,
|
||||
update_to_subtask: Optional[Update] = None,
|
||||
new_status: Literal["Planned", "In-process", "Done"] = "In-process",
|
||||
) -> ToolResponse:
|
||||
"""After subtasks are done by worker agents, use this function to
|
||||
revise the progress and details of the current roadmap.
|
||||
|
||||
Updates the status of subtasks and potentially revises input/output
|
||||
descriptions and required tools for tasks based on current progress
|
||||
and available information.
|
||||
|
||||
Args:
|
||||
action (
|
||||
`Literal["add_subtask", "revise_subtask", "remove_subtask"]`
|
||||
):
|
||||
Action to perform on the roadmap.
|
||||
subtask_idx (`int`):
|
||||
Index of the subtask to revise its status. This index starts
|
||||
with 0.
|
||||
subtask_specification (`SubTaskSpecification`):
|
||||
Revised subtask specification. When you use `add_subtask` or
|
||||
`revise_subtask` action, you MUST provide this field with
|
||||
revised `exact_input` and `expected_output` according to
|
||||
the execution context.
|
||||
update_to_subtask (`Update`):
|
||||
Generate an update record for this subtask based on the
|
||||
worker execution report. When you use `revise_subtask` action,
|
||||
you MUST provide this field.
|
||||
new_status (`Literal["Planned", "In-process", "Done"]`):
|
||||
The new status of the subtask.
|
||||
|
||||
Returns:
|
||||
ToolResponse:
|
||||
Response indicating success/failure of the revision
|
||||
and any updates made. May request additional human
|
||||
input if needed.
|
||||
"""
|
||||
num_subtasks = len(self.planner_notebook.roadmap.decomposed_tasks)
|
||||
if isinstance(subtask_specification, dict):
|
||||
subtask_specification = SubTaskSpecification(
|
||||
**subtask_specification,
|
||||
)
|
||||
elif subtask_specification is None and action in [
|
||||
"add_subtask",
|
||||
"revise_subtask",
|
||||
]:
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
f"Choosing {action} must have valid "
|
||||
f"`subtask_specification` field."
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
if isinstance(update_to_subtask, dict):
|
||||
update_to_subtask = Update(
|
||||
**update_to_subtask,
|
||||
)
|
||||
elif update_to_subtask is None and action == "revise_subtask":
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
f"Choosing {action} must have valid "
|
||||
f"`update_to_subtask` field."
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
if subtask_idx >= num_subtasks and action == "add_subtask":
|
||||
self.planner_notebook.roadmap.decomposed_tasks.append(
|
||||
SubTaskStatus(
|
||||
subtask_specification=subtask_specification,
|
||||
status="Planned",
|
||||
updates=update_to_subtask,
|
||||
),
|
||||
)
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=f"add new subtask with index {subtask_idx}.",
|
||||
),
|
||||
],
|
||||
)
|
||||
elif subtask_idx >= num_subtasks:
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
f"Fail to update subtask {subtask_idx} status."
|
||||
f"There are {num_subtasks} subtasks, "
|
||||
f"idx {subtask_idx} is not supported with "
|
||||
f"action {action}."
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
elif action == "revise_subtask" and update_to_subtask:
|
||||
subtask = self.planner_notebook.roadmap.decomposed_tasks[
|
||||
subtask_idx
|
||||
]
|
||||
subtask.status = new_status
|
||||
subtask.updates.append(update_to_subtask)
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=f"Update subtask {subtask_idx} status.",
|
||||
),
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=self.planner_notebook.roadmap.decomposed_tasks[
|
||||
subtask_idx
|
||||
].model_dump_json(indent=2),
|
||||
),
|
||||
],
|
||||
)
|
||||
elif action == "remove_subtask":
|
||||
self.planner_notebook.roadmap.decomposed_tasks.pop(subtask_idx)
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=f"Remove subtask {subtask_idx} from roadmap.",
|
||||
),
|
||||
],
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Not support action {action} on subtask {subtask_idx}",
|
||||
)
|
||||
@@ -0,0 +1,525 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Coordination handler module for meta planner
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from agentscope import logger
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.formatter import DashScopeChatFormatter, FormatterBase
|
||||
from agentscope.memory import InMemoryMemory, MemoryBase
|
||||
from agentscope.message import Msg, TextBlock, ToolResultBlock, ToolUseBlock
|
||||
from agentscope.model import ChatModelBase, DashScopeChatModel
|
||||
from agentscope.module import StateModule
|
||||
from agentscope.tool import Toolkit, ToolResponse
|
||||
|
||||
from ._planning_notebook import PlannerNoteBook, WorkerInfo, WorkerResponse
|
||||
|
||||
|
||||
def rebuild_reactworker(
|
||||
worker_info: WorkerInfo,
|
||||
old_toolkit: Toolkit,
|
||||
new_toolkit: Toolkit,
|
||||
memory: Optional[MemoryBase] = None,
|
||||
model: Optional[ChatModelBase] = None,
|
||||
formatter: Optional[FormatterBase] = None,
|
||||
exclude_tools: Optional[list[str]] = None,
|
||||
) -> ReActAgent:
|
||||
"""
|
||||
Rebuild a ReActAgent worker with specified configuration and tools.
|
||||
|
||||
Creates a new ReActAgent using worker information and toolkit
|
||||
configuration. Tools are shared from the old toolkit to the new one,
|
||||
excluding any specified tools.
|
||||
|
||||
Args:
|
||||
worker_info (WorkerInfo): Information about the worker including name,
|
||||
system prompt, and tool lists.
|
||||
old_toolkit (Toolkit): Source toolkit containing available tools.
|
||||
new_toolkit (Toolkit): Destination toolkit to receive shared tools.
|
||||
memory (Optional[MemoryBase], optional): Memory instance for the agent.
|
||||
Defaults to InMemoryMemory() if None.
|
||||
model (Optional[ChatModelBase], optional): Chat model instance.
|
||||
Defaults to DashscopeChatModel with deepseek-r1 if None.
|
||||
formatter (Optional[FormatterBase], optional): Message formatter.
|
||||
Defaults to DashScopeChatFormatter() if None.
|
||||
exclude_tools (Optional[list[str]], optional): List of tool names to
|
||||
exclude from sharing. Defaults to empty list if None.
|
||||
|
||||
Returns:
|
||||
ReActAgent: A configured ReActAgent instance ready for use.
|
||||
|
||||
Note:
|
||||
- The default model uses the DASHSCOPE_API_KEY environment variable
|
||||
- Tools are shared based on worker_info.tool_lists minus excluded tools
|
||||
- The agent is configured with thinking enabled and streaming support
|
||||
"""
|
||||
if exclude_tools is None:
|
||||
exclude_tools = []
|
||||
tool_list = [
|
||||
tool_name
|
||||
for tool_name in worker_info.tool_lists
|
||||
if tool_name not in exclude_tools
|
||||
]
|
||||
share_tools(old_toolkit, new_toolkit, tool_list)
|
||||
model = (
|
||||
model
|
||||
if model
|
||||
else DashScopeChatModel(
|
||||
api_key=os.environ.get("DASHSCOPE_API_KEY"),
|
||||
model_name="deepseek-r1",
|
||||
enable_thinking=True,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
return ReActAgent(
|
||||
name=worker_info.worker_name,
|
||||
sys_prompt=worker_info.sys_prompt,
|
||||
model=model,
|
||||
formatter=formatter if formatter else DashScopeChatFormatter(),
|
||||
toolkit=new_toolkit,
|
||||
memory=InMemoryMemory() if memory is None else memory,
|
||||
max_iters=20, # hardcoded the max iteration for now
|
||||
)
|
||||
|
||||
|
||||
async def check_file_existence(file_path: str, toolkit: Toolkit) -> bool:
|
||||
"""
|
||||
Check if a file exists using the read_file tool from the provided toolkit.
|
||||
|
||||
This function attempts to verify file existence by calling the read_file
|
||||
tool and checking the response for error indicators. It requires the
|
||||
toolkit to have a 'read_file' tool available.
|
||||
|
||||
Args:
|
||||
file_path (str): The path to the file to check for existence.
|
||||
toolkit (Toolkit): The toolkit containing the read_file tool.
|
||||
|
||||
Returns:
|
||||
bool: True if the file exists and is readable, False otherwise.
|
||||
|
||||
Note:
|
||||
- Returns False if the 'read_file' tool is not available in the toolkit
|
||||
- Returns False if any exception occurs during the file read attempt
|
||||
- Uses error message detection ("no such file or directory") to
|
||||
determine existence
|
||||
"""
|
||||
if "read_file" in toolkit.tools:
|
||||
params = {
|
||||
"path": file_path,
|
||||
}
|
||||
read_file_block = ToolUseBlock(
|
||||
type="tool_use",
|
||||
id="manual_check_file_existence",
|
||||
name="read_file",
|
||||
input=params,
|
||||
)
|
||||
try:
|
||||
tool_res = await toolkit.call_tool_function(read_file_block)
|
||||
tool_res_msg = Msg(
|
||||
"system",
|
||||
[
|
||||
ToolResultBlock(
|
||||
type="tool_result",
|
||||
id="",
|
||||
name="read_file",
|
||||
output=[],
|
||||
),
|
||||
],
|
||||
"system",
|
||||
)
|
||||
async for chunk in tool_res:
|
||||
# Turn into a tool result block
|
||||
tool_res_msg.content[0]["output"] = chunk.content # type: ignore[index]
|
||||
if "no such file or directory" in str(tool_res_msg.content):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except Exception as _: # noqa: F841
|
||||
return False
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def share_tools(
|
||||
old_toolkit: Toolkit,
|
||||
new_toolkit: Toolkit,
|
||||
tool_list: list[str],
|
||||
) -> None:
|
||||
"""
|
||||
Share specified tools from an old toolkit to a new toolkit.
|
||||
|
||||
This function copies tools from one toolkit to another based on the
|
||||
provided tool list. If a tool doesn't exist in the old toolkit,
|
||||
a warning is logged.
|
||||
|
||||
Args:
|
||||
old_toolkit (Toolkit):
|
||||
The source toolkit containing tools to be shared.
|
||||
new_toolkit (Toolkit):
|
||||
The destination toolkit to receive the tools.
|
||||
tool_list (list[str]):
|
||||
List of tool names to be copied from old to new toolkit.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Note:
|
||||
This function modifies the new_toolkit in place.
|
||||
If a tool in tool_list is not found in old_toolkit,
|
||||
a warning is logged but execution continues.
|
||||
"""
|
||||
for tool in tool_list:
|
||||
if tool in old_toolkit.tools and tool not in new_toolkit.tools:
|
||||
new_toolkit.tools[tool] = old_toolkit.tools[tool]
|
||||
else:
|
||||
logger.warning(
|
||||
"No tool %s in the provided worker_tool_toolkit",
|
||||
tool,
|
||||
)
|
||||
|
||||
|
||||
class WorkerManager(StateModule):
|
||||
"""
|
||||
Handles coordination between meta planner and worker agents.
|
||||
|
||||
This class manages the creation, selection, and execution of worker agents
|
||||
to accomplish subtasks in a roadmap. It provides functionality for dynamic
|
||||
worker creation, worker selection based on task requirements, and
|
||||
processing worker responses to update the overall task progress.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
worker_model: ChatModelBase,
|
||||
worker_formatter: FormatterBase,
|
||||
planner_notebook: PlannerNoteBook,
|
||||
worker_full_toolkit: Toolkit,
|
||||
agent_working_dir: str,
|
||||
worker_pool: Optional[dict[str, tuple[WorkerInfo, ReActAgent]]] = None,
|
||||
):
|
||||
"""Initialize the CoordinationHandler.
|
||||
Args:
|
||||
worker_model (ChatModelBase):
|
||||
Main language model for coordination decisions
|
||||
worker_formatter (FormatterBase):
|
||||
Message formatter for model communication
|
||||
planner_notebook (PlannerNoteBook):
|
||||
Notebook containing roadmap and file information
|
||||
worker_full_toolkit (Toolkit):
|
||||
Complete toolkit available to workers
|
||||
agent_working_dir (str):
|
||||
Working directory for the agent operations
|
||||
worker_pool: dict[str, tuple[WorkerInfo, ReActAgent]]:
|
||||
workers that has already been created
|
||||
"""
|
||||
super().__init__()
|
||||
self.planner_notebook = planner_notebook
|
||||
self.worker_model = worker_model
|
||||
self.worker_formatter = worker_formatter
|
||||
self.worker_pool: dict[str, tuple[WorkerInfo, ReActAgent]] = (
|
||||
worker_pool if worker_pool else {}
|
||||
)
|
||||
self.agent_working_dir = agent_working_dir
|
||||
self.worker_full_toolkit = worker_full_toolkit
|
||||
|
||||
def reconstruct_workerpool(worker_pool_dict: dict) -> dict:
|
||||
rebuild_worker_pool = {}
|
||||
for k, v in worker_pool_dict.items():
|
||||
worker_info = WorkerInfo(**v)
|
||||
rebuild_worker_pool[k] = (
|
||||
worker_info,
|
||||
rebuild_reactworker(
|
||||
worker_info=worker_info,
|
||||
old_toolkit=self.worker_full_toolkit,
|
||||
new_toolkit=Toolkit(),
|
||||
model=self.worker_model,
|
||||
formatter=self.worker_formatter,
|
||||
exclude_tools=["generate_response"],
|
||||
),
|
||||
)
|
||||
return rebuild_worker_pool
|
||||
|
||||
self.register_state(
|
||||
"worker_pool",
|
||||
lambda x: {k: v[0].model_dump() for k, v in x.items()},
|
||||
custom_from_json=reconstruct_workerpool,
|
||||
)
|
||||
|
||||
def _register_worker(
|
||||
self,
|
||||
agent: ReActAgent,
|
||||
description: Optional[str] = None,
|
||||
worker_type: Literal["built-in", "dynamic-built"] = "dynamic",
|
||||
) -> None:
|
||||
"""
|
||||
Register a worker agent in the worker pool.
|
||||
|
||||
Adds a worker agent to the available pool with appropriate metadata.
|
||||
Handles name conflicts by appending version numbers when necessary.
|
||||
|
||||
Args:
|
||||
agent (ReActAgent):
|
||||
The worker agent to register
|
||||
description (Optional[str]):
|
||||
Description of the worker's capabilities
|
||||
worker_type (Literal["built-in", "dynamic-built"]):
|
||||
Type of worker agent
|
||||
"""
|
||||
worker_info = WorkerInfo(
|
||||
worker_name=agent.name,
|
||||
description=description,
|
||||
worker_type=worker_type,
|
||||
status="ready-to-work",
|
||||
)
|
||||
if worker_type == "dynamic-built":
|
||||
worker_info.sys_prompt = agent.sys_prompt
|
||||
worker_info.tool_lists = list(agent.toolkit.tools.keys())
|
||||
|
||||
if agent.name in self.worker_pool:
|
||||
name = agent.name
|
||||
version = 1
|
||||
while name in self.worker_pool:
|
||||
name = agent.name + f"_v{version}"
|
||||
version += 1
|
||||
agent.name, worker_info.worker_name = name, name
|
||||
self.worker_pool[name] = (worker_info, agent)
|
||||
else:
|
||||
self.worker_pool[agent.name] = (worker_info, agent)
|
||||
|
||||
@staticmethod
|
||||
def _no_more_subtask_return() -> ToolResponse:
|
||||
"""
|
||||
Return response when no more unfinished subtasks exist.
|
||||
|
||||
Returns:
|
||||
ToolResponse: Response indicating no more subtasks are available
|
||||
"""
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text="No more subtask exists. "
|
||||
"Check whether the task is "
|
||||
"completed solved.",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def create_worker(
|
||||
self,
|
||||
worker_name: str,
|
||||
worker_system_prompt: str,
|
||||
tool_names: Optional[List[str]] = None,
|
||||
agent_description: str = "",
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Create a worker agent for the next unfinished subtask.
|
||||
|
||||
Dynamically creates a specialized worker agent based on the
|
||||
requirements of the next unfinished subtask in the roadmap.
|
||||
The worker is configured with appropriate tools and system prompts
|
||||
based on the task needs.
|
||||
|
||||
Args:
|
||||
worker_name (str): The name of the worker agent.
|
||||
worker_system_prompt (str): The system prompt for the worker agent.
|
||||
tool_names (Optional[List[str]], optional):
|
||||
List of tools that should be assigned to the worker agent so
|
||||
that it can finish the subtask. MUST be from the
|
||||
`Available Tools for workers`
|
||||
agent_description (str, optional):
|
||||
A brief description of the worker's capabilities.
|
||||
|
||||
Returns:
|
||||
ToolResponse: Response containing the creation result and worker
|
||||
details
|
||||
"""
|
||||
if tool_names is None:
|
||||
tool_names = []
|
||||
worker_toolkit = Toolkit()
|
||||
share_tools(
|
||||
self.worker_full_toolkit,
|
||||
worker_toolkit,
|
||||
tool_names
|
||||
+ [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"search_files",
|
||||
"list_directory",
|
||||
],
|
||||
)
|
||||
with open(
|
||||
Path(__file__).parent.parent
|
||||
/ "_built_in_long_sys_prompt"
|
||||
/ "_worker_additional_sys_prompt.md",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
additional_worker_prompt = f.read()
|
||||
with open(
|
||||
Path(__file__).parent.parent
|
||||
/ "_built_in_long_sys_prompt"
|
||||
/ "_tool_usage_rules.md",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
additional_worker_prompt += str(f.read()).format_map(
|
||||
{"agent_working_dir": self.agent_working_dir},
|
||||
)
|
||||
worker = ReActAgent(
|
||||
name=worker_name,
|
||||
sys_prompt=(worker_system_prompt + additional_worker_prompt),
|
||||
model=self.worker_model,
|
||||
formatter=self.worker_formatter,
|
||||
memory=InMemoryMemory(),
|
||||
toolkit=worker_toolkit,
|
||||
)
|
||||
|
||||
self._register_worker(
|
||||
worker,
|
||||
description=agent_description,
|
||||
worker_type="dynamic-built",
|
||||
)
|
||||
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
f"Successfully created a worker agent:\n"
|
||||
f"Worker name: {worker_name}"
|
||||
f"Worker tools: {tool_names}"
|
||||
f"Worker system prompt: {worker.sys_prompt}"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def show_current_worker_pool(self) -> ToolResponse:
|
||||
"""
|
||||
List all currently available worker agents with
|
||||
their system prompts and tools.
|
||||
"""
|
||||
worker_info: dict[str, dict] = {
|
||||
name: info.model_dump()
|
||||
for name, (info, _) in self.worker_pool.items()
|
||||
}
|
||||
return ToolResponse(
|
||||
metadata={"success": True},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=json.dumps(worker_info, ensure_ascii=False, indent=2),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def execute_worker(
|
||||
self,
|
||||
subtask_idx: int,
|
||||
selected_worker_name: str,
|
||||
detailed_instruction: str,
|
||||
) -> ToolResponse:
|
||||
"""
|
||||
Execute a worker agent for the next unfinished subtask.
|
||||
|
||||
Args:
|
||||
subtask_idx (int):
|
||||
Index of the subtask to execute.
|
||||
selected_worker_name (str):
|
||||
Select a worker agent to execute by its name. If you are unsure
|
||||
what are the available agents, call `show_current_worker_pool`
|
||||
before using this function.
|
||||
detailed_instruction (str):
|
||||
Generate detailed instruction for the worker based on the
|
||||
next unfinished subtask in the roadmap. If you are unsure
|
||||
what is the next unavailable subtask, check with
|
||||
`get_next_unfinished_subtask_from_roadmap` to get more info.
|
||||
"""
|
||||
if selected_worker_name not in self.worker_pool:
|
||||
worker_info: dict[str, WorkerInfo] = {
|
||||
name: info for name, (info, _) in self.worker_pool.items()
|
||||
}
|
||||
current_agent_pool = json.dumps(
|
||||
worker_info,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
return ToolResponse(
|
||||
metadata={"success": False},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=(
|
||||
f"There is no {selected_worker_name} in current"
|
||||
"agent pool."
|
||||
"Current agent pool:\n```json"
|
||||
f"{current_agent_pool}\n"
|
||||
"```"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
worker = self.worker_pool[selected_worker_name][1]
|
||||
question_msg = Msg(
|
||||
role="user",
|
||||
name="user",
|
||||
content=detailed_instruction,
|
||||
)
|
||||
worker_response_msg = await worker(
|
||||
question_msg,
|
||||
structured_model=WorkerResponse,
|
||||
)
|
||||
if worker_response_msg.metadata is not None:
|
||||
worker_response = WorkerResponse(
|
||||
**worker_response_msg.metadata,
|
||||
)
|
||||
self.planner_notebook.roadmap.decomposed_tasks[
|
||||
subtask_idx
|
||||
].workers.append(
|
||||
self.worker_pool[selected_worker_name][0],
|
||||
)
|
||||
# double-check to ensure the generated files exists
|
||||
for filepath, desc in worker_response.generated_files.items():
|
||||
if await check_file_existence(
|
||||
filepath,
|
||||
self.worker_full_toolkit,
|
||||
):
|
||||
self.planner_notebook.files[filepath] = desc
|
||||
else:
|
||||
worker_response.generated_files.pop(filepath)
|
||||
|
||||
return ToolResponse(
|
||||
metadata={
|
||||
"success": True,
|
||||
"worker_response": worker_response.model_dump_json(),
|
||||
},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=worker_response.model_dump_json(),
|
||||
),
|
||||
],
|
||||
)
|
||||
else:
|
||||
return ToolResponse(
|
||||
metadata={
|
||||
"success": False,
|
||||
"worker_response": worker_response_msg.content,
|
||||
},
|
||||
content=[
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=str(worker_response_msg.content),
|
||||
),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user