"""洛书枢 deliberative 规划器领域模块。 纯数据结构和纯函数(禁止 IO),定义显式计划的数据模型与解析逻辑。 IO 相关编排在 ``app/application/planning/`` 中完成。 """ from __future__ import annotations import json import re from dataclasses import dataclass, field @dataclass class PlanStep: """计划步骤。""" id: int title: str description: str tool: str | None = None risk_level: str = "L2" @dataclass class DeliberativePlan: """显式计划。""" title: str steps: list[PlanStep] assumptions: list[str] = field(default_factory=list) risks: list[str] = field(default_factory=list) def parse_plan(raw: str) -> DeliberativePlan | None: """解析 LLM 返回的 JSON 计划文本。 Args: raw: LLM 原始输出(可能含 ```json 围栏)。 Returns: 解析成功的 DeliberativePlan,失败返回 None。 """ text = raw.strip() matched = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL) if matched: text = matched.group(1) else: brace_start = text.find("{") brace_end = text.rfind("}") if brace_start != -1 and brace_end > brace_start: text = text[brace_start : brace_end + 1] try: data = json.loads(text) except json.JSONDecodeError: return None if not isinstance(data, dict): return None title = str(data.get("title", "")).strip() raw_steps = data.get("steps", []) if not title or not isinstance(raw_steps, list) or not raw_steps: return None steps: list[PlanStep] = [] for i, s in enumerate(raw_steps): if not isinstance(s, dict): continue step_title = str(s.get("title", "")).strip() if not step_title: continue tool_raw = str(s.get("tool", "")).strip() tool = tool_raw if tool_raw and tool_raw != "None" else None steps.append( PlanStep( id=s.get("id", i + 1), title=step_title, description=str(s.get("description", "")).strip(), tool=tool, risk_level=str(s.get("risk_level", "L2")).upper(), ) ) if not steps: return None return DeliberativePlan( title=title, steps=steps, assumptions=[str(a).strip() for a in data.get("assumptions", []) if a], risks=[str(r).strip() for r in data.get("risks", []) if r], ) def build_planning_prompt( request: str, *, goal_directive: str = "", available_tools: list[str] | None = None, memory_context: str = "", ) -> str: """构建规划 prompt。 当请求明显需要多步骤(调用多个工具、查询多处信息等)时, 要求 LLM 先生成一个显式计划,再按计划逐步执行。 Args: request: 用户请求文本。 goal_directive: 当前活跃目标描述。 available_tools: 可用工具列表。 memory_context: 相关记忆上下文。 Returns: 规划 prompt 文本。 """ tools_str = "" if available_tools: tools_str = "\n可用工具:\n" + "\n".join(f"- {t}" for t in available_tools) return ( "请判断以下用户请求是否需要多步计划。如果需要,请输出 JSON 格式的计划。\n\n" f"用户请求:{request}\n" f"{f'当前目标:{goal_directive}' if goal_directive else ''}" f"{tools_str}" f"{f'\n上下文记忆:{memory_context}' if memory_context else ''}" "\n\n" "输出格式(如需多步计划):\n" '```json\n{\n "title": "计划标题",\n' ' "assumptions": ["假设1", "假设2"],\n' ' "risks": ["风险1", "风险2"],\n' ' "steps": [\n' ' {"id": 1, "title": "步骤1", "description": "说明", ' '"tool": "工具名(可选)", "risk_level": "L2"}\n' " ]\n" "}\n```\n\n" "如果不需要多步计划(简单问题),只回复:NO_PLAN_NEEDED" ) def format_plan_for_prompt(plan: DeliberativePlan) -> str: """将计划格式化为 system prompt 可用文本。""" lines = [f"## 计划:{plan.title}\n"] if plan.assumptions: lines.append("假设:") lines.extend(f"- {a}" for a in plan.assumptions) lines.append("") if plan.risks: lines.append("风险:") lines.extend(f"- {r}" for r in plan.risks) lines.append("") lines.append("执行步骤:") for step in plan.steps: tool_info = f" [工具:{step.tool}]" if step.tool else "" lines.append(f"{step.id}. {step.title}{tool_info}") if step.description: lines.append(f" {step.description}") return "\n".join(lines) def should_plan(request: str) -> bool: """判断请求是否需要多步计划。 简单规则:包含需要多工具的关键词时返回 True。 """ triggers = [ "搜索", "查找", "研究", "调研", "分析", "比较", "整理", "总结", "汇总", "规划", "安排", "计划", "search", "research", "analyze", "compare", "summarize", "plan", "organize", ] return any(t in request for t in triggers)