"""InformationExtractor + BackReferenceValidator 测试(任务 6)。 覆盖 Property 1(无捏造)/ 需求 4.1、4.2、4.5、9.5。 """ from __future__ import annotations from kernel.task_sheet import DocumentProfile, Intent from kernel.understanding import BackReferenceValidator, InformationExtractor SOURCE = "有关物质检测结果 总杂% 0.00 含量 99.19 限度 总杂≤2.0%" def test_extractor_skips_header_label_as_value(): llm_out = { "items": [ {"field": "value", "value": "样品名称", "source_ref": "样品名称"}, # 表头,应剔除 {"field": "value", "value": "0.00", "source_ref": "总杂% 0.00", "group": {"attribute": "总杂"}}, ] } items = InformationExtractor(llm=None).extract(SOURCE, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) values = [it.value for it in items] assert "样品名称" not in values assert "0.00" in values def test_extractor_empty_without_llm_and_no_cache(): items = InformationExtractor(llm=None).extract(SOURCE, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY) assert items == [] def test_validator_drops_unlocatable_items(): llm_out = { "items": [ {"field": "value", "value": "0.00", "source_ref": "总杂% 0.00"}, # 可定位 {"field": "value", "value": "-10", "source_ref": "降解 -10%"}, # 原文不存在 ] } items = InformationExtractor(llm=None).extract(SOURCE, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, missing = BackReferenceValidator.validate(items, SOURCE) kept_vals = [it.value for it in kept] assert "0.00" in kept_vals assert "-10" not in kept_vals assert "value" in missing def test_validator_whitespace_insensitive_match(): llm_out = {"items": [{"field": "spec_limit", "value": "总杂≤2.0%", "source_ref": "总杂 ≤ 2.0%"}]} items = InformationExtractor(llm=None).extract(SOURCE, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, missing = BackReferenceValidator.validate(items, SOURCE) assert len(kept) == 1 assert missing == [] def test_validator_no_fabrication_property(): # Property 1:保留项要么数值存在于原文,要么引文为原文子串;捏造项被丢弃。 import re llm_out = { "items": [ {"field": "value", "value": "0.00", "source_ref": "总杂% 0.00"}, {"field": "value", "value": "fake", "source_ref": "不存在的内容xyz"}, ] } items = InformationExtractor(llm=None).extract(SOURCE, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, _ = BackReferenceValidator.validate(items, SOURCE) norm_hay = re.sub(r"\s+", "", SOURCE) assert all(it.value != "fake" for it in kept) # 捏造文字项被丢弃 for it in kept: nums = re.findall(r"-?\d+(?:\.\d+)?", it.value) grounded = (all(n in SOURCE or any(float(n) == float(m) for m in re.findall(r"-?\d+(?:\.\d+)?", SOURCE)) for n in nums) if nums else re.sub(r"\s+", "", it.source_ref) in norm_hay) assert grounded def test_validator_value_centric_keeps_when_number_present(): """逐属性提示词下,source_ref 非连续子串但数值在原文 → 应保留(回归修复)。""" source = "涂布厚度/mm 膜厚/mm 溶化时限/s 0.5 0.05 60" llm_out = { "items": [ {"field": "膜厚", "value": "0.05", "source_ref": "膜厚 0.05", "group": {"attribute": "膜厚"}}, ] } items = InformationExtractor(llm=None).extract(source, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, missing = BackReferenceValidator.validate(items, source) assert len(kept) == 1 assert kept[0].value == "0.05" assert missing == [] def test_validator_drops_fabricated_number_not_in_source(): source = "涂布厚度/mm 膜厚/mm 0.5 0.05 60" llm_out = { "items": [ {"field": "膜厚", "value": "99.99", "source_ref": "膜厚 99.99", "group": {"attribute": "膜厚"}}, ] } items = InformationExtractor(llm=None).extract(source, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, missing = BackReferenceValidator.validate(items, source) assert kept == [] assert "膜厚" in missing def test_validator_tolerates_float_format_difference(): source = "含量 99.50 %" llm_out = {"items": [{"field": "含量", "value": "99.5", "source_ref": "含量 99.5"}]} items = InformationExtractor(llm=None).extract(source, DocumentProfile(), Intent.DESCRIPTIVE_SUMMARY, llm_out=llm_out) kept, _ = BackReferenceValidator.validate(items, source) assert len(kept) == 1 # 99.5 ↔ 99.50 视为同值