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

312 lines
13 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.

"""本地委派执行器。
这个类不再负责“后台任务管理”和“结果回流”,只保留一件事:
在统一委派层要求执行本地任务时,提供一个受限工具集的本地 agent 执行环境。
"""
from __future__ import annotations
import json
import re
import time as _time
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Awaitable, Callable
from loguru import logger
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
from nanobot.agent.tools.spawn import NestedDelegateTool
from nanobot.agent.tools.web import WebFetchTool, WebSearchTool
from nanobot.providers.base import LLMProvider
if TYPE_CHECKING:
from nanobot.agent.delegation import DelegationManager
from nanobot.config.schema import ExecToolConfig
class SubagentManager:
"""用受限工具集在本地执行委派任务。"""
def __init__(
self,
provider: LLMProvider,
workspace: Path,
model: str | None = None,
temperature: float = 0.7,
max_tokens: int = 4096,
brave_api_key: str | None = None,
exec_config: ExecToolConfig | None = None,
restrict_to_workspace: bool = False,
):
from nanobot.config.schema import ExecToolConfig
# 这里保存的都是本地执行所需的静态配置,不再维护后台任务表。
self.provider = provider
self.workspace = workspace
self.model = model or provider.get_default_model()
self.temperature = temperature
self.max_tokens = max_tokens
self.brave_api_key = brave_api_key
self.exec_config = exec_config or ExecToolConfig()
self.restrict_to_workspace = restrict_to_workspace
self._nested_delegate: DelegationManager | None = None
def set_nested_delegate(self, manager: "DelegationManager | None") -> None:
"""注入 delegated worker 可用的受控下游委派器。"""
self._nested_delegate = manager
async def run_local_task(
self,
task: str,
label: str | None = None,
agent_id: str = "local-subagent",
agent_name: str = "Local Subagent",
system_prompt: str | None = None,
model: str | None = None,
progress_callback: Callable[..., Awaitable[None]] | None = None,
allow_nested_delegation: bool = True,
skill_context: str = "",
skill_names: list[str] | None = None,
) -> AgentRunResult:
"""执行一次本地委派任务,并返回结构化结果。"""
# 每次任务都新建一套局部工具注册表,避免不同任务之间共享临时状态。
tools = self._build_local_tools(
allow_nested_delegation=allow_nested_delegation,
skill_names=skill_names,
)
prompt = self._build_subagent_prompt(
task,
agent_name=agent_name,
custom_system_prompt=system_prompt,
allow_nested_delegation=allow_nested_delegation,
skill_context=skill_context,
)
# 本地委派不共享主会话历史,只带“专用 system prompt + 当前任务”。
messages: list[dict[str, Any]] = [
{"role": "system", "content": prompt},
{"role": "user", "content": task},
]
# 本地子 agent 也走“模型 -> 工具 -> 模型”的短循环,但轮数更保守。
max_iterations = 15
iteration = 0
final_result: str | None = None
while iteration < max_iterations:
iteration += 1
response = await self.provider.chat(
messages=messages,
tools=tools.get_definitions(),
model=model or self.model,
temperature=self.temperature,
max_tokens=self.max_tokens,
)
if response.has_tool_calls:
if progress_callback:
# 进度回调只发对用户有价值的文本,不把 `<think>` 之类内部推理暴露出去。
clean = self._strip_think(response.content)
if clean:
await progress_callback(clean, tool_hint=False)
# 额外补一条短工具提示,让上层 UI 知道当前在做什么。
await progress_callback(self._tool_hint(response.tool_calls), tool_hint=True)
tool_call_dicts = [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": json.dumps(tc.arguments, ensure_ascii=False),
},
}
for tc in response.tool_calls
]
messages.append({
"role": "assistant",
"content": response.content or "",
"tool_calls": tool_call_dicts,
})
for tool_call in response.tool_calls:
args_str = json.dumps(tool_call.arguments, ensure_ascii=False)
logger.debug("Agent [{}] executing: {} with arguments: {}", agent_id, tool_call.name, args_str)
# 真正执行工具后,把结果回填到 messages让下一轮模型能看到执行结果。
result = await tools.execute(tool_call.name, tool_call.arguments)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.name,
"content": result,
})
else:
# 没有继续调用工具时,视为任务已收敛,直接采纳当前回复。
final_result = response.content
break
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=status,
summary=final_result,
raw=raw,
)
def _build_local_tools(
self,
*,
allow_nested_delegation: bool,
skill_names: list[str] | None = None,
) -> ToolRegistry:
"""构建本地委派可用的受限工具集。"""
tools = ToolRegistry()
allowed_dir = self.workspace if self.restrict_to_workspace else None
protected_skill_paths = [self.workspace / "skills"]
# 文件工具统一按相同的 workspace / allowed_dir 约束注册。
tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir))
tools.register(
WriteFileTool(
workspace=self.workspace,
allowed_dir=allowed_dir,
protected_paths=protected_skill_paths,
)
)
tools.register(
EditFileTool(
workspace=self.workspace,
allowed_dir=allowed_dir,
protected_paths=protected_skill_paths,
)
)
# 本地命令执行沿用主配置里的超时和 workspace 限制。
tools.register(ExecTool(
working_dir=str(self.workspace),
timeout=self.exec_config.timeout,
restrict_to_workspace=self.restrict_to_workspace,
protected_paths=protected_skill_paths,
))
# 网络能力保持只读:搜索和抓取,不提供消息发送/再次委派等工具。
tools.register(WebSearchTool(api_key=self.brave_api_key))
tools.register(WebFetchTool())
if allow_nested_delegation and self._nested_delegate is not None:
tools.register(NestedDelegateTool(manager=self._nested_delegate, default_skills=skill_names))
return tools
@staticmethod
def _strip_think(text: str | None) -> str | None:
"""Remove provider-specific think blocks from visible progress text."""
if not text:
return None
return re.sub(r"<think>[\s\S]*?</think>", "", text).strip() or None
@staticmethod
def _tool_hint(tool_calls: list) -> str:
"""把工具调用列表格式化成简短进度提示。"""
def _fmt(tc):
val = next(iter(tc.arguments.values()), None) if tc.arguments else None
if not isinstance(val, str):
return tc.name
return f'{tc.name}("{val[:40]}...")' if len(val) > 40 else f'{tc.name}("{val}")'
return ", ".join(_fmt(tc) for tc in tool_calls)
def _build_subagent_prompt(
self,
task: str,
agent_name: str = "Local Subagent",
custom_system_prompt: str | None = None,
allow_nested_delegation: bool = True,
skill_context: str = "",
) -> str:
"""构建子代理专用 system prompt。"""
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
tz = _time.strftime("%Z") or "UTC"
# plugin agent 的自定义系统提示拼到末尾,保留通用约束,再叠加个性化指令。
extra = f"\n\n## Agent Instructions\n{custom_system_prompt.strip()}" if custom_system_prompt else ""
can_do_lines = [
"- Read and write files in the workspace",
"- Execute shell commands",
"- Search the web and fetch web pages",
"- Complete the task thoroughly",
]
cannot_do_lines = [
"- Send messages directly to users (no message tool available)",
"- Access the main agent's conversation history",
]
delegation_section = (
"\n## Downstream Delegation\n"
"- Do not delegate further. Complete the task yourself with the tools you have."
)
if allow_nested_delegation and self._nested_delegate is not None:
can_do_lines.append(
"- Use `delegate_task` for controlled downstream delegation when specialized help is required"
)
cannot_do_lines.append("- Do not start agent teams or use background delegation tools")
nested_summary = self._nested_delegate.build_nested_agents_summary()
summary_block = f"\n\n{nested_summary}" if nested_summary else ""
delegation_section = (
"\n## Downstream Delegation\n"
"- Use `delegate_task` only when a specialized downstream worker is actually needed.\n"
"- `strategy=\"a2a\"` delegates directly to an available A2A agent.\n"
"- `strategy=\"ephemeral_subagent\"` runs a temporary local worker for this task only.\n"
"- Never create, register, or persist a new local sub-agent through `subagentctl.py`, `/api/subagents`, or registry edits."
f"{summary_block}"
)
else:
cannot_do_lines.append("- Spawn other subagents or downstream workers")
skill_section = f"\n## Required Skills\n{skill_context.strip()}" if skill_context.strip() else ""
return f"""# {agent_name}
## Current Time
{now} ({tz})
You are a delegated agent spawned by the main agent to complete a specific task.
## Rules
1. Stay focused - complete only the assigned task, nothing else
2. Your final response will be reported back to the main agent
3. Do not initiate conversations or take on side tasks
4. Be concise but informative in your findings
5. Do not create or modify persistent local sub-agents unless the task explicitly requires that workflow
## What You Can Do
{chr(10).join(can_do_lines)}
## What You Cannot Do
{chr(10).join(cannot_do_lines)}
{delegation_section}
{skill_section}
## Workspace
Your workspace is at: {self.workspace}
Skills are available at: {self.workspace}/skills/ (read SKILL.md files as needed)
## Special Workflow
- If the task is about creating, updating, repairing, or deleting a persistent local sub-agent, read `skills/subagent-manager/SKILL.md` before making changes.
- For persistent local sub-agents, follow only the canonical workflow from that skill.
- Do not manually create `workspace/agents/<id>/agent.json` as a substitute for a persistent sub-agent.
- Do not manually edit `workspace/agents/registry.json` to register a persistent sub-agent.
- A valid persistent sub-agent must be created through `subagentctl.py` or `/api/subagents` and must end up at `workspace/agents/<id>_agent/AGENTS.json`.
When you have completed the task, provide a clear summary of your findings or actions.{extra}"""