Preformu / tests /test_intent_slots.py
Kevinshh's picture
feat: 意图保真(intent-fidelity) + 描述性梳理技能 + 相容性引擎升级; 修复转置宽表解析/CQA对账/澄清交互/功能切换串显; .gitignore 排除专利与机密Demo数据
0e6887b
Raw
History Blame Contribute Delete
9.99 kB
"""意图保真:意图槽位(IntentSlot)派生与不变量测试。
覆盖「确保用户真实意图被采纳」的核心保真规则:
- 用户明示 → STATED;仅可推断 → INFERRED;缺失 / 歧义 → MISSING(**绝不填默认**)。
- 选择性澄清:仅对「影响结论且不确定且未锁定」的槽位追问。
- 用户锁定后不再被澄清(采纳真实意图的硬保证)。
- 序列化往返一致(含 intent_slots)。
"""
from __future__ import annotations
from kernel.task_sheet import (
AnalysisTaskSheet,
ExtractedItem,
Intent,
IntentSlot,
SlotState,
)
from kernel.understanding import (
SLOT_PRIMARY_CQA,
SLOT_SPEC_LIMIT,
SLOT_TARGET_TIMEPOINTS,
build_intent_slots,
)
def _slot(slots, name):
return next((s for s in slots if s.name == name), None)
# ---------------------------------------------------------------------------
# build_intent_slots:来源态判定
# ---------------------------------------------------------------------------
def test_explicit_goal_yields_stated_slots():
"""用户明示主 CQA 与目标时间点 → STATED,置信 1.0。"""
slots = build_intent_slots(
goal="请把总杂质外推到36个月", intent=Intent.SHELF_LIFE, extracted_items=[]
)
tp = _slot(slots, SLOT_TARGET_TIMEPOINTS)
cqa = _slot(slots, SLOT_PRIMARY_CQA)
assert tp.state is SlotState.STATED and tp.value == [36] and tp.confidence == 1.0
assert cqa.state is SlotState.STATED and cqa.value == "总杂"
def test_missing_slots_have_no_default_values():
"""指令与文档都未给出关键槽位 → MISSING 且 value 为 None(禁止 0.5 / 总杂质 / [24,36])。"""
slots = build_intent_slots(
goal="预测一下稳定性", intent=Intent.SHELF_LIFE, extracted_items=[]
)
tp = _slot(slots, SLOT_TARGET_TIMEPOINTS)
cqa = _slot(slots, SLOT_PRIMARY_CQA)
assert tp.state is SlotState.MISSING and tp.value is None
assert cqa.state is SlotState.MISSING and cqa.value is None
# 关键:绝无 ASSUMED 态,也无任何被悄悄填入的默认值。
assert all(s.state is not SlotState.MISSING or s.value is None for s in slots)
def test_single_attribute_infers_cqa():
"""文档仅含单一属性 → 主 CQA 推断为 INFERRED(带来源 evidence)。"""
items = [ExtractedItem(field="总杂", value="0.5", group={"attribute": "总杂"})]
slots = build_intent_slots(
goal="外推到24个月", intent=Intent.SHELF_LIFE, extracted_items=items
)
cqa = _slot(slots, SLOT_PRIMARY_CQA)
assert cqa.state is SlotState.INFERRED
assert cqa.value == "总杂"
assert cqa.evidence
def test_multiple_attributes_are_ambiguous_not_guessed():
"""文档含多个属性且指令未指定 → 主 CQA 视为歧义 → MISSING(不臆断择一)。"""
items = [
ExtractedItem(field="总杂", value="0.5", group={"attribute": "总杂"}),
ExtractedItem(field="含量", value="99.5", group={"attribute": "含量"}),
]
slots = build_intent_slots(
goal="外推到24个月", intent=Intent.SHELF_LIFE, extracted_items=items
)
cqa = _slot(slots, SLOT_PRIMARY_CQA)
assert cqa.state is SlotState.MISSING and cqa.value is None
def test_spec_limit_inferred_from_items():
items = [ExtractedItem(field="spec_limit", value="总杂≤2.0%", source_ref="限度 总杂≤2.0%")]
slots = build_intent_slots(
goal="把总杂外推到36个月", intent=Intent.SHELF_LIFE, extracted_items=items
)
spec = _slot(slots, SLOT_SPEC_LIMIT)
assert spec.state is SlotState.INFERRED and spec.value == "总杂≤2.0%"
def test_non_shelf_life_intent_yields_no_slots():
"""相容性 / 未知意图暂不强加槽位,保持向后兼容(描述性梳理见下方专项测试)。"""
assert build_intent_slots("相容性评估", Intent.COMPATIBILITY, []) == []
assert build_intent_slots("随便问问", Intent.UNKNOWN, []) == []
# ---------------------------------------------------------------------------
# 选择性澄清(信息价值驱动) + 锁定粘性
# ---------------------------------------------------------------------------
def test_selective_clarification_targets_only_result_affecting_missing():
"""缺失的目标时间点 / 主 CQA(影响结论)应被追问;缺失规格(可降级)不强制追问。"""
slots = build_intent_slots("预测稳定性", Intent.SHELF_LIFE, [])
sheet = AnalysisTaskSheet(intent=Intent.SHELF_LIFE, intent_slots=slots)
names = {s.name for s in sheet.slots_needing_clarification()}
assert SLOT_TARGET_TIMEPOINTS in names
assert SLOT_PRIMARY_CQA in names
assert SLOT_SPEC_LIMIT not in names # affects_result=False → 不过度澄清
def test_locked_slot_is_not_reclarified():
"""用户锁定的缺失槽位不再触发澄清(采纳真实意图,模型不得推翻用户决定)。"""
s = IntentSlot(name=SLOT_TARGET_TIMEPOINTS, value=None, state=SlotState.MISSING,
affects_result=True, locked=True)
assert s.needs_clarification() is False
def test_stated_slot_does_not_need_clarification():
s = IntentSlot(name=SLOT_PRIMARY_CQA, value="总杂", state=SlotState.STATED,
confidence=1.0, affects_result=True)
assert s.needs_clarification() is False
# ---------------------------------------------------------------------------
# 序列化往返(含 intent_slots)
# ---------------------------------------------------------------------------
def test_intent_slot_roundtrip():
s = IntentSlot(name=SLOT_TARGET_TIMEPOINTS, value=[24, 36], state=SlotState.STATED,
confidence=1.0, evidence="36个月", locked=True, affects_result=True)
assert IntentSlot.from_dict(s.to_dict()) == s
def test_sheet_with_slots_roundtrip():
slots = build_intent_slots("把总杂外推到36个月", Intent.SHELF_LIFE, [])
sheet = AnalysisTaskSheet(intent=Intent.SHELF_LIFE, intent_slots=slots,
proposed_skill_id="stability")
assert AnalysisTaskSheet.from_dict(sheet.to_dict()) == sheet
def test_get_slot_helper():
slots = build_intent_slots("外推到36个月", Intent.SHELF_LIFE, [])
sheet = AnalysisTaskSheet(intent=Intent.SHELF_LIFE, intent_slots=slots)
assert sheet.get_slot(SLOT_TARGET_TIMEPOINTS) is not None
assert sheet.get_slot("nonexistent") is None
# ---------------------------------------------------------------------------
# 描述性梳理意图保真推广(需求 15)
# ---------------------------------------------------------------------------
from kernel.understanding import SLOT_TARGET_ATTRIBUTES # noqa: E402
def test_descriptive_target_attributes_stated_when_goal_names_them():
"""指令点名质量属性 → target_attributes 为 STATED,值为属性列表。"""
slots = build_intent_slots("梳理总杂和含量", Intent.DESCRIPTIVE_SUMMARY, [])
s = next(s for s in slots if s.name == SLOT_TARGET_ATTRIBUTES)
assert s.state is SlotState.STATED
assert "总杂" in s.value and "含量" in s.value
# 描述性梳理目标属性不强制澄清(未指定时默认梳理全部)。
assert s.affects_result is False
def test_descriptive_target_attributes_missing_when_unspecified():
"""未点名属性 → MISSING 且 value 为 None(不臆造默认),但不强制澄清。"""
slots = build_intent_slots("帮我梳理一下检测数据", Intent.DESCRIPTIVE_SUMMARY, [])
s = next(s for s in slots if s.name == SLOT_TARGET_ATTRIBUTES)
assert s.state is SlotState.MISSING
assert s.value is None
assert s.affects_result is False
# affects_result=False → 不进入选择性澄清(默认梳理全部)。
sheet = AnalysisTaskSheet(intent=Intent.DESCRIPTIVE_SUMMARY, intent_slots=slots)
assert sheet.slots_needing_clarification() == []
def test_descriptive_skill_filters_to_confirmed_attributes():
"""描述性技能尊重已确认/锁定的 target_attributes,按属性过滤观测(用户说了算)。"""
from kernel.skill_base import RawInput
from kernel.services import Services
from skills.descriptive_summary.skill import SKILL
task_sheet = {
"extracted_items": [
{"field": "总杂", "value": "0.30", "group": {"strength": "20μg", "attribute": "总杂"}},
{"field": "含量", "value": "99.5", "group": {"strength": "20μg", "attribute": "含量"}},
{"field": "水分", "value": "3.1", "group": {"strength": "20μg", "attribute": "水分"}},
],
"intent_slots": [
{"name": SLOT_TARGET_ATTRIBUTES, "value": ["总杂"],
"state": "stated", "locked": True},
],
}
raw = RawInput(goal="梳理总杂", extra={"task_sheet": task_sheet})
data = SKILL.extract(raw, Services())
attrs = {o["attribute"] for o in data.payload["observations"]}
assert attrs == {"总杂"} # 仅保留用户确认的属性
assert "filtered to user-confirmed attributes" in data.notes
def test_descriptive_skill_no_confirmation_keeps_all():
"""未确认 target_attributes → 梳理全部观测(向后兼容、行为零变化)。"""
from kernel.skill_base import RawInput
from kernel.services import Services
from skills.descriptive_summary.skill import SKILL
task_sheet = {
"extracted_items": [
{"field": "总杂", "value": "0.30", "group": {"attribute": "总杂"}},
{"field": "含量", "value": "99.5", "group": {"attribute": "含量"}},
],
"intent_slots": [
{"name": SLOT_TARGET_ATTRIBUTES, "value": None, "state": "missing", "locked": False},
],
}
raw = RawInput(goal="梳理数据", extra={"task_sheet": task_sheet})
data = SKILL.extract(raw, Services())
attrs = {o["attribute"] for o in data.payload["observations"]}
assert attrs == {"总杂", "含量"} # 全部保留