"""搜索 Agent 提示词管理模块。 提供 PromptManager 类,统一管理循环级 prompt 的加载与组装。 工具级 prompt(extract/verify)不在管理范围内。 与 TRM4 ``core/search/prompt.py`` 的差异: - 工具描述从 ``app.search.tools.get_tool_descriptions`` 获取(路径变更); - ``format_user_prompt`` 参数显式化(question/options/l1_node_ids/task_type)。 """ from __future__ import annotations from typing import TYPE_CHECKING from app.search.tools import get_tool_descriptions if TYPE_CHECKING: from pathlib import Path class PromptManager: """管理循环级 prompt 的加载与组装。 构造时缓存 system.md 作为 inference 基础模板。 后续步骤(diagnose/evolve/question_gen)通过 load() 按文件名读取。 参数: prompts_dir: prompt 文件目录的绝对路径。 """ def __init__(self, prompts_dir: Path) -> None: self._prompts_dir = prompts_dir system_path = prompts_dir / "system.md" if not system_path.exists(): raise FileNotFoundError(f"system.md 不存在: {system_path}") self._system_base = system_path.read_text(encoding="utf-8") def build_inference_prompt( self, skill_mode: str, task_type: str, always_skills_text: str, task_skill_map: dict[str, str], catalog_text: str, ) -> str: """组装 inference 步骤的完整 system prompt。 参数: skill_mode: "auto" / "manual" / "none"。 task_type: 当前 QA 的题型。 always_skills_text: always 层 skill 正文(已拼接)。 task_skill_map: {task_type: skill_body} 映射。 catalog_text: manual 模式的 skill 目录文本。 返回: 拼装后的完整 system prompt。 """ include_read_skill = skill_mode == "manual" parts = [ self._system_base, f"\n\n---\n\n{get_tool_descriptions(include_read_skill=include_read_skill)}", ] if always_skills_text: parts.append(f"\n\n---\n\n# 通用搜索策略\n\n{always_skills_text}") if skill_mode == "auto": skill_text = task_skill_map.get(task_type) or task_skill_map.get("_default") if skill_text: parts.append(f"\n\n---\n\n# 当前题型搜索策略\n\n{skill_text}") elif skill_mode == "manual": if catalog_text: parts.append( "\n\n---\n\n# 可用搜索策略\n\n" "以下技能扩展了你的导航能力。当问题匹配某技能的适用题型时," "用 read_skill 工具加载该技能,然后按其指引操作。\n\n" f"{catalog_text}" ) return "".join(parts) def format_user_prompt( self, question: str, options: list[str], l1_node_ids: list[str], task_type: str | None = None, ) -> str: """格式化 inference 步骤的用户提示词。 参数: question: 问题文本。 options: 选项列表(如 ["A. 历史", "B. 科学"])。 l1_node_ids: L1 根节点 ID 列表(如 ["L1_000", "L1_001"])。 task_type: 可选题型标签,非 None 时插入题型行(oracle 实验用)。 返回: 格式化后的用户提示词。 """ options_text = "\n".join(options) roots_text = ", ".join(l1_node_ids) task_type_line = f"**题型**: {task_type}\n" if task_type else "" return ( f"请回答以下关于这个视频的多选题:\n\n" f"{task_type_line}" f"**问题**: {question}\n" f"**选项**:\n{options_text}\n\n" f"**视频树 L1 根节点**: {roots_text}\n" f"请从以上 L1 节点开始导航,收集证据后回答。" ) def load(self, name: str) -> str: """按文件名加载 prompt 内容。 参数: name: prompt 文件名(如 "diagnose_span.md")。 返回: 文件内容字符串。 异常: FileNotFoundError: 文件不存在。 """ path = self._prompts_dir / name if not path.exists(): raise FileNotFoundError(f"prompt 文件不存在: {path}") return path.read_text(encoding="utf-8")