Files
aiagent/backend/app/agent_runtime/plan_mode.py
renjianbo beff3fac8d 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>
2026-06-29 01:17:21 +08:00

264 lines
9.4 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.
"""
计划模式 — 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)