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:
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 (
|
||||
build_brief_context,
|
||||
build_character_gen_context,
|
||||
build_continuation_context,
|
||||
build_outline_chapter_context,
|
||||
build_precheck_context,
|
||||
build_text_input_context,
|
||||
build_worldbuilder_context,
|
||||
precheck_generated_cards,
|
||||
run_character_gen,
|
||||
@@ -106,6 +108,7 @@ __all__ = [
|
||||
"SseEvent",
|
||||
"build_brief_context",
|
||||
"build_character_gen_context",
|
||||
"build_continuation_context",
|
||||
"build_outline_chapter_context",
|
||||
"build_outline_request",
|
||||
"build_precheck_context",
|
||||
@@ -113,6 +116,7 @@ __all__ = [
|
||||
"build_review_graph",
|
||||
"build_review_request",
|
||||
"build_style_extract_request",
|
||||
"build_text_input_context",
|
||||
"build_worldbuilder_context",
|
||||
"build_write_request",
|
||||
"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(
|
||||
spec: AgentSpec,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user