按照comment进行一些小改动

This commit is contained in:
advent259141
2026-01-26 23:30:29 +08:00
parent b3a1f4ca7d
commit 1bd8eae25a
8 changed files with 92 additions and 79 deletions
@@ -71,10 +71,13 @@ class ProcessLLMRequest:
tmgr = self.ctx.get_llm_tool_manager()
# SubAgent orchestrator mode: main LLM only sees handoff tools.
orch_cfg = cfg.get("subagent_orchestrator", {})
# NOTE: subagent_orchestrator config lives at top-level now.
orch_cfg = self.ctx.get_config().get("subagent_orchestrator", {})
if orch_cfg.get("main_enable", False):
toolset = ToolSet()
for tool in tmgr.func_list:
# Prevent recursion / confusion: in handoff-only mode, the main LLM
# should only be able to call transfer_to_* tools.
if isinstance(tool, HandoffTool) and tool.active:
toolset.add_tool(tool)
req.func_tool = toolset
@@ -83,16 +86,12 @@ class ProcessLLMRequest:
# Use the built-in default router prompt; user overrides are disabled for now.
router_prompt = (
self.ctx.get_config()
.get("provider_settings", {})
.get("subagent_orchestrator", {})
.get("router_system_prompt", "")
).strip()
if router_prompt:
req.system_prompt += f"\n{router_prompt}\n"
logger.debug(
f"Subagent orchestrator enabled; main tool set (handoff_only): {toolset.names()}"
)
return
# Default behavior: follow persona tool selection.
+4 -4
View File
@@ -12,6 +12,7 @@ class HandoffTool(FunctionTool, Generic[TContext]):
self,
agent: Agent[TContext],
parameters: dict | None = None,
tool_description: str | None = None,
**kwargs,
):
self.agent = agent
@@ -20,10 +21,9 @@ class HandoffTool(FunctionTool, Generic[TContext]):
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
# to override what the main agent sees, while we also compute a default
# description here.
description = kwargs.pop(
"description",
agent.instructions or self.default_description(agent.name),
)
# `tool_description` is the public description shown to the main LLM.
# Keep a separate kwarg to avoid conflicting with FunctionTool's `description`.
description = tool_description or self.default_description(agent.name)
super().__init__(
name=f"transfer_to_{agent.name}",
parameters=parameters or self.default_parameters(),
+12 -12
View File
@@ -121,18 +121,18 @@ DEFAULT_CONFIG = {
"shipyard_ttl": 3600,
"shipyard_max_sessions": 10,
},
# SubAgent orchestrator mode: the main LLM only delegates tasks to subagents
# (via transfer_to_{agent} tools). Domain tools are mounted on subagents.
"subagent_orchestrator": {
"main_enable": False,
"main_tools_policy": "handoff_only", # reserved for future; main_enable implies handoff_only
"router_system_prompt": (
"You are a task router. Your job is to chat naturally, recognize user intent, "
"and delegate work to the most suitable subagent using transfer_to_* tools. "
"Do not try to use domain tools yourself. If no subagent fits, respond directly."
),
"agents": [],
},
},
# SubAgent orchestrator mode: the main LLM only delegates tasks to subagents
# (via transfer_to_{agent} tools). Domain tools are mounted on subagents.
"subagent_orchestrator": {
"main_enable": False,
"main_tools_policy": "handoff_only", # reserved for future; main_enable implies handoff_only
"router_system_prompt": (
"You are a task router. Your job is to chat naturally, recognize user intent, "
"and delegate work to the most suitable subagent using transfer_to_* tools. "
"Do not try to use domain tools yourself. If no subagent fits, respond directly."
),
"agents": [],
},
"provider_stt_settings": {
"enable": False,
+1 -1
View File
@@ -89,7 +89,7 @@ class AstrBotCoreLifecycle:
self.provider_manager.llm_tools,
)
self.subagent_orchestrator.reload_from_config(
self.astrbot_config.get("provider_settings", {}),
self.astrbot_config.get("subagent_orchestrator", {}),
)
except Exception as e:
logger.error(f"Subagent orchestrator init failed: {e}", exc_info=True)
@@ -11,6 +11,7 @@ import aiohttp
from astrbot import logger
from astrbot.core import sp
from astrbot.core.agent.handoff import HandoffTool
from astrbot.core.agent.mcp_client import MCPClient, MCPTool
from astrbot.core.agent.tool import FunctionTool, ToolSet
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
@@ -179,6 +180,48 @@ class FunctionToolManager:
tool_set = ToolSet(self.func_list.copy())
return tool_set
def sync_dynamic_handoff_tools(
self,
handoffs: list[HandoffTool],
*,
handler_module_path: str,
) -> None:
"""Sync dynamic transfer_to_* tools in-place.
This removes any existing tools previously registered under the same
handler_module_path and then registers the provided HandoffTool list.
NOTE: add_func() stores a FunctionTool wrapper; for handoff tools we
want to keep the real HandoffTool objects in func_list so other parts
of the system can inspect agent/provider_id metadata.
"""
# Remove previously registered dynamic handoff tools.
self.func_list = [
t for t in self.func_list if t.handler_module_path != handler_module_path
]
for handoff in handoffs:
handoff.handler_module_path = handler_module_path
# Register tool (ensures the handler is reachable by name).
self.add_func(
name=handoff.name,
func_args=[
{
"type": "string",
"name": "input",
"description": "Task input delegated from the main agent.",
}
],
desc=handoff.description,
handler=handoff.handler,
)
# Replace wrapper with the actual HandoffTool instance.
self.remove_func(handoff.name)
self.func_list.append(handoff)
async def init_mcp_clients(self) -> None:
"""从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下:
```
+17 -49
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from astrbot import logger
@@ -10,19 +9,6 @@ from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.provider.func_tool_manager import FunctionToolManager
@dataclass(frozen=True)
class SubAgentConfig:
"""Runtime representation of a configured subagent."""
name: str
# Instructions are used as the subagent's system prompt.
instructions: str
# Public description is what the main LLM sees for transfer_to_* tool description.
public_description: str
tools: list[str]
enabled: bool = True
class SubAgentOrchestrator:
"""Loads subagent definitions from config and registers handoff tools.
@@ -32,23 +18,16 @@ class SubAgentOrchestrator:
def __init__(self, tool_mgr: FunctionToolManager):
self._tool_mgr = tool_mgr
self._registered_handoff_names: set[str] = set()
def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
cfg = provider_settings.get("subagent_orchestrator", {})
def reload_from_config(self, cfg: dict[str, Any]) -> None:
enabled = bool(cfg.get("main_enable", False))
# Remove previously registered dynamic handoff tools.
if self._registered_handoff_names:
for name in list(self._registered_handoff_names):
try:
self._tool_mgr.remove_func(name)
except Exception:
# remove_func is best-effort; keep going.
pass
self._registered_handoff_names.clear()
if not enabled:
# Ensure any previous dynamic handoff tools are cleared.
self._tool_mgr.sync_dynamic_handoff_tools(
[],
handler_module_path="core.subagent_orchestrator",
)
return
agents = cfg.get("agents", [])
@@ -56,6 +35,7 @@ class SubAgentOrchestrator:
logger.warning("subagent_orchestrator.agents must be a list")
return
handoffs: list[HandoffTool] = []
for item in agents:
if not isinstance(item, dict):
continue
@@ -83,32 +63,20 @@ class SubAgentOrchestrator:
)
# The tool description should be a short description for the main LLM,
# while the subagent system prompt can be longer/more specific.
handoff = HandoffTool(agent=agent, description=public_description or None)
handoff = HandoffTool(
agent=agent,
tool_description=public_description or None,
)
# Optional per-subagent chat provider override.
handoff.provider_id = provider_id
# Mark as dynamic so we can replace/remove later.
handoff.handler_module_path = "core.subagent_orchestrator"
handoffs.append(handoff)
# Register tool (replaces if same name exists).
self._tool_mgr.add_func(
name=handoff.name,
func_args=[
{
"type": "string",
"name": "input",
"description": "Task input delegated from the main agent.",
}
],
desc=handoff.description,
handler=handoff.handler,
)
self._tool_mgr.sync_dynamic_handoff_tools(
handoffs,
handler_module_path="core.subagent_orchestrator",
)
# NOTE: add_func wraps handler into a FunctionTool; we want the actual HandoffTool.
# Therefore, directly append the HandoffTool to func_list (and remove any wrapper).
self._tool_mgr.remove_func(handoff.name)
self._tool_mgr.func_list.append(handoff)
self._registered_handoff_names.add(handoff.name)
for handoff in handoffs:
logger.info(f"Registered subagent handoff tool: {handoff.name}")
+10 -6
View File
@@ -3,6 +3,7 @@ import traceback
from quart import jsonify, request
from astrbot.core import logger
from astrbot.core.agent.handoff import HandoffTool
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from .route import Response, Route, RouteContext
@@ -28,8 +29,7 @@ class SubAgentRoute(Route):
async def get_config(self):
try:
cfg = self.core_lifecycle.astrbot_config
provider_settings = cfg.get("provider_settings", {})
data = provider_settings.get("subagent_orchestrator")
data = cfg.get("subagent_orchestrator")
# First-time access: return a sane default instead of erroring.
if not isinstance(data, dict):
@@ -70,9 +70,7 @@ class SubAgentRoute(Route):
return jsonify(Response().error("配置必须为 JSON 对象").__dict__)
cfg = self.core_lifecycle.astrbot_config
provider_settings = cfg.get("provider_settings", {})
provider_settings["subagent_orchestrator"] = data
cfg["provider_settings"] = provider_settings
cfg["subagent_orchestrator"] = data
# Persist to cmd_config.json
# AstrBotConfigManager does not expose a `save()` method; persist via AstrBotConfig.
@@ -81,7 +79,7 @@ class SubAgentRoute(Route):
# Reload dynamic handoff tools if orchestrator exists
orch = getattr(self.core_lifecycle, "subagent_orchestrator", None)
if orch is not None:
orch.reload_from_config(provider_settings)
orch.reload_from_config(data)
return jsonify(Response().ok(message="保存成功").__dict__)
except Exception as e:
@@ -97,6 +95,12 @@ class SubAgentRoute(Route):
tool_mgr = self.core_lifecycle.provider_manager.llm_tools
tools_dict = []
for tool in tool_mgr.func_list:
# Prevent recursive routing: subagents should not be able to select
# the handoff (transfer_to_*) tools as their own mounted tools.
if isinstance(tool, HandoffTool):
continue
if tool.handler_module_path == "core.subagent_orchestrator":
continue
tools_dict.append(
{
"name": tool.name,
+1 -2
View File
@@ -132,8 +132,7 @@ class AstrBotDashboard:
r = jsonify(Response().error("未授权").__dict__)
r.status_code = 401
return r
# Be tolerant of different header casing / formatting.
token = token.strip().removeprefix("Bearer ").strip()
token = token.removeprefix("Bearer ")
try:
payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
g.username = payload["username"]