feat(backend): 整章再沟通/重写 SSE 端点——喂当前草稿+意见+完整记忆注入流式重写一版
WFW-8 整章级再沟通(后端)。新增 POST /projects/{id}/chapters/{no}/rewrite(SSE):
- rewrite.md 教条(品类无关内核,非 SPEC,仿 write_craft 经 load_prompt 读盘);
- rewrite_node:build_rewrite_request 纯函数(system=[rewrite教条,stable_core] cache 前缀;
input=近况+当前草稿+作者意见,断点后不入缓存前缀——守不变量 #9)+ stream_chapter_rewrite;
- RewriteStreamRequest{feedback,prior_draft}(带 max_length 上界);端点复用 assemble 记忆注入
+ normalize_deltas SSE,只读不写库(HITL,新版停前端接受才落,不变量 #3),是工具非写节点
(不破坏 章=f(outline,state) 纯函数,不变量 #7),项目不存在触网关前 404。
门禁绿:ruff / mypy 224 files / pytest(新增 test_rewrite_node 5 例 + 更新 doctrine 一一对应)/
alembic 无漂移。OpenAPI 含新路径 + schema(前端待 gen:api)。
This commit is contained in:
95
packages/core/tests/test_rewrite_node.py
Normal file
95
packages/core/tests/test_rewrite_node.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""整章重写节点单测:纯构造 + 转发网关 Delta(注入 mock 网关,无图运行时、无 DB)。
|
||||
|
||||
守不变量 ②(只声明 tier=writer)/#9(rewrite 教条 + stable_core 进缓存前缀,草稿/意见进
|
||||
断点后 input,不入缓存前缀)。asyncio_mode=auto。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from ww_core.orchestrator import (
|
||||
build_rewrite_input,
|
||||
build_rewrite_request,
|
||||
stream_chapter_rewrite,
|
||||
)
|
||||
from ww_llm_gateway.types import Delta, LlmRequest
|
||||
|
||||
|
||||
class _FakeGateway:
|
||||
"""记录收到的请求并按给定文本吐 Delta。"""
|
||||
|
||||
def __init__(self, texts: list[str]) -> None:
|
||||
self._texts = texts
|
||||
self.last_req: LlmRequest | None = None
|
||||
|
||||
async def stream(self, req: LlmRequest) -> AsyncIterator[Delta]:
|
||||
self.last_req = req
|
||||
for text in self._texts:
|
||||
yield Delta(text=text)
|
||||
|
||||
|
||||
def test_build_rewrite_input_orders_volatile_draft_feedback() -> None:
|
||||
out = build_rewrite_input(volatile="近况X", prior_draft="草稿Y", feedback="意见Z")
|
||||
assert "【当前整章草稿】" in out
|
||||
assert "【作者意见】" in out
|
||||
# 顺序:近况 → 草稿 → 意见。
|
||||
assert out.index("近况X") < out.index("草稿Y") < out.index("意见Z")
|
||||
|
||||
|
||||
def test_build_rewrite_input_omits_empty_volatile() -> None:
|
||||
out = build_rewrite_input(volatile=" ", prior_draft="D", feedback="F")
|
||||
assert out.startswith("【当前整章草稿】")
|
||||
assert "D" in out and "F" in out
|
||||
|
||||
|
||||
def test_build_rewrite_request_declares_writer_tier_and_streams() -> None:
|
||||
req = build_rewrite_request(
|
||||
stable_core="SC",
|
||||
volatile="V",
|
||||
prior_draft="DRAFT",
|
||||
feedback="FEEDBACK",
|
||||
user_id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
)
|
||||
assert req.tier == "writer" # 不变量 ②
|
||||
assert req.stream is True
|
||||
|
||||
|
||||
def test_rewrite_cached_prefix_excludes_draft_and_feedback() -> None:
|
||||
"""不变量 #9:草稿/意见是逐轮易变,绝不进 system 缓存前缀,只进断点后 input。"""
|
||||
req = build_rewrite_request(
|
||||
stable_core="STABLE_CORE",
|
||||
volatile="V",
|
||||
prior_draft="THE_DRAFT_BODY",
|
||||
feedback="THE_FEEDBACK",
|
||||
user_id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
)
|
||||
cached_prefix = "".join(block.text for block in req.system)
|
||||
assert all(block.cache for block in req.system)
|
||||
assert "STABLE_CORE" in cached_prefix
|
||||
assert "THE_DRAFT_BODY" not in cached_prefix
|
||||
assert "THE_FEEDBACK" not in cached_prefix
|
||||
assert "THE_DRAFT_BODY" in req.input
|
||||
assert "THE_FEEDBACK" in req.input
|
||||
|
||||
|
||||
async def test_stream_chapter_rewrite_forwards_gateway_deltas() -> None:
|
||||
gateway = _FakeGateway(["新版一", "新版二"])
|
||||
out = [
|
||||
delta.text
|
||||
async for delta in stream_chapter_rewrite(
|
||||
gateway,
|
||||
stable_core="SC",
|
||||
volatile="V",
|
||||
prior_draft="D",
|
||||
feedback="F",
|
||||
user_id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
)
|
||||
]
|
||||
assert out == ["新版一", "新版二"]
|
||||
assert gateway.last_req is not None
|
||||
assert gateway.last_req.tier == "writer"
|
||||
Reference in New Issue
Block a user