- 新增 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)
64 lines
2.7 KiB
Python
64 lines
2.7 KiB
Python
"""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)
|