File size: 15,824 Bytes
0e6887b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | """《分析任务单》数据模型(理解层产物,intent-understanding-layer 任务 1)。
对应 design.md「Components and Interfaces / 1. Data model」。本模块定义理解层
(``UnderstandingLayer``)在正式分析(``compute``)之前产出的**强结构化、可审计、
可序列化**的中间契约——《分析任务单》(:class:`AnalysisTaskSheet`),以及其内嵌的
文档画像、提取项与澄清决策类型。
设计取向(与需求 / 设计一致):
- **不含任何分析数值计算**:任务单只承载「文档画像 + 意图 + 来源可定位的提取项 +
缺失/歧义 + 澄清决策」,所有分析数值由后续 ``compute`` 阶段(纯 Python)产出
(需求 1.4、Property 4)。
- **可序列化且往返一致**::meth:`AnalysisTaskSheet.to_dict` /
:meth:`AnalysisTaskSheet.from_dict` 互为逆操作(``from_dict(to_dict(x)) == x``,
需求 9.3、Property 9),便于会话状态保存、审计与无 UI / 无 LLM 的单元测试。
- **仅依赖标准库**:纯 dataclass + enum,不触网、无重依赖,便于稳定测试。
枚举值采用稳定的字符串字面量,作为对外序列化契约的一部分(不可随意更名)。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
# ---------------------------------------------------------------------------
# 枚举:意图、表类型、澄清原因码
# ---------------------------------------------------------------------------
class Intent(str, Enum):
"""用户请求意图分类(需求 3)。
- ``DESCRIPTIVE_SUMMARY``:描述性质量属性梳理(分组/统计/限度符合性,不外推)。
- ``SHELF_LIFE``:货架期外推 / 动力学建模。
- ``UNKNOWN``:无法自信分类 → 进入澄清模式(需求 3.4 / 6.4)。
"""
DESCRIPTIVE_SUMMARY = "descriptive_summary"
SHELF_LIFE = "shelf_life_extrapolation"
COMPATIBILITY = "compatibility"
UNKNOWN = "unknown"
class SlotState(str, Enum):
"""意图槽位的**来源态**(意图保真核心元信息)。
意图是潜变量——系统只能从「用户话语 + 文档上下文」观测它。为确保**用户真实意图
被采纳**,每个槽位必须显式携带其取值来源,杜绝「系统凭空具象化用户从未表达的意图」:
- ``STATED``:用户在指令中**明示**——最高优先级,经确认后锁定,永不被重推断覆盖。
- ``INFERRED``:由文档画像 / 输入信号**语用推断**——可用,但必须向用户**回显来源**
并允许一键改正(推断可以激进,但绝不隐瞒「这是推断」)。
- ``MISSING``:既无明示也无可靠推断——触发**选择性澄清**,绝不填默认值。
注意:**刻意不设 ``ASSUMED`` 态**。静默默认值(如规格 0.5 / 主 CQA「总杂质」/
目标时间点 [24,36])是篡改用户意图的暗门,在本保真模型中被禁止——缺失即 ``MISSING``。
"""
STATED = "stated"
INFERRED = "inferred"
MISSING = "missing"
class TableType(str, Enum):
"""文档中检测到的表类型(需求 2.2)。"""
RELATED_SUBSTANCES = "related_substances"
ASSAY = "assay"
DISSOLUTION = "dissolution"
CONTENT_UNIFORMITY = "content_uniformity"
PHYSICAL_PROPERTIES = "physical_properties"
STABILITY_TIME_SERIES = "stability_time_series"
UNKNOWN = "unknown"
#: 澄清原因码(需求 6):无 LLM / 意图未知 / 数据与意图不匹配。
CLARIFY_NO_LLM = "no_llm"
CLARIFY_INTENT_UNKNOWN = "intent_unknown"
CLARIFY_DATA_INTENT_MISMATCH = "data_intent_mismatch"
def _coerce_enum(enum_cls, value, default):
"""把字符串 / 枚举安全归一为 ``enum_cls`` 成员;无法识别回退 ``default``。"""
if isinstance(value, enum_cls):
return value
try:
return enum_cls(str(value))
except ValueError:
return default
# ---------------------------------------------------------------------------
# 文档画像
# ---------------------------------------------------------------------------
@dataclass
class DetectedTable:
"""文档中检测到的单张表(需求 2.1 / 2.2 / 2.3)。"""
table_type: TableType
title: str = ""
source_document: str = "" # 多文档聚合:记录来源文件名(需求 2.2)
is_stability_time_series: bool = False
columns: list[str] = field(default_factory=list)
sample_rows: list[list[str]] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"table_type": self.table_type.value,
"title": self.title,
"source_document": self.source_document,
"is_stability_time_series": bool(self.is_stability_time_series),
"columns": list(self.columns),
"sample_rows": [list(r) for r in self.sample_rows],
}
@staticmethod
def from_dict(d: dict) -> "DetectedTable":
d = d or {}
return DetectedTable(
table_type=_coerce_enum(TableType, d.get("table_type"), TableType.UNKNOWN),
title=str(d.get("title", "")),
source_document=str(d.get("source_document", "")),
is_stability_time_series=bool(d.get("is_stability_time_series", False)),
columns=[str(c) for c in (d.get("columns") or [])],
sample_rows=[[str(c) for c in row] for row in (d.get("sample_rows") or [])],
)
@dataclass
class DocumentProfile:
"""文档画像:检测到的表集合 + 是否含稳定性时序(需求 2)。"""
tables: list[DetectedTable] = field(default_factory=list)
has_stability_time_series: bool = False
notes: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"tables": [t.to_dict() for t in self.tables],
"has_stability_time_series": bool(self.has_stability_time_series),
"notes": list(self.notes),
}
@staticmethod
def from_dict(d: dict) -> "DocumentProfile":
d = d or {}
return DocumentProfile(
tables=[DetectedTable.from_dict(t) for t in (d.get("tables") or [])],
has_stability_time_series=bool(d.get("has_stability_time_series", False)),
notes=[str(n) for n in (d.get("notes") or [])],
)
# ---------------------------------------------------------------------------
# 提取项与澄清决策
# ---------------------------------------------------------------------------
@dataclass
class ExtractedItem:
"""单个提取项,必须可回溯到原文(需求 4)。
- ``source_ref``:可在原文定位的片段 / 单元格内容(回填校验依据,需求 4.1)。
- ``located``:是否在原文成功定位;``False`` 表示被标记缺失、不得带入捏造值
(需求 4.2 / Property 1)。
- ``group``:分组维度(如 ``{"strength": "20μg", "batch": "B1", "attribute": "总杂"}``),
供描述性梳理按规格/批次/属性组织(需求 7.2)。
"""
field: str
value: str = ""
source_ref: str = ""
located: bool = True
group: dict = field(default_factory=dict)
source: str = "llm" # "llm"(模型提取)| "user_edited"(用户手动修改/新增)
def to_dict(self) -> dict:
return {
"field": self.field,
"value": self.value,
"source_ref": self.source_ref,
"located": bool(self.located),
"group": dict(self.group),
"source": self.source,
}
@staticmethod
def from_dict(d: dict) -> "ExtractedItem":
d = d or {}
return ExtractedItem(
field=str(d.get("field", "")),
value=str(d.get("value", "")),
source_ref=str(d.get("source_ref", "")),
located=bool(d.get("located", True)),
group=dict(d.get("group") or {}),
source=str(d.get("source", "llm")),
)
@dataclass
class IntentSlot:
"""单个**意图槽位**及其保真元信息(意图保真核心载体)。
把「意图」从一次性扁平分类,升级为「带来源、带置信、可锁定、缺失即问」的可确认
结构。每个槽位回答三个保真关键问题:这是用户**说的**还是系统**猜的**(``state`` /
``evidence``)?系统有**多确定**(``confidence``)?用户**改得动**吗(``locked``)?
- ``name``:槽位名(如 ``primary_cqa`` / ``spec_limit`` / ``target_timepoints``)。
- ``value``:取值;``MISSING`` 态下应为 ``None``(绝不填默认)。
- ``state``:来源态(:class:`SlotState`)。
- ``confidence``:0..1 置信度,驱动选择性澄清与确认策略(STATED≈1.0 / INFERRED 居中 / MISSING=0)。
- ``evidence``:明示 / 推断的原文依据片段(对话接地溯源,供回显)。
- ``locked``:用户确认 / 编辑后置 ``True``——**任何重推断都不得覆盖**(采纳真实意图的硬保证)。
- ``affects_result``:是否影响计算结论,驱动「信息价值」澄清决策(关键槽缺失才必问)。
"""
name: str
value: Any = None
state: SlotState = SlotState.MISSING
confidence: float = 0.0
evidence: str = ""
locked: bool = False
affects_result: bool = True
def needs_clarification(self) -> bool:
"""该槽位是否应触发澄清:缺失、且影响结论、且未被用户锁定。"""
return (
self.state is SlotState.MISSING
and self.affects_result
and not self.locked
)
def to_dict(self) -> dict:
return {
"name": self.name,
"value": self.value,
"state": self.state.value,
"confidence": float(self.confidence),
"evidence": self.evidence,
"locked": bool(self.locked),
"affects_result": bool(self.affects_result),
}
@staticmethod
def from_dict(d: dict) -> "IntentSlot":
d = d or {}
return IntentSlot(
name=str(d.get("name", "")),
value=d.get("value"),
state=_coerce_enum(SlotState, d.get("state"), SlotState.MISSING),
confidence=float(d.get("confidence", 0.0) or 0.0),
evidence=str(d.get("evidence", "")),
locked=bool(d.get("locked", False)),
affects_result=bool(d.get("affects_result", True)),
)
@dataclass
class ClarificationDecision:
"""澄清决策(需求 6):是否需要向用户澄清及其原因。"""
needed: bool = False
reason_code: str = "" # CLARIFY_* 之一
message_key: str = "" # i18n key(界面提示文案)
options: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"needed": bool(self.needed),
"reason_code": self.reason_code,
"message_key": self.message_key,
"options": list(self.options),
}
@staticmethod
def from_dict(d: dict) -> "ClarificationDecision":
d = d or {}
return ClarificationDecision(
needed=bool(d.get("needed", False)),
reason_code=str(d.get("reason_code", "")),
message_key=str(d.get("message_key", "")),
options=[str(o) for o in (d.get("options") or [])],
)
# ---------------------------------------------------------------------------
# 分析任务单
# ---------------------------------------------------------------------------
@dataclass
class AnalysisTaskSheet:
"""《分析任务单》:理解层的强结构化产物,经用户确认后驱动分析(需求 5)。
字段:
- ``intent``:分类意图(需求 3.5)。
- ``document_profile``:文档画像(需求 2)。
- ``extracted_items``:来源可定位的提取项(需求 4)。
- ``proposed_skill_id``:建议路由的 Skill id(用户可编辑,需求 5.3 / 8 路由)。
- ``missing_items`` / ``ambiguous_items``:缺失与歧义清单(需求 4.5)。
- ``clarification``:澄清决策;``None`` 表示无需澄清(需求 6)。
- ``source_method``:``"llm"`` | ``"clarification"``,标注任务单来源。
"""
intent: Intent = Intent.UNKNOWN
document_profile: DocumentProfile = field(default_factory=DocumentProfile)
extracted_items: list[ExtractedItem] = field(default_factory=list)
proposed_skill_id: str = ""
missing_items: list[str] = field(default_factory=list)
ambiguous_items: list[str] = field(default_factory=list)
clarification: Optional[ClarificationDecision] = None
source_method: str = "llm"
#: 意图槽位(保真核心):带来源态 / 置信度 / 锁定的可确认意图维度(需求 3 / 5)。
#: 向后兼容默认空列表——旧任务单与未启用槽位的技能不受影响。
intent_slots: list[IntentSlot] = field(default_factory=list)
def needs_clarification(self) -> bool:
"""是否处于澄清状态(澄清决策存在且 needed=True)。"""
return self.clarification is not None and bool(self.clarification.needed)
def slots_needing_clarification(self) -> list[IntentSlot]:
"""返回应触发澄清的关键意图槽位(缺失、影响结论、未锁定)。
供选择性澄清(信息价值驱动)使用:只对真正影响结果且不确定的槽位发问,
避免过度澄清。返回空列表表示无需就意图槽位追问。
"""
return [s for s in self.intent_slots if s.needs_clarification()]
def get_slot(self, name: str) -> Optional[IntentSlot]:
"""按名取意图槽位;不存在返回 ``None``。"""
for s in self.intent_slots:
if s.name == name:
return s
return None
def to_dict(self) -> dict:
return {
"intent": self.intent.value,
"document_profile": self.document_profile.to_dict(),
"extracted_items": [it.to_dict() for it in self.extracted_items],
"proposed_skill_id": self.proposed_skill_id,
"missing_items": list(self.missing_items),
"ambiguous_items": list(self.ambiguous_items),
"clarification": self.clarification.to_dict() if self.clarification else None,
"source_method": self.source_method,
"intent_slots": [s.to_dict() for s in self.intent_slots],
}
@staticmethod
def from_dict(d: dict) -> "AnalysisTaskSheet":
d = d or {}
clar = d.get("clarification")
return AnalysisTaskSheet(
intent=_coerce_enum(Intent, d.get("intent"), Intent.UNKNOWN),
document_profile=DocumentProfile.from_dict(d.get("document_profile") or {}),
extracted_items=[ExtractedItem.from_dict(it) for it in (d.get("extracted_items") or [])],
proposed_skill_id=str(d.get("proposed_skill_id", "")),
missing_items=[str(m) for m in (d.get("missing_items") or [])],
ambiguous_items=[str(a) for a in (d.get("ambiguous_items") or [])],
clarification=ClarificationDecision.from_dict(clar) if clar else None,
source_method=str(d.get("source_method", "llm")),
intent_slots=[IntentSlot.from_dict(s) for s in (d.get("intent_slots") or [])],
)
__all__ = [
"Intent",
"SlotState",
"TableType",
"CLARIFY_NO_LLM",
"CLARIFY_INTENT_UNKNOWN",
"CLARIFY_DATA_INTENT_MISMATCH",
"DetectedTable",
"DocumentProfile",
"ExtractedItem",
"IntentSlot",
"ClarificationDecision",
"AnalysisTaskSheet",
]
|