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:
2026-06-29 01:17:21 +08:00
parent 86b98865e3
commit beff3fac8d
1084 changed files with 117315 additions and 1281 deletions

View File

@@ -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",
]

View File

@@ -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 = []

View File

@@ -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")

View File

@@ -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

View File

@@ -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:
"""判断工具是否只读"""

View 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)

View File

@@ -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 — 惰性导入避免循环依赖

View 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 [],
)

View File

@@ -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)