注册表 TOOLBOX +4 entry(continue/expand/de-ai/teardown,全 preview-only writes=[]); ContextStrategy +with_prior_chapter/text_input;ToolGenerateRequest +text 字段。 toolbox_context 分派新策略(续写→build_continuation_context、原文→build_text_input_context); 路由续写经 chapter_repo 读最新 accepted/draft 正文注入 builder(core 不 import apps/api,仿 accept_op)。 预览仅 commit ledger 不写业务表(#3);只声明 tier(#2);system_prompt 进缓存前缀(#9)。 单测:4 生成器 generate 预览 + 续写读前文/草稿路由 + 扩写/降AI text 路由 + 拆书结构化; 更新 registry/strategy 计数测试(15 工具 / 6 策略)。门禁绿:ruff/format/mypy 196 files/pytest 168(api)+32(skills)。
108 lines
4.9 KiB
Python
108 lines
4.9 KiB
Python
"""T6 创作工具箱 · 声明式生成器描述符类型(创作工具箱通用框架的契约)。
|
||
|
||
竞品把创作辅助做成一排「XX 生成器」卡片;本仓的 `AgentSpec`(ARCH §5.1)本就是
|
||
「内置 Agent 与用户 Skill 同构声明」。这里再加一层**生成器描述符**:把「一个生成器」
|
||
声明成一条 `GeneratorTool`——复用 §5.1 的 `spec`(tier/system_prompt/reads/writes),
|
||
再补前端渲染所需的卡片文案 + 表单字段 + 注入策略 + 可选入库目标。
|
||
|
||
之后「加一个生成器」= 加一份 `GeneratorTool` 声明(descriptor 在代码、不进 DB——
|
||
保 Pydantic schema 为真类,避免迁移;DB `skills` 表与只读注册表/权限故事不变)。
|
||
|
||
本模块只定义**类型**(Wave-0 契约);`TOOLBOX: dict[str, GeneratorTool]` 注册表 seeding
|
||
属 Wave A(需 @llm 落地的 8 个新 spec/output_schema)。
|
||
|
||
不可变:所有描述符 frozen——构造后不得改动(全局 immutability 约定)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Literal
|
||
|
||
from pydantic import BaseModel, ConfigDict, model_validator
|
||
from ww_agents import AgentSpec
|
||
from ww_shared import AppError, ErrorCode
|
||
|
||
from ww_skills.skill_permissions import KNOWN_TABLES
|
||
|
||
# 注入策略:决定为该生成器组哪些材料喂给网关(覆盖全部生成器)。
|
||
# - brief_only:项目立意 + 用户一句话(脑洞/书名);
|
||
# - with_project:+ 主线/卖点(简介);
|
||
# - with_world:+ world_entities 卡(名字/金手指/词条——须契合世界观硬规则);
|
||
# - with_outline_chapter:+ 指定章 outline beats(细纲/黄金开篇);
|
||
# - with_prior_chapter:+ 最新已写正文(续写——端点先读最新 accepted/draft 正文注入);
|
||
# - text_input:+ 作者提供的原文(扩写/降AI率/拆书样本——经请求 text 字段注入)。
|
||
ContextStrategy = Literal[
|
||
"brief_only",
|
||
"with_project",
|
||
"with_world",
|
||
"with_outline_chapter",
|
||
"with_prior_chapter",
|
||
"text_input",
|
||
]
|
||
|
||
|
||
class InputField(BaseModel):
|
||
"""声明式表单字段描述符(前端据此渲染输入控件;frozen;snake_case)。
|
||
|
||
`type` 是前端控件类型("text"/"textarea"/"number"/"select" 等);`default` 统一以
|
||
字符串携带(声明层不区分数值/枚举的运行期类型,前端按 `type` 解释)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
name: str # 表单字段名(提交时的 key)
|
||
label: str # 展示标签
|
||
type: str # 控件类型:text / textarea / number / select ...
|
||
required: bool = True
|
||
default: str | None = None
|
||
help: str | None = None # 字段说明/占位提示
|
||
|
||
|
||
class IngestSpec(BaseModel):
|
||
"""可选入库目标描述符(None 表示纯预览不写库;frozen)。
|
||
|
||
`table` 必须在 `KNOWN_TABLES` 创作表白名单内——驱动既有入库 gate
|
||
(continuity 预检 + `partition_writes` 白名单 + schema→JSONB 形变,不变量 #3)。
|
||
越权/不存在的入库表 → AppError(VALIDATION)(不可信声明的第一道闸)。
|
||
KISS:当前只需 `table` 即可驱动 gate,更多形变字段属 Wave A 按需再加。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
table: str
|
||
|
||
@model_validator(mode="after")
|
||
def _check_table(self) -> IngestSpec:
|
||
if self.table not in KNOWN_TABLES:
|
||
raise AppError(
|
||
ErrorCode.VALIDATION,
|
||
f"IngestSpec 声明了未知入库表:{self.table}",
|
||
{"table": self.table},
|
||
)
|
||
return self
|
||
|
||
|
||
class GeneratorTool(BaseModel):
|
||
"""单个生成器的声明式描述符(创作工具箱卡片 + 执行路径的真源;frozen)。
|
||
|
||
两类形态:
|
||
- 新工具:带 `spec`(§5.1 声明)+ `output_schema`(结构化产出真类),走通用执行器;
|
||
- legacy 工具(已上架的 世界观/人设/大纲):`spec=None`,带 `legacy_route` 指向
|
||
现有更丰富的入库/冲突页面,不回归既测代码。
|
||
|
||
`ingest=None` 表示纯预览(不写库);`genre` 标注题材适用(前端徽标/过滤)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
||
|
||
key: str # 路由用稳定标识("brainstorm" / "book-title" ...)
|
||
title: str # 卡片标题("脑洞生成器")
|
||
subtitle: str # 卡片副标题("突破想象,脑洞大开")
|
||
spec: AgentSpec | None # §5.1 声明(tier/system_prompt/reads/writes);legacy 为 None
|
||
output_schema: type[BaseModel] | None # 结构化产出真类;legacy/纯文本可为 None
|
||
context_strategy: ContextStrategy # 注入策略(组哪些材料)
|
||
input_fields: list[InputField] # 声明式表单
|
||
ingest: IngestSpec | None = None # 可入库目标;None=纯预览
|
||
legacy_route: str | None = None # legacy 工具指向的现有页面/端点
|
||
genre: str | None = None # 题材适用(卡片徽标/过滤)
|