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:
Yaojia Wang
2026-07-08 07:04:42 +02:00
parent 4977c760d8
commit 6ed9c89430
7 changed files with 294 additions and 2 deletions

View File

@@ -53,6 +53,11 @@ from .review_node import (
make_review_node,
run_review,
)
from .rewrite_node import (
build_rewrite_input,
build_rewrite_request,
stream_chapter_rewrite,
)
from .sse import (
EVENT_CHARACTERIZATION,
EVENT_CONFLICT,
@@ -150,6 +155,9 @@ __all__ = [
"run_worldbuilder",
"section_event",
"setup_checkpointer",
"build_rewrite_input",
"build_rewrite_request",
"stream_chapter_rewrite",
"stream_chapter_draft",
"style_event",
"token_event",

View File

@@ -0,0 +1,95 @@
"""整章再沟通 / 重写节点ARCH §5.2 writer 行的工具变体)。
把当前整章草稿 + 作者意见流式重写成新一版。与 write 节点的关系(不变量纪律):
- rewrite 是**独立工具,不是写图节点**:吃「当前草稿 + 作者意见」,不参与
`章=f(outline, state, fingerprint)` 纯函数写章(不变量 #7也不写库
HITL新版停前端/草稿,接受才落,不变量 #3
- 与 write 一样:节点逻辑无 LLM 非确定性(藏在网关后),`build_rewrite_request` 是纯函数、
`stream_chapter_rewrite` 注入 mock 网关即可单测。
- 不变量 ②:只传 `tier="writer"`,绝不传具体 model。
- 不变量 #9rewrite 教条 + `stable_core` 进 `system` 缓存断点前cache=True易变的
`volatile` + 当前草稿 + 作者意见进 `input`(断点后,绝不进缓存前缀)。
"""
from __future__ import annotations
import uuid
from collections.abc import AsyncIterator
from typing import Protocol
from ww_agents import load_prompt
from ww_llm_gateway.types import Block, Delta, LlmRequest, Scope
# 整章重写教条品类无关内核import 期读盘 → 缺失即 fail-fast崩 import
# 字节稳定的稳定内容,作为最前的 cache=True system 块(跨调用前缀复用,不变量 #9
_REWRITE_DOCTRINE = load_prompt("rewrite")
class GatewayStream(Protocol):
"""rewrite 节点对网关的最小依赖——只需 `stream`(注入真网关或 mock"""
def stream(self, req: LlmRequest) -> AsyncIterator[Delta]: ...
def build_rewrite_input(*, volatile: str, prior_draft: str, feedback: str) -> str:
"""组重写输入(断点后 volatile 段):近况 + 当前整章草稿 + 作者意见。
三段都进 `input`(不进缓存前缀):草稿/意见逐轮变,是最易变的部分。
"""
parts: list[str] = []
if volatile.strip():
parts.append(volatile)
parts.append(f"【当前整章草稿】\n{prior_draft}")
parts.append(f"【作者意见】\n{feedback}")
return "\n\n".join(parts)
def build_rewrite_request(
*,
stable_core: str,
volatile: str,
prior_draft: str,
feedback: str,
user_id: uuid.UUID,
project_id: uuid.UUID,
) -> LlmRequest:
"""据组装上下文 + 当前草稿 + 作者意见构造重写请求(纯函数)。
`system` = [rewrite 教条(cache), stable_core(cache)]`input` = 近况 + 草稿 + 意见(断点后)。
"""
return LlmRequest(
tier="writer",
system=[
Block(text=_REWRITE_DOCTRINE, cache=True),
Block(text=stable_core, cache=True),
],
input=build_rewrite_input(volatile=volatile, prior_draft=prior_draft, feedback=feedback),
stream=True,
scope=Scope(user_id=user_id, project_id=project_id),
)
async def stream_chapter_rewrite(
gateway: GatewayStream,
*,
stable_core: str,
volatile: str,
prior_draft: str,
feedback: str,
user_id: uuid.UUID,
project_id: uuid.UUID,
) -> AsyncIterator[Delta]:
"""流式重写本章增量——构造请求 → 转发网关 `Delta`。
本身不累积/不落库;瞬时失败已在网关重试,此处异常向上抛给 SSE 归一层(发 `error` 事件)。
"""
req = build_rewrite_request(
stable_core=stable_core,
volatile=volatile,
prior_draft=prior_draft,
feedback=feedback,
user_id=user_id,
project_id=project_id,
)
async for delta in gateway.stream(req):
yield delta