Files
writer-work-flow/packages/agents/tests/test_clarify_spec.py
Yaojia Wang 1652ad9d20 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 无漂移。
2026-07-08 08:47:13 +02:00

126 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""WFW-9 M1 clarify_refine 契约测试schema + spec 声明路线A 两阶段之「问题」阶段)。
契约测试——构造符合 schema 的 mock 响应,校验字段/默认值/tier/读写权限。
ClarifyDecision`{need_clarification, questions[≤1], verification?}`
analyst 档、reads=()、writes=()。全字段带默认值守解析韧性LLM 漏产字段降级为
need_clarification=false不阻塞润色。不联网、无 DB。
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from ww_agents import (
AgentSpec,
ClarifyDecision,
ClarifyOption,
ClarifyQuestion,
clarify_refine_spec,
)
# ---- ClarifyDecision / ClarifyQuestion / ClarifyOption schema ----
def test_clarify_decision_parses_need_clarification_with_one_question() -> None:
# Arrange含糊 → 反问 1 问 + 3 个锚定选项 + 自由输入兜底
mock = {
"need_clarification": True,
"questions": [
{
"question": "你想让这段更有张力,是指哪种方向?",
"options": [
{"label": "加快节奏", "value": "把长句拆短,压缩铺陈,加快推进"},
{"label": "加重冲突", "value": "强化人物对立,增加正面交锋"},
{"label": "收紧对白", "value": "删掉旁白,让张力靠对话推进"},
],
"allow_free_text": True,
}
],
"verification": None,
}
# Act
decision = ClarifyDecision.model_validate(mock)
# Assert
assert decision.need_clarification is True
assert len(decision.questions) == 1
q = decision.questions[0]
assert q.question.startswith("你想让这段")
assert len(q.options) == 3
assert q.options[0].label == "加快节奏"
assert q.options[0].value.startswith("把长句拆短")
assert q.allow_free_text is True
assert decision.verification is None
def test_clarify_decision_parses_clear_with_verification() -> None:
# 明确 → 不问、给确认语
mock = {
"need_clarification": False,
"questions": [],
"verification": "我会把第二句的被字句改成主动句,其余不动,对吗?",
}
decision = ClarifyDecision.model_validate(mock)
assert decision.need_clarification is False
assert decision.questions == []
assert decision.verification is not None
def test_clarify_decision_all_fields_default_for_resilience() -> None:
# 全字段默认值守解析韧性:空对象降级为 need_clarification=false不问、放行
decision = ClarifyDecision.model_validate({})
assert decision.need_clarification is False
assert decision.questions == []
assert decision.verification is None
def test_clarify_question_options_default_empty_and_free_text_true() -> None:
# 凑不出具体选项 → 空 options + 常驻自由输入兜底
q = ClarifyQuestion.model_validate({"question": "你具体想改哪里?"})
assert q.options == []
assert q.allow_free_text is True
def test_clarify_option_requires_label_and_value() -> None:
with pytest.raises(ValidationError):
ClarifyOption.model_validate({"label": "加快节奏"})
def test_clarify_question_requires_question_text() -> None:
with pytest.raises(ValidationError):
ClarifyQuestion.model_validate({"options": []})
# ---- clarify_refine_spec 声明 ----
def test_clarify_refine_spec_is_analyst_tier() -> None:
# 不变量 #2含糊判别 + 反问构造用分析档,只声明 tier
assert clarify_refine_spec.tier == "analyst"
assert clarify_refine_spec.name == "clarify_refine"
def test_clarify_refine_spec_is_read_only() -> None:
# 不变量 #3只读——不读库真相源材料端点注入、不写任何表
assert clarify_refine_spec.reads == ()
assert clarify_refine_spec.writes == ()
def test_clarify_refine_spec_output_schema() -> None:
assert clarify_refine_spec.output_schema is ClarifyDecision
def test_clarify_refine_spec_prompt_documents_at_most_one_question() -> None:
# v1 硬上限 1 问 + 自由输入兜底须在教条里显式约束
prompt = clarify_refine_spec.system_prompt
assert "1 问" in prompt
assert "自由输入" in prompt
assert "防循环" in prompt
def test_clarify_refine_spec_is_agent_spec_and_immutable() -> None:
assert isinstance(clarify_refine_spec, AgentSpec)
with pytest.raises(ValidationError):
clarify_refine_spec.tier = "writer"