88 lines
4.2 KiB
Python
88 lines
4.2 KiB
Python
"""需求理解的输出协议,描述用户诉求,不包含前端执行动作。"""
|
|
|
|
from typing import Literal, Self
|
|
|
|
from pydantic import Field, field_validator, model_validator
|
|
|
|
from .base import ExtensibleModel, JsonObject, NonBlankString
|
|
from .operations import OPERATION_GUIDE, normalize_operation
|
|
|
|
Intent = Literal["create_screen", "modify_screen", "qa", "unclear"]
|
|
TargetScope = Literal["nodes", "canvas", "screen", "unresolved"]
|
|
|
|
|
|
class RequirementChange(ExtensibleModel):
|
|
"""一项需求变更。目标范围显式声明,避免空 ID 被理解成修改整个大屏。"""
|
|
|
|
target_scope: TargetScope = Field(
|
|
description="nodes=已有节点;canvas=画布属性;screen=整体或新增内容;unresolved=目标待确认"
|
|
)
|
|
target_ids: list[NonBlankString] = Field(
|
|
default_factory=list, description="已有节点的 ID;新增组件不能虚构 ID"
|
|
)
|
|
target_description: NonBlankString | None = Field(
|
|
default=None, description="目标的自然语言描述,例如右侧区域、用户所说的那个图"
|
|
)
|
|
candidate_ids: list[NonBlankString] = Field(
|
|
default_factory=list,
|
|
description="仅 unresolved 时填写:当前大屏中可能是该目标的真实组件 ID;没有可能的组件时留空,不虚构",
|
|
)
|
|
operation: NonBlankString = Field(description=f"需求操作。{OPERATION_GUIDE}")
|
|
parameters: JsonObject = Field(
|
|
default_factory=dict,
|
|
description="需求参数,例如 delta_x=50;保留用户要求的单位与相对变化语义;属性修改用用户的说法描述改什么、改成什么,例如 {\"名字\": \"月度销售\"},不推断字段路径",
|
|
)
|
|
|
|
@field_validator("operation")
|
|
@classmethod
|
|
def normalize_operation_name(cls, value: str) -> str:
|
|
# 在协议层统一同义操作名,模型输出、测试构造和检查点恢复都经过这里
|
|
return normalize_operation(value)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_target(self) -> Self:
|
|
if len(self.target_ids) != len(set(self.target_ids)):
|
|
raise ValueError("target_ids 不能包含重复 ID")
|
|
if self.target_scope == "nodes":
|
|
if not self.target_ids:
|
|
raise ValueError("nodes 范围必须提供 target_ids")
|
|
elif self.target_ids:
|
|
raise ValueError("只有 nodes 范围可以提供 target_ids")
|
|
if self.target_scope == "unresolved" and self.target_description is None:
|
|
raise ValueError("目标待确认时必须提供 target_description")
|
|
# 目标已确定时候选没有意义,直接丢弃,避免模型多填字段导致整份需求失效
|
|
if self.target_scope != "unresolved" and self.candidate_ids:
|
|
self.candidate_ids = []
|
|
return self
|
|
|
|
|
|
class Requirement(ExtensibleModel):
|
|
intent: Intent
|
|
goal: NonBlankString = Field(
|
|
description="结合上下文改写后的独立完整需求,补全可确定的指代,保留目标、操作和约束,不扩大修改范围"
|
|
)
|
|
changes: list[RequirementChange] = Field(default_factory=list)
|
|
needs_clarification: bool
|
|
clarification_question: NonBlankString | None = None
|
|
assumptions: list[NonBlankString] = Field(
|
|
default_factory=list,
|
|
description="非阻塞信息采用的默认假设,不可用来猜测删除目标",
|
|
)
|
|
constraints: list[NonBlankString] = Field(
|
|
default_factory=list,
|
|
description="用户明确提出的约束,例如只修改右侧、保留其他组件",
|
|
)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_consistency(self) -> Self:
|
|
if self.needs_clarification != (self.clarification_question is not None):
|
|
raise ValueError("需要澄清时必须提供问题,不需要澄清时问题应为 null")
|
|
if self.intent == "unclear" and not self.needs_clarification:
|
|
raise ValueError("意图不明确时必须澄清")
|
|
if any(change.target_scope == "unresolved" for change in self.changes):
|
|
if not self.needs_clarification:
|
|
raise ValueError("存在待确认目标时必须澄清")
|
|
if self.intent == "qa" and self.changes:
|
|
raise ValueError("纯问答需求不应包含大屏变更")
|
|
return self
|