feat(backend): AI 反问澄清预检端点——refine 侧结构化 clarify(WFW-9 M1,路线A 两阶段)
润色「再沟通」意见含糊时先反问给选项(路线A:问题走独立非流式 JSON 预检端点,正文仍走
既有 refine 一字不改)。新增:
- ClarifyDecision/ClarifyQuestion/ClarifyOption 结构化 schema(既有 output-schema 处,供
producer 与端点共用);clarify_refine.md 教条(含糊→need_clarification+≤1问+2–4锚定选项+
自由输入;明确→verification 放行;防循环);注册 clarify_refine_spec(analyst 档,#24)+
SCHEMA_CATALOG + 重生成金标准。
- clarify_node:build_clarify_request 纯函数(缓存前缀不含易变) + run_clarify(gateway.run
结构化,判别/校验失败确定性回退 need_clarification=false,只读不写库)。
- POST /projects/{id}/chapters/{no}/refine/clarify → ClarifyDecision(analyst 网关,404/503,
末尾 commit 记账)。**既有 refine 端点/RefineRequest/Response 完全未改**。
门禁绿:ruff/mypy 227/pytest 900(+test_clarify_spec/_node/style clarify)/alembic 无漂移。
This commit is contained in:
@@ -22,10 +22,10 @@ from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from ww_agents import refiner_spec, style_extract_spec
|
||||
from ww_agents import ClarifyDecision, clarify_refine_spec, refiner_spec, style_extract_spec
|
||||
from ww_core.domain import JobRepo, ProjectRepo, StyleFingerprintWriteRepo
|
||||
from ww_core.domain.style_repo import SqlStyleFingerprintWriteRepo
|
||||
from ww_core.orchestrator import run_style_extraction
|
||||
from ww_core.orchestrator import run_clarify, run_style_extraction
|
||||
from ww_db import get_session
|
||||
from ww_llm_gateway import Gateway
|
||||
from ww_llm_gateway.types import Block, LlmRequest, Scope
|
||||
@@ -34,6 +34,7 @@ from ww_shared import AppError, ErrorCode, ErrorEnvelope
|
||||
from ww_api.logging_config import get_logger
|
||||
from ww_api.schemas.style import (
|
||||
DimensionEntry,
|
||||
RefineClarifyRequest,
|
||||
RefineRequest,
|
||||
RefineResponse,
|
||||
StyleFingerprintResponse,
|
||||
@@ -45,6 +46,7 @@ from ww_api.services.foreshadow_scan import SessionFactory
|
||||
from ww_api.services.job_runner import run_job
|
||||
from ww_api.services.project_deps import (
|
||||
build_gateway_for_tier,
|
||||
get_clarify_gateway,
|
||||
get_job_repo,
|
||||
get_project_repo,
|
||||
get_refine_gateway,
|
||||
@@ -62,6 +64,7 @@ JobRepoDep = Annotated[JobRepo, Depends(get_job_repo)]
|
||||
StyleWriteRepoDep = Annotated[StyleFingerprintWriteRepo, Depends(get_style_write_repo)]
|
||||
StyleExtractGatewayDep = Annotated[Gateway, Depends(get_style_extract_gateway)]
|
||||
RefineGatewayDep = Annotated[Gateway, Depends(get_refine_gateway)]
|
||||
ClarifyGatewayDep = Annotated[Gateway, Depends(get_clarify_gateway)]
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
SessionFactoryDep = Annotated[SessionFactory, Depends(get_session_factory)]
|
||||
|
||||
@@ -238,3 +241,59 @@ async def refine_segment(
|
||||
has_instruction=body.instruction is not None,
|
||||
)
|
||||
return RefineResponse(original=body.segment, refined=resp.text)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/chapters/{chapter_no}/refine/clarify",
|
||||
responses={
|
||||
404: {"model": ErrorEnvelope, "description": "项目不存在"},
|
||||
503: {"model": ErrorEnvelope, "description": "LLM 不可用"},
|
||||
},
|
||||
)
|
||||
async def clarify_refine(
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
body: RefineClarifyRequest,
|
||||
request: Request,
|
||||
project_repo: ProjectRepoDep,
|
||||
gateway: ClarifyGatewayDep,
|
||||
session: SessionDep,
|
||||
) -> ClarifyDecision:
|
||||
"""润色预检澄清(WFW-9 M1 路线A 两阶段之「问题」阶段):非流式 JSON,只判不改。
|
||||
|
||||
独立于既有 refine 端点——只判「作者这条再沟通意见清不清楚」,含糊则反问 1 问 + 给选项,
|
||||
清楚则给确认语放行;**绝不改正文**(正文仍走 refine 端点)。analyst 档(不变量 #2)。
|
||||
|
||||
项目不存在 → 404;无凭据 → 503(dep 解析阶段拦下)。**只读不写库**(不变量 #3);
|
||||
末尾 `commit()` 让网关 usage_ledger 落库(add-only,否则记账静默丢失,同 refine 纪律)。
|
||||
判别/校验失败在 `run_clarify` 内确定性回退 `need_clarification=false`(不阻塞润色)。
|
||||
"""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
|
||||
project = await project_repo.get(STUB_OWNER_ID, project_id)
|
||||
if project is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")
|
||||
|
||||
context = _build_refine_input(body.segment, body.instruction or None)
|
||||
decision = await run_clarify(
|
||||
clarify_refine_spec, # 不变量 #2:analyst 档,不传 model
|
||||
context=context,
|
||||
gateway=gateway,
|
||||
user_id=STUB_OWNER_ID,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
# 提交边界:只读不写业务表,但网关 ledger add-only → 端点末尾 commit(否则记账丢失)。
|
||||
await session.commit()
|
||||
|
||||
log.info(
|
||||
"refine_clarify_done",
|
||||
project_id=str(project_id),
|
||||
chapter_no=chapter_no,
|
||||
request_id=request_id,
|
||||
segment_len=len(body.segment),
|
||||
instruction_len=len(body.instruction),
|
||||
need_clarification=decision.need_clarification,
|
||||
question_count=len(decision.questions),
|
||||
)
|
||||
return decision
|
||||
|
||||
@@ -56,3 +56,20 @@ class RefineResponse(BaseModel):
|
||||
|
||||
original: str
|
||||
refined: str
|
||||
|
||||
|
||||
# 上界:选段/意见文本长度守卫(防超长入参空跑 LLM;预检只看本段,无需整章)。
|
||||
_MAX_CLARIFY_SEGMENT_LEN = 20000
|
||||
_MAX_CLARIFY_INSTRUCTION_LEN = 2000
|
||||
|
||||
|
||||
class RefineClarifyRequest(BaseModel):
|
||||
"""润色预检澄清:选段 + 再沟通意见(WFW-9 M1 路线A 两阶段之「问题」阶段)。
|
||||
|
||||
与 `RefineRequest` 独立——预检只判「意见清不清楚」,不改正文。`instruction` 为作者的
|
||||
再沟通意见(可空/极短,正是触发反问的场景);带长度上界防超长入参。响应=`ClarifyDecision`
|
||||
(在 ww_agents,端点直接返回,供前端 gen:api 生成强类型客户端)。
|
||||
"""
|
||||
|
||||
segment: str = Field(min_length=1, max_length=_MAX_CLARIFY_SEGMENT_LEN)
|
||||
instruction: str = Field(default="", max_length=_MAX_CLARIFY_INSTRUCTION_LEN)
|
||||
|
||||
@@ -519,6 +519,18 @@ async def get_refine_gateway(
|
||||
return await build_gateway_for_tier(session, store, "writer")
|
||||
|
||||
|
||||
async def get_clarify_gateway(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> Gateway:
|
||||
"""润色预检澄清(analyst 档位)的可注入网关缝。
|
||||
|
||||
`POST .../refine/clarify` 在 dep 解析阶段构建网关 → 无凭据时这里抛 `LLM_UNAVAILABLE`
|
||||
(503,进入端点前拦下)。测试经 override 注 mock(产 `ClarifyDecision`)。
|
||||
"""
|
||||
store = SqlCredentialStore(session)
|
||||
return await build_gateway_for_tier(session, store, "analyst")
|
||||
|
||||
|
||||
async def get_chain_gateway(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> Gateway:
|
||||
|
||||
Reference in New Issue
Block a user