303 lines
13 KiB
Python
303 lines
13 KiB
Python
"""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 {}),
|
||
},
|
||
)
|