按照comment进行一些小改动
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 服务列表。文件格式如下:
|
||||
```
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user