feat: integrate subagent orchestrator with configuration options for tool management

This commit is contained in:
Soulter
2026-02-01 20:43:08 +08:00
parent bf587765de
commit 3f8d8b5033
8 changed files with 54 additions and 124 deletions
+18 -25
View File
@@ -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
+3 -5
View File
@@ -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. "
+4 -4
View File
@@ -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())
@@ -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 服务列表。文件格式如下:
```
+3
View File
@@ -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,
+4 -15
View File
@@ -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
+3 -6
View File
@@ -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:
+19 -26
View File
@@ -20,21 +20,24 @@
<v-card class="rounded-lg" variant="flat">
<v-card-text>
<v-row>
<v-col cols="12" md="8">
<v-select v-model="cfg.main_mode" :items="mainModes" item-title="label" item-value="value"
label="SubAgent 模式" variant="outlined" density="comfortable" hide-details />
<v-col cols="12" md="6">
<v-switch v-model="cfg.main_enable" label="启用 SubAgent 编排"
inset color="primary" hide-details density="comfortable" />
</v-col>
<v-col cols="12" md="6">
<v-switch v-model="cfg.remove_main_duplicate_tools" :disabled="!cfg.main_enable"
label="主 LLM 去重重复工具(与 SubAgent 重叠的工具将被隐藏)"
inset color="primary" hide-details density="comfortable" />
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-1">
<div v-if="cfg.main_mode === 'disabled'">
<div v-if="!cfg.main_enable">
不启动SubAgent 关闭 LLM persona 规则挂载工具默认全部并直接调用
</div>
<div v-else-if="cfg.main_mode === 'unassigned_to_main'">
启动SubAgent 可分派未分配给任何 SubAgent 的工具仍挂载到主 LLM
</div>
<div v-else>
启动 SubAgent LLM 保留 transfer_to_* 这类委派工具不挂载其他工具
启动 LLM 保留自身工具并挂载 transfer_to_* 委派工具
若开启去重重复工具 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除
</div>
</div>
@@ -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<SubAgentConfig>({
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,