Preformu / core /kernel /task_sheet.py
Kevinshh's picture
feat: 意图保真(intent-fidelity) + 描述性梳理技能 + 相容性引擎升级; 修复转置宽表解析/CQA对账/澄清交互/功能切换串显; .gitignore 排除专利与机密Demo数据
0e6887b
Raw
History Blame Contribute Delete
15.8 kB
"""《分析任务单》数据模型(理解层产物,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",
]