fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,19 @@ from app.agent_runtime.orchestrator import (
|
||||
OrchestratorResult,
|
||||
OrchestratorStep,
|
||||
)
|
||||
from app.agent_runtime.swarm import (
|
||||
SwarmRuntime,
|
||||
SwarmConfig,
|
||||
SwarmMode,
|
||||
SwarmLeader,
|
||||
SwarmTeammate,
|
||||
SwarmMailbox,
|
||||
SwarmTask,
|
||||
SwarmResult,
|
||||
TaskStatus,
|
||||
TeammateResult,
|
||||
create_swarm,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgentRuntime",
|
||||
@@ -44,4 +57,16 @@ __all__ = [
|
||||
"OrchestratorAgentConfig",
|
||||
"OrchestratorResult",
|
||||
"OrchestratorStep",
|
||||
# Swarm
|
||||
"SwarmRuntime",
|
||||
"SwarmConfig",
|
||||
"SwarmMode",
|
||||
"SwarmLeader",
|
||||
"SwarmTeammate",
|
||||
"SwarmMailbox",
|
||||
"SwarmTask",
|
||||
"SwarmResult",
|
||||
"TaskStatus",
|
||||
"TeammateResult",
|
||||
"create_swarm",
|
||||
]
|
||||
|
||||
@@ -80,6 +80,43 @@ class AgentContext:
|
||||
if not self._messages:
|
||||
self._system_prompt = prompt
|
||||
|
||||
# ──────────────────── 消息操作(为 Compaction 提供) ────────────────────
|
||||
|
||||
@property
|
||||
def raw_messages(self) -> List[Dict[str, Any]]:
|
||||
"""获取原始消息列表(不含自动 prepend 的 system prompt)。
|
||||
|
||||
用于 CompactionEngine 直接操作 _messages,避免 system prompt 重复。
|
||||
"""
|
||||
return self._messages
|
||||
|
||||
def replace_internal_messages(self, new_messages: List[Dict[str, Any]]) -> None:
|
||||
"""替换全部内部消息(CompactionEngine 用)。"""
|
||||
self._messages = new_messages
|
||||
|
||||
def remove_messages_before(self, index: int) -> None:
|
||||
"""移除指定索引之前的所有消息(保留 system prompt 在首位时的位置)。
|
||||
|
||||
注意:不修改 _system_prompt;调用方通过 messages 属性获取时会自动 prepend。
|
||||
"""
|
||||
if index > 0:
|
||||
self._messages = self._messages[index:]
|
||||
|
||||
def replace_message_range(
|
||||
self, start: int, end: int, new_messages: List[Dict[str, Any]]
|
||||
) -> None:
|
||||
"""替换消息列表中 [start, end) 区间的消息。"""
|
||||
self._messages[start:end] = new_messages
|
||||
|
||||
def estimate_tokens(self, token_counter=None) -> int:
|
||||
"""估算当前消息列表的总 token 数(含 system prompt)。"""
|
||||
from app.core.token_counter import TokenCounter
|
||||
if token_counter is None:
|
||||
token_counter = TokenCounter()
|
||||
return token_counter.count_messages(self.messages)
|
||||
|
||||
# ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
def reset(self) -> None:
|
||||
"""重置上下文(保留 system prompt 和 session_id)。"""
|
||||
self._messages = []
|
||||
|
||||
@@ -126,6 +126,7 @@ class AgentRuntime:
|
||||
team_share_enabled=self.config.memory.team_share_enabled,
|
||||
memory_dir_enabled=self.config.memory.memory_dir_enabled,
|
||||
memory_dir_path=self.config.memory.memory_dir_path,
|
||||
parent_agent_id=self.config.memory.parent_agent_id,
|
||||
)
|
||||
self.tool_manager = tool_manager or AgentToolManager(
|
||||
include_tools=self.config.tools.include_tools,
|
||||
@@ -1821,8 +1822,17 @@ class _LLMClient:
|
||||
except Exception as ce:
|
||||
logger.error("ReactiveCompact 失败: %s", ce)
|
||||
|
||||
# 降级回退:主模型失败时尝试 fallback_llm
|
||||
# 降级回退:主模型失败时尝试 fallback_llm(优先每 Agent 配置,其次全局配置)
|
||||
fallback = self._config.fallback_llm
|
||||
if not fallback:
|
||||
# 全局降级配置兜底
|
||||
fb_model = settings.FALLBACK_LLM_MODEL
|
||||
if fb_model:
|
||||
fallback = {
|
||||
"model": fb_model,
|
||||
"api_key": settings.FALLBACK_LLM_API_KEY or None,
|
||||
"base_url": settings.FALLBACK_LLM_BASE_URL or None,
|
||||
}
|
||||
if fallback and isinstance(fallback, dict) and not _is_fallback:
|
||||
fb_model = fallback.get("model")
|
||||
fb_api_key = fallback.get("api_key")
|
||||
|
||||
@@ -44,6 +44,7 @@ class AgentMemory:
|
||||
team_share_enabled: bool = False,
|
||||
memory_dir_enabled: bool = False,
|
||||
memory_dir_path: str = "",
|
||||
parent_agent_id: Optional[str] = None,
|
||||
):
|
||||
self.scope_kind = scope_kind
|
||||
self.scope_id = scope_id or "default"
|
||||
@@ -54,6 +55,7 @@ class AgentMemory:
|
||||
self.vector_memory_top_k = vector_memory_top_k
|
||||
self.vector_memory_rerank = vector_memory_rerank
|
||||
self.memory_type_filter = memory_type_filter # None = 全部类型
|
||||
self.parent_agent_id = parent_agent_id # 父 Agent ID(继承经验)
|
||||
self.team_id = team_id # 团队共享 ID
|
||||
self.team_share_enabled = team_share_enabled # 是否自动发布到团队池
|
||||
# 文件式记忆
|
||||
@@ -138,6 +140,12 @@ class AgentMemory:
|
||||
if global_text:
|
||||
parts.append(global_text)
|
||||
|
||||
# 5. 父 Agent 知识继承:加载父级经验
|
||||
if self.parent_agent_id and self.scope_id:
|
||||
parent_text = await self._parent_knowledge_search(query)
|
||||
if parent_text:
|
||||
parts.append(parent_text)
|
||||
|
||||
return "\n\n".join(parts) if parts else ""
|
||||
|
||||
async def _vector_search(self, query: str = "") -> str:
|
||||
@@ -341,6 +349,65 @@ class AgentMemory:
|
||||
logger.warning("LLM Rerank 失败,使用向量排序: %s", e)
|
||||
return candidates[: self.vector_memory_top_k]
|
||||
|
||||
async def _parent_knowledge_search(self, query: str = "") -> str:
|
||||
"""从父 Agent 的知识库中检索相关经验。"""
|
||||
from app.models.knowledge_entry import KnowledgeEntry
|
||||
from app.models.agent import Agent
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 向上追溯祖先 Agent(最多 3 层)
|
||||
ancestor_ids: List[str] = []
|
||||
current_id = self.parent_agent_id
|
||||
for _ in range(3):
|
||||
if not current_id:
|
||||
break
|
||||
agent = db.query(Agent).filter(Agent.id == current_id).first()
|
||||
if not agent:
|
||||
break
|
||||
ancestor_ids.append(current_id)
|
||||
current_id = getattr(agent, "parent_agent_id", None)
|
||||
if current_id in ancestor_ids:
|
||||
break
|
||||
|
||||
if not ancestor_ids:
|
||||
return ""
|
||||
|
||||
# 查询祖先的知识条目
|
||||
entries = (
|
||||
db.query(KnowledgeEntry)
|
||||
.filter(
|
||||
KnowledgeEntry.agent_id.in_(ancestor_ids),
|
||||
KnowledgeEntry.is_active == True,
|
||||
KnowledgeEntry.confidence >= 0.5,
|
||||
)
|
||||
.order_by(KnowledgeEntry.retrieval_count.desc())
|
||||
.limit(8)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not entries:
|
||||
return ""
|
||||
|
||||
lines = [f"## 父级 Agent 经验(来自 {len(ancestor_ids)} 个祖先 Agent)"]
|
||||
for i, entry in enumerate(entries, 1):
|
||||
cat = entry.category or "经验"
|
||||
title = entry.title or ""
|
||||
sol = (entry.solution or entry.situation or "")[:300]
|
||||
caveat = f" 注意: {entry.caveats[:100]}" if entry.caveats else ""
|
||||
lines.append(f"{i}. [{cat}] {title}: {sol}{caveat}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("父级知识检索失败: %s", e)
|
||||
return ""
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
async def _global_knowledge_search(self, query: str = "") -> str:
|
||||
"""从 GlobalKnowledge 表检索相关的全局知识条目。"""
|
||||
from datetime import datetime
|
||||
|
||||
@@ -65,6 +65,21 @@ EDIT_TOOLS: set = {
|
||||
"file_edit", "file_write", "notebook_edit",
|
||||
}
|
||||
|
||||
# 默认需要人工审批的工具列表 — 创建 Agent 时自动生效
|
||||
DEFAULT_REQUIRE_APPROVAL_TOOLS: List[str] = [
|
||||
"deploy_push",
|
||||
"send_email",
|
||||
"send_sms",
|
||||
"git_push",
|
||||
"git_reset_hard",
|
||||
"database_execute",
|
||||
"docker_manage",
|
||||
"command_exec",
|
||||
"shell_exec",
|
||||
"file_delete",
|
||||
"agent_delete",
|
||||
]
|
||||
|
||||
|
||||
def is_read_only_tool(tool_name: str) -> bool:
|
||||
"""判断工具是否只读"""
|
||||
|
||||
263
backend/app/agent_runtime/plan_mode.py
Normal file
263
backend/app/agent_runtime/plan_mode.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
计划模式 — Plan-before-Execute
|
||||
|
||||
参考 Claude Code EnterPlanModeTool.ts 设计:
|
||||
- 先生成执行计划(Plan)
|
||||
- 向用户展示计划,等待审批
|
||||
- 审批通过后逐步执行
|
||||
- 计划阶段自动限制为只读工具
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.agent_runtime.schemas import AgentLLMConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ──────────────────────────── 数据结构 ────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class PlanStep:
|
||||
"""单个执行步骤"""
|
||||
action: str # 步骤描述
|
||||
tool_name: Optional[str] = None # 使用的工具名称
|
||||
tool_args: Optional[Dict[str, Any]] = None # 工具参数
|
||||
expected_output: str = "" # 预期产出
|
||||
dependencies: List[int] = field(default_factory=list) # 依赖的步骤索引
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plan:
|
||||
"""执行计划"""
|
||||
goal: str # 目标描述
|
||||
steps: List[PlanStep] = field(default_factory=list)
|
||||
risks: List[str] = field(default_factory=list) # 风险提示
|
||||
assumptions: List[str] = field(default_factory=list) # 假设条件
|
||||
estimated_iterations: int = 5 # 预估 ReAct 步数
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"goal": self.goal,
|
||||
"steps": [
|
||||
{
|
||||
"index": i + 1,
|
||||
"action": s.action,
|
||||
"tool_name": s.tool_name,
|
||||
"expected_output": s.expected_output,
|
||||
}
|
||||
for i, s in enumerate(self.steps)
|
||||
],
|
||||
"risks": self.risks,
|
||||
"assumptions": self.assumptions,
|
||||
"estimated_iterations": self.estimated_iterations,
|
||||
}
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""渲染计划为 Markdown(用于展示给用户)。"""
|
||||
lines = [f"## 执行计划: {self.goal}\n"]
|
||||
lines.append("### 步骤\n")
|
||||
for i, s in enumerate(self.steps, 1):
|
||||
tool_info = f" (工具: {s.tool_name})" if s.tool_name else ""
|
||||
lines.append(f"{i}. {s.action}{tool_info}")
|
||||
if s.expected_output:
|
||||
lines.append(f" - 预期产出: {s.expected_output}")
|
||||
if self.risks:
|
||||
lines.append("\n### 风险提示\n")
|
||||
for r in self.risks:
|
||||
lines.append(f"- {r}")
|
||||
if self.assumptions:
|
||||
lines.append("\n### 假设条件\n")
|
||||
for a in self.assumptions:
|
||||
lines.append(f"- {a}")
|
||||
lines.append(f"\n*预估需要 {self.estimated_iterations} 次 ReAct 迭代*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class PlanStatus:
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
EXECUTING = "executing"
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
# ──────────────────────────── 计划生成器 ────────────────────────────
|
||||
|
||||
class PlanMode:
|
||||
"""
|
||||
计划模式:先规划,再执行。
|
||||
|
||||
用法:
|
||||
plan_mode = PlanMode(llm_config)
|
||||
plan = await plan_mode.generate_plan("重构用户模块", tool_names, history)
|
||||
# 展示给用户,等待审批
|
||||
if approved:
|
||||
async for step_result in plan_mode.execute_plan(plan, tool_executor):
|
||||
...
|
||||
"""
|
||||
|
||||
PLAN_SYSTEM_PROMPT = """你是一个资深软件架构师和分析专家。你的任务是为用户的需求生成详细的执行计划。
|
||||
|
||||
请分析用户需求,拆解为具体的执行步骤。每个步骤应包含:
|
||||
1. 具体操作描述
|
||||
2. 可能需要的工具
|
||||
3. 预期产出
|
||||
|
||||
注意:
|
||||
- 你处于**计划模式**,只能使用只读工具(搜索、读取文件等)来了解上下文
|
||||
- 不能修改任何文件或执行破坏性操作
|
||||
- 优先探索现有的代码和文档,基于实际情况制定计划
|
||||
- 考虑风险点和假设条件
|
||||
|
||||
最终输出一个 JSON 格式的执行计划:
|
||||
|
||||
```json
|
||||
{
|
||||
"goal": "目标描述",
|
||||
"steps": [
|
||||
{"action": "步骤1", "tool_name": "工具名或null", "expected_output": "预期产出"},
|
||||
...
|
||||
],
|
||||
"risks": ["风险1", "风险2"],
|
||||
"assumptions": ["假设1"],
|
||||
"estimated_iterations": 5
|
||||
}
|
||||
```"""
|
||||
|
||||
def __init__(self, llm_config: Optional[AgentLLMConfig] = None):
|
||||
self.llm_config = llm_config or AgentLLMConfig()
|
||||
# 计划模式使用更低的温度以获得更稳定的规划
|
||||
if self.llm_config.temperature > 0.3:
|
||||
self._plan_temperature = 0.3
|
||||
else:
|
||||
self._plan_temperature = self.llm_config.temperature
|
||||
|
||||
async def generate_plan(
|
||||
self,
|
||||
user_input: str,
|
||||
available_tools: List[str],
|
||||
messages_history: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> Plan:
|
||||
"""
|
||||
使用 LLM 生成结构化执行计划。
|
||||
|
||||
Args:
|
||||
user_input: 用户需求描述
|
||||
available_tools: 当前可用的工具名称列表
|
||||
messages_history: 历史消息(可选)
|
||||
|
||||
Returns:
|
||||
结构化的执行计划
|
||||
"""
|
||||
from app.agent_runtime.core import _LLMClient
|
||||
|
||||
tools_str = ", ".join(available_tools) if available_tools else "无"
|
||||
history_text = ""
|
||||
if messages_history:
|
||||
recent = messages_history[-6:] # 取最近 6 条
|
||||
history_text = "\n".join(
|
||||
f"[{m.get('role', '?')}]: {str(m.get('content', ''))[:300]}"
|
||||
for m in recent
|
||||
)
|
||||
|
||||
user_prompt = (
|
||||
f"## 用户需求\n{user_input}\n\n"
|
||||
f"## 可用工具\n{tools_str}\n"
|
||||
)
|
||||
if history_text:
|
||||
user_prompt += f"\n## 对话历史(最近)\n{history_text}\n"
|
||||
|
||||
user_prompt += "\n请生成 JSON 格式的执行计划。"
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.PLAN_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
plan_config = AgentLLMConfig(
|
||||
provider=self.llm_config.provider,
|
||||
model=self.llm_config.plan_model or self.llm_config.model,
|
||||
temperature=self._plan_temperature,
|
||||
max_tokens=2000,
|
||||
api_key=self.llm_config.api_key,
|
||||
base_url=self.llm_config.base_url,
|
||||
request_timeout=60.0,
|
||||
)
|
||||
|
||||
client = _LLMClient(plan_config)
|
||||
response = await client.chat(messages=messages, tools=None, iteration=0)
|
||||
|
||||
content = getattr(response, 'content', '') or (
|
||||
response.get('content', '') if isinstance(response, dict) else str(response)
|
||||
)
|
||||
|
||||
return self._parse_plan(content, user_input)
|
||||
|
||||
def _parse_plan(self, llm_output: str, fallback_goal: str = "") -> Plan:
|
||||
"""从 LLM 输出解析为 Plan 对象。"""
|
||||
try:
|
||||
# 提取 JSON 块
|
||||
json_text = llm_output
|
||||
if "```json" in llm_output:
|
||||
start = llm_output.index("```json") + 7
|
||||
end = llm_output.index("```", start)
|
||||
json_text = llm_output[start:end]
|
||||
elif "```" in llm_output:
|
||||
start = llm_output.index("```") + 3
|
||||
end = llm_output.index("```", start)
|
||||
json_text = llm_output[start:end]
|
||||
|
||||
data = json.loads(json_text.strip())
|
||||
return Plan(
|
||||
goal=data.get("goal", fallback_goal),
|
||||
steps=[
|
||||
PlanStep(
|
||||
action=s.get("action", f"步骤 {i+1}"),
|
||||
tool_name=s.get("tool_name"),
|
||||
tool_args=s.get("tool_args"),
|
||||
expected_output=s.get("expected_output", ""),
|
||||
dependencies=s.get("dependencies", []),
|
||||
)
|
||||
for i, s in enumerate(data.get("steps", []))
|
||||
],
|
||||
risks=data.get("risks", []),
|
||||
assumptions=data.get("assumptions", []),
|
||||
estimated_iterations=data.get("estimated_iterations", 5),
|
||||
)
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
logger.warning("计划解析失败,使用降级计划: %s", e)
|
||||
# 降级: 构建一个简单的单步计划
|
||||
return Plan(
|
||||
goal=fallback_goal or llm_output[:200],
|
||||
steps=[PlanStep(action=llm_output[:500] or "执行用户请求")],
|
||||
risks=["无法解析 LLM 输出为结构化计划"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def present_plan(plan: Plan) -> bool:
|
||||
"""
|
||||
向用户展示计划并等待审批。
|
||||
|
||||
在实际使用中,这会通过回调/事件机制向 UI 发送计划并等待用户决策。
|
||||
此处提供同步版本的基础实现。
|
||||
|
||||
Returns:
|
||||
True = 批准, False = 拒绝
|
||||
"""
|
||||
# 默认返回 True(在流式模式下由外部决策)
|
||||
# UI 层应展示 plan.to_markdown() 并收集用户决定
|
||||
logger.info("计划已生成,等待审批:\n%s", plan.to_markdown())
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_read_only_step(step: PlanStep) -> bool:
|
||||
"""判断计划步骤是否只读(在计划阶段安全执行)。"""
|
||||
if not step.tool_name:
|
||||
return True
|
||||
from app.agent_runtime.permissions import is_read_only_tool
|
||||
return is_read_only_tool(step.tool_name)
|
||||
@@ -9,6 +9,7 @@ logger.warning("SCHEMAS_MODULE_LOADED_V3_FIELD_VALIDATOR")
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from app.agent_runtime.permissions import DEFAULT_REQUIRE_APPROVAL_TOOLS
|
||||
|
||||
|
||||
class AgentToolConfig(BaseModel):
|
||||
@@ -16,7 +17,7 @@ class AgentToolConfig(BaseModel):
|
||||
# 若为空列表则使用全部已注册工具
|
||||
include_tools: List[str] = Field(default_factory=list, description="允许的工具名称白名单")
|
||||
exclude_tools: List[str] = Field(default_factory=list, description="排除的工具名称黑名单")
|
||||
require_approval: List[str] = Field(default_factory=list, description="需要人工审批的工具名列表")
|
||||
require_approval: List[str] = Field(default_factory=lambda: list(DEFAULT_REQUIRE_APPROVAL_TOOLS), description="需要人工审批的工具名列表(默认包含 destructive 工具)")
|
||||
|
||||
@field_validator("include_tools", "exclude_tools", "require_approval", "cache_tool_whitelist", "auto_approve_rules", "deny_tools", mode="before")
|
||||
@classmethod
|
||||
@@ -57,6 +58,7 @@ class AgentMemoryConfig(BaseModel):
|
||||
# 文件式记忆 (MEMORY.md — 参考 Claude Code memdir)
|
||||
memory_dir_enabled: bool = False # 是否启用文件式自动记忆
|
||||
memory_dir_path: str = "" # 记忆目录路径(空=自动使用项目 .claude/memory)
|
||||
parent_agent_id: Optional[str] = None # 父 Agent ID(继承经验)
|
||||
# 对话自动压缩 (参考 Claude Code src/services/compact/)
|
||||
compaction: Optional[Any] = None # CompactionConfig — 惰性导入避免循环依赖
|
||||
|
||||
|
||||
690
backend/app/agent_runtime/swarm.py
Normal file
690
backend/app/agent_runtime/swarm.py
Normal file
@@ -0,0 +1,690 @@
|
||||
"""
|
||||
Agent 蜂群 (Swarm) — Leader/Teammate 并行协作引擎。
|
||||
|
||||
参考 Claude Code:
|
||||
- src/tools/AgentTool/ — 子 Agent 生成与执行
|
||||
- src/tools/AgentTool/forkSubagent.ts — Fork 模式(后台并行)
|
||||
- buildInAgents.ts — 内置 Agent 类型(explore/plan/verify)
|
||||
|
||||
核心概念:
|
||||
- SwarmLeader: 接收用户输入 → 分解为子任务 → 分配给 Teammates → 汇总结果
|
||||
- SwarmTeammate: 独立执行单个子任务的 Agent,可通过 Mailbox 与其他 Agent 通信
|
||||
- SwarmMailbox: Agent 间消息传递(发布/订阅模式)
|
||||
- 并行执行: 无依赖的任务通过 asyncio.gather 并发运行
|
||||
|
||||
与现有 Orchestrator 的区别:
|
||||
- Orchestrator: 预定义编排模式 (route/sequential/debate/pipeline/graph)
|
||||
- Swarm: Leader 自主决策,动态分解任务,Agent 间可通信
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Set
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent_runtime.schemas import (
|
||||
AgentConfig,
|
||||
AgentResult,
|
||||
AgentStep,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentMemoryConfig,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ──────────────────────────── 枚举 / 配置 ────────────────────────────
|
||||
|
||||
|
||||
class SwarmMode(str, Enum):
|
||||
PARALLEL = "parallel" # 所有 Teammate 并发执行
|
||||
PIPELINE = "pipeline" # 流水线:A→B→C
|
||||
DEBATE = "debate" # 多方辩论后汇总
|
||||
LEADER_ONLY = "leader_only" # Leader 自行处理(不生成 Teammate)
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
DONE = "done"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class SwarmConfig(BaseModel):
|
||||
"""蜂群配置"""
|
||||
mode: SwarmMode = SwarmMode.PARALLEL
|
||||
max_teammates: int = Field(default=5, ge=1, le=20, description="最大 Teammate 数量")
|
||||
timeout_ms: int = Field(default=300_000, description="单个 Teammate 超时(毫秒)")
|
||||
total_timeout_ms: int = Field(default=600_000, description="整个 Swarm 总超时")
|
||||
leader_model: str = Field(default="deepseek-v4-pro", description="Leader 使用的模型")
|
||||
teammate_model: str = Field(default="deepseek-v4-flash", description="Teammate 默认模型")
|
||||
mailbox_enabled: bool = Field(default=True, description="启用 Agent 间消息传递")
|
||||
leader_aggregation: bool = Field(default=True, description="Leader 汇总所有结果")
|
||||
retry_failed: bool = Field(default=True, description="失败任务是否重试")
|
||||
|
||||
|
||||
# ──────────────────────────── Mailbox ────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class MailboxMessage:
|
||||
"""Agent 间消息"""
|
||||
id: str
|
||||
from_agent: str # 发送者 agent_id
|
||||
to_agent: str # 接收者 agent_id("*" = 广播)
|
||||
content: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
class SwarmMailbox:
|
||||
"""Agent 间消息传递系统(发布/订阅模式)。
|
||||
|
||||
每个 Teammate 执行过程中可以把发现/中间结果写入 Mailbox,
|
||||
其他 Teammate 可以读取相关消息来避免重复工作或协调行动。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._messages: List[MailboxMessage] = []
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def send(self, from_agent: str, to_agent: str, content: str) -> MailboxMessage:
|
||||
"""发送消息。to_agent="*" 为广播。"""
|
||||
msg = MailboxMessage(
|
||||
id=str(uuid.uuid4())[:8],
|
||||
from_agent=from_agent,
|
||||
to_agent=to_agent,
|
||||
content=content,
|
||||
)
|
||||
async with self._lock:
|
||||
self._messages.append(msg)
|
||||
logger.debug("Mailbox: %s → %s: %s", from_agent, to_agent, content[:80])
|
||||
return msg
|
||||
|
||||
async def receive(self, agent_id: str, since: float = 0) -> List[MailboxMessage]:
|
||||
"""接收发给该 Agent 的消息(含广播)。"""
|
||||
async with self._lock:
|
||||
return [
|
||||
m for m in self._messages
|
||||
if m.timestamp >= since and (m.to_agent == agent_id or m.to_agent == "*")
|
||||
]
|
||||
|
||||
async def broadcast(self, from_agent: str, content: str) -> MailboxMessage:
|
||||
"""向所有 Agent 广播消息。"""
|
||||
return await self.send(from_agent, "*", content)
|
||||
|
||||
def snapshot(self) -> List[Dict[str, Any]]:
|
||||
"""返回消息列表快照(用于序列化)。"""
|
||||
return [
|
||||
{"id": m.id, "from": m.from_agent, "to": m.to_agent,
|
||||
"content": m.content[:500], "timestamp": m.timestamp}
|
||||
for m in self._messages
|
||||
]
|
||||
|
||||
|
||||
# ──────────────────────────── Task ────────────────────────────
|
||||
|
||||
|
||||
class SwarmTask(BaseModel):
|
||||
"""蜂群中的子任务"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
description: str = Field(..., description="任务描述")
|
||||
assigned_agent_id: Optional[str] = Field(default=None, description="分配的 Agent ID")
|
||||
dependencies: List[str] = Field(default_factory=list, description="依赖的任务 ID 列表")
|
||||
context_hint: str = Field(default="", description="额外上下文提示")
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
result: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
iterations_used: int = 0
|
||||
tool_calls_made: int = 0
|
||||
duration_ms: int = 0
|
||||
|
||||
|
||||
# ──────────────────────────── Leader ────────────────────────────
|
||||
|
||||
|
||||
_LEADER_DECOMPOSE_PROMPT = """你是一个任务分解专家。将用户的问题拆解为多个可并行执行的子任务。
|
||||
|
||||
用户问题: {user_input}
|
||||
|
||||
{context_hint}
|
||||
|
||||
请返回 JSON 格式(不要 markdown 包裹):
|
||||
{{
|
||||
"mode": "parallel",
|
||||
"sub_tasks": [
|
||||
{{
|
||||
"description": "子任务描述(清晰具体,包含所需上下文)",
|
||||
"dependencies": [],
|
||||
"context_hint": "额外说明"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
规则:
|
||||
- 每个子任务应该是独立的、可单独完成的
|
||||
- 标注依赖关系(如果任务 B 需要任务 A 的结果)
|
||||
- 子任务数量不超过 {max_tasks} 个
|
||||
- 模式可选: "parallel" (全部并行) 或 "pipeline" (需要顺序执行)
|
||||
- 如果用户问题很简单不需要分解,返回空 sub_tasks 列表"""
|
||||
|
||||
_LEADER_AGGREGATE_PROMPT = """你是一个信息汇总专家。将多个子任务的执行结果整合为统一回答。
|
||||
|
||||
用户原始问题: {user_input}
|
||||
|
||||
各子任务执行结果:
|
||||
{task_results}
|
||||
|
||||
请综合以上信息,给出一个完整、有条理的回答。如果结果之间有矛盾,说明矛盾所在。
|
||||
直接输出最终回答,不要用 JSON 包裹。"""
|
||||
|
||||
|
||||
class SwarmLeader:
|
||||
"""蜂群 Leader — 负责任务分解、分发和结果汇总。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: SwarmConfig,
|
||||
llm_client: Any, # _LLMClient
|
||||
):
|
||||
self.config = config
|
||||
self.llm = llm_client
|
||||
|
||||
async def decompose(self, user_input: str, context_hint: str = "") -> List[SwarmTask]:
|
||||
"""使用 LLM 将用户输入分解为子任务列表。"""
|
||||
prompt = _LEADER_DECOMPOSE_PROMPT.format(
|
||||
user_input=user_input,
|
||||
context_hint=context_hint or "无",
|
||||
max_tasks=self.config.max_teammates,
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self.llm.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
tools=None,
|
||||
iteration=0,
|
||||
)
|
||||
plan = self._parse_json(response.content or "")
|
||||
except Exception as e:
|
||||
logger.warning("Leader 任务分解失败: %s,退化为单任务模式", e)
|
||||
plan = {"mode": "parallel", "sub_tasks": []}
|
||||
|
||||
if not plan or not plan.get("sub_tasks"):
|
||||
# 无需分解,Leader 自行处理
|
||||
return []
|
||||
|
||||
tasks = []
|
||||
for i, t in enumerate(plan.get("sub_tasks", [])):
|
||||
if i >= self.config.max_teammates:
|
||||
break
|
||||
task = SwarmTask(
|
||||
description=t.get("description", f"子任务 {i+1}"),
|
||||
dependencies=t.get("dependencies", []),
|
||||
context_hint=t.get("context_hint", ""),
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
logger.info("Leader 分解完成: %d 个子任务 (mode=%s)", len(tasks), plan.get("mode", "parallel"))
|
||||
return tasks
|
||||
|
||||
async def aggregate(self, user_input: str, tasks: List[SwarmTask]) -> str:
|
||||
"""汇总所有子任务结果。"""
|
||||
task_results_parts = []
|
||||
for t in tasks:
|
||||
status = "✓" if t.status == TaskStatus.DONE else "✗"
|
||||
result_text = t.result[:1000] if t.result else "(无输出)"
|
||||
task_results_parts.append(
|
||||
f"[{status}] {t.description}\n结果: {result_text}"
|
||||
)
|
||||
task_results_text = "\n\n".join(task_results_parts)
|
||||
|
||||
prompt = _LEADER_AGGREGATE_PROMPT.format(
|
||||
user_input=user_input,
|
||||
task_results=task_results_text,
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self.llm.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
tools=None,
|
||||
iteration=0,
|
||||
)
|
||||
return response.content or "汇总失败"
|
||||
except Exception as e:
|
||||
logger.error("Leader 汇总失败: %s", e)
|
||||
# 降级:直接拼接
|
||||
return "\n\n".join(
|
||||
f"## {t.description}\n{t.result or '无输出'}"
|
||||
for t in tasks if t.status == TaskStatus.DONE
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_json(text: str) -> Optional[Dict[str, Any]]:
|
||||
"""从 LLM 输出中提取 JSON。"""
|
||||
import re
|
||||
cleaned = text.strip()
|
||||
cleaned = re.sub(r'^```(?:json)?\s*', '', cleaned)
|
||||
cleaned = re.sub(r'\s*```$', '', cleaned)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
m = re.search(r'\{[\s\S]*\}', cleaned)
|
||||
if m:
|
||||
try:
|
||||
return json.loads(m.group(0))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ──────────────────────────── Teammate ────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeammateResult:
|
||||
"""单个 Teammate 的执行结果"""
|
||||
task_id: str
|
||||
agent_id: str
|
||||
agent_name: str
|
||||
success: bool
|
||||
output: str
|
||||
iterations_used: int
|
||||
tool_calls_made: int
|
||||
duration_ms: int
|
||||
error: Optional[str] = None
|
||||
steps: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
class SwarmTeammate:
|
||||
"""蜂群 Teammate — 执行单个子任务的 Agent 包装器。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent_id: str,
|
||||
agent_name: str,
|
||||
config: AgentConfig,
|
||||
mailbox: Optional[SwarmMailbox] = None,
|
||||
):
|
||||
self.agent_id = agent_id
|
||||
self.agent_name = agent_name
|
||||
self.config = config
|
||||
self.mailbox = mailbox
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
task: SwarmTask,
|
||||
swarm_context: str = "",
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
) -> TeammateResult:
|
||||
"""执行子任务。"""
|
||||
from app.agent_runtime import AgentRuntime
|
||||
|
||||
task.status = TaskStatus.RUNNING
|
||||
start = time.time()
|
||||
|
||||
# 构建增强的输入:包含 Swarm 上下文 + Mailbox 消息
|
||||
enhanced_input = task.description
|
||||
extra_context: List[str] = []
|
||||
|
||||
if swarm_context:
|
||||
extra_context.append(f"[Swarm 上下文]\n{swarm_context}")
|
||||
|
||||
# 读取 Mailbox 中相关消息
|
||||
if self.mailbox:
|
||||
msgs = await self.mailbox.receive(self.agent_id)
|
||||
if msgs:
|
||||
mailbox_text = "\n".join(
|
||||
f"[{m.from_agent}]: {m.content[:300]}" for m in msgs[-5:] # 最近 5 条
|
||||
)
|
||||
extra_context.append(f"[Mailbox 消息]\n{mailbox_text}")
|
||||
|
||||
if extra_context:
|
||||
enhanced_input = "\n\n".join(extra_context) + "\n\n---\n任务:\n" + enhanced_input
|
||||
|
||||
try:
|
||||
runtime = AgentRuntime(config=self.config, on_llm_call=on_llm_call)
|
||||
result = await asyncio.wait_for(
|
||||
runtime.run(enhanced_input),
|
||||
timeout=self.config.llm.request_timeout,
|
||||
)
|
||||
|
||||
task.result = result.content
|
||||
task.status = TaskStatus.DONE if result.success else TaskStatus.FAILED
|
||||
task.iterations_used = result.iterations_used
|
||||
task.tool_calls_made = result.tool_calls_made
|
||||
task.error = result.error
|
||||
|
||||
# 将重要发现写入 Mailbox(供其他 Teammate 参考)
|
||||
if self.mailbox and result.success and result.content:
|
||||
await self._share_findings(task, result)
|
||||
|
||||
return TeammateResult(
|
||||
task_id=task.id,
|
||||
agent_id=self.agent_id,
|
||||
agent_name=self.agent_name,
|
||||
success=result.success,
|
||||
output=result.content,
|
||||
iterations_used=result.iterations_used,
|
||||
tool_calls_made=result.tool_calls_made,
|
||||
duration_ms=int((time.time() - start) * 1000),
|
||||
error=result.error,
|
||||
steps=[s.model_dump() for s in result.steps] if result.steps else [],
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
task.status = TaskStatus.FAILED
|
||||
task.error = f"超时({self.config.llm.request_timeout}s)"
|
||||
return TeammateResult(
|
||||
task_id=task.id, agent_id=self.agent_id, agent_name=self.agent_name,
|
||||
success=False, output="", iterations_used=0, tool_calls_made=0,
|
||||
duration_ms=int((time.time() - start) * 1000),
|
||||
error=task.error,
|
||||
)
|
||||
except Exception as e:
|
||||
task.status = TaskStatus.FAILED
|
||||
task.error = str(e)
|
||||
logger.error("Teammate %s 执行失败: %s", self.agent_name, e)
|
||||
return TeammateResult(
|
||||
task_id=task.id, agent_id=self.agent_id, agent_name=self.agent_name,
|
||||
success=False, output="", iterations_used=0, tool_calls_made=0,
|
||||
duration_ms=int((time.time() - start) * 1000),
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def _share_findings(self, task: SwarmTask, result: AgentResult) -> None:
|
||||
"""将完成的任务结果广播到 Mailbox。"""
|
||||
if not self.mailbox:
|
||||
return
|
||||
summary = (
|
||||
f"完成任务: {task.description}\n"
|
||||
f"关键发现: {result.content[:300]}"
|
||||
)
|
||||
await self.mailbox.broadcast(self.agent_id, summary)
|
||||
|
||||
|
||||
# ──────────────────────────── Swarm Runtime ────────────────────────────
|
||||
|
||||
|
||||
class SwarmResult(BaseModel):
|
||||
"""蜂群执行结果"""
|
||||
success: bool = True
|
||||
final_answer: str = ""
|
||||
mode: SwarmMode = SwarmMode.PARALLEL
|
||||
tasks: List[SwarmTask] = Field(default_factory=list)
|
||||
teammate_results: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
mailbox_messages: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
total_duration_ms: int = 0
|
||||
total_iterations: int = 0
|
||||
total_tool_calls: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class SwarmRuntime:
|
||||
"""Agent 蜂群运行时。
|
||||
|
||||
完整生命周期:
|
||||
1. Leader 分解用户输入 → 子任务列表
|
||||
2. 并行执行无依赖的子任务(asyncio.gather)
|
||||
3. 检查 Mailbox 消息,有依赖的先等依赖完成
|
||||
4. Leader 汇总所有结果 → 最终回答
|
||||
|
||||
用法::
|
||||
|
||||
swarm = SwarmRuntime(config=SwarmConfig(mode=SwarmMode.PARALLEL))
|
||||
result = await swarm.run("帮我做三件事: ...")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[SwarmConfig] = None,
|
||||
leader_config: Optional[AgentConfig] = None,
|
||||
teammate_configs: Optional[List[AgentConfig]] = None,
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
):
|
||||
self.config = config or SwarmConfig()
|
||||
self.leader_config = leader_config or AgentConfig(
|
||||
name="SwarmLeader",
|
||||
llm=AgentLLMConfig(model=self.config.leader_model, temperature=0.3, max_iterations=10),
|
||||
)
|
||||
self.teammate_configs = teammate_configs or []
|
||||
self.on_llm_call = on_llm_call
|
||||
self.mailbox = SwarmMailbox() if self.config.mailbox_enabled else None
|
||||
|
||||
async def run(self, user_input: str) -> SwarmResult:
|
||||
"""运行蜂群。"""
|
||||
from app.agent_runtime.core import _LLMClient
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 1. Leader 分解任务
|
||||
leader_llm = _LLMClient(self.leader_config.llm)
|
||||
leader = SwarmLeader(config=self.config, llm_client=leader_llm)
|
||||
tasks = await leader.decompose(user_input)
|
||||
|
||||
# 无子任务 → Leader 自行处理
|
||||
if not tasks:
|
||||
logger.info("Swarm: 无需分解,Leader 直接处理")
|
||||
from app.agent_runtime import AgentRuntime
|
||||
runtime = AgentRuntime(config=self.leader_config, on_llm_call=self.on_llm_call)
|
||||
result = await runtime.run(user_input)
|
||||
return SwarmResult(
|
||||
success=result.success,
|
||||
final_answer=result.content,
|
||||
mode=SwarmMode.LEADER_ONLY,
|
||||
total_duration_ms=int((time.time() - start_time) * 1000),
|
||||
total_iterations=result.iterations_used,
|
||||
total_tool_calls=result.tool_calls_made,
|
||||
error=result.error,
|
||||
)
|
||||
|
||||
# 2. 构建 Teammates(不足则自动生成)
|
||||
teammates = self._build_teammates(tasks)
|
||||
|
||||
# 3. 按依赖关系分组执行
|
||||
teammate_results = await self._execute_with_deps(
|
||||
tasks=tasks,
|
||||
teammates=teammates,
|
||||
swarm_context=user_input,
|
||||
)
|
||||
|
||||
# 4. Leader 汇总
|
||||
final_answer = await leader.aggregate(user_input, tasks)
|
||||
|
||||
total_duration = int((time.time() - start_time) * 1000)
|
||||
logger.info(
|
||||
"Swarm 完成: %d tasks, %d success, %d fail, %dms",
|
||||
len(tasks),
|
||||
sum(1 for t in tasks if t.status == TaskStatus.DONE),
|
||||
sum(1 for t in tasks if t.status == TaskStatus.FAILED),
|
||||
total_duration,
|
||||
)
|
||||
|
||||
return SwarmResult(
|
||||
success=all(t.status == TaskStatus.DONE for t in tasks),
|
||||
final_answer=final_answer,
|
||||
mode=self.config.mode,
|
||||
tasks=tasks,
|
||||
teammate_results=[
|
||||
{
|
||||
"agent_id": tr.agent_id, "agent_name": tr.agent_name,
|
||||
"task_id": tr.task_id, "success": tr.success,
|
||||
"output": tr.output[:500], "duration_ms": tr.duration_ms,
|
||||
"iterations_used": tr.iterations_used, "tool_calls_made": tr.tool_calls_made,
|
||||
"error": tr.error,
|
||||
}
|
||||
for tr in teammate_results
|
||||
],
|
||||
mailbox_messages=self.mailbox.snapshot() if self.mailbox else [],
|
||||
total_duration_ms=total_duration,
|
||||
total_iterations=sum(tr.iterations_used for tr in teammate_results),
|
||||
total_tool_calls=sum(tr.tool_calls_made for tr in teammate_results),
|
||||
)
|
||||
|
||||
def _build_teammates(self, tasks: List[SwarmTask]) -> Dict[str, SwarmTeammate]:
|
||||
"""为每个子任务构建 Teammate。
|
||||
|
||||
策略:
|
||||
- 如果有预配置的 teammate_configs,按数量分配
|
||||
- 不足部分自动基于 leader_config 生成轻量配置
|
||||
"""
|
||||
teammates: Dict[str, SwarmTeammate] = {}
|
||||
pre_configs = list(self.teammate_configs)
|
||||
|
||||
for i, task in enumerate(tasks):
|
||||
if i < len(pre_configs):
|
||||
cfg = pre_configs[i]
|
||||
agent_id = cfg.name or f"teammate_{i}"
|
||||
agent_name = cfg.name or f"Teammate-{i+1}"
|
||||
else:
|
||||
# 自动生成:使用 teammate_model(轻量模型)
|
||||
agent_id = f"teammate_{i}"
|
||||
agent_name = f"Teammate-{i+1}"
|
||||
cfg = AgentConfig(
|
||||
name=agent_name,
|
||||
system_prompt=f"你是一个专门处理以下类型任务的 AI Agent: {task.description[:200]}",
|
||||
llm=AgentLLMConfig(
|
||||
model=self.config.teammate_model,
|
||||
temperature=0.7,
|
||||
max_iterations=10,
|
||||
),
|
||||
tools=AgentToolConfig(),
|
||||
memory=AgentMemoryConfig(enabled=False), # Teammate 不需要长记忆
|
||||
user_id=self.leader_config.user_id,
|
||||
)
|
||||
|
||||
task.assigned_agent_id = agent_id
|
||||
teammates[agent_id] = SwarmTeammate(
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
config=cfg,
|
||||
mailbox=self.mailbox,
|
||||
)
|
||||
|
||||
return teammates
|
||||
|
||||
async def _execute_with_deps(
|
||||
self,
|
||||
tasks: List[SwarmTask],
|
||||
teammates: Dict[str, SwarmTeammate],
|
||||
swarm_context: str,
|
||||
) -> List[TeammateResult]:
|
||||
"""按依赖关系分批并行执行。
|
||||
|
||||
算法:
|
||||
1. 找出所有无依赖的"就绪"任务 → 并行执行
|
||||
2. 等待它们完成 → 标记依赖已解决
|
||||
3. 重复直到所有任务完成
|
||||
"""
|
||||
results: List[TeammateResult] = []
|
||||
pending = list(tasks)
|
||||
completed_ids: Set[str] = set()
|
||||
failed_ids: Set[str] = set()
|
||||
|
||||
while pending:
|
||||
# 找出就绪任务(依赖全部满足)
|
||||
ready = [
|
||||
t for t in pending
|
||||
if all(dep in completed_ids for dep in t.dependencies)
|
||||
]
|
||||
|
||||
if not ready:
|
||||
# 死锁检测:剩余任务都有未完成的依赖
|
||||
stuck_ids = {t.id for t in pending}
|
||||
unresolved = set()
|
||||
for t in pending:
|
||||
for dep in t.dependencies:
|
||||
if dep in stuck_ids and dep not in completed_ids and dep not in failed_ids:
|
||||
unresolved.add(dep)
|
||||
if unresolved:
|
||||
logger.warning("Swarm: 检测到未解决的依赖 %s,跳过阻塞任务", unresolved)
|
||||
# 将阻塞任务的依赖标记为 failed(跳过)
|
||||
for t in pending:
|
||||
for dep in unresolved:
|
||||
if dep in t.dependencies:
|
||||
t.dependencies.remove(dep)
|
||||
continue
|
||||
break # 不应该到这里
|
||||
|
||||
# 并行执行就绪任务
|
||||
batch_results = await asyncio.gather(
|
||||
*[
|
||||
teammates[t.assigned_agent_id or f"teammate_{i}"].execute(
|
||||
task=t,
|
||||
swarm_context=swarm_context,
|
||||
on_llm_call=self.on_llm_call,
|
||||
)
|
||||
for i, t in enumerate(ready)
|
||||
],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
# 处理结果
|
||||
for task, tr in zip(ready, batch_results):
|
||||
if isinstance(tr, Exception):
|
||||
task.status = TaskStatus.FAILED
|
||||
task.error = str(tr)
|
||||
failed_ids.add(task.id)
|
||||
logger.error("Swarm teammate %s 异常: %s", task.assigned_agent_id, tr)
|
||||
results.append(TeammateResult(
|
||||
task_id=task.id, agent_id=task.assigned_agent_id or "unknown",
|
||||
agent_name="unknown", success=False, output="",
|
||||
iterations_used=0, tool_calls_made=0, duration_ms=0,
|
||||
error=str(tr),
|
||||
))
|
||||
else:
|
||||
results.append(tr)
|
||||
if tr.success:
|
||||
completed_ids.add(task.id)
|
||||
else:
|
||||
failed_ids.add(task.id)
|
||||
|
||||
# 失败重试
|
||||
if self.config.retry_failed and task.status == TaskStatus.FAILED:
|
||||
logger.info("Swarm: 重试失败任务 %s", task.description[:50])
|
||||
task.status = TaskStatus.PENDING
|
||||
|
||||
# 更新 pending 列表
|
||||
pending = [t for t in pending if t.id not in (completed_ids | failed_ids)]
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ──────────────────────────── 便捷工厂 ────────────────────────────
|
||||
|
||||
|
||||
def create_swarm(
|
||||
user_id: Optional[str] = None,
|
||||
mode: SwarmMode = SwarmMode.PARALLEL,
|
||||
max_teammates: int = 5,
|
||||
leader_model: str = "deepseek-v4-pro",
|
||||
teammate_model: str = "deepseek-v4-flash",
|
||||
mailbox_enabled: bool = True,
|
||||
teammate_configs: Optional[List[AgentConfig]] = None,
|
||||
) -> SwarmRuntime:
|
||||
"""创建预配置的 Swarm 运行时。"""
|
||||
config = SwarmConfig(
|
||||
mode=mode,
|
||||
max_teammates=max_teammates,
|
||||
leader_model=leader_model,
|
||||
teammate_model=teammate_model,
|
||||
mailbox_enabled=mailbox_enabled,
|
||||
)
|
||||
leader_config = AgentConfig(
|
||||
name="SwarmLeader",
|
||||
system_prompt="你是一个AI任务协调者。你将复杂问题分解为子任务,协调多个AI Agent并行处理,并汇总结果。",
|
||||
llm=AgentLLMConfig(model=leader_model, temperature=0.3, max_iterations=10),
|
||||
user_id=user_id,
|
||||
)
|
||||
return SwarmRuntime(
|
||||
config=config,
|
||||
leader_config=leader_config,
|
||||
teammate_configs=teammate_configs or [],
|
||||
)
|
||||
@@ -9,6 +9,12 @@ import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.services.tool_registry import tool_registry
|
||||
from app.agent_runtime.permissions import (
|
||||
PermissionChecker,
|
||||
PermissionLevel,
|
||||
PermissionAction,
|
||||
AutoApproveRule,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,7 +38,10 @@ class AgentToolManager:
|
||||
exclude_tools: Optional[List[str]] = None,
|
||||
cache_enabled: bool = True,
|
||||
cache_tool_whitelist: Optional[List[str]] = None,
|
||||
cache_ttl_ms: int = 3600000):
|
||||
cache_ttl_ms: int = 3600000,
|
||||
permission_level: str = "default",
|
||||
auto_approve_rules: Optional[List[Dict[str, Any]]] = None,
|
||||
deny_tools: Optional[List[str]] = None):
|
||||
self._include_tools: set = set(include_tools or [])
|
||||
self._exclude_tools: set = set(exclude_tools or [])
|
||||
self._cache_enabled = cache_enabled
|
||||
@@ -40,6 +49,25 @@ class AgentToolManager:
|
||||
self._cache_ttl_s = max(1, int(cache_ttl_ms / 1000))
|
||||
self._cache_store: Dict[str, str] = {} # 内存 fallback
|
||||
|
||||
# Permission checker (P3 — 参考 Claude Code Tool.ts)
|
||||
try:
|
||||
_perm_level = PermissionLevel(permission_level)
|
||||
except ValueError:
|
||||
_perm_level = PermissionLevel.DEFAULT
|
||||
_auto_rules = [
|
||||
AutoApproveRule(
|
||||
tool_pattern=r.get("tool_pattern", "*"),
|
||||
param_conditions=r.get("param_conditions"),
|
||||
description=r.get("description", ""),
|
||||
)
|
||||
for r in (auto_approve_rules or [])
|
||||
]
|
||||
self._permission = PermissionChecker(
|
||||
level=_perm_level,
|
||||
auto_approve_rules=_auto_rules,
|
||||
deny_rules=deny_tools or [],
|
||||
)
|
||||
|
||||
def _is_cacheable(self, tool_name: str) -> bool:
|
||||
"""判断工具结果是否可缓存。"""
|
||||
if not self._cache_enabled:
|
||||
@@ -116,6 +144,20 @@ class AgentToolManager:
|
||||
Returns:
|
||||
工具执行结果的字符串表示
|
||||
"""
|
||||
# 权限检查 (P3)
|
||||
perm = self._permission.check(name, args)
|
||||
if perm.action == PermissionAction.DENY:
|
||||
err = json.dumps({"error": perm.message}, ensure_ascii=False)
|
||||
logger.warning("工具 %s 被权限系统拒绝: %s", name, perm.message)
|
||||
return err
|
||||
if perm.action == PermissionAction.ASK:
|
||||
err = json.dumps({
|
||||
"error": f"工具 {name} 需要用户确认: {perm.message}",
|
||||
"requires_confirmation": True,
|
||||
}, ensure_ascii=False)
|
||||
logger.info("工具 %s 需用户确认: %s", name, perm.message)
|
||||
return err
|
||||
|
||||
# 缓存检查
|
||||
if self._is_cacheable(name):
|
||||
ck = self._cache_key(name, args)
|
||||
|
||||
Reference in New Issue
Block a user