Files
writer-work-flow/packages/agents/ww_agents/prompt_loader.py
Yaojia Wang 452bd56d95 feat(agents,core,config): 写章 craft 教条块 + genre-aware 片段 + 开篇黄金三章特判
- 新增 write_craft.md 品类无关写章 craft 内核(网文语域/show-don't-tell/对话 voice/
  反注水/结构性文字质感,灵活原则非穷举清单)+ load_craft_doctrine()(import 期读盘,
  不入 SPECS/金标准)
- build_write_request 把 craft 教条作为最前的 cache=True system 块注入(craft→stable_core,
  守缓存前缀 #9,tier=writer 不写 model)
- 新增 packages/config GENRE_CRAFT 题材片段表,assemble 按 project.genre 注入 stable_core;
  ProjectSpecView + SqlProjectSpecRepo 透传 genre
- 开篇前 3 章在 volatile 注入黄金三章提示(含章号→每章易变,不进缓存前缀)
- 修正 toolbox_registry 注释:11 条 → 15 条(legacy 3 + 新 8 + Scope B 4)
2026-07-06 14:29:43 +02:00

64 lines
2.7 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.

"""Prompt 加载器Prompt 外置方案A · 步2
`load_prompt(name)` 在模块 import 期一次性读盘 `prompts/<name>.md` → 规整 → 内存缓存
→ 返回完整 UTF-8 文本。保证 `spec.system_prompt` 字节稳定(缓存断点前块,不变量 #9
且单测可复现。文件缺失 = fail-fast`PromptNotFoundError`),不 fallback、不返回 ""
规整规则(锁死缓存字节,设计 §3.2),依次:
1. 去 BOM`encoding="utf-8-sig"` 读(`utf-8` 不自动剥 BOM带 BOM 会污染首字节破缓存);
2. 行尾归一CRLF/CR → LF
3. Unicode 归一化:`unicodedata.normalize("NFC", text)`(中文/全角标点的跨平台缝隙);
4. 尾换行策略(方案 B顺生态`text.rstrip("\n")`——吞掉文件尾部所有 LF、不补回。
`.md` 允许带 ≤1 个尾换行(顺 Prettier / editorconfig运行时字符串无尾换行
(与旧常量逐字等价)。
`load_prompt` 不做任何动态插值(无 `.format`);需运行期占位符的 prompt 不走此路径。
"""
from __future__ import annotations
import unicodedata
from pathlib import Path
from typing import Final
PROMPTS_DIR: Final = Path(__file__).parent / "prompts"
_CACHE: dict[str, str] = {}
class PromptNotFoundError(FileNotFoundError):
"""按 spec.name 找不到对应 `prompts/<name>.md`import 期 fail-fast"""
def load_prompt(name: str) -> str:
"""按 spec.name 读 `prompts/<name>.md` → 规整 → 缓存 → 返回完整 UTF-8 文本。"""
cached = _CACHE.get(name)
if cached is not None:
return cached
path = PROMPTS_DIR / f"{name}.md"
if not path.is_file():
raise PromptNotFoundError(f"prompt not found for spec name {name!r}: {path}")
raw = path.read_text(encoding="utf-8-sig") # 去 BOM
normalized = raw.replace("\r\n", "\n").replace("\r", "\n") # 行尾归一
normalized = unicodedata.normalize("NFC", normalized) # Unicode 归一化
text = normalized.rstrip("\n") # 尾换行:吞掉、不补回
_CACHE[name] = text
return text
# 主写章 craft 教条文件名(灵感①)。它是散文教条、不是结构化产物 spec
# 不入 SPECS、不进 prompt_hashes 金标准、不参与计数断言——但复用同一读盘/规整/缓存路径,
# 保证字节稳定(不变量 #9
CRAFT_DOCTRINE_NAME: Final = "write_craft"
def load_craft_doctrine() -> str:
"""读 `prompts/write_craft.md`(品类无关写章 craft 内核)→ 规整 → 缓存 → 返回。
与 `load_prompt` 同一读盘/规整/缓存逻辑DRY文件缺失同样 fail-fast
`PromptNotFoundError`)。调用方在 import 期取常量即触发读盘。
"""
return load_prompt(CRAFT_DOCTRINE_NAME)