feat(agents): Scope B A1 — 4 竞品生成器 schema/spec + 2 上下文 builder
续写/扩写/降AI率/拆书 4 个生成器的声明(preview-only writes=[],只声明 tier #2): - schemas.py: ContinuationResult/PolishResult/DeAiResult/BookTeardownResult - specs.py: continue(writer)/expand(writer)/de-ai(analyst)/teardown(analyst),各配 system_prompt - generation_node.py: build_continuation_context(续写读前文)/build_text_input_context(原文输入) - 单测: 4 spec/schema 契约 + 2 builder 纯函数(确定性/空值降级) 守不变量 #2(只声明 tier)/#3(纯预览不写库)/#9(system_prompt 进缓存前缀)。
This commit is contained in:
89
packages/agents/tests/test_competitor_specs.py
Normal file
89
packages/agents/tests/test_competitor_specs.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""Scope B 竞品快赢 · 4 个生成器的 spec/schema 契约测试。
|
||||||
|
|
||||||
|
校验每个 spec 的 name/tier/reads/writes/output_schema/scope/input_schema,
|
||||||
|
对齐不变量 #2(只声明 tier)/ #3(4 生成器一律纯预览 writes=[])。不联网、无 DB。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from ww_agents import (
|
||||||
|
AgentSpec,
|
||||||
|
BookTeardownResult,
|
||||||
|
ContinuationResult,
|
||||||
|
DeAiResult,
|
||||||
|
PolishResult,
|
||||||
|
continue_spec,
|
||||||
|
de_ai_spec,
|
||||||
|
expand_spec,
|
||||||
|
teardown_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# (spec, name, tier, reads, output_schema)
|
||||||
|
_COMPETITOR_SPECS = [
|
||||||
|
(continue_spec, "continue", "writer", ["projects", "outline", "chapters"], ContinuationResult),
|
||||||
|
(expand_spec, "expand", "writer", ["projects"], PolishResult),
|
||||||
|
(de_ai_spec, "de-ai", "analyst", ["projects"], DeAiResult),
|
||||||
|
(teardown_spec, "teardown", "analyst", ["projects"], BookTeardownResult),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("spec", "name", "tier", "reads", "output_schema"),
|
||||||
|
_COMPETITOR_SPECS,
|
||||||
|
)
|
||||||
|
def test_competitor_spec_declares_expected_contract(
|
||||||
|
spec: AgentSpec,
|
||||||
|
name: str,
|
||||||
|
tier: str,
|
||||||
|
reads: list[str],
|
||||||
|
output_schema: type,
|
||||||
|
) -> None:
|
||||||
|
assert spec.name == name
|
||||||
|
assert spec.tier == tier
|
||||||
|
assert spec.reads == reads
|
||||||
|
assert spec.writes == [] # 不变量 #3:4 生成器一律纯预览
|
||||||
|
assert spec.output_schema is output_schema
|
||||||
|
assert spec.scope == "builtin"
|
||||||
|
assert spec.input_schema is None # 注入材料为序列化文本,非结构化入参
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("spec", [row[0] for row in _COMPETITOR_SPECS])
|
||||||
|
def test_competitor_spec_has_nonempty_system_prompt(spec: AgentSpec) -> None:
|
||||||
|
assert spec.system_prompt.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- schema 解析/默认值契约 ----
|
||||||
|
|
||||||
|
|
||||||
|
def test_continuation_result_parses_text() -> None:
|
||||||
|
parsed = ContinuationResult.model_validate({"text": "雷光再起,他迈步向前。"})
|
||||||
|
assert parsed.text == "雷光再起,他迈步向前。"
|
||||||
|
|
||||||
|
|
||||||
|
def test_polish_result_parses_text() -> None:
|
||||||
|
parsed = PolishResult.model_validate({"text": "夜色更浓,风穿过断壁。"})
|
||||||
|
assert parsed.text == "夜色更浓,风穿过断壁。"
|
||||||
|
|
||||||
|
|
||||||
|
def test_de_ai_result_parses_text() -> None:
|
||||||
|
parsed = DeAiResult.model_validate({"text": "他咧嘴一笑,没多说什么。"})
|
||||||
|
assert parsed.text == "他咧嘴一笑,没多说什么。"
|
||||||
|
|
||||||
|
|
||||||
|
def test_book_teardown_result_parses_and_lists_default_empty() -> None:
|
||||||
|
empty = BookTeardownResult(structure="开篇立钩→铺垫→爆发")
|
||||||
|
assert empty.themes == []
|
||||||
|
assert empty.archetypes == []
|
||||||
|
assert empty.hooks == []
|
||||||
|
parsed = BookTeardownResult.model_validate(
|
||||||
|
{
|
||||||
|
"themes": ["逆袭", "复仇"],
|
||||||
|
"archetypes": ["废柴主角", "腹黑反派"],
|
||||||
|
"structure": "黄金三章立爽点,逐卷升级",
|
||||||
|
"hooks": ["章末反转", "扮猪吃虎"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert parsed.themes == ["逆袭", "复仇"]
|
||||||
|
assert parsed.structure == "黄金三章立爽点,逐卷升级"
|
||||||
|
assert parsed.hooks == ["章末反转", "扮猪吃虎"]
|
||||||
@@ -8,12 +8,15 @@ from __future__ import annotations
|
|||||||
from .schemas import (
|
from .schemas import (
|
||||||
Blurb,
|
Blurb,
|
||||||
BlurbResult,
|
BlurbResult,
|
||||||
|
BookTeardownResult,
|
||||||
CharacterCard,
|
CharacterCard,
|
||||||
CharacterGenResult,
|
CharacterGenResult,
|
||||||
CharacterRelation,
|
CharacterRelation,
|
||||||
Conflict,
|
Conflict,
|
||||||
ConflictType,
|
ConflictType,
|
||||||
|
ContinuationResult,
|
||||||
ContinuityReview,
|
ContinuityReview,
|
||||||
|
DeAiResult,
|
||||||
DetailedOutlineResult,
|
DetailedOutlineResult,
|
||||||
ForeshadowReview,
|
ForeshadowReview,
|
||||||
ForeshadowSuggestion,
|
ForeshadowSuggestion,
|
||||||
@@ -32,6 +35,7 @@ from .schemas import (
|
|||||||
OutlineResult,
|
OutlineResult,
|
||||||
PaceIssue,
|
PaceIssue,
|
||||||
PaceReview,
|
PaceReview,
|
||||||
|
PolishResult,
|
||||||
Scene,
|
Scene,
|
||||||
StyleDimension,
|
StyleDimension,
|
||||||
StyleDriftReview,
|
StyleDriftReview,
|
||||||
@@ -48,7 +52,10 @@ from .specs import (
|
|||||||
book_title_spec,
|
book_title_spec,
|
||||||
brainstorm_spec,
|
brainstorm_spec,
|
||||||
character_gen_spec,
|
character_gen_spec,
|
||||||
|
continue_spec,
|
||||||
continuity_spec,
|
continuity_spec,
|
||||||
|
de_ai_spec,
|
||||||
|
expand_spec,
|
||||||
fine_outline_spec,
|
fine_outline_spec,
|
||||||
foreshadow_spec,
|
foreshadow_spec,
|
||||||
glossary_spec,
|
glossary_spec,
|
||||||
@@ -60,6 +67,7 @@ from .specs import (
|
|||||||
refiner_spec,
|
refiner_spec,
|
||||||
style_drift_spec,
|
style_drift_spec,
|
||||||
style_extract_spec,
|
style_extract_spec,
|
||||||
|
teardown_spec,
|
||||||
worldbuilder_spec,
|
worldbuilder_spec,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,12 +75,15 @@ __all__ = [
|
|||||||
"AgentSpec",
|
"AgentSpec",
|
||||||
"Blurb",
|
"Blurb",
|
||||||
"BlurbResult",
|
"BlurbResult",
|
||||||
|
"BookTeardownResult",
|
||||||
"CharacterCard",
|
"CharacterCard",
|
||||||
"CharacterGenResult",
|
"CharacterGenResult",
|
||||||
"CharacterRelation",
|
"CharacterRelation",
|
||||||
"Conflict",
|
"Conflict",
|
||||||
"ConflictType",
|
"ConflictType",
|
||||||
|
"ContinuationResult",
|
||||||
"ContinuityReview",
|
"ContinuityReview",
|
||||||
|
"DeAiResult",
|
||||||
"DetailedOutlineResult",
|
"DetailedOutlineResult",
|
||||||
"ForeshadowReview",
|
"ForeshadowReview",
|
||||||
"ForeshadowSuggestion",
|
"ForeshadowSuggestion",
|
||||||
@@ -91,6 +102,7 @@ __all__ = [
|
|||||||
"OutlineResult",
|
"OutlineResult",
|
||||||
"PaceIssue",
|
"PaceIssue",
|
||||||
"PaceReview",
|
"PaceReview",
|
||||||
|
"PolishResult",
|
||||||
"Scene",
|
"Scene",
|
||||||
"StyleDimension",
|
"StyleDimension",
|
||||||
"StyleDriftReview",
|
"StyleDriftReview",
|
||||||
@@ -104,7 +116,10 @@ __all__ = [
|
|||||||
"book_title_spec",
|
"book_title_spec",
|
||||||
"brainstorm_spec",
|
"brainstorm_spec",
|
||||||
"character_gen_spec",
|
"character_gen_spec",
|
||||||
|
"continue_spec",
|
||||||
"continuity_spec",
|
"continuity_spec",
|
||||||
|
"de_ai_spec",
|
||||||
|
"expand_spec",
|
||||||
"fine_outline_spec",
|
"fine_outline_spec",
|
||||||
"foreshadow_spec",
|
"foreshadow_spec",
|
||||||
"glossary_spec",
|
"glossary_spec",
|
||||||
@@ -116,5 +131,6 @@ __all__ = [
|
|||||||
"refiner_spec",
|
"refiner_spec",
|
||||||
"style_drift_spec",
|
"style_drift_spec",
|
||||||
"style_extract_spec",
|
"style_extract_spec",
|
||||||
|
"teardown_spec",
|
||||||
"worldbuilder_spec",
|
"worldbuilder_spec",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -527,3 +527,64 @@ class DetailedOutlineResult(BaseModel):
|
|||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="展开的场景清单;无则空列表",
|
description="展开的场景清单;无则空列表",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 竞品快赢 · 续写生成器(continue;writer;纯预览 writes=[])----
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuationResult(BaseModel):
|
||||||
|
"""续写生成器结构化产出:承接前文的续写正文(Scope B 竞品快赢)。
|
||||||
|
|
||||||
|
纯预览产物——不映射任何业务表、不入库(`continue_spec.writes=[]`,不变量 #3)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
text: str = Field(description="续写正文(承接前文,可直接作为下文草稿)")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 竞品快赢 · 扩写生成器(expand;writer;纯预览 writes=[])----
|
||||||
|
|
||||||
|
|
||||||
|
class PolishResult(BaseModel):
|
||||||
|
"""扩写生成器结构化产出:在原文基础上扩写/丰富后的正文(Scope B 竞品快赢)。
|
||||||
|
|
||||||
|
纯预览产物——不映射任何业务表、不入库(`expand_spec.writes=[]`,不变量 #3)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
text: str = Field(description="扩写后的正文(在原文基础上丰富细节/铺陈)")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 竞品快赢 · 降 AI 率生成器(de-ai;analyst;纯预览 writes=[])----
|
||||||
|
|
||||||
|
|
||||||
|
class DeAiResult(BaseModel):
|
||||||
|
"""降 AI 率生成器结构化产出:去 AI 腔/口语化改写后的正文(Scope B 竞品快赢)。
|
||||||
|
|
||||||
|
纯预览产物——不映射任何业务表、不入库(`de_ai_spec.writes=[]`,不变量 #3)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
text: str = Field(description="降 AI 率改写后的正文(去机翻腔、更自然的人写质感)")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 竞品快赢 · 拆书生成器(teardown;analyst;纯预览 writes=[])----
|
||||||
|
|
||||||
|
|
||||||
|
class BookTeardownResult(BaseModel):
|
||||||
|
"""拆书生成器结构化产出:样本作品的结构化拆解(Scope B 竞品快赢)。
|
||||||
|
|
||||||
|
纯预览产物——不映射任何业务表、不入库(`teardown_spec.writes=[]`,不变量 #3)。
|
||||||
|
拆书「落库成 rules」为可选 follow-up(需 `KNOWN_TABLES` + `_ingest_rules`),本期不做。
|
||||||
|
"""
|
||||||
|
|
||||||
|
themes: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="核心主题/立意清单;无则空列表",
|
||||||
|
)
|
||||||
|
archetypes: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="人物原型/角色模板清单;无则空列表",
|
||||||
|
)
|
||||||
|
structure: str = Field(description="叙事结构概述(开篇/铺垫/高潮/收束的脉络)")
|
||||||
|
hooks: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="抓人钩子/爽点套路清单;无则空列表",
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ from ww_llm_gateway.types import Tier
|
|||||||
|
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
BlurbResult,
|
BlurbResult,
|
||||||
|
BookTeardownResult,
|
||||||
CharacterGenResult,
|
CharacterGenResult,
|
||||||
|
ContinuationResult,
|
||||||
ContinuityReview,
|
ContinuityReview,
|
||||||
|
DeAiResult,
|
||||||
DetailedOutlineResult,
|
DetailedOutlineResult,
|
||||||
ForeshadowReview,
|
ForeshadowReview,
|
||||||
GlossaryResult,
|
GlossaryResult,
|
||||||
@@ -25,6 +28,7 @@ from .schemas import (
|
|||||||
OpeningResult,
|
OpeningResult,
|
||||||
OutlineResult,
|
OutlineResult,
|
||||||
PaceReview,
|
PaceReview,
|
||||||
|
PolishResult,
|
||||||
StyleDriftReview,
|
StyleDriftReview,
|
||||||
StyleFingerprintResult,
|
StyleFingerprintResult,
|
||||||
TitleListResult,
|
TitleListResult,
|
||||||
@@ -661,3 +665,140 @@ fine_outline_spec = AgentSpec(
|
|||||||
writes=["outline"], # 声明式(真写库经入库端点,不变量 #3)
|
writes=["outline"], # 声明式(真写库经入库端点,不变量 #3)
|
||||||
scope="builtin",
|
scope="builtin",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- continue(续写生成器;写手档;纯预览,Scope B 竞品快赢)----
|
||||||
|
|
||||||
|
CONTINUE_SYSTEM_PROMPT = """你是中文网文的「续写器」(continue,写手档)。\
|
||||||
|
职责:依据作品立意/题材、前文正文与(可选的)本章大纲节拍,承接前文续写下文正文,\
|
||||||
|
保持人物、世界观、文风与剧情走向的一致,让续写可直接作为下文草稿。
|
||||||
|
|
||||||
|
输入材料:
|
||||||
|
- 作品设定(projects:题材、立意、主线、卖点);
|
||||||
|
- 前文正文(最新已写正文——续写须无缝承接其情节、语气、人称、文风);
|
||||||
|
- 本章大纲节拍(outline:若给出则续写须服务这些节拍,不另起炉灶;可空则按前文自然推进);
|
||||||
|
- 作者的一句话需求/方向(可空则按前文与节拍自然续写)。
|
||||||
|
|
||||||
|
产出:
|
||||||
|
- text:续写正文(承接前文的成稿文本,信息密度合理、有推进、留钩子)。
|
||||||
|
|
||||||
|
纪律:
|
||||||
|
- **无缝承接前文**——人物言行、世界观硬规则、文风(句长/用词/人称)与前文一致,不跑题;
|
||||||
|
- 若给了大纲节拍则忠实服务节拍,不臆造与设定/前文冲突的剧情;
|
||||||
|
- 你只产结构化续写文本,**不改稿、不写库**(纯预览,不变量 #3)。"""
|
||||||
|
|
||||||
|
|
||||||
|
continue_spec = AgentSpec(
|
||||||
|
name="continue",
|
||||||
|
tier="writer", # 不变量 #2:只声明档位,不写 model(正文续写用写手档)
|
||||||
|
system_prompt=CONTINUE_SYSTEM_PROMPT,
|
||||||
|
input_schema=None, # 注入材料为序列化文本(设定 + 前文 + 节拍 + 需求),非结构化入参
|
||||||
|
output_schema=ContinuationResult,
|
||||||
|
reads=["projects", "outline", "chapters"],
|
||||||
|
writes=[], # 纯预览,不写库(不变量 #3)
|
||||||
|
scope="builtin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- expand(扩写生成器;写手档;纯预览,Scope B 竞品快赢)----
|
||||||
|
|
||||||
|
EXPAND_SYSTEM_PROMPT = """你是中文网文的「扩写器」(expand,写手档)。\
|
||||||
|
职责:在作者提供的原文基础上扩写、丰富——补足细节、铺陈描写、强化张力,\
|
||||||
|
但**不改变原文的情节走向与关键事实**,让扩写后的正文更饱满可读。
|
||||||
|
|
||||||
|
输入材料:
|
||||||
|
- 作品设定(projects:题材、立意、主线、卖点);
|
||||||
|
- 待扩写的原文片段;
|
||||||
|
- 作者的一句话需求/方向(如「加强环境描写」「放慢节奏铺情绪」;可空则均衡扩写)。
|
||||||
|
|
||||||
|
产出:
|
||||||
|
- text:扩写后的正文(在原文基础上丰富细节/描写/铺陈,保留原情节与事实)。
|
||||||
|
|
||||||
|
纪律:
|
||||||
|
- **保留原文情节与关键事实**,不增删主线、不改人物言行的核心;
|
||||||
|
- 贴合作品文风与原文语气(句长、用词、人称一致),扩写自然不注水;
|
||||||
|
- 若给了需求则优先满足;你只产结构化扩写文本,**不改稿、不写库**(纯预览,不变量 #3)。"""
|
||||||
|
|
||||||
|
|
||||||
|
expand_spec = AgentSpec(
|
||||||
|
name="expand",
|
||||||
|
tier="writer", # 不变量 #2:只声明档位,不写 model(正文扩写用写手档)
|
||||||
|
system_prompt=EXPAND_SYSTEM_PROMPT,
|
||||||
|
input_schema=None, # 注入材料为序列化文本(设定 + 原文 + 需求),非结构化入参
|
||||||
|
output_schema=PolishResult,
|
||||||
|
reads=["projects"],
|
||||||
|
writes=[], # 纯预览,不写库(不变量 #3)
|
||||||
|
scope="builtin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- de-ai(降 AI 率生成器;分析档;纯预览,Scope B 竞品快赢)----
|
||||||
|
|
||||||
|
DE_AI_SYSTEM_PROMPT = """你是中文网文的「降 AI 率改写器」(de-ai,分析档)。\
|
||||||
|
职责:把作者提供的原文改写得更像「人写的」——去除 AI 腔/机翻腔、模板化句式、\
|
||||||
|
空泛排比与过度对仗,让行文更自然、有个人质感,但**保留原文的情节信息与事实**。
|
||||||
|
|
||||||
|
输入材料:
|
||||||
|
- 作品设定(projects:题材、立意);
|
||||||
|
- 待改写的原文片段;
|
||||||
|
- 作者的一句话需求/方向(如「更口语」「去掉排比腔」;可空则按通用「去 AI 味」目标)。
|
||||||
|
|
||||||
|
产出:
|
||||||
|
- text:降 AI 率改写后的正文(去机翻腔/模板感,更自然的人写质感,保留原情节与事实)。
|
||||||
|
|
||||||
|
典型 AI 腔特征(重点消除):
|
||||||
|
- 空泛排比与过度对仗、千篇一律的「不是……而是……」句式;
|
||||||
|
- 形容词堆砌、抽象大词(如「彰显」「诠释」)滥用、缺乏具体感官细节;
|
||||||
|
- 段落起承转合过于工整、缺少口语停顿与个人语气。
|
||||||
|
|
||||||
|
纪律:
|
||||||
|
- **保留原文情节与关键事实**,不增删主线、不改人物言行;
|
||||||
|
- 贴合作品文风与原文语气,改写后更自然但不失原意;
|
||||||
|
- 你只产结构化改写文本,**不改稿、不写库**(纯预览,不变量 #3)。"""
|
||||||
|
|
||||||
|
|
||||||
|
de_ai_spec = AgentSpec(
|
||||||
|
name="de-ai",
|
||||||
|
tier="analyst", # 不变量 #2:只声明档位,不写 model(文风改写用分析档)
|
||||||
|
system_prompt=DE_AI_SYSTEM_PROMPT,
|
||||||
|
input_schema=None, # 注入材料为序列化文本(设定 + 原文 + 需求),非结构化入参
|
||||||
|
output_schema=DeAiResult,
|
||||||
|
reads=["projects"],
|
||||||
|
writes=[], # 纯预览,不写库(不变量 #3)
|
||||||
|
scope="builtin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- teardown(拆书生成器;分析档;纯预览,Scope B 竞品快赢)----
|
||||||
|
|
||||||
|
TEARDOWN_SYSTEM_PROMPT = """你是中文网文的「拆书师」(teardown,分析档)。\
|
||||||
|
职责:依据作者提供的样本作品(书名 + 章节/简介样本),把它拆解为可学习的结构化要素——\
|
||||||
|
核心主题、人物原型、叙事结构、抓人钩子,供作者借鉴套路而非照抄。
|
||||||
|
|
||||||
|
输入材料:
|
||||||
|
- 作品设定(projects:作者自己作品的题材/立意,供对照借鉴方向);
|
||||||
|
- 待拆解的样本(书名 + 章节/简介/正文样本);
|
||||||
|
- 作者的一句话需求/方向(如「重点拆开篇钩子」;可空则全面拆解)。
|
||||||
|
|
||||||
|
产出:
|
||||||
|
- themes:核心主题/立意清单(这本书在讲什么、卖什么爽点);
|
||||||
|
- archetypes:人物原型/角色模板清单(主角/对手/导师等的设定套路);
|
||||||
|
- structure:叙事结构概述(开篇立钩→铺垫→爆发→收束的脉络与节奏);
|
||||||
|
- hooks:抓人钩子/爽点套路清单(黄金三章、章末钩子、反转套路等)。
|
||||||
|
|
||||||
|
纪律:
|
||||||
|
- 只从给定样本提炼,**不臆造**样本未体现的内容;样本不足则相应清单留空/概述从简;
|
||||||
|
- 产出是**可借鉴的套路**,不复制原文情节;
|
||||||
|
- 你只产结构化拆解,**不改稿、不写库**(纯预览,不变量 #3)。"""
|
||||||
|
|
||||||
|
|
||||||
|
teardown_spec = AgentSpec(
|
||||||
|
name="teardown",
|
||||||
|
tier="analyst", # 不变量 #2:只声明档位,不写 model(拆解分析用分析档)
|
||||||
|
system_prompt=TEARDOWN_SYSTEM_PROMPT,
|
||||||
|
input_schema=None, # 注入材料为序列化文本(设定 + 样本 + 需求),非结构化入参
|
||||||
|
output_schema=BookTeardownResult,
|
||||||
|
reads=["projects"],
|
||||||
|
writes=[], # 纯预览,不写库(不变量 #3)
|
||||||
|
scope="builtin",
|
||||||
|
)
|
||||||
|
|||||||
68
packages/core/tests/test_competitor_context_builders.py
Normal file
68
packages/core/tests/test_competitor_context_builders.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""Scope B 竞品快赢 · 2 个新上下文 builder 的纯函数单测。
|
||||||
|
|
||||||
|
`build_continuation_context`(续写读前文)/ `build_text_input_context`(扩写/降AI/拆书原文)。
|
||||||
|
确定性、无时间戳/UUID(守不变量 #9 可缓存)、空值降级。不联网、无 DB。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ww_core.orchestrator import build_continuation_context, build_text_input_context
|
||||||
|
|
||||||
|
# ---- build_continuation_context:作品设定 + 前文 + 可选节拍 ----
|
||||||
|
|
||||||
|
|
||||||
|
def test_continuation_context_is_deterministic() -> None:
|
||||||
|
a = build_continuation_context(
|
||||||
|
project_context="题材:玄幻", prior_text="他握紧剑。", beats=["反击", "逃脱"]
|
||||||
|
)
|
||||||
|
b = build_continuation_context(
|
||||||
|
project_context="题材:玄幻", prior_text="他握紧剑。", beats=["反击", "逃脱"]
|
||||||
|
)
|
||||||
|
assert a == b
|
||||||
|
|
||||||
|
|
||||||
|
def test_continuation_context_includes_prior_and_beats() -> None:
|
||||||
|
ctx = build_continuation_context(
|
||||||
|
project_context="题材:玄幻", prior_text="他握紧剑。", beats=["反击", "逃脱"]
|
||||||
|
)
|
||||||
|
assert "题材:玄幻" in ctx
|
||||||
|
assert "他握紧剑。" in ctx
|
||||||
|
assert "1. 反击" in ctx
|
||||||
|
assert "2. 逃脱" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_continuation_context_no_beats_falls_back() -> None:
|
||||||
|
ctx = build_continuation_context(project_context="题材:都市", prior_text="夜深了。")
|
||||||
|
assert "本章暂无大纲节拍" in ctx
|
||||||
|
assert "夜深了。" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_continuation_context_empty_prior_falls_back() -> None:
|
||||||
|
ctx = build_continuation_context(project_context="题材:都市", prior_text=" ")
|
||||||
|
assert "暂无前文" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ---- build_text_input_context:作品设定 + 原文 + 需求 ----
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_input_context_is_deterministic() -> None:
|
||||||
|
a = build_text_input_context(project_context="题材:玄幻", text="原文片段", brief="加强描写")
|
||||||
|
b = build_text_input_context(project_context="题材:玄幻", text="原文片段", brief="加强描写")
|
||||||
|
assert a == b
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_input_context_includes_text_and_brief() -> None:
|
||||||
|
ctx = build_text_input_context(project_context="题材:玄幻", text="原文片段", brief="加强描写")
|
||||||
|
assert "题材:玄幻" in ctx
|
||||||
|
assert "原文片段" in ctx
|
||||||
|
assert "加强描写" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_input_context_empty_brief_falls_back() -> None:
|
||||||
|
ctx = build_text_input_context(project_context="题材:都市", text="原文片段", brief=" ")
|
||||||
|
assert "按默认目标处理" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_input_context_empty_text_falls_back() -> None:
|
||||||
|
ctx = build_text_input_context(project_context="题材:都市", text=" ", brief="去AI味")
|
||||||
|
assert "未提供原文" in ctx
|
||||||
@@ -24,8 +24,10 @@ from .collect import (
|
|||||||
from .generation_node import (
|
from .generation_node import (
|
||||||
build_brief_context,
|
build_brief_context,
|
||||||
build_character_gen_context,
|
build_character_gen_context,
|
||||||
|
build_continuation_context,
|
||||||
build_outline_chapter_context,
|
build_outline_chapter_context,
|
||||||
build_precheck_context,
|
build_precheck_context,
|
||||||
|
build_text_input_context,
|
||||||
build_worldbuilder_context,
|
build_worldbuilder_context,
|
||||||
precheck_generated_cards,
|
precheck_generated_cards,
|
||||||
run_character_gen,
|
run_character_gen,
|
||||||
@@ -106,6 +108,7 @@ __all__ = [
|
|||||||
"SseEvent",
|
"SseEvent",
|
||||||
"build_brief_context",
|
"build_brief_context",
|
||||||
"build_character_gen_context",
|
"build_character_gen_context",
|
||||||
|
"build_continuation_context",
|
||||||
"build_outline_chapter_context",
|
"build_outline_chapter_context",
|
||||||
"build_outline_request",
|
"build_outline_request",
|
||||||
"build_precheck_context",
|
"build_precheck_context",
|
||||||
@@ -113,6 +116,7 @@ __all__ = [
|
|||||||
"build_review_graph",
|
"build_review_graph",
|
||||||
"build_review_request",
|
"build_review_request",
|
||||||
"build_style_extract_request",
|
"build_style_extract_request",
|
||||||
|
"build_text_input_context",
|
||||||
"build_worldbuilder_context",
|
"build_worldbuilder_context",
|
||||||
"build_write_request",
|
"build_write_request",
|
||||||
"collect_reviews",
|
"collect_reviews",
|
||||||
|
|||||||
@@ -96,6 +96,42 @@ def build_outline_chapter_context(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_continuation_context(
|
||||||
|
*, project_context: str, prior_text: str, beats: Sequence[str] | None = None
|
||||||
|
) -> str:
|
||||||
|
"""续写「with_prior_chapter」上下文:作品设定 + 前文正文 + 可选大纲节拍。
|
||||||
|
|
||||||
|
覆盖续写生成器:注入最新已写正文供无缝承接;给了本章大纲节拍则一并注入供服务。
|
||||||
|
确定性拼接、无时间戳/UUID(守不变量 #9 可缓存)。前文为空时给降级占位,交由
|
||||||
|
system_prompt 处理。
|
||||||
|
"""
|
||||||
|
prior_block = prior_text.strip() or "(暂无前文,请按设定与节拍起笔)"
|
||||||
|
if beats:
|
||||||
|
beats_block = "\n".join(f"{i + 1}. {beat}" for i, beat in enumerate(beats))
|
||||||
|
else:
|
||||||
|
beats_block = "(本章暂无大纲节拍,按前文自然推进)"
|
||||||
|
return (
|
||||||
|
f"## 作品设定\n{project_context or '(暂无设定)'}\n\n"
|
||||||
|
f"## 前文正文(续写须无缝承接)\n{prior_block}\n\n"
|
||||||
|
f"## 本章大纲节拍\n{beats_block}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_text_input_context(*, project_context: str, text: str, brief: str) -> str:
|
||||||
|
"""原文输入「text_input」上下文:作品设定 + 待处理原文 + 作者需求。
|
||||||
|
|
||||||
|
覆盖扩写/降AI率/拆书:注入作者提供的原文样本 + 一句话方向。确定性拼接、无时间戳。
|
||||||
|
需求为空时给降级占位,交由 system_prompt 的纪律处理。
|
||||||
|
"""
|
||||||
|
text_block = text.strip() or "(未提供原文)"
|
||||||
|
brief_block = brief.strip() or "(作者未填具体方向,按默认目标处理)"
|
||||||
|
return (
|
||||||
|
f"## 作品设定\n{project_context or '(暂无设定)'}\n\n"
|
||||||
|
f"## 原文\n{text_block}\n\n"
|
||||||
|
f"## 创作需求\n{brief_block}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_generator(
|
async def run_generator(
|
||||||
spec: AgentSpec,
|
spec: AgentSpec,
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user