4baf92c93f
从 TRM4 core/search/prompt.py 迁移。有意变更: - 工具描述从 app.search.tools.get_tool_descriptions 获取 - format_user_prompt 参数显式化(question/options/l1_node_ids/task_type) 16 个单元测试覆盖 __init__、build_inference_prompt(auto/manual/none 三种 skill_mode)、format_user_prompt、load。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
"""搜索 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")
|