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:
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)
|
||||
Reference in New Issue
Block a user