Files
beaver_project/app-instance/backend/nanobot/agent_team/provisioning.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

186 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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}"
)