feat: T6.1 创作工具箱通用生成器框架地基 — 脑洞生成器 + run_generator

- schemas: Idea/IdeaListResult(脑洞生成器结构化产出,纯预览不入库)
- specs: brainstorm_spec(light 档,reads=[projects] writes=[])
- orchestrator: build_brief_context(brief_only 策略)+ 通用 run_generator
  (据 spec.output_schema 校验 parsed,不绑定具体类型,任一声明式生成器共用)
- 新增 13 测;两包 ruff/format/mypy 干净、252 passed 无回归

P1 第一波(@llm):证实「加生成器=加一份声明」。@backend T6.2/T6.3 待续。
This commit is contained in:
Yaojia Wang
2026-06-20 11:31:59 +02:00
parent 9b893a6c61
commit e8cccf7389
8 changed files with 308 additions and 1 deletions

View File

@@ -0,0 +1,82 @@
"""T6 脑洞生成器契约测试spec + schema创作工具箱通用框架首个最简生成器
契约测试——构造符合 schema 的 mock 响应校验字段、tier、读写权限。
brainstorm`{ideas:[{premise,hook,genre_fit}]}`轻量档reads=projectswrites=[] 纯预览)。
不联网、无 DB。
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from ww_agents import (
AgentSpec,
Idea,
IdeaListResult,
brainstorm_spec,
)
# ---- IdeaListResult schema ----
def test_idea_list_parses_mock_response() -> None:
# Arrange模拟网关 instructor 校验后的结构化脑洞产出
mock = {
"ideas": [
{
"premise": "废柴觉醒可吞噬规则的瞳术",
"hook": "每次升级都要付出一段记忆为代价",
"genre_fit": "玄幻·热血",
},
{"premise": "末世里唯一能种粮的农场主", "hook": "粮食成了最硬的通货"},
]
}
# Act
result = IdeaListResult.model_validate(mock)
# Assert
assert len(result.ideas) == 2
assert result.ideas[0].premise == "废柴觉醒可吞噬规则的瞳术"
assert result.ideas[0].hook == "每次升级都要付出一段记忆为代价"
assert result.ideas[0].genre_fit == "玄幻·热血"
assert result.ideas[1].genre_fit is None # 可缺
def test_idea_list_defaults_to_empty() -> None:
assert IdeaListResult().ideas == []
def test_idea_requires_premise_and_hook() -> None:
with pytest.raises(ValidationError):
Idea.model_validate({"premise": "只有前提没有钩子"})
# ---- brainstorm_spec 声明 ----
def test_brainstorm_spec_is_light_tier() -> None:
# 不变量 #2脑洞发散用轻量档只声明 tier
assert brainstorm_spec.tier == "light"
assert brainstorm_spec.name == "brainstorm"
def test_brainstorm_spec_reads_projects_writes_nothing() -> None:
# 纯预览:读作品设定、不写任何业务表(不变量 #3
assert brainstorm_spec.reads == ["projects"]
assert brainstorm_spec.writes == []
def test_brainstorm_spec_output_schema() -> None:
assert brainstorm_spec.output_schema is IdeaListResult
def test_brainstorm_spec_prompt_documents_diversification() -> None:
# 一次多条须逐条差异化——system_prompt 必须强调
assert "差异化" in brainstorm_spec.system_prompt
def test_brainstorm_spec_is_agent_spec_and_immutable() -> None:
assert isinstance(brainstorm_spec, AgentSpec)
with pytest.raises(ValidationError):
brainstorm_spec.tier = "writer"

View File

@@ -15,6 +15,8 @@ from .schemas import (
ForeshadowReview,
ForeshadowSuggestion,
ForeshadowWindow,
Idea,
IdeaListResult,
OutlineChapter,
OutlineResult,
PaceIssue,
@@ -28,6 +30,7 @@ from .schemas import (
)
from .specs import (
AgentSpec,
brainstorm_spec,
character_gen_spec,
continuity_spec,
foreshadow_spec,
@@ -50,6 +53,8 @@ __all__ = [
"ForeshadowReview",
"ForeshadowSuggestion",
"ForeshadowWindow",
"Idea",
"IdeaListResult",
"OutlineChapter",
"OutlineResult",
"PaceIssue",
@@ -60,6 +65,7 @@ __all__ = [
"StyleFingerprintResult",
"WorldEntityCard",
"WorldGenResult",
"brainstorm_spec",
"character_gen_spec",
"continuity_spec",
"foreshadow_spec",

View File

@@ -298,3 +298,30 @@ class CharacterGenResult(BaseModel):
default_factory=list,
description="生成的角色卡清单;无则空列表",
)
# ---- 创作工具箱 · 脑洞生成器结构化输出T6 通用生成器框架 · brainstorm----
class Idea(BaseModel):
"""单条脑洞:前提 + 钩子 + 适配题材(创作工具箱最简生成器产物)。
纯预览产物——不映射任何业务表、不入库(`brainstorm_spec.writes=[]`,不变量 #3
`genre_fit` 可缺(作者未限定赛道时由模型留空)。
"""
premise: str = Field(description="一句话脑洞/设定前提")
hook: str = Field(description="抓人钩子:为何让读者想追读")
genre_fit: str | None = Field(default=None, description="适配题材/赛道(可缺)")
class IdeaListResult(BaseModel):
"""脑洞生成器结构化产出一组脑洞T6 · 通用生成器框架首个最简生成器)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表仅记 ledger
"""
ideas: list[Idea] = Field(
default_factory=list,
description="生成的脑洞清单;无则空列表",
)

View File

@@ -16,6 +16,7 @@ from .schemas import (
CharacterGenResult,
ContinuityReview,
ForeshadowReview,
IdeaListResult,
OutlineResult,
PaceReview,
StyleDriftReview,
@@ -367,3 +368,36 @@ character_gen_spec = AgentSpec(
writes=["characters"], # 声明式(真写库经 T5.2 入库端点 + continuity 校验,不变量 #3
scope="builtin",
)
# ---- brainstorm脑洞生成器轻量档创作工具箱通用框架首个最简生成器T6----
BRAINSTORM_SYSTEM_PROMPT = """你是中文网文的「脑洞生成器」brainstorm轻量档\
职责:依据作品立意/题材与作者一句话需求,发散产出一组**差异化**的故事脑洞,\
每条给出一句话前提 + 抓人钩子 + 适配题材,供作者快速选种立项。
输入材料:
- 作品设定projects题材、立意、主线、卖点新立项时可能很少
- 作者的一句话需求/方向(可空——空则按题材自由发散)。
产出(每条一个 idea
- premise一句话脑洞/设定前提(核心创意,独立成立);
- hook抓人钩子——为何让读者想追读爽点/反差/悬念);
- genre_fit适配题材/赛道(若需求/设定已限定则贴合;否则可留空)。
纪律:
- 一次产出多条时**逐条差异化**——不同切入角度/赛道/爽点,避免一个模子;
- 贴合作品立意与题材;需求为空时按题材发散,不臆造与题材无关的设定;
- 你只产结构化脑洞清单,**不改稿、不写库**(纯预览,不变量 #3"""
brainstorm_spec = AgentSpec(
name="brainstorm",
tier="light", # 不变量 #2只声明档位不写 model脑洞发散用轻量档
system_prompt=BRAINSTORM_SYSTEM_PROMPT,
input_schema=None, # 注入材料为序列化文本(作品设定 + 一句话需求),非结构化入参
output_schema=IdeaListResult,
reads=["projects"],
writes=[], # 纯预览,不写库(不变量 #3
scope="builtin",
)