修改了nanobot,往Hermes agent的风格走,进度1/3
This commit is contained in:
267
app-instance/backend-old/nanobot/agent_team/target_resolver.py
Normal file
267
app-instance/backend-old/nanobot/agent_team/target_resolver.py
Normal 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 直接选择最合适的已有 agent;LLM 选不出来时才 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
|
||||
Reference in New Issue
Block a user