Files
writer-work-flow/packages/skills/ww_skills/toolbox_registry.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

250 lines
8.4 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.

"""T6 创作工具箱 · 生成器注册表(`TOOLBOX`:把每个生成器声明成一条 `GeneratorTool`)。
「加一个生成器」= 在本表加一份声明descriptor 在代码、不进 DB——保 Pydantic schema
为真类避免迁移DB `skills` 表与只读注册表/权限故事不变)。
两类形态:
- **legacy**(已上架的 世界观/人设/大纲):`spec=None`,带 `legacy_route` 指向现有更丰富的
入库/冲突页面,前端据 `legacy_route` 直接跳页,不回归既测代码(通用 generate/ingest
端点对它们返 404/422
- **新工具**8 个):带 `spec`§5.1 声明)+ `output_schema`(结构化产物真类)+
`context_strategy`(注入策略)+ `input_fields`(声明式表单),走通用执行器;声明 `ingest`
者可经通用 ingest 端点入库(复用既有 continuity 预检 + 白名单 gate不变量 #3
不变量 #2只声明 tierspec 已守)/ #3AI 产出入库必经验收 gate预览不写库
"""
from __future__ import annotations
from ww_agents import SPECS
from ww_agents.schemas import (
BlurbResult,
BookTeardownResult,
ContinuationResult,
DeAiResult,
DetailedOutlineResult,
GlossaryResult,
GoldenFingerResult,
IdeaListResult,
NameListResult,
OpeningResult,
PolishResult,
TitleListResult,
)
from ww_skills.toolbox import GeneratorTool, IngestSpec, InputField
# 复用的常见输入字段DRY每个生成器都有「一句话需求」textarea
_BRIEF_FIELD = InputField(
name="brief",
label="一句话需求",
type="textarea",
required=False,
help="描述创作方向/约束;留空则按作品设定与题材自由发散",
)
def _chapter_no_field() -> InputField:
"""按章展开的生成器(开篇/细纲)所需的章号输入。"""
return InputField(
name="chapter_no",
label="章号",
type="number",
required=True,
default="1",
help="要生成/展开的章节号(读取该章大纲节拍)",
)
# 原文输入(扩写/降AI率/拆书样本所需的大段正文)。
_SOURCE_TEXT_FIELD = InputField(
name="text",
label="原文",
type="textarea",
required=True,
help="待处理的正文/样本片段(扩写/降AI 的原文;拆书的章节样本)",
)
# 创作工具箱注册表key → 描述符。legacy 3 + 新 8 = 11 条。
TOOLBOX: dict[str, GeneratorTool] = {
# ---- legacyspec=None前端走 legacy_route 跳现有页面)----
"worldbuilding": GeneratorTool(
key="worldbuilding",
title="世界观生成器",
subtitle="构建内部自洽的世界观与硬规则",
spec=None,
output_schema=None,
context_strategy="with_project",
input_fields=[],
legacy_route="/projects/{id}/codex?gen=world",
),
"character": GeneratorTool(
key="character",
title="人设生成器",
subtitle="群像防雷同的结构化角色卡",
spec=None,
output_schema=None,
context_strategy="with_world",
input_fields=[],
legacy_route="/projects/{id}/codex?gen=character",
),
"outline": GeneratorTool(
key="outline",
title="大纲生成器",
subtitle="分卷分章 + 伏笔回收窗口",
spec=None,
output_schema=None,
context_strategy="with_project",
input_fields=[],
legacy_route="/projects/{id}/outline",
),
# ---- 新工具(走通用执行器)----
"brainstorm": GeneratorTool(
key="brainstorm",
title="脑洞生成器",
subtitle="突破想象,脑洞大开",
spec=SPECS["brainstorm"],
output_schema=IdeaListResult,
context_strategy="brief_only",
input_fields=[_BRIEF_FIELD],
),
"book-title": GeneratorTool(
key="book-title",
title="书名生成器",
subtitle="一秒生成抓人书名",
spec=SPECS["book-title"],
output_schema=TitleListResult,
context_strategy="brief_only",
input_fields=[_BRIEF_FIELD],
),
"blurb": GeneratorTool(
key="blurb",
title="简介生成器",
subtitle="多版差异化书页文案",
spec=SPECS["blurb"],
output_schema=BlurbResult,
context_strategy="with_project",
input_fields=[_BRIEF_FIELD],
),
"name": GeneratorTool(
key="name",
title="名字生成器",
subtitle="契合世界观的人/物/地命名",
spec=SPECS["name"],
output_schema=NameListResult,
context_strategy="with_world",
input_fields=[
_BRIEF_FIELD,
InputField(
name="kind",
label="命名对象",
type="text",
required=False,
help="人物 / 势力 / 地点 / 功法 / 物品 等",
),
],
),
"golden-finger": GeneratorTool(
key="golden-finger",
title="金手指生成器",
subtitle="自洽机制 + 成长 + 限制代价",
spec=SPECS["golden-finger"],
output_schema=GoldenFingerResult,
context_strategy="with_world",
input_fields=[_BRIEF_FIELD],
ingest=IngestSpec(table="world_entities"),
),
"glossary": GeneratorTool(
key="glossary",
title="词条生成器",
subtitle="带硬规则的世界观术语表",
spec=SPECS["glossary"],
output_schema=GlossaryResult,
context_strategy="with_world",
input_fields=[_BRIEF_FIELD],
ingest=IngestSpec(table="world_entities"),
),
"opening": GeneratorTool(
key="opening",
title="黄金开篇生成器",
subtitle="多版高代入感开篇正文",
spec=SPECS["opening"],
output_schema=OpeningResult,
context_strategy="with_outline_chapter",
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
),
"fine-outline": GeneratorTool(
key="fine-outline",
title="细纲生成器",
subtitle="把章节粗节拍展开为场景序列",
spec=SPECS["fine-outline"],
output_schema=DetailedOutlineResult,
context_strategy="with_outline_chapter",
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
ingest=IngestSpec(table="outline"),
),
# ---- 竞品快赢Scope B续写/扩写/降AI率 preview-only拆书 F1 可落库 rules ----
"continue": GeneratorTool(
key="continue",
title="续写生成器",
subtitle="承接前文,无缝续写下文",
spec=SPECS["continue"],
output_schema=ContinuationResult,
context_strategy="with_prior_chapter",
input_fields=[
InputField(
name="chapter_no",
label="续写章号",
type="number",
required=False,
help="续写的目标章号(读取该章已写正文承接;缺则按设定起笔)",
),
_BRIEF_FIELD,
],
),
"expand": GeneratorTool(
key="expand",
title="扩写生成器",
subtitle="在原文基础上丰富细节铺陈",
spec=SPECS["expand"],
output_schema=PolishResult,
context_strategy="text_input",
input_fields=[_SOURCE_TEXT_FIELD, _BRIEF_FIELD],
),
"de-ai": GeneratorTool(
key="de-ai",
title="降 AI 率生成器",
subtitle="去机翻腔,更自然的人写质感",
spec=SPECS["de-ai"],
output_schema=DeAiResult,
context_strategy="text_input",
input_fields=[_SOURCE_TEXT_FIELD, _BRIEF_FIELD],
),
"teardown": GeneratorTool(
key="teardown",
title="拆书生成器",
subtitle="拆解主题/原型/结构/钩子套路",
spec=SPECS["teardown"],
output_schema=BookTeardownResult,
context_strategy="text_input",
input_fields=[
InputField(
name="kind",
label="书名",
type="text",
required=False,
help="待拆解作品的书名(供拆解对照)",
),
_SOURCE_TEXT_FIELD,
_BRIEF_FIELD,
],
ingest=IngestSpec(table="rules"), # F1拆书结论拍平为项目 rules 条目落库
),
}
def get_tool(key: str) -> GeneratorTool | None:
"""按 key 取生成器描述符;未知 key → None端点据此返 404"""
return TOOLBOX.get(key)