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:
@ -21,6 +21,7 @@ from nanobot.agent.plugins import PluginLoader
|
||||
from nanobot.agent.skills import SkillsLoader
|
||||
|
||||
_TOKEN_RE = re.compile(r"[a-z0-9_-]+")
|
||||
_CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -55,7 +56,6 @@ class AgentDescriptor:
|
||||
aliases: list[str] = field(default_factory=list)
|
||||
capabilities: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
support_group: bool = True
|
||||
support_streaming: bool = False
|
||||
|
||||
def matches(self, target: str) -> bool:
|
||||
@ -236,7 +236,6 @@ class AgentRegistry:
|
||||
kind="local_fallback",
|
||||
protocol=None,
|
||||
aliases=["subagent", "local"],
|
||||
support_group=True,
|
||||
)
|
||||
)
|
||||
|
||||
@ -263,27 +262,51 @@ class AgentRegistry:
|
||||
|
||||
def suggest_agents(self, query: str, limit: int = 5) -> list[AgentDescriptor]:
|
||||
"""基于简单词项打分为一段任务文本推荐 agent。"""
|
||||
tokens = {token for token in _TOKEN_RE.findall((query or "").lower()) if len(token) > 2}
|
||||
if not tokens:
|
||||
return []
|
||||
query_text = query or ""
|
||||
query_lower = query_text.lower()
|
||||
tokens = {token for token in _TOKEN_RE.findall(query_lower) if len(token) > 2}
|
||||
query_cjk_bigrams = self._cjk_bigrams(query_text)
|
||||
|
||||
scored: list[tuple[int, AgentDescriptor]] = []
|
||||
for agent in self.list_agents(include_local_fallback=False):
|
||||
haystack = agent.searchable_text()
|
||||
haystack_cjk_bigrams = self._cjk_bigrams(haystack)
|
||||
score = 0
|
||||
for token in tokens:
|
||||
# token 命中一次给基础分。
|
||||
if token in haystack:
|
||||
score += 2
|
||||
# 如果查询里直接出现了 agent 名或 id,再给更高权重。
|
||||
if agent.name.lower() in query.lower() or agent.id.lower() in query.lower():
|
||||
if agent.name.lower() in query_lower or agent.id.lower() in query_lower:
|
||||
score += 5
|
||||
for phrase in [agent.name, agent.id, *agent.tags, *agent.aliases]:
|
||||
phrase_text = str(phrase or "").strip()
|
||||
if not phrase_text:
|
||||
continue
|
||||
if phrase_text.lower() in query_lower or phrase_text in query_text:
|
||||
score += 3
|
||||
if query_cjk_bigrams and haystack_cjk_bigrams:
|
||||
# 中文任务没有空格分词,先用 bigram overlap 做粗粒度召回。
|
||||
score += min(6, len(query_cjk_bigrams & haystack_cjk_bigrams))
|
||||
if score > 0:
|
||||
scored.append((score, agent))
|
||||
|
||||
scored.sort(key=lambda item: (-item[0], item[1].name.lower()))
|
||||
return [agent for _, agent in scored[:limit]]
|
||||
|
||||
@staticmethod
|
||||
def _cjk_bigrams(text: str) -> set[str]:
|
||||
"""提取中文 bigram,用于中文任务的轻量召回。"""
|
||||
chunks = _CJK_RE.findall(str(text or ""))
|
||||
result: set[str] = set()
|
||||
for chunk in chunks:
|
||||
if len(chunk) == 1:
|
||||
result.add(chunk)
|
||||
continue
|
||||
for index in range(len(chunk) - 1):
|
||||
result.add(chunk[index:index + 2])
|
||||
return result
|
||||
|
||||
def build_agents_summary(self) -> str:
|
||||
"""把 agent 列表格式化成 prompt 可直接嵌入的 XML 片段。"""
|
||||
agents = self.list_agents()
|
||||
@ -310,9 +333,6 @@ class AgentRegistry:
|
||||
lines.append(f" <protocol>{esc(agent.protocol)}</protocol>")
|
||||
if agent.tags:
|
||||
lines.append(f" <tags>{esc(', '.join(agent.tags))}</tags>")
|
||||
lines.append(
|
||||
f" <supports-group>{str(agent.support_group).lower()}</supports-group>"
|
||||
)
|
||||
lines.append(" </agent>")
|
||||
lines.append("</agents>")
|
||||
return "\n".join(lines)
|
||||
@ -358,7 +378,6 @@ class AgentRegistry:
|
||||
],
|
||||
capabilities=record.get("capabilities", {}) if isinstance(record.get("capabilities"), dict) else {},
|
||||
metadata=record.get("metadata", {}) if isinstance(record.get("metadata"), dict) else {},
|
||||
support_group=bool(record.get("support_group", True)),
|
||||
support_streaming=bool(record.get("support_streaming", False)),
|
||||
)
|
||||
|
||||
@ -396,6 +415,5 @@ class AgentRegistry:
|
||||
],
|
||||
capabilities=card.get("capabilities", {}) if isinstance(card.get("capabilities"), dict) else {},
|
||||
metadata=card.get("metadata", {}) if isinstance(card.get("metadata"), dict) else {},
|
||||
support_group=bool(card.get("support_group", True)),
|
||||
support_streaming=bool(card.get("support_streaming", False)),
|
||||
)
|
||||
|
||||
@ -28,6 +28,8 @@ from nanobot.agent.process_events import (
|
||||
process_run_context,
|
||||
)
|
||||
from nanobot.agent.run_result import AgentRunResult
|
||||
from nanobot.agent_team.orchestrator import AgentTeamOrchestrator
|
||||
from nanobot.agent_team.types import BridgeResult
|
||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.providers.base import LLMProvider
|
||||
@ -61,12 +63,13 @@ class DelegationManager:
|
||||
def __init__(
|
||||
self,
|
||||
provider: LLMProvider,
|
||||
model: str | None,
|
||||
workspace: Path,
|
||||
bus: MessageBus,
|
||||
registry: AgentRegistry,
|
||||
skills_loader: "SkillsLoader | None",
|
||||
local_executor: Any,
|
||||
timeout_seconds: int = 30,
|
||||
timeout_seconds: int = 600,
|
||||
poll_interval_seconds: int = 2,
|
||||
card_cache_ttl_seconds: int = 300,
|
||||
max_parallel_agents: int = 4,
|
||||
@ -76,6 +79,7 @@ class DelegationManager:
|
||||
allow_local_delegation: bool = True,
|
||||
allow_plugin_delegation: bool = True,
|
||||
allow_local_fallback: bool = True,
|
||||
gateway_port: int = 18790,
|
||||
):
|
||||
self.provider = provider
|
||||
self.workspace = workspace
|
||||
@ -97,6 +101,18 @@ class DelegationManager:
|
||||
authz_config=authz_config,
|
||||
backend_identity=backend_identity,
|
||||
)
|
||||
# 新 orchestrator 只负责 agent team 路径;单 agent 委派仍走原有逻辑。
|
||||
self.agent_team_orchestrator = AgentTeamOrchestrator(
|
||||
workspace=workspace,
|
||||
provider=provider,
|
||||
model=model,
|
||||
registry=registry,
|
||||
bus=bus,
|
||||
local_executor=local_executor,
|
||||
member_runner=self._run_team_member_for_swarms,
|
||||
max_parallel_agents=self.max_parallel_agents,
|
||||
gateway_port=gateway_port,
|
||||
)
|
||||
self._running_tasks: dict[str, DelegationRun] = {}
|
||||
self._direct_announcement_callback: DirectAnnouncementCallback | None = None
|
||||
|
||||
@ -273,6 +289,20 @@ class DelegationManager:
|
||||
"""返回当前正在执行的委派数量。"""
|
||||
return len(self._running_tasks)
|
||||
|
||||
@staticmethod
|
||||
def _clean_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
"""删除空值,避免过程事件 metadata 出现大量噪声字段。"""
|
||||
cleaned: dict[str, Any] = {}
|
||||
for key, value in metadata.items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str) and not value.strip():
|
||||
continue
|
||||
if isinstance(value, (list, tuple, set, dict)) and not value:
|
||||
continue
|
||||
cleaned[key] = value
|
||||
return cleaned
|
||||
|
||||
@staticmethod
|
||||
def _ui_status(status: str | None) -> str:
|
||||
"""把底层状态归一化成前端更稳定的显示状态。"""
|
||||
@ -287,6 +317,29 @@ class DelegationManager:
|
||||
return "error"
|
||||
return probe or "running"
|
||||
|
||||
async def _emit_team_progress(
|
||||
self,
|
||||
run_id: str,
|
||||
text: str,
|
||||
*,
|
||||
stage_label: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""为 agent team 根 run 发一条过程可观察事件。"""
|
||||
await emit_process_event(
|
||||
"process_run_progress",
|
||||
run_id=run_id,
|
||||
actor_type="system",
|
||||
actor_id="agent-group",
|
||||
actor_name="Agent Team",
|
||||
text=text,
|
||||
metadata=self._clean_metadata({
|
||||
"source": "agent_team_dispatch",
|
||||
"stage_label": stage_label,
|
||||
**(metadata or {}),
|
||||
}),
|
||||
)
|
||||
|
||||
async def _emit_agent_started(
|
||||
self,
|
||||
run_id: str,
|
||||
@ -310,7 +363,6 @@ class DelegationManager:
|
||||
metadata={
|
||||
"kind": descriptor.kind,
|
||||
"protocol": descriptor.protocol,
|
||||
"support_group": descriptor.support_group,
|
||||
"support_streaming": descriptor.support_streaming,
|
||||
"delegated_task": task,
|
||||
},
|
||||
@ -371,31 +423,56 @@ class DelegationManager:
|
||||
actor_type="system",
|
||||
actor_id="agent-group",
|
||||
actor_name="Agent Team",
|
||||
source="agent_team",
|
||||
title=label,
|
||||
status="running",
|
||||
metadata={"targets": targets},
|
||||
metadata=self._clean_metadata({
|
||||
"source": "agent_team_dispatch",
|
||||
"phase": "dispatch",
|
||||
"stage_label": "团队任务已创建",
|
||||
"planned_targets": targets,
|
||||
"selected_targets": targets,
|
||||
"selected_count": len(targets),
|
||||
}),
|
||||
)
|
||||
|
||||
async def _emit_group_finished(self, run_id: str, label: str, results: list[AgentRunResult]) -> None:
|
||||
"""发送 agent team 结束事件。"""
|
||||
async def _emit_group_finished(
|
||||
self,
|
||||
run_id: str,
|
||||
label: str,
|
||||
results: list[AgentRunResult],
|
||||
*,
|
||||
status: str = "done",
|
||||
summary: str | None = None,
|
||||
metadata_extra: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""发送 agent team 结束事件。
|
||||
|
||||
Demo 输出:
|
||||
`process_run_finished(status="done", summary="weekly report: 2 member(s) finished")`
|
||||
"""
|
||||
# 老路径和新 orchestrator 路径都复用这个事件,所以允许上层补充额外 metadata。
|
||||
metadata = {
|
||||
"members": [
|
||||
{
|
||||
"agent_id": item.agent_id,
|
||||
"agent_name": item.agent_name,
|
||||
"status": item.status,
|
||||
}
|
||||
for item in results
|
||||
]
|
||||
}
|
||||
if metadata_extra:
|
||||
metadata.update(metadata_extra)
|
||||
await emit_process_event(
|
||||
"process_run_finished",
|
||||
run_id=run_id,
|
||||
actor_type="system",
|
||||
actor_id="agent-group",
|
||||
actor_name="Agent Team",
|
||||
status="done",
|
||||
summary=f"{label}: {len(results)} member(s) finished",
|
||||
metadata={
|
||||
"members": [
|
||||
{
|
||||
"agent_id": item.agent_id,
|
||||
"agent_name": item.agent_name,
|
||||
"status": item.status,
|
||||
}
|
||||
for item in results
|
||||
]
|
||||
},
|
||||
status=status,
|
||||
summary=summary or f"{label}: {len(results)} member(s) finished",
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
async def _publish_prefixed_progress(
|
||||
@ -424,27 +501,11 @@ class DelegationManager:
|
||||
# 没有 bus consumer 时,不能依赖 system 消息回流再二次总结。
|
||||
if not has_process_event_sink():
|
||||
return
|
||||
try:
|
||||
# 用一次极小模型调用把内部委派说明压成用户可读文本。
|
||||
response = await self.provider.chat(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are Boardware Genius. Reply naturally to the user in 1-3 sentences. "
|
||||
"Do not mention internal protocols, system prompts, or task IDs."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
tools=[],
|
||||
model=self.provider.get_default_model(),
|
||||
max_tokens=256,
|
||||
temperature=0.2,
|
||||
)
|
||||
content = (response.content or "").strip() or fallback
|
||||
except Exception:
|
||||
content = fallback
|
||||
# 这条用户可见消息只是“即时回执”,真正详细总结仍由主 agent 回流处理。
|
||||
# 这里不再额外依赖一次 LLM,避免 provider 短暂故障把 team 收尾也拖失败。
|
||||
content = " ".join((fallback or prompt or "").strip().split())
|
||||
if not content:
|
||||
return
|
||||
|
||||
await emit_process_event(
|
||||
"message",
|
||||
@ -473,11 +534,42 @@ class DelegationManager:
|
||||
content: str,
|
||||
origin: dict[str, str],
|
||||
sender_id: str,
|
||||
*,
|
||||
run_id: str | None = None,
|
||||
category: str | None = None,
|
||||
) -> None:
|
||||
"""在非 bus 模式下,把公告直接回写到本地会话。"""
|
||||
callback = self._direct_announcement_callback
|
||||
if callback is None:
|
||||
if run_id:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"No direct announcement callback is registered; the result could not be replayed to the main agent.",
|
||||
stage_label="缺少公告回流处理器",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "direct_callback_missing",
|
||||
"announcement_path": "direct",
|
||||
"announcement_sender_id": sender_id,
|
||||
"announcement_category": category,
|
||||
},
|
||||
)
|
||||
return
|
||||
if run_id:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"Sending the agent-team result back through the direct announcement callback.",
|
||||
stage_label="请求主 Agent 总结",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "direct_callback_start",
|
||||
"announcement_path": "direct",
|
||||
"announcement_sender_id": sender_id,
|
||||
"announcement_category": category,
|
||||
"origin_channel": origin.get("channel"),
|
||||
"origin_chat_id": origin.get("chat_id"),
|
||||
},
|
||||
)
|
||||
try:
|
||||
await callback(
|
||||
content,
|
||||
@ -485,7 +577,34 @@ class DelegationManager:
|
||||
sender_id,
|
||||
not has_process_event_sink(),
|
||||
)
|
||||
if run_id:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"The direct announcement callback completed successfully.",
|
||||
stage_label="主 Agent 总结完成",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "direct_callback_complete",
|
||||
"announcement_path": "direct",
|
||||
"announcement_sender_id": sender_id,
|
||||
"announcement_category": category,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
if run_id:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
f"Direct announcement callback failed: {exc}",
|
||||
stage_label="主 Agent 总结失败",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "direct_callback_failed",
|
||||
"announcement_path": "direct",
|
||||
"announcement_sender_id": sender_id,
|
||||
"announcement_category": category,
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
logger.warning("Failed to handle direct delegation announcement: {}", exc)
|
||||
|
||||
async def _run_dispatch(
|
||||
@ -510,23 +629,80 @@ class DelegationManager:
|
||||
if is_group:
|
||||
planned_targets = list(targets)
|
||||
await self._emit_group_started(run_id, label, planned_targets)
|
||||
results = await self._run_group(
|
||||
task,
|
||||
label,
|
||||
None,
|
||||
targets,
|
||||
strategy,
|
||||
skills,
|
||||
origin=origin,
|
||||
run_id=run_id,
|
||||
announce_via_bus=announce_via_bus,
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"Agent team dispatch accepted and moved into swarms orchestration.",
|
||||
stage_label="开始团队编排",
|
||||
metadata={
|
||||
"phase": "dispatch",
|
||||
"strategy": strategy,
|
||||
"execution_path": "swarms",
|
||||
"announce_via_bus": announce_via_bus,
|
||||
"requested_targets": planned_targets,
|
||||
},
|
||||
)
|
||||
await self._emit_group_finished(run_id, label, results)
|
||||
await self._announce_group_result(
|
||||
logger.info(
|
||||
"Agent team [{}] dispatch started: mode=swarms announce_via_bus={} requested_targets={}",
|
||||
run_id,
|
||||
announce_via_bus,
|
||||
planned_targets,
|
||||
)
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"DelegationManager handed the task to AgentTeamOrchestrator.",
|
||||
stage_label="编排器接管任务",
|
||||
metadata={
|
||||
"phase": "orchestrator",
|
||||
"step": "handoff_to_orchestrator",
|
||||
"requested_targets": planned_targets,
|
||||
},
|
||||
)
|
||||
orchestrated = await self.agent_team_orchestrator.run_task(
|
||||
task=task,
|
||||
label=label,
|
||||
skills=skills,
|
||||
origin=origin,
|
||||
announce_via_bus=announce_via_bus,
|
||||
run_id=run_id,
|
||||
)
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"AgentTeamOrchestrator returned a final bridge result.",
|
||||
stage_label="编排器已返回结果",
|
||||
metadata={
|
||||
"phase": "orchestrator",
|
||||
"step": "orchestrator_result_ready",
|
||||
"execution_mode": orchestrated.mode.value,
|
||||
"candidate_procedure_id": (
|
||||
orchestrated.candidate_procedure.id
|
||||
if orchestrated.candidate_procedure is not None
|
||||
else None
|
||||
),
|
||||
"attempt_count": len(orchestrated.attempts),
|
||||
"success": orchestrated.success,
|
||||
},
|
||||
)
|
||||
await self._emit_group_finished(
|
||||
run_id,
|
||||
label,
|
||||
orchestrated.last_member_results(),
|
||||
status="done" if orchestrated.success else "error",
|
||||
summary=orchestrated.summary,
|
||||
metadata_extra={
|
||||
"execution_mode": orchestrated.mode.value,
|
||||
"candidate_procedure_id": (
|
||||
orchestrated.candidate_procedure.id
|
||||
if orchestrated.candidate_procedure is not None
|
||||
else None
|
||||
),
|
||||
"attempts": [attempt.to_dict() for attempt in orchestrated.attempts],
|
||||
},
|
||||
)
|
||||
await self._announce_orchestrator_result(
|
||||
run_id,
|
||||
label,
|
||||
task,
|
||||
results,
|
||||
orchestrated,
|
||||
origin,
|
||||
announce_via_bus=announce_via_bus,
|
||||
)
|
||||
@ -591,6 +767,16 @@ class DelegationManager:
|
||||
summary=f"Error: {exc}",
|
||||
)
|
||||
if is_group:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
f"Agent team execution failed before announcement: {exc}",
|
||||
stage_label="团队执行失败",
|
||||
metadata={
|
||||
"phase": "error",
|
||||
"step": "dispatch_failed",
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
await emit_process_event(
|
||||
"process_run_finished",
|
||||
run_id=run_id,
|
||||
@ -777,94 +963,57 @@ class DelegationManager:
|
||||
and ("subagent" in lowered or "sub-agent" in lowered)
|
||||
)
|
||||
|
||||
async def _run_group(
|
||||
async def _run_team_member_for_swarms(
|
||||
self,
|
||||
descriptor: AgentDescriptor,
|
||||
task: str,
|
||||
label: str,
|
||||
target: str | None,
|
||||
targets: list[str],
|
||||
strategy: str,
|
||||
parent_run_id: str,
|
||||
skills: list[str],
|
||||
origin: dict[str, str],
|
||||
run_id: str,
|
||||
announce_via_bus: bool,
|
||||
) -> list[AgentRunResult]:
|
||||
"""并行执行一组 agent,并汇总结果。"""
|
||||
resolved_targets = list(targets)
|
||||
if target:
|
||||
resolved_targets.append(target)
|
||||
if not resolved_targets:
|
||||
# 未显式给出目标时,根据任务文本自动挑若干个候选 agent。
|
||||
suggestions = [
|
||||
agent for agent in self.registry.suggest_agents(task, limit=self.max_parallel_agents * 2)
|
||||
if self._descriptor_allowed(agent)
|
||||
]
|
||||
resolved_targets = [agent.id for agent in suggestions]
|
||||
if not resolved_targets:
|
||||
descriptor = self.registry.get_agent("local-subagent")
|
||||
if descriptor and self._descriptor_allowed(descriptor):
|
||||
resolved_targets = [descriptor.id]
|
||||
if not resolved_targets:
|
||||
raise ValueError("No agents available for group delegation")
|
||||
resolved_targets = list(dict.fromkeys(resolved_targets))
|
||||
|
||||
descriptors: list[AgentDescriptor] = []
|
||||
missing: list[str] = []
|
||||
for item in resolved_targets:
|
||||
descriptor = self.registry.get_agent(item)
|
||||
if descriptor is None:
|
||||
missing.append(item)
|
||||
else:
|
||||
self._ensure_descriptor_allowed(descriptor)
|
||||
descriptors.append(descriptor)
|
||||
if missing:
|
||||
raise ValueError(f"Agent(s) not found: {', '.join(missing)}")
|
||||
|
||||
semaphore = asyncio.Semaphore(self.max_parallel_agents)
|
||||
|
||||
async def _run_one(descriptor: AgentDescriptor) -> AgentRunResult:
|
||||
# group 内每个成员都分配独立 child run_id,便于前端区分子树。
|
||||
child_run_id = new_run_id("agent")
|
||||
async with semaphore:
|
||||
try:
|
||||
await self._emit_agent_started(
|
||||
child_run_id,
|
||||
descriptor,
|
||||
label,
|
||||
parent_run_id=run_id,
|
||||
task=task,
|
||||
)
|
||||
result = await self._execute_descriptor(
|
||||
descriptor,
|
||||
task,
|
||||
label,
|
||||
skill_names=skills,
|
||||
event_callback=self._build_progress_callback(
|
||||
origin,
|
||||
descriptor,
|
||||
event_run_id=child_run_id,
|
||||
tracking_run_id=run_id,
|
||||
publish_via_bus=announce_via_bus,
|
||||
),
|
||||
task_callback=self._build_task_callback(run_id, descriptor),
|
||||
process_run_id=child_run_id,
|
||||
)
|
||||
await self._emit_agent_finished(child_run_id, descriptor, result)
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
await self._emit_agent_cancelled(child_run_id, descriptor, label)
|
||||
raise
|
||||
except Exception as exc:
|
||||
result = AgentRunResult(
|
||||
agent_id=descriptor.id,
|
||||
agent_name=descriptor.name,
|
||||
status="error",
|
||||
summary=f"Error: {exc}",
|
||||
)
|
||||
await self._emit_agent_finished(child_run_id, descriptor, result)
|
||||
return result
|
||||
results = await asyncio.gather(*[_run_one(agent) for agent in descriptors])
|
||||
return results
|
||||
) -> AgentRunResult:
|
||||
"""Execute one swarms-selected nanobot agent as a process child run."""
|
||||
state = self._running_tasks.get(parent_run_id)
|
||||
label = "Agent Team" if state is None else state.label
|
||||
origin = {"channel": "system", "chat_id": "direct"} if state is None else state.origin
|
||||
announce_via_bus = True if state is None else state.announce_via_bus
|
||||
child_run_id = new_run_id("agent")
|
||||
try:
|
||||
self._ensure_descriptor_allowed(descriptor)
|
||||
await self._emit_agent_started(
|
||||
child_run_id,
|
||||
descriptor,
|
||||
label,
|
||||
parent_run_id=parent_run_id,
|
||||
task=task,
|
||||
)
|
||||
result = await self._execute_descriptor(
|
||||
descriptor,
|
||||
task,
|
||||
label,
|
||||
skill_names=skills,
|
||||
event_callback=self._build_progress_callback(
|
||||
origin,
|
||||
descriptor,
|
||||
event_run_id=child_run_id,
|
||||
tracking_run_id=parent_run_id,
|
||||
publish_via_bus=announce_via_bus,
|
||||
),
|
||||
task_callback=self._build_task_callback(parent_run_id, descriptor),
|
||||
process_run_id=child_run_id,
|
||||
)
|
||||
await self._emit_agent_finished(child_run_id, descriptor, result)
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
await self._emit_agent_cancelled(child_run_id, descriptor, label)
|
||||
raise
|
||||
except Exception as exc:
|
||||
result = AgentRunResult(
|
||||
agent_id=descriptor.id,
|
||||
agent_name=descriptor.name,
|
||||
status="error",
|
||||
summary=f"Error: {exc}",
|
||||
)
|
||||
await self._emit_agent_finished(child_run_id, descriptor, result)
|
||||
return result
|
||||
|
||||
async def _execute_descriptor(
|
||||
self,
|
||||
@ -1164,52 +1313,102 @@ class DelegationManager:
|
||||
)
|
||||
logger.debug("Delegation [{}] announced result", run_id)
|
||||
|
||||
async def _announce_group_result(
|
||||
async def _announce_orchestrator_result(
|
||||
self,
|
||||
run_id: str,
|
||||
label: str,
|
||||
task: str,
|
||||
results: list[AgentRunResult],
|
||||
result: BridgeResult,
|
||||
origin: dict[str, str],
|
||||
*,
|
||||
announce_via_bus: bool,
|
||||
) -> None:
|
||||
"""公告 agent team 汇总结果。"""
|
||||
lines = [f"[Agent team '{label}' completed]", "", f"Task: {task}", "", "Members:"]
|
||||
for result in results:
|
||||
lines.append(f"- {result.agent_name} ({result.agent_id}): {result.status}")
|
||||
lines.extend(["", "Results:"])
|
||||
for result in results:
|
||||
lines.append(f"### {result.agent_name} ({result.status})")
|
||||
lines.append(result.summary)
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Summarize this naturally for the user. Mention disagreements or failures if any."
|
||||
"""公告 orchestrator 驱动的 agent team 结果。
|
||||
|
||||
Demo 输出:
|
||||
`[Agent team 'weekly report' completed]\nExecution mode: swarms\nMatched procedure: procedure-a1b2c3d4`
|
||||
"""
|
||||
# 这里显式保留 mode / procedure 信息,方便主 agent 做更准确的用户总结。
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"Preparing orchestrated agent-team summary for the main agent.",
|
||||
stage_label="整理团队结果",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "build_orchestrator_summary",
|
||||
"execution_mode": result.mode.value,
|
||||
"attempt_count": len(result.attempts),
|
||||
},
|
||||
)
|
||||
status_text = "completed" if result.success else "failed"
|
||||
lines = [
|
||||
f"[Agent team '{label}' {status_text}]",
|
||||
"",
|
||||
f"Task: {task}",
|
||||
f"Execution mode: {result.mode.value}",
|
||||
]
|
||||
if result.matched_procedure is not None:
|
||||
lines.append(
|
||||
"Matched procedure: "
|
||||
f"{result.matched_procedure.id} "
|
||||
f"(confidence={result.matched_procedure.confidence:.2f})"
|
||||
)
|
||||
if result.attempts:
|
||||
lines.extend(["", "Attempts:"])
|
||||
for attempt in result.attempts:
|
||||
attempt_status = "ok" if attempt.success else "error"
|
||||
lines.append(f"- {attempt.mode.value}: {attempt_status}")
|
||||
if attempt.error:
|
||||
lines.append(f" error: {attempt.error}")
|
||||
|
||||
member_results = result.last_member_results()
|
||||
if member_results:
|
||||
lines.extend(["", "Members:"])
|
||||
for item in member_results:
|
||||
lines.append(f"- {item.agent_name} ({item.agent_id}): {item.status}")
|
||||
lines.extend(["", "Results:"])
|
||||
for item in member_results:
|
||||
lines.append(f"### {item.agent_name} ({item.status})")
|
||||
lines.append(item.summary)
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"Final summary:",
|
||||
result.summary,
|
||||
"",
|
||||
"Summarize this naturally for the user. Mention disagreements or failures if any.",
|
||||
])
|
||||
summary = "\n".join(lines).strip()
|
||||
if announce_via_bus:
|
||||
await self._publish_announcement(
|
||||
summary,
|
||||
origin,
|
||||
sender_id="delegation-group",
|
||||
sender_id="delegation-team",
|
||||
run_id=run_id,
|
||||
category="agent_team_orchestrated",
|
||||
)
|
||||
else:
|
||||
await self._notify_direct_announcement(
|
||||
summary,
|
||||
origin,
|
||||
"delegation-group",
|
||||
"delegation-team",
|
||||
run_id=run_id,
|
||||
category="agent_team_orchestrated",
|
||||
)
|
||||
await self._emit_direct_user_message(
|
||||
summary,
|
||||
"Agent team 已完成,请查看各 agent 的结果与最终结论。",
|
||||
"Agent team 已完成,请查看最终结论与各次尝试摘要。",
|
||||
)
|
||||
logger.debug("Agent team [{}] announced result", run_id)
|
||||
logger.debug("Agent team [{}] announced orchestrated result", run_id)
|
||||
|
||||
async def _publish_announcement(
|
||||
self,
|
||||
content: str,
|
||||
origin: dict[str, str],
|
||||
sender_id: str,
|
||||
*,
|
||||
run_id: str | None = None,
|
||||
category: str | None = None,
|
||||
) -> None:
|
||||
"""通过 system inbound 消息把公告重新送回主 agent 链路。"""
|
||||
msg = InboundMessage(
|
||||
@ -1219,3 +1418,18 @@ class DelegationManager:
|
||||
content=content,
|
||||
)
|
||||
await self.bus.publish_inbound(msg)
|
||||
if run_id:
|
||||
await self._emit_team_progress(
|
||||
run_id,
|
||||
"Team summary has been published back to the main agent via the system bus.",
|
||||
stage_label="团队结果已回流",
|
||||
metadata={
|
||||
"phase": "announcement",
|
||||
"step": "bus_publish_complete",
|
||||
"announcement_path": "bus",
|
||||
"announcement_sender_id": sender_id,
|
||||
"announcement_category": category,
|
||||
"origin_channel": origin.get("channel"),
|
||||
"origin_chat_id": origin.get("chat_id"),
|
||||
},
|
||||
)
|
||||
|
||||
@ -83,6 +83,7 @@ class AgentLoop:
|
||||
allow_local_delegation: bool = True,
|
||||
allow_plugin_delegation: bool = True,
|
||||
include_plugin_agents: bool = True,
|
||||
gateway_port: int = 18790,
|
||||
):
|
||||
from nanobot.config.schema import A2AConfig, ExecToolConfig
|
||||
# 基础依赖与运行参数。
|
||||
@ -142,6 +143,7 @@ class AgentLoop:
|
||||
)
|
||||
self.delegation = DelegationManager(
|
||||
provider=provider,
|
||||
model=self.model,
|
||||
workspace=workspace,
|
||||
bus=bus,
|
||||
registry=self.agent_registry,
|
||||
@ -157,6 +159,7 @@ class AgentLoop:
|
||||
allow_local_delegation=self.allow_local_delegation,
|
||||
allow_plugin_delegation=self.allow_plugin_delegation,
|
||||
allow_local_fallback=self.include_local_fallback,
|
||||
gateway_port=gateway_port,
|
||||
)
|
||||
self.subagents.set_nested_delegate(self.delegation)
|
||||
|
||||
|
||||
@ -6,6 +6,42 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
_PLACEHOLDER_SUMMARY_MARKERS = (
|
||||
"task completed but no final response was generated",
|
||||
"no final response was generated",
|
||||
"已启动代理团队",
|
||||
"代理团队正在后台工作",
|
||||
"agent team [",
|
||||
"spawn_agent_team",
|
||||
"error calling llm",
|
||||
"litellm.timeout",
|
||||
"dashscopeexception",
|
||||
"service temporarily unavailable",
|
||||
"planner调用失败",
|
||||
"本任务当前不可执行",
|
||||
"无法由单一非sop工具完成",
|
||||
)
|
||||
|
||||
|
||||
def normalize_summary_text(text: str | None) -> str:
|
||||
"""把摘要文本压成便于判定的稳定形式。"""
|
||||
return " ".join(str(text or "").strip().split())
|
||||
|
||||
|
||||
def contains_placeholder_summary(text: str | None) -> bool:
|
||||
"""判断摘要是否只是占位兜底文本。"""
|
||||
normalized = normalize_summary_text(text).lower()
|
||||
if not normalized:
|
||||
return True
|
||||
return any(marker in normalized for marker in _PLACEHOLDER_SUMMARY_MARKERS)
|
||||
|
||||
|
||||
def has_meaningful_summary(text: str | None) -> bool:
|
||||
"""判断摘要是否包含可复用的真实结果。"""
|
||||
normalized = normalize_summary_text(text)
|
||||
return bool(normalized) and not contains_placeholder_summary(normalized)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRunResult:
|
||||
"""统一描述一次 agent 执行结果。"""
|
||||
|
||||
@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from nanobot.agent.run_result import AgentRunResult
|
||||
from nanobot.agent.run_result import AgentRunResult, has_meaningful_summary
|
||||
from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
|
||||
from nanobot.agent.tools.registry import ToolRegistry
|
||||
from nanobot.agent.tools.shell import ExecTool
|
||||
@ -147,15 +147,24 @@ class SubagentManager:
|
||||
final_result = response.content
|
||||
break
|
||||
|
||||
if final_result is None:
|
||||
# 兜底避免出现“任务做完了但完全没文本”的空结果。
|
||||
status = "ok"
|
||||
raw: dict[str, Any] | None = None
|
||||
if not has_meaningful_summary(final_result):
|
||||
# 兜底避免出现“任务做完了但完全没文本”的空结果,并显式标记为失败,
|
||||
# 防止上层把这类占位结果学习成 procedure。
|
||||
final_result = "Task completed but no final response was generated."
|
||||
status = "error"
|
||||
raw = {
|
||||
"reason": "no_final_response_generated",
|
||||
"iterations": iteration,
|
||||
}
|
||||
|
||||
return AgentRunResult(
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
status="ok",
|
||||
status=status,
|
||||
summary=final_result,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
def _build_local_tools(
|
||||
|
||||
@ -174,7 +174,6 @@ class LocalSubagentStore:
|
||||
"local_subagent": True,
|
||||
},
|
||||
"capabilities": {"streaming": False},
|
||||
"support_group": False,
|
||||
"support_streaming": False,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user