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

242 lines
8.2 KiB
Python

"""Thin swarms orchestrator for `spawn_agent_team`."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from loguru import logger
from nanobot.agent.agent_registry import AgentRegistry
from nanobot.agent.process_events import emit_process_event
from nanobot.agent_team.memory import ProcedureMemory, RunMemory
from nanobot.agent_team.swarms_adapter import MemberRunner
from nanobot.agent_team.swarms_bridge import SwarmsBridge
from nanobot.agent_team.swarms_planner import SwarmsRunPlanner
from nanobot.agent_team.swarms_policy import SwarmsPolicy
from nanobot.agent_team.target_resolver import TargetResolver
from nanobot.agent_team.types import BridgeResult, ExecutionMode
from nanobot.providers.base import LLMProvider
class AgentTeamOrchestrator:
"""Plan a swarms run, execute it, and persist the normalized result."""
def __init__(
self,
*,
workspace: Path,
provider: LLMProvider,
model: str | None,
registry: AgentRegistry,
bus: Any,
local_executor: Any,
member_runner: MemberRunner,
max_parallel_agents: int = 4,
gateway_port: int = 18790,
) -> None:
self.workspace = workspace
self.registry = registry
self.bus = bus
self.local_executor = local_executor
self.procedure_memory = ProcedureMemory(workspace)
self.run_memory = RunMemory(workspace)
self.policy = SwarmsPolicy(max_agents=max_parallel_agents)
self.target_resolver = TargetResolver(
workspace=workspace,
registry=registry,
provider=provider,
model=model,
max_parallel_agents=max_parallel_agents,
gateway_port=gateway_port,
)
self.planner = SwarmsRunPlanner(
model=model,
registry=registry,
target_resolver=self.target_resolver,
procedure_memory=self.procedure_memory,
policy=self.policy,
)
self.swarms = SwarmsBridge(
workspace=workspace,
registry=registry,
member_runner=member_runner,
)
@staticmethod
def _clean_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in metadata.items()
if value is not None
and not (isinstance(value, str) and not value.strip())
and not (isinstance(value, (list, tuple, set, dict)) and not value)
}
async def _emit_trace(
self,
run_id: str,
text: str,
*,
stage_label: str,
metadata: dict[str, Any] | None = None,
) -> None:
await emit_process_event(
"process_run_progress",
run_id=run_id,
actor_type="system",
actor_id="agent-team",
actor_name="Agent Team",
text=text,
metadata=self._clean_metadata({
"source": "agent_team_orchestrator",
"stage_label": stage_label,
**(metadata or {}),
}),
)
async def run_task(
self,
*,
task: str,
label: str,
skills: list[str],
origin: dict[str, str],
announce_via_bus: bool,
run_id: str,
) -> BridgeResult:
"""Run the team task through swarms only."""
await self._emit_trace(
run_id,
"Preparing a swarms run specification for the agent team.",
stage_label="准备 swarms 运行规格",
metadata={
"phase": "planning",
"skills": list(skills),
"origin": dict(origin),
"announce_via_bus": announce_via_bus,
},
)
spec = await self.planner.plan(task=task, label=label, skills=list(skills))
await self._emit_trace(
run_id,
f"Swarms run spec is ready: {spec.swarm_type} with {len(spec.agent_ids)} agent(s).",
stage_label="swarms 运行规格已就绪",
metadata={
"phase": "planning",
"spec": spec.to_dict(),
},
)
logger.info(
"Agent team [{}] running swarms type={} agents={}",
run_id,
spec.swarm_type,
spec.agent_ids,
)
cleanup: dict[str, Any] = {}
try:
result = await self.swarms.run_spec(spec=spec, run_id=run_id)
finally:
cleanup = await self._cleanup_created_specialists(spec, run_id)
if cleanup:
result.raw.setdefault("provisioning_cleanup", cleanup)
if cleanup.get("created_targets"):
# The run used temporary specialists that have now been removed; do not
# persist a reusable procedure pointing at deleted agent ids.
result.candidate_procedure = None
result.raw.setdefault("origin", dict(origin))
result.raw.setdefault("announce_via_bus", announce_via_bus)
stored_procedure = None
if result.success:
stored_procedure = await self.procedure_memory.record_candidate(task, result)
await self.run_memory.record_run(
task,
ExecutionMode.SWARMS,
result,
procedure_id=(
stored_procedure.id
if stored_procedure is not None
else (
result.matched_procedure.id
if result.matched_procedure is not None
else None
)
),
)
await self._emit_trace(
run_id,
"Swarms agent team run completed.",
stage_label="swarms 团队执行完成",
metadata={
"phase": "completed",
"success": result.success,
"mode": result.mode.value,
"stored_procedure_id": stored_procedure.id if stored_procedure else None,
"attempt_count": len(result.attempts),
},
)
return result
async def _cleanup_created_specialists(
self,
spec: Any,
run_id: str,
) -> dict[str, Any]:
created_targets = self._created_provisioned_targets(spec)
if not created_targets:
return {}
error = None
try:
deleted_targets = self.target_resolver.provisioning.cleanup_local_specialists(created_targets)
except Exception as exc:
deleted_targets = []
error = str(exc)
logger.warning("Failed to clean up auto-provisioned agent-team specialists: {}", exc)
deleted_set = set(deleted_targets)
cleanup = {
"created_targets": created_targets,
"deleted_targets": deleted_targets,
"skipped_targets": [
target
for target in created_targets
if target not in deleted_set
],
}
if error is not None:
cleanup["error"] = error
try:
await self._emit_trace(
run_id,
"Cleaned up auto-provisioned agent-team specialists.",
stage_label="清理自动创建的团队成员",
metadata={
"phase": "cleanup",
**cleanup,
},
)
except Exception as exc:
logger.warning("Failed to emit agent-team cleanup trace: {}", exc)
return cleanup
@staticmethod
def _created_provisioned_targets(spec: Any) -> list[str]:
metadata = getattr(spec, "metadata", {})
if not isinstance(metadata, dict):
return []
target_plan = metadata.get("target_plan")
if not isinstance(target_plan, dict):
return []
created_targets = target_plan.get("created_provisioned_targets")
if not created_targets:
plan_metadata = target_plan.get("metadata")
if isinstance(plan_metadata, dict):
created_targets = plan_metadata.get("created_provisioned_targets")
return [
target
for target in dict.fromkeys(str(item).strip() for item in (created_targets or []))
if target
]