feat: integrate subagent orchestrator with configuration options for tool management
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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 服务列表。文件格式如下:
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user