From 3f8d8b5033ca54292a9bb1679a729193ef4c31ea Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 1 Feb 2026 20:43:08 +0800 Subject: [PATCH] feat: integrate subagent orchestrator with configuration options for tool management --- astrbot/core/astr_main_agent.py | 43 +++++++++------------ astrbot/core/config/default.py | 8 ++-- astrbot/core/core_lifecycle.py | 8 ++-- astrbot/core/provider/func_tool_manager.py | 43 --------------------- astrbot/core/star/context.py | 3 ++ astrbot/core/subagent_orchestrator.py | 19 ++------- astrbot/dashboard/routes/subagent.py | 9 ++--- dashboard/src/views/SubAgentPage.vue | 45 +++++++++------------- 8 files changed, 54 insertions(+), 124 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 41aa0fe42..211cce8e2 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -324,11 +324,11 @@ async def _ensure_persona_and_skills( tmgr = plugin_context.get_llm_tool_manager() + # sub agents integration orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {}) - if orch_cfg.get("main_enable", False): - policy = str(orch_cfg.get("main_tools_policy", "handoff_only")).strip() - if policy not in {"handoff_only", "unassigned_to_main"}: - policy = "handoff_only" + so = plugin_context.subagent_orchestrator + if orch_cfg.get("main_enable", False) and so: + remove_dup = bool(orch_cfg.get("remove_main_duplicate_tools", False)) assigned_tools: set[str] = set() agents = orch_cfg.get("agents", []) @@ -368,22 +368,21 @@ async def _ensure_persona_and_skills( if name: assigned_tools.add(name) - toolset = ToolSet() - for tool in tmgr.func_list: - if isinstance(tool, HandoffTool) and tool.active: - toolset.add_tool(tool) + if req.func_tool is None: + toolset = ToolSet() + else: + toolset = req.func_tool - if policy == "unassigned_to_main": - for tool in tmgr.func_list: - if not tool.active: - continue - if isinstance(tool, HandoffTool): - continue - if tool.handler_module_path == "core.subagent_orchestrator": - continue - if tool.name in assigned_tools: - continue - toolset.add_tool(tool) + # add subagent handoff tools + for tool in so.handoffs: + toolset.add_tool(tool) + + # check duplicates + if remove_dup: + names = toolset.names() + for tool_name in assigned_tools: + if tool_name in names: + toolset.remove_tool(tool_name) req.func_tool = toolset @@ -394,12 +393,6 @@ async def _ensure_persona_and_skills( ).strip() if router_prompt: req.system_prompt += f"\n{router_prompt}\n" - if policy == "unassigned_to_main": - req.system_prompt += ( - "\n[Note: You may directly call the tools visible to the main LLM " - "if they are not assigned to any subagent; otherwise prefer delegating " - "to subagents via transfer_to_*.]\n" - ) return # inject toolset in the persona diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 702316d2e..a752dfc55 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -126,13 +126,11 @@ DEFAULT_CONFIG = { }, # SubAgent orchestrator mode: # - main_enable = False: disabled; main LLM mounts tools normally (persona selection). - # - main_enable = True: enabled; main LLM tool mounting is controlled by main_tools_policy. + # - main_enable = True: enabled; main LLM will include handoff tools and can optionally + # remove tools that are duplicated on subagents via remove_main_duplicate_tools. "subagent_orchestrator": { "main_enable": False, - # - handoff_only: main LLM only sees transfer_to_* tools (recommended default when enabled). - # - unassigned_to_main: tools not assigned to any subagent are still mounted on main LLM. - # - disabled: UI convenience value; ignored when main_enable is False. - "main_tools_policy": "disabled", + "remove_main_duplicate_tools": False, "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. " diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index f619b64af..6b36cca0d 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -55,8 +55,6 @@ class AstrBotCoreLifecycle: self.astrbot_config = astrbot_config # 初始化配置 self.db = db # 初始化数据库 - # Optional orchestrator that registers dynamic handoff tools (transfer_to_*) - # from provider_settings.subagent_orchestrator. self.subagent_orchestrator: SubAgentOrchestrator | None = None self.cron_manager: CronJobManager | None = None @@ -169,6 +167,9 @@ class AstrBotCoreLifecycle: # 初始化 CronJob 管理器 self.cron_manager = CronJobManager(self.db) + # Dynamic subagents (handoff tools) from config. + await self._init_or_reload_subagent_orchestrator() + # 初始化提供给插件的上下文 self.star_context = Context( self.event_queue, @@ -182,6 +183,7 @@ class AstrBotCoreLifecycle: self.astrbot_config_mgr, self.kb_manager, self.cron_manager, + self.subagent_orchestrator, ) # 初始化插件管理器 @@ -208,8 +210,6 @@ class AstrBotCoreLifecycle: self.astrbot_config_mgr, ) - # Dynamic subagents (handoff tools) from config. - await self._init_or_reload_subagent_orchestrator() # 记录启动时间 self.start_time = int(time.time()) diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index 93990d87c..7aad86bdd 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -11,7 +11,6 @@ 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 @@ -180,48 +179,6 @@ 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 服务列表。文件格式如下: ``` diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index fee20640a..ec7cbbe9e 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -24,6 +24,7 @@ from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryMana from astrbot.core.provider.entities import LLMResponse, ProviderRequest, ProviderType from astrbot.core.provider.func_tool_manager import FunctionTool, FunctionToolManager from astrbot.core.provider.manager import ProviderManager +from astrbot.core.subagent_orchestrator import SubAgentOrchestrator from astrbot.core.provider.provider import ( EmbeddingProvider, Provider, @@ -67,6 +68,7 @@ class Context: astrbot_config_mgr: AstrBotConfigManager, knowledge_base_manager: KnowledgeBaseManager, cron_manager: CronJobManager, + subagent_orchestrator: SubAgentOrchestrator | None = None, ): self._event_queue = event_queue """事件队列。消息平台通过事件队列传递消息事件。""" @@ -90,6 +92,7 @@ class Context: """知识库管理器""" self.cron_manager = cron_manager """Cron job manager, initialized by core lifecycle.""" + self.subagent_orchestrator = subagent_orchestrator async def llm_generate( self, diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index 1e9c9ea9e..62ddc0fd3 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -5,7 +5,6 @@ from typing import Any from astrbot import logger from astrbot.core.agent.agent import Agent from astrbot.core.agent.handoff import HandoffTool -from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.persona_mgr import PersonaManager from astrbot.core.provider.func_tool_manager import FunctionToolManager @@ -20,17 +19,10 @@ class SubAgentOrchestrator: def __init__(self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager): self._tool_mgr = tool_mgr self._persona_mgr = persona_mgr + self.handoffs: list[HandoffTool] = [] async def reload_from_config(self, cfg: dict[str, Any]) -> None: - enabled = bool(cfg.get("main_enable", False)) - - 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 + from astrbot.core.astr_agent_context import AstrAgentContext agents = cfg.get("agents", []) if not isinstance(agents, list): @@ -98,10 +90,7 @@ class SubAgentOrchestrator: handoffs.append(handoff) - self._tool_mgr.sync_dynamic_handoff_tools( - handoffs, - handler_module_path="core.subagent_orchestrator", - ) - for handoff in handoffs: logger.info(f"Registered subagent handoff tool: {handoff.name}") + + self.handoffs = handoffs diff --git a/astrbot/dashboard/routes/subagent.py b/astrbot/dashboard/routes/subagent.py index 997d01eb8..e3d77f73a 100644 --- a/astrbot/dashboard/routes/subagent.py +++ b/astrbot/dashboard/routes/subagent.py @@ -35,7 +35,7 @@ class SubAgentRoute(Route): if not isinstance(data, dict): data = { "main_enable": False, - "main_tools_policy": "disabled", + "remove_main_duplicate_tools": False, "agents": [], } @@ -49,10 +49,7 @@ class SubAgentRoute(Route): # Ensure required keys exist. data.setdefault("main_enable", False) - if "main_tools_policy" not in data: - data["main_tools_policy"] = ( - "handoff_only" if data.get("main_enable", False) else "disabled" - ) + data.setdefault("remove_main_duplicate_tools", False) data.setdefault("agents", []) # Backward/forward compatibility: ensure each agent contains provider_id. @@ -83,7 +80,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(data) + await orch.reload_from_config(data) return jsonify(Response().ok(message="保存成功").__dict__) except Exception as e: diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index 2aa404ac3..4474172fd 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -20,21 +20,24 @@ - - + + + + +
-
+
不启动:SubAgent 关闭;主 LLM 按 persona 规则挂载工具(默认全部),并直接调用。
-
- 启动:SubAgent 可分派;未分配给任何 SubAgent 的工具仍挂载到主 LLM 上。 -
- 启动:仅 SubAgent;主 LLM 只保留 transfer_to_* 这类委派工具,不挂载其他工具。 + 启动:主 LLM 会保留自身工具并挂载 transfer_to_* 委派工具。 + 若开启“去重重复工具”,与 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除。
@@ -135,10 +138,9 @@ type SubAgentItem = { provider_id?: string } -type MainMode = 'disabled' | 'unassigned_to_main' | 'handoff_only' - type SubAgentConfig = { - main_mode: MainMode + main_enable: boolean + remove_main_duplicate_tools: boolean agents: SubAgentItem[] } @@ -155,14 +157,9 @@ function toast(message: string, color: 'success' | 'error' | 'warning' = 'succes snackbar.value = { show: true, message, color } } -const mainModes: Array<{ label: string; value: MainMode }> = [ - { label: '不启动:SubAgent 关闭(主 LLM 按 persona 挂载工具)', value: 'disabled' }, - { label: '启动:未分配工具仍挂载到主 LLM', value: 'unassigned_to_main' }, - { label: '启动:仅 SubAgent(主 LLM 仅 transfer_to_*)', value: 'handoff_only' } -] - const cfg = ref({ - main_mode: 'disabled', + main_enable: false, + remove_main_duplicate_tools: false, agents: [] }) @@ -171,10 +168,7 @@ const personaLoading = ref(false) function normalizeConfig(raw: any): SubAgentConfig { const main_enable = !!raw?.main_enable - const policy = (raw?.main_tools_policy ?? '').toString().trim() - const main_mode: MainMode = !main_enable - ? 'disabled' - : (policy === 'unassigned_to_main' ? 'unassigned_to_main' : 'handoff_only') + const remove_main_duplicate_tools = !!raw?.remove_main_duplicate_tools const agentsRaw = Array.isArray(raw?.agents) ? raw.agents : [] const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => { @@ -195,7 +189,7 @@ function normalizeConfig(raw: any): SubAgentConfig { } }) - return { main_mode, agents } + return { main_enable, remove_main_duplicate_tools, agents } } async function loadConfig() { @@ -278,10 +272,9 @@ async function save() { saving.value = true try { // Strip UI-only fields - const mode = cfg.value.main_mode const payload = { - main_enable: mode !== 'disabled', - main_tools_policy: mode, + main_enable: cfg.value.main_enable, + remove_main_duplicate_tools: cfg.value.remove_main_duplicate_tools, agents: cfg.value.agents.map(a => ({ name: a.name, persona_id: a.persona_id,