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,185 @@
"""Provision managed local A2A specialists for agent teams."""
from __future__ import annotations
import hashlib
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from loguru import logger
from nanobot.agent.subagents import LocalSubagentStore, normalize_subagent_id
from nanobot.config.schema import Config
@dataclass(frozen=True)
class SpecialistProvisionResult:
"""Result of ensuring a managed specialist exists."""
agent_id: str
created: bool
class ProvisioningManager:
"""Manage local specialists through LocalSubagentStore."""
def __init__(self, workspace: Path, *, gateway_port: int = 18790) -> None:
self.workspace = workspace
self.gateway_port = int(os.getenv("APP_BACKEND_PORT") or gateway_port)
self.store = LocalSubagentStore(workspace)
async def ensure_local_specialist_with_result(
self,
*,
role: str,
task: str,
skills: list[str] | None = None,
) -> SpecialistProvisionResult:
"""创建或刷新一个本地 specialist并返回它是否是首次创建。"""
# role 可能来自上游 planner、用户输入或其他动态流程这里先做兜底和规范化
# 1. 空值时退回到通用角色 "general specialist"
# 2. 去掉首尾空白,避免生成不稳定的 agent 标识
# 这样可以保证后续 id、显示名、标签等字段都基于同一个干净的角色名生成。
role_name = str(role or "general specialist").strip() or "general specialist"
# agent_id 由“角色名 + 任务指纹”组成:
# - 同一角色处理同一任务时会命中同一个 id从而实现刷新/复用
# - 同一角色处理不同任务时会得到不同 id避免不同任务上下文互相污染
agent_id = self._specialist_id(role_name, task)
# display_name 主要用于人类可读展示;它不影响真正的唯一性,
# 唯一性仍由 agent_id 保证。
display_name = self._display_name(role_name)
# 为即将 upsert 的 subagent 构造运行时配置。
# 这里显式覆盖两个关键字段:
# - workspace确保 specialist 和当前 agent team 运行在同一个工作目录
# - gateway.port确保它连接到当前后端实例暴露的网关端口
# 这样新建/刷新出来的本地 specialist 才能在正确的环境里工作。
config = Config()
config.agents.defaults.workspace = str(self.workspace)
config.gateway.port = self.gateway_port
# payload 是写入 LocalSubagentStore 的完整声明式规格。
# store.upsert_subagent(...) 会根据这份规格创建或刷新 subagent。
payload = {
# 稳定唯一 id用于判断“是否已存在”以及后续更新同一个 specialist。
"id": agent_id,
# 人类可读名称,便于在 UI、日志或调试信息中识别角色。
"name": display_name,
# 简短描述说明该 agent 的来源和用途:它是 agent team 自动托管的本地 A2A specialist。
"description": f"Managed local A2A specialist for {role_name}.",
# system_prompt 注入角色视角、原始任务以及本次要求携带的技能上下文,
# 是 specialist 实际行为边界和任务目标的核心输入。
"system_prompt": self._system_prompt(role_name, task, skills or []),
# 允许它进行完整委派;也就是说该 specialist 自己可以继续向下分派任务,
# 而不是被限制为只能本地直接回答。
"delegation_mode": "full",
# 允许访问 MCP表示这个 specialist 在受外层权限控制的前提下可以使用 MCP 能力。
"allow_mcp": True,
# tags 用于分类、筛选和后续清理:
# - auto-provisioned / agent-team标明它是系统自动创建的团队成员
# - role_name.replace(" ", "-"):保留一个角色维度标签,便于检索
# - skills把本次技能要求也落到标签中方便观测和调试
# 使用 set 去重、sorted 排序,保证结果稳定。
"tags": sorted(set(["auto-provisioned", "agent-team", role_name.replace(" ", "-")] + list(skills or []))),
# aliases 提供额外可匹配名称,既支持原始角色名,也支持格式化后的展示名。
"aliases": [role_name, display_name],
# metadata 存放程序消费的结构化信息:
# - managed_by标记由哪个模块托管后续 cleanup 时会用来判定是否允许删除
# - role记录规范化后的角色名
# - task_fingerprint记录任务指纹便于追踪这个 specialist 绑定的是哪类任务上下文
"metadata": {
"managed_by": "agent_team_provisioning",
"role": role_name,
"task_fingerprint": self._fingerprint(task),
},
}
# 先读取一次已有记录,用于区分“首次创建”还是“刷新已有 specialist”。
# 注意:真正的写入动作由后面的 upsert 完成。
existing = self.store.get_subagent(agent_id)
# upsert 语义是:
# - 不存在则创建
# - 已存在则按新的 payload/config 刷新
# 这样调用方不需要区分 create / update 两条路径。
spec = self.store.upsert_subagent(payload, config)
# 日志区分 provisioned 和 refreshed便于排查
# - 为什么这次新建了一个 specialist
# - 或者为什么只是把旧的配置重新覆盖了一次
if existing is None:
logger.info("Provisioned local A2A specialist {} for role '{}'", spec.id, role_name)
else:
logger.info("Refreshed local A2A specialist {} for role '{}'", spec.id, role_name)
# 返回两类关键信息:
# - agent_id供上游继续引用这个 specialist
# - created明确告知这次是首次创建还是命中了已有对象并完成刷新
return SpecialistProvisionResult(agent_id=spec.id, created=existing is None)
def cleanup_local_specialists(self, agent_ids: list[str]) -> list[str]:
"""Delete managed specialists and return the ids actually removed."""
deleted: list[str] = []
for agent_id in dict.fromkeys(str(item).strip() for item in agent_ids if str(item).strip()):
spec = self.store.get_subagent(agent_id)
if spec is None:
continue
if not self._is_managed_specialist(spec.metadata, spec.tags):
logger.warning("Skipping cleanup for unmanaged local specialist candidate {}", agent_id)
continue
if self.store.delete_subagent(agent_id):
deleted.append(agent_id)
logger.info("Cleaned up local A2A specialist {}", agent_id)
return deleted
@staticmethod
def _is_managed_specialist(metadata: dict[str, Any], tags: list[str]) -> bool:
return (
metadata.get("managed_by") == "agent_team_provisioning"
or "auto-provisioned" in tags
)
def _specialist_id(self, role: str, task: str) -> str:
base = normalize_subagent_id(role)
return normalize_subagent_id(f"{base}-{self._fingerprint(task)}")
@staticmethod
def _fingerprint(task: str) -> str:
return hashlib.sha1(str(task or "").encode("utf-8")).hexdigest()[:8]
@staticmethod
def _display_name(role: str) -> str:
return " ".join(part.capitalize() for part in re.split(r"[\s_-]+", role.strip()) if part)
def _system_prompt(self, role: str, task: str, skills: list[str]) -> str:
# skills 是本次 team run 要求携带的技能上下文;这里仅写入提示词,
# 真正的工具可用性和权限仍由外层 AgentLoop / tool registry 控制。
skills_text = ", ".join(skills) if skills else "none"
role_text = re.sub(r"\s+", " ", str(role or "").strip()) or "general specialist"
# 这里保持一套完全通用的提示模板:
# - 不对具体角色做领域特化
# - 不规定固定输出格式
# - 只强调“按该角色名称隐含的职责边界来贡献结果”
return (
f"你是 nanobot agent team 中的 {role_text}\n\n"
"请围绕这个角色名称所隐含的职责边界处理原始团队任务。根据任务本身选择"
"合适的方法、工具、下游委派方式和输出格式,不要强行套用固定报告模板。"
"你的结果应该便于团队合并成最终答案;如果关键假设、阻塞点或风险会影响"
"结论,请明确指出。\n\n"
f"原始团队任务:\n{task}\n\n"
f"本次要求的技能:\n{skills_text}"
)