Files
writer-work-flow/packages/agents/ww_agents/specs.py
Yaojia Wang dca4d45d4e refactor(agents): prompt 外置方案A — 21 prompt 散文外迁 .md + SpecResolver
把 21 个内置 agent 的 system_prompt 从 specs.py 的 Python 常量外置为
prompts/<spec.name>.md,import 期由 load_prompt 确定性加载;建立 SPECS 名册
+ SCHEMA_CATALOG(Pydantic 类型留 Python)+ 统一只读解析入口 SpecResolver。
纯重构、零功能/schema 变更,缓存断点前块字节级不变(不变量 #9)。

@llm packages/agents(步骤1-3)
- spec_model.py:抽出 AgentSpec(frozen,字段不变)
- prompt_loader.py:load_prompt = utf-8-sig 去BOM → LF 归一 → NFC → rstrip尾LF,
  内存缓存 + fail-fast(PromptNotFoundError),import 期确定性
- schema_catalog.py:SCHEMA_CATALOG[name]→output type 唯一真相源(refiner=None)
- prompts/*.md ×21:取常量「运行时值」程序化外迁(反斜杠折行已塌缩,
  物理换行≡运行时换行);文件名按 spec.name 连字符(style.md/character-gen.md 等)
- specs.py:删 21 常量 + AgentSpec 类;system_prompt=load_prompt(name)、
  output_schema=SCHEMA_CATALOG[name];建 SPECS + REVIEW_RESERVED_NAMES;
  *_spec 兼容期保留且 SPECS[name] is *_spec(同一实例)。804→337 行
- __init__.py:显式 __all__ 重导出(避 F401)

@backend packages/skills(步骤4-5)
- SpecResolver:内置 SPECS(纯内存、零 DB)+ 用户 SkillRegistry 统一 get;
  内置 name 永不触发 DB;output_schema_for 精确匹配
- skill_registry:保留命名空间守卫前移至入库校验,拒同名内置 → VALIDATION
- toolbox_registry:GeneratorTool.spec 改走 SPECS[...],删 12 个 *_spec 直接 import

@devops repo-root
- .gitattributes:prompts/*.md text eol=lf(修正:须用完整嵌套路径才匹配)
- packages/agents/pyproject:hatchling artifacts 纳入 prompts/*.md 随 wheel/sdist 分发
- ci.yml:新增 build wheel → 裸装 → import ww_agents.SPECS 冒烟

TDD 全程 mock 网关;门禁绿:ruff/format clean · mypy 209 files · pytest 744 passed
(含金标准 sha256 回归 / md↔spec↔catalog 一一对应 / fail-fast / BOM+NFC /
内置守卫 / 同一实例 / 编排器无回归 / apps/api import-smoke / 打包冒烟)
2026-06-24 04:49:44 +02:00

338 lines
14 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.

"""Agent 声明式抽象ARCH §5.1+ 续审 Agent 实例Prompt 外置方案A · 步3/5
`AgentSpec` 是内置 Agent 与用户 Skill 的同构声明:一份只读声明,由编排器加载、
经网关按 `tier` 执行。不变量 #2agent 只声明 `tier`**不**写具体 model。
不变量 #3四审 `writes=()`(只读),任何 AI 产出入库必经验收事务。
prompt 散文已外置至 `prompts/<spec.name>.md``system_prompt=load_prompt(name)`
import 期读盘+缓存+规整去BOM/LF/NFC/rstrip尾LF/fail-fast。output schema 由
`SCHEMA_CATALOG[name]` 单向派生(唯一真相源)。`AgentSpec` 从 `spec_model` re-export
import 路径不变,现有消费方不受影响)。
不可变:`AgentSpec` 为 frozen Pydantic 模型——加载后不得改动。
"""
from __future__ import annotations
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
from .prompt_loader import load_prompt
from .schema_catalog import SCHEMA_CATALOG
from .spec_model import AgentSpec
__all__ = [
"REVIEW_RESERVED_NAMES",
"SPECS",
"AgentSpec",
"blurb_spec",
"book_title_spec",
"brainstorm_spec",
"character_gen_spec",
"continue_spec",
"continuity_spec",
"de_ai_spec",
"expand_spec",
"fine_outline_spec",
"foreshadow_spec",
"glossary_spec",
"golden_finger_spec",
"name_spec",
"opening_spec",
"outliner_spec",
"pace_spec",
"refiner_spec",
"style_drift_spec",
"style_extract_spec",
"teardown_spec",
"worldbuilder_spec",
]
continuity_spec = AgentSpec(
name="continuity",
tier="analyst",
system_prompt=load_prompt("continuity"),
input_schema=None, # 注入材料为序列化文本(经记忆 assemble非结构化入参
output_schema=SCHEMA_CATALOG["continuity"],
reads=("chapter_digests", "characters", "world_entities"),
writes=(), # 只读(不变量 #3
scope="builtin",
)
outliner_spec = AgentSpec(
name="outliner",
tier="analyst", # 不变量 #2只声明档位不写 model
system_prompt=load_prompt("outliner"),
input_schema=None, # 注入材料为序列化文本(设定/伏笔/人物/世界观),非结构化入参
output_schema=SCHEMA_CATALOG["outliner"],
reads=("projects", "foreshadow", "characters", "world_entities"),
writes=("outline",), # 声明式(经验收/T3.5 才真写库,不变量 #3
scope="builtin",
)
foreshadow_spec = AgentSpec(
name="foreshadow",
tier="analyst", # 不变量 #2只声明档位不写 model
system_prompt=load_prompt("foreshadow"),
input_schema=None, # 注入材料为序列化文本(已登记伏笔 + 草稿),非结构化入参
output_schema=SCHEMA_CATALOG["foreshadow"],
reads=("foreshadow",),
writes=(), # 只读(不变量 #3
scope="builtin",
)
pace_spec = AgentSpec(
name="pace",
tier="light", # 不变量 #2只声明档位不写 model节奏审用轻量档
system_prompt=load_prompt("pace"),
input_schema=None, # 注入材料为序列化文本(题材模板 + 草稿),非结构化入参
output_schema=SCHEMA_CATALOG["pace"],
reads=("rules",), # genre 模板 DSL 经 rulesgenre 级)注入,复用 review_context 规则合并
writes=(), # 只读(不变量 #3
scope="builtin",
)
# ---- 文风提取轨style-auditor「提取」轨analyst 档;独立生成,仿 outliner不进 review 图)----
style_extract_spec = AgentSpec(
name="style_extract",
tier="analyst", # 不变量 #2只声明档位不写 model
system_prompt=load_prompt("style_extract"),
input_schema=None, # 注入材料为序列化样本文本,非结构化入参
output_schema=SCHEMA_CATALOG["style_extract"],
reads=("style_fingerprint",),
writes=("style_fingerprint",), # 声明式(真写库经 T4.3 端点,不变量 #3
scope="builtin",
)
# ---- 文风漂移轨style-auditor「打分」轨 = 第四审light 档;并入 REVIEW_SPECS----
style_drift_spec = AgentSpec(
name="style", # 第四审 section/列名 = "style"
tier="light", # 不变量 #2只声明档位不写 model漂移打分用轻量档
system_prompt=load_prompt("style"),
input_schema=None, # 注入材料为序列化文本(指纹 + 草稿),非结构化入参
output_schema=SCHEMA_CATALOG["style"],
reads=("style_fingerprint",), # 指纹经 assemble 的 stable_core 注入 review_context
writes=(), # 只读(不变量 #3
scope="builtin",
)
# ---- 回炉refinerwriter 档;纯文本重写;非 Agent 流水线、非持久M4-e----
refiner_spec = AgentSpec(
name="refiner",
tier="writer", # 不变量 #2重写正文用 writer 档
system_prompt=load_prompt("refiner"),
input_schema=None, # 注入材料为序列化文本(选中段 + 指令 + 上下文)
output_schema=SCHEMA_CATALOG["refiner"], # 纯文本产出(重写段),无结构化 schema
reads=(),
writes=(), # 非持久(不变量 #3端点同步返回 {original, refined},不写库
scope="builtin",
)
# ---- worldbuilder世界观设计师写手档独立生成仿 outliner不进 review 图)----
worldbuilder_spec = AgentSpec(
name="worldbuilder",
tier="writer", # 不变量 #2只声明档位不写 model世界观创意用写手档
system_prompt=load_prompt("worldbuilder"),
input_schema=None, # 注入材料为序列化文本(设定 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["worldbuilder"],
reads=("projects",),
writes=("world_entities",), # 声明式(真写库经 T5.2 入库端点,不变量 #3
scope="builtin",
)
# ---- character-gen角色设计师写手档单/批量;群像防雷同;独立生成)----
character_gen_spec = AgentSpec(
name="character-gen",
tier="writer", # 不变量 #2只声明档位不写 model角色创意用写手档
system_prompt=load_prompt("character-gen"),
input_schema=None, # 注入材料为序列化文本(需求 + 约束 + 已有 + 已生成),非结构化入参
output_schema=SCHEMA_CATALOG["character-gen"],
reads=("world_entities", "characters"),
writes=("characters",), # 声明式(真写库经 T5.2 入库端点 + continuity 校验,不变量 #3
scope="builtin",
)
# ---- brainstorm脑洞生成器轻量档创作工具箱通用框架首个最简生成器T6----
brainstorm_spec = AgentSpec(
name="brainstorm",
tier="light", # 不变量 #2只声明档位不写 model脑洞发散用轻量档
system_prompt=load_prompt("brainstorm"),
input_schema=None, # 注入材料为序列化文本(作品设定 + 一句话需求),非结构化入参
output_schema=SCHEMA_CATALOG["brainstorm"],
reads=("projects",),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- book-title书名生成器轻量档纯预览T6 创作工具箱)----
book_title_spec = AgentSpec(
name="book-title",
tier="light", # 不变量 #2只声明档位不写 model书名发散用轻量档
system_prompt=load_prompt("book-title"),
input_schema=None, # 注入材料为序列化文本(作品设定 + 一句话需求),非结构化入参
output_schema=SCHEMA_CATALOG["book-title"],
reads=("projects",),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- blurb简介生成器分析档纯预览T6 创作工具箱)----
blurb_spec = AgentSpec(
name="blurb",
tier="analyst", # 不变量 #2只声明档位不写 model文案打磨用分析档
system_prompt=load_prompt("blurb"),
input_schema=None, # 注入材料为序列化文本(作品设定 + 一句话需求),非结构化入参
output_schema=SCHEMA_CATALOG["blurb"],
reads=("projects",),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- name取名生成器轻量档纯预览T6 创作工具箱)----
name_spec = AgentSpec(
name="name",
tier="light", # 不变量 #2只声明档位不写 model取名发散用轻量档
system_prompt=load_prompt("name"),
input_schema=None, # 注入材料为序列化文本(设定 + 世界观 + 已有角色 + 需求)
output_schema=SCHEMA_CATALOG["name"],
reads=("projects", "world_entities", "characters"),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- golden-finger金手指生成器写手档声明式 writes=world_entitiesT6 创作工具箱)----
golden_finger_spec = AgentSpec(
name="golden-finger",
tier="writer", # 不变量 #2只声明档位不写 model力量体系创意用写手档
system_prompt=load_prompt("golden-finger"),
input_schema=None, # 注入材料为序列化文本(设定 + 世界观 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["golden-finger"],
reads=("projects", "world_entities"),
writes=("world_entities",), # 声明式(真写库经入库端点,不变量 #3
scope="builtin",
)
# ---- glossary术语表生成器分析档声明式 writes=world_entitiesT6 创作工具箱)----
glossary_spec = AgentSpec(
name="glossary",
tier="analyst", # 不变量 #2只声明档位不写 model术语梳理用分析档
system_prompt=load_prompt("glossary"),
input_schema=None, # 注入材料为序列化文本(设定 + 世界观 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["glossary"],
reads=("projects", "world_entities"),
writes=("world_entities",), # 声明式(真写库经入库端点,不变量 #3
scope="builtin",
)
# ---- opening开篇生成器写手档纯预览T6 创作工具箱)----
opening_spec = AgentSpec(
name="opening",
tier="writer", # 不变量 #2只声明档位不写 model正文创作用写手档
system_prompt=load_prompt("opening"),
input_schema=None, # 注入材料为序列化文本(设定 + 大纲 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["opening"],
reads=("projects", "outline"),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- fine-outline细纲生成器分析档声明式 writes=outlineT6 创作工具箱)----
fine_outline_spec = AgentSpec(
name="fine-outline",
tier="analyst", # 不变量 #2只声明档位不写 model细纲拆解用分析档
system_prompt=load_prompt("fine-outline"),
input_schema=None, # 注入材料为序列化文本(设定 + 本章大纲 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["fine-outline"],
reads=("projects", "outline"),
writes=("outline",), # 声明式(真写库经入库端点,不变量 #3
scope="builtin",
)
# ---- continue续写生成器写手档纯预览Scope B 竞品快赢)----
continue_spec = AgentSpec(
name="continue",
tier="writer", # 不变量 #2只声明档位不写 model正文续写用写手档
system_prompt=load_prompt("continue"),
input_schema=None, # 注入材料为序列化文本(设定 + 前文 + 节拍 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["continue"],
reads=("projects", "outline", "chapters"),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- expand扩写生成器写手档纯预览Scope B 竞品快赢)----
expand_spec = AgentSpec(
name="expand",
tier="writer", # 不变量 #2只声明档位不写 model正文扩写用写手档
system_prompt=load_prompt("expand"),
input_schema=None, # 注入材料为序列化文本(设定 + 原文 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["expand"],
reads=("projects",),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- de-ai降 AI 率生成器分析档纯预览Scope B 竞品快赢)----
de_ai_spec = AgentSpec(
name="de-ai",
tier="analyst", # 不变量 #2只声明档位不写 model文风改写用分析档
system_prompt=load_prompt("de-ai"),
input_schema=None, # 注入材料为序列化文本(设定 + 原文 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["de-ai"],
reads=("projects",),
writes=(), # 纯预览,不写库(不变量 #3
scope="builtin",
)
# ---- teardown拆书生成器分析档纯预览Scope B 竞品快赢)----
teardown_spec = AgentSpec(
name="teardown",
tier="analyst", # 不变量 #2只声明档位不写 model拆解分析用分析档
system_prompt=load_prompt("teardown"),
input_schema=None, # 注入材料为序列化文本(设定 + 样本 + 需求),非结构化入参
output_schema=SCHEMA_CATALOG["teardown"],
reads=("projects",),
writes=("rules",), # F1拆书结论可落库为项目 rules入库仍经 ingest 白名单 gate#3
scope="builtin",
)
# 集中注册表name → spec同一实例兼容期 *_spec 与 SPECS[name] 为同对象,不变量)
# MappingProxyType只读视图运行时 `SPECS[x] = ...` / `del SPECS[x]` 抛 TypeError
# `Final` 仅静态检查,挡不住运行时变异;注册表是单一真相源,须运行时不可变)。
_SPECS_BY_NAME: dict[str, AgentSpec] = {
s.name: s
for s in (
continuity_spec,
outliner_spec,
foreshadow_spec,
pace_spec,
style_extract_spec,
style_drift_spec,
refiner_spec,
worldbuilder_spec,
character_gen_spec,
brainstorm_spec,
book_title_spec,
blurb_spec,
name_spec,
golden_finger_spec,
glossary_spec,
opening_spec,
fine_outline_spec,
continue_spec,
expand_spec,
de_ai_spec,
teardown_spec,
)
}
assert len(_SPECS_BY_NAME) == 21, "SPECS name 冲突或缺失" # 唯一性 + 数量自检import 期)
SPECS: Final[Mapping[str, AgentSpec]] = MappingProxyType(_SPECS_BY_NAME)
# 四审受信保留名 —— 独立显式白名单(安全边界锚在此,不依附派生集合)
REVIEW_RESERVED_NAMES: Final[frozenset[str]] = frozenset(
{"continuity", "foreshadow", "style", "pace"}
)