This commit is contained in:
raykkk
2025-10-17 21:40:45 +08:00
commit 7d0451131f
155 changed files with 14873 additions and 0 deletions

View 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",
]

View File

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

View File

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

View File

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