修改了nanobot,往Hermes agent的风格走,进度1/3

This commit is contained in:
2026-04-20 18:11:14 +08:00
parent cdfc222c9f
commit 36882a7d7b
261 changed files with 12659 additions and 604 deletions

View File

@ -0,0 +1,267 @@
"""Resolve and provision team targets before execution.
该模块负责在真正启动 agent-team / swarms 执行前,把“任务需要哪些角色”
转换成一组可执行的 agent id。它优先复用 registry 里已有的 agent当没有合适
agent 覆盖某个角色时,再通过 ProvisioningManager 在本地创建 A2A specialist。
"""
from __future__ import annotations
from pathlib import Path
from loguru import logger
from nanobot.agent.agent_registry import AgentDescriptor, AgentRegistry
from nanobot.agent_team.provisioning import ProvisioningManager
from nanobot.agent_team.types import ResolvedTeamPlan
from nanobot.providers.base import LLMProvider
class TargetResolver:
"""把任务级的 specialist 需求解析成最终可执行的 agent id 列表。
解析策略分两层:
1. 先读取当前 registry 里所有可见 agent并过滤掉 router/planner 等
不适合作为群聊工作成员的 agent。
2. 如果调用方明确给出 required_specialists则把 role 和候选 agent 交给
LLM 直接选择最合适的已有 agentLLM 选不出来时才 provision 本地
specialist。没有明确角色时则直接使用过滤后的已有 agent若为空再
兜底创建 general specialist。
"""
def __init__(
self,
*,
workspace: Path,
registry: AgentRegistry,
provider: LLMProvider,
model: str | None = None,
max_parallel_agents: int = 16,
gateway_port: int = 18790,
provisioning: ProvisioningManager | None = None,
) -> None:
# max_parallel_agents 同时限制“最多尝试的角色数”和“最终返回的 agent 数”,
# 避免一次 team run 生成过多并行成员。
self.workspace = workspace
self.registry = registry
self.provider = provider
self.model = model or provider.get_default_model()
self.max_parallel_agents = max(1, max_parallel_agents)
self.provisioning = provisioning or ProvisioningManager(workspace, gateway_port=gateway_port)
async def resolve_team_targets(
self,
*,
task: str,
skills: list[str] | None = None,
required_specialists: list[str] | None = None,
) -> ResolvedTeamPlan:
"""解析一次 team run 的目标 agent。
Args:
task: 用户原始任务,用于 LLM 选 agent 和 specialist provision prompt。
skills: 本次任务要求携带的技能列表,会传给新 provision 的 specialist。
required_specialists: 上游 planner 推导出的角色需求。例如来自
AutoSwarmBuilder config 的 agent description或 skills 的简单映射。
Returns:
ResolvedTeamPlan: 包含已复用 agent、已 provision agent、最终执行目标、
选择理由和审计 metadata。
"""
# 清理空字符串/空白角色,避免后续创建出没有意义的 specialist。
required = [item for item in (required_specialists or []) if str(item).strip()]
# 直接读取 registry 当前所有可见 agent再过滤掉 router、planner、
# local-subagent 这类不适合作为 swarms/group worker 的 agent。
suggestions = [
agent
for agent in self.registry.list_agents(include_local_fallback=False)
if self._is_group_worker_candidate(agent)
]
# selected: 从 registry 复用的已有 agent id。
# covered_roles: 哪些 required role 已经被已有 agent 覆盖,用于 metadata。
# provisioned: 为缺失角色新建/确保存在的本地 specialist id。
# created_provisioned: 本次 run 真正新建出来的 specialist id后续自动清理只看它
# 避免把之前已经存在、只是被刷新/复用的 specialist 误删。
# actions: provision 审计记录,方便上层解释“为什么创建了某个 agent”。
selected: list[str] = []
covered_roles: list[str] = []
provisioned: list[str] = []
created_provisioned: list[str] = []
actions: list[dict[str, str]] = []
if required:
# 调用方给出了明确角色时,不再做本地词法规则匹配,而是直接把
# role + task + 候选 agent 交给 LLM 判断最适合复用哪个已有 agent。
# 这里切片是为了遵守 max_parallel_agents 上限。
for role in required[: self.max_parallel_agents]:
existing = await self._select_existing_for_role_with_llm(
task=task,
role=role,
suggestions=suggestions,
selected=selected,
)
if existing is not None:
selected.append(existing.id)
covered_roles.append(role)
continue
provision_result = await self.provisioning.ensure_local_specialist_with_result(
role=role,
task=task,
skills=skills or [],
)
agent_id = provision_result.agent_id
provisioned.append(agent_id)
if provision_result.created:
created_provisioned.append(agent_id)
actions.append({
"action": "ensure_local_specialist",
"role": role,
"agent_id": agent_id,
"created": str(provision_result.created).lower(),
})
else:
# 没有明确角色需求时,直接使用当前可见的已有 agent最多取并行上限。
selected = [agent.id for agent in suggestions[: self.max_parallel_agents]]
if not selected:
# 当前 registry 没有可用 worker 时,创建一个通用 specialist 作为最低可执行兜底。
provision_result = await self.provisioning.ensure_local_specialist_with_result(
role="general specialist",
task=task,
skills=skills or [],
)
agent_id = provision_result.agent_id
provisioned.append(agent_id)
if provision_result.created:
created_provisioned.append(agent_id)
actions.append({
"action": "ensure_local_specialist",
"role": "general specialist",
"agent_id": agent_id,
"created": str(provision_result.created).lower(),
})
# 合并已有 agent 和新 provision 的 agent
# - dict.fromkeys 保留顺序并去重,避免同一个 agent 被重复加入;
# - 最后再次截断,防止 selected + provisioned 总数超过并行上限。
final_targets = list(dict.fromkeys([*selected, *provisioned]))[: self.max_parallel_agents]
# selection_reason 是给上层/日志展示的粗粒度解释metadata 里会保留更细的明细。
reason = (
"已选择现有 registry agent。"
if selected and not provisioned
else "已选择现有 registry agent并为缺失角色补充了 specialist。"
if selected and provisioned
else "没有匹配到合适的现有 agent已补充本地 A2A specialist。"
if provisioned
else "没有匹配到合适的现有 agent且未补充任何 specialist。"
)
logger.info(
"Resolved agent-team targets selected={} provisioned={} final={}",
selected,
provisioned,
final_targets,
)
# ResolvedTeamPlan 是后续 orchestrator/swarms planner 使用的稳定边界:
# final_targets 用于实际执行selected/provisioned/actions/metadata 用于解释和调试。
return ResolvedTeamPlan(
selected_existing_targets=selected,
provisioned_targets=provisioned,
created_provisioned_targets=created_provisioned,
final_targets=final_targets,
selection_reason=reason,
provision_actions=actions,
metadata={
"required_specialists": required,
"available_agent_count": len(suggestions),
"covered_roles": covered_roles,
"created_provisioned_targets": created_provisioned,
"max_parallel_agents": self.max_parallel_agents,
},
)
@staticmethod
def _is_group_worker_candidate(agent: AgentDescriptor) -> bool:
"""判断一个 registry agent 是否适合作为 team/group worker。
router/planner 类 agent 通常负责调度,不应被当作普通成员加入 GroupChat 或
swarms worker 列表local-subagent 是通用本地代理入口,也避免在这里重复选中。
"""
probe = " ".join([
agent.id,
agent.name,
agent.description,
" ".join(agent.tags),
" ".join(agent.aliases),
]).lower()
if agent.id == "local-subagent":
return False
return not any(marker in probe for marker in ("chat-router", "router", "planner"))
async def _select_existing_for_role_with_llm(
self,
*,
task: str,
role: str,
suggestions: list[AgentDescriptor],
selected: list[str],
) -> AgentDescriptor | None:
"""让 LLM 从已有候选 agent 中为 role 选择最合适的一个。"""
candidates = [agent for agent in suggestions if agent.id not in selected]
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]
lines = []
for agent in candidates:
tags = ", ".join(agent.tags) if agent.tags else "none"
aliases = ", ".join(agent.aliases) if agent.aliases else "none"
lines.append(
f"- id: {agent.id}\n"
f" name: {agent.name}\n"
f" description: {agent.description}\n"
f" tags: {tags}\n"
f" aliases: {aliases}"
)
try:
response = await self.provider.chat(
messages=[
{
"role": "system",
"content": (
"You select one existing agent for a required team role.\n"
"Return exactly one agent id from the candidate list, or NONE.\n"
"Do not explain your reasoning."
),
},
{
"role": "user",
"content": (
f"Task:\n{task}\n\n"
f"Required role:\n{role}\n\n"
"Candidates:\n"
f"{chr(10).join(lines)}\n\n"
"Return exactly one candidate id, or NONE if none of them clearly fits."
),
},
],
model=self.model,
temperature=0,
max_tokens=32,
)
except Exception as exc:
logger.warning("LLM role selection failed for role '{}': {}", role, exc)
return None
raw = str(response.content or "").strip()
choice = raw.splitlines()[0].strip().strip("`'\"") if raw else ""
candidate_map = {agent.id: agent for agent in candidates}
if choice in candidate_map:
return candidate_map[choice]
if choice.upper() not in {"", "NONE"}:
logger.info("LLM role selection returned unknown agent id '{}' for role '{}'", choice, role)
return None