feat: 添加swarms团队编排功能并优化agent委派系统

- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
2026-04-14 14:34:23 +08:00
parent fee9007da6
commit cdfc222c9f
85 changed files with 5443 additions and 1392 deletions

View File

@ -0,0 +1,302 @@
"""Bridge from nanobot agent-team tasks into the vendored swarms runtime."""
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Any
from nanobot.agent.agent_registry import AgentRegistry
from nanobot.agent.process_events import emit_process_event
from nanobot.agent.run_result import has_meaningful_summary
from nanobot.agent_team.swarms_adapter import MemberRunner, NanobotAgentAdapter, load_swarms_runtime
from nanobot.agent_team.types import (
BridgeAttempt,
BridgeResult,
ExecutionMode,
ProcedureRecord,
SwarmsRunResult,
SwarmsRunSpec,
)
class SwarmsBridge:
"""Execute a `SwarmsRunSpec` with `SwarmRouter` and normalize the output."""
def __init__(
self,
*,
workspace: Path,
registry: AgentRegistry,
member_runner: MemberRunner,
) -> None:
self.workspace = workspace
self.registry = registry
self.member_runner = member_runner
async def run_spec(self, *, spec: SwarmsRunSpec, run_id: str) -> BridgeResult:
# 先发一条过程事件告诉上层“swarms 执行阶段已经开始”。
# metadata 里带完整 spec便于前端或日志侧排查本次实际执行参数。
await self._emit_progress(
run_id,
f"Starting swarms run: {spec.swarm_type}.",
stage_label="启动 swarms runtime",
metadata={"spec": spec.to_dict()},
)
# 真正调用 swarms runtime返回的是“桥接层内部使用”的 SwarmsRunResult。
swarms_result = await self._run_swarms(spec=spec, run_id=run_id)
# success 不只看 swarms_result.success还要求 summary 有实际内容。
# 这样可以避免 runtime technically 跑完了,但最终没有任何可消费结论时,
# 上层误把它当成一次成功执行。
success = swarms_result.success and has_meaningful_summary(swarms_result.summary)
error = None if success else (swarms_result.error or swarms_result.summary)
# BridgeAttempt 表示“这次 swarms 模式尝试”的完整快照;
# 后续 BridgeResult.attempts 可以累计不同执行策略/回退路径的尝试记录。
attempt = BridgeAttempt(
mode=ExecutionMode.SWARMS,
success=success,
summary=swarms_result.summary,
error=error,
member_results=list(swarms_result.member_results),
targets=list(spec.agent_ids),
raw={
"spec": spec.to_dict(),
"swarms_result": swarms_result.to_dict(),
},
)
# 只有成功时才生成 candidate procedure避免把失败或空结果学习成可复用流程。
candidate = self._candidate_procedure(spec, swarms_result, run_id) if success else None
# 再发一条归一化完成事件,让编排层知道 bridge 已经把 swarms 原始输出
# 压成了 nanobot 可消费的标准结果结构。
await self._emit_progress(
run_id,
"Swarms run returned a normalized bridge result.",
stage_label="swarms 输出已归一",
metadata={
"success": success,
"swarm_type": spec.swarm_type,
"candidate_procedure_id": candidate.id if candidate else None,
},
)
# BridgeResult 是 swarms bridge 对外暴露的稳定边界:
# - summary/member_results 给上层公告和持久化使用
# - attempts/raw 保留足够多细节,便于后续解释和调试
return BridgeResult(
mode=ExecutionMode.SWARMS,
success=success,
summary=swarms_result.summary,
error=error,
member_results=list(swarms_result.member_results),
candidate_procedure=candidate,
attempts=[attempt],
raw={
"spec": spec.to_dict(),
"swarms_result": swarms_result.to_dict(),
},
)
async def _run_swarms(self, *, spec: SwarmsRunSpec, run_id: str) -> SwarmsRunResult:
try:
# 先把 spec.agent_ids 解析成当前 registry 中的 AgentDescriptor。
# 这里显式校验 agent 必须存在,避免 swarms runtime 在更深处才报模糊错误。
descriptors = []
for agent_id in spec.agent_ids:
descriptor = self.registry.get_agent(agent_id)
if descriptor is None:
raise ValueError(f"Agent not found for swarms run: {agent_id}")
descriptors.append(descriptor)
# swarms runtime 运行在线程池里,但每个 NanobotAgentAdapter 最终仍要把执行
# 切回当前事件循环中的 member_runner因此这里提前拿到 running loop。
loop = asyncio.get_running_loop()
# 把 nanobot 的 AgentDescriptor 包装成 swarms 可以直接调用的 adapter。
# swarms 视角下它们只是“可调用 agent”nanobot 视角下它们会回流到
# member_runner再由本地执行器或 A2A client 真正完成任务。
adapters = [
NanobotAgentAdapter(
descriptor=descriptor,
run_id=run_id,
loop=loop,
member_runner=self.member_runner,
skills=list(spec.skills),
)
for descriptor in descriptors
]
# SwarmRouter 是 vendored swarms runtime 的核心入口。
# 这里把 planner 产出的 swarm_type / loops / flow / rules 全部映射进去。
runtime = load_swarms_runtime()
router = runtime["SwarmRouter"](
name=spec.label or "nanobot-agent-team",
description="Nanobot agent-team swarms router",
agents=adapters,
swarm_type=spec.swarm_type,
max_loops=max(1, spec.max_loops),
rearrange_flow=spec.rearrange_flow,
rules=self._rules_with_skills(spec),
autosave=False,
verbose=False,
)
# swarms 的 router.run 是同步阻塞调用,因此放到线程池中执行,
# 避免阻塞当前 asyncio 事件循环。
raw_output = await asyncio.to_thread(router.run, task=self._task_with_skills(spec))
# swarms 原始输出结构并不稳定,统一在这里归一成 SwarmsRunResult。
return self._normalize_swarms_output(raw_output, adapters)
except Exception as exc:
# 桥接层把异常收口成失败结果,而不是继续向上抛,
# 这样 orchestrator 可以用统一的 BridgeResult 流程处理失败。
return SwarmsRunResult(
success=False,
summary=f"Swarms execution failed: {exc}",
raw_output=None,
error=str(exc),
)
def _rules_with_skills(self, spec: SwarmsRunSpec) -> str:
# 把上层规则和桥接层的硬约束拼到一起:
# 1. 保留 planner 指定的 rules
# 2. 明确禁止 swarms 擅自引入额外 agent、工具或凭证
# 3. 把 skills 也写入规则,确保团队行为不偏离 nanobot 约束
parts = [
spec.rules or "Run the nanobot agent team through swarms and produce a concise synthesis.",
"Do not add tools, credentials, network endpoints, or agents outside the provided nanobot adapters.",
]
if spec.skills:
parts.append("Required nanobot skills: " + ", ".join(spec.skills))
return "\n".join(parts)
def _task_with_skills(self, spec: SwarmsRunSpec) -> str:
# skills 既体现在 rules 中,也直接拼到任务文本里,
# 这样无论 swarms runtime 更依赖哪部分上下文,都能看到技能约束。
if not spec.skills:
return spec.task
return (
f"{spec.task}\n\n"
"Required skills for this swarms run:\n"
f"{', '.join(spec.skills)}"
).strip()
def _normalize_swarms_output(
self,
raw_output: Any,
adapters: list[NanobotAgentAdapter],
) -> SwarmsRunResult:
# 优先从 adapters 收集每个成员真实执行后的 AgentRunResult。
# 这些结果比 swarms runtime 的自由格式输出更稳定、也更适合后续持久化。
member_results = [
result
for adapter in adapters
for result in adapter.results
]
# summary 优先从成员结果推导;如果成员结果拿不到,再从 swarms 原始输出中兜底提取。
summary = self._summary_from_swarms_output(raw_output, member_results)
return SwarmsRunResult(
success=bool(summary.strip()),
summary=summary.strip(),
raw_output=self._jsonable(raw_output),
member_results=member_results,
)
def _summary_from_swarms_output(self, raw_output: Any, member_results: list[Any]) -> str:
# 如果已经拿到了结构化 member_results就优先用它们生成总结
# 因为这比直接依赖 swarms 的原始输出更稳定、更贴近 nanobot 的结果模型。
if member_results:
return "\n\n".join(
f"{result.agent_name} ({result.status}):\n{result.summary}"
for result in member_results
if str(result.summary or "").strip()
)
# swarms 有时直接返回字符串,那就把它当作最终 summary。
if isinstance(raw_output, str):
return raw_output.strip()
# swarms 也可能返回 transcript/list 结构;这里尝试提取非 user/system 的发言,
# 拼成一个可读摘要。
if isinstance(raw_output, list):
lines: list[str] = []
for item in raw_output:
if not isinstance(item, dict):
continue
role = str(item.get("role") or item.get("speaker") or "").strip()
content = str(item.get("content") or item.get("message") or "").strip()
if not content or role.lower() in {"user", "system"}:
continue
lines.append(f"{role}: {content}" if role else content)
if lines:
return "\n\n".join(lines)
# 最后兜底把原始输出尽量序列化成 JSON 文本;再不行就直接 str(...)。
try:
return json.dumps(raw_output, ensure_ascii=False, indent=2)
except TypeError:
return str(raw_output)
def _jsonable(self, value: Any) -> Any:
# raw_output 最终要落到 BridgeResult / RunMemory 里,因此这里尽量保证它可序列化。
# 若原值无法直接 JSON 化,则退回字符串表示,避免整个持久化流程失败。
try:
json.dumps(value, ensure_ascii=False)
return value
except TypeError:
return str(value)
def _candidate_procedure(
self,
spec: SwarmsRunSpec,
result: SwarmsRunResult,
run_id: str,
) -> ProcedureRecord:
# bridge 只负责产出一个“可候选复用”的 procedure 草稿:
# - task_template/agent_ids/strategy 记录执行骨架
# - summary 提供人类可读概览
# - metadata 记录它来自 swarms bridge
# 真正是否持久化、如何更新统计,由更上层的 procedure memory 决定。
return ProcedureRecord(
task_template=spec.task,
summary=result.summary,
agent_ids=list(spec.agent_ids),
strategy=spec.swarm_type,
confidence=0.6,
source_run_id=run_id,
metadata={
"source": "swarms_bridge",
"swarm_type": spec.swarm_type,
"auto_generated": spec.auto_generated,
"skills": list(spec.skills),
},
)
async def _emit_progress(
self,
run_id: str,
text: str,
*,
stage_label: str,
metadata: dict[str, Any] | None = None,
) -> None:
# 统一发 process_run_progress让前端/日志看到 swarms bridge 当前阶段。
await emit_process_event(
"process_run_progress",
run_id=run_id,
actor_type="system",
actor_id="swarms-bridge",
actor_name="Swarms Bridge",
text=text,
metadata={
"source": "swarms_bridge",
"stage_label": stage_label,
**(metadata or {}),
},
)