File size: 9,281 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 | """质量属性梳理 Skill 测试(intent-understanding-layer 任务 10)。
覆盖需求 7.1、7.5、7.6:注册发现、extract→compute→explain 全链路、无外推。
"""
from __future__ import annotations
from kernel.registry import SkillRegistry
from kernel.skill_base import RawInput
from skills.descriptive_summary.skill import DescriptiveSummarySkill
def _confirmed_sheet_raw():
"""构造一个含已确认任务单的 RawInput(SL-0010 风格)。"""
task_sheet = {
"extracted_items": [
{"field": "spec_limit", "value": "总杂≤2.0%", "source_ref": "限度 总杂≤2.0%",
"group": {"attribute": "总杂"}},
{"field": "value", "value": "0.00", "source_ref": "总杂% 0",
"group": {"strength": "20μg", "batch": "B1", "attribute": "总杂"}},
{"field": "value", "value": "0.00", "source_ref": "总杂% 0",
"group": {"strength": "40μg", "batch": "B2", "attribute": "总杂"}},
{"field": "value", "value": "99.19", "source_ref": "含量 99.19",
"group": {"strength": "20μg", "batch": "B1", "attribute": "含量"}},
]
}
return RawInput(goal="梳理各规格质量属性", extra={"task_sheet": task_sheet})
class _Svc:
llm = None
lang = "zh"
def test_skill_discovered_by_registry():
reg = SkillRegistry()
reg.discover("skills")
assert reg.get("descriptive_summary") is not None
def test_full_extract_compute_explain():
skill = DescriptiveSummarySkill()
svc = _Svc()
data = skill.extract(_confirmed_sheet_raw(), svc)
assert data.method == "task_sheet"
result = skill.compute(data)
assert result.can_proceed is True
assert result.summary["n_groups"] == 3
sections = skill.explain(result, svc)
assert "overview" in sections.sections
assert "data_table" in sections.sections
assert "<table" in sections.sections["data_table"]
def test_compute_refuses_without_observations():
skill = DescriptiveSummarySkill()
data = skill.extract(RawInput(goal="x", extra={"task_sheet": {"extracted_items": []}}), _Svc())
result = skill.compute(data)
assert result.can_proceed is False
assert result.refusal is not None
def test_no_extrapolation_in_result():
skill = DescriptiveSummarySkill()
result = skill.compute(skill.extract(_confirmed_sheet_raw(), _Svc()))
blob = str(result.summary).lower()
for forbidden in ("shelf_life", "arrhenius", "k_value", "target_timepoints"):
assert forbidden not in blob
def test_conformance_evaluated_when_spec_present():
skill = DescriptiveSummarySkill()
result = skill.compute(skill.extract(_confirmed_sheet_raw(), _Svc()))
total_imp_groups = [g for g in result.summary["groups"] if g["attribute"] == "总杂"]
assert total_imp_groups
assert all(g["within_spec"] is True for g in total_imp_groups)
def test_headless_render_inputs_returns_rawinput():
skill = DescriptiveSummarySkill()
out = skill.render_inputs(object()) # 无任何控件属性
assert isinstance(out, RawInput)
def test_tolerates_llm_variable_field_names():
"""LLM 把 field 命名为属性名(非严格 "value")时仍能识别为观测(回归 bug)。"""
skill = DescriptiveSummarySkill()
task_sheet = {
"extracted_items": [
# LLM 风格:field 是属性名,没有严格的 "value"/"spec_limit"
{"field": "膜厚", "value": "0.05", "source_ref": "膜厚 0.05",
"group": {"strength": "20μg", "attribute": "膜厚"}},
{"field": "总杂%", "value": "0.00", "source_ref": "总杂 0",
"group": {"strength": "40μg", "attribute": "总杂"}},
# 规格限度按内容识别(含 ≤ 符号、含"限度"词)
{"field": "限度", "value": "总杂≤2.0%", "source_ref": "限度 总杂≤2.0%",
"group": {"attribute": "总杂"}},
]
}
raw = RawInput(goal="梳理", extra={"task_sheet": task_sheet})
data = skill.extract(raw, _Svc())
obs = data.payload["observations"]
assert len(obs) == 2 # 两个观测项被识别
assert data.payload["spec_limits"].get("总杂") == "总杂≤2.0%"
result = skill.compute(data)
assert result.can_proceed is True
def test_strength_recovered_from_field_name():
"""group 缺 strength 时,从 field/source 中回退提取规格 token。"""
skill = DescriptiveSummarySkill()
task_sheet = {
"extracted_items": [
{"field": "物理特性-20μg 膜厚", "value": "0.05", "source_ref": "20μg 膜厚 0.05",
"group": {"attribute": "膜厚"}},
]
}
data = skill.extract(RawInput(goal="x", extra={"task_sheet": task_sheet}), _Svc())
assert data.payload["observations"][0]["strength"] == "20μg"
def _cu_sheet_raw():
"""构造含含量均匀度逐单位含量(%)的任务单(SL-0010 20μg 五单位)。"""
vals = ["99.19", "100.0", "97.68", "96.0", "98.81"]
items = [
{"field": "含量%", "value": v, "source_ref": f"含量% {v}",
"group": {"strength": "20μg", "batch": "SL-0010-25052601",
"attribute": "含量%", "table": "含量均匀度"}}
for v in vals
]
return RawInput(goal="梳理含量均匀度", extra={"task_sheet": {"extracted_items": items}})
def test_content_uniformity_acceptance_value_in_report():
"""含量均匀度百分含量列应计算 AV 并进入报告专段;判定来自确定性药典计算。"""
skill = DescriptiveSummarySkill()
svc = _Svc()
result = skill.compute(skill.extract(_cu_sheet_raw(), svc))
cu_groups = [g for g in result.summary["groups"] if g.get("acceptance_value")]
assert cu_groups, "应识别出含量均匀度百分含量分组并计算 AV"
av = cu_groups[0]["acceptance_value"]
assert av["standard"] == "chp" and abs(av["acceptance_value"] - 5.074) < 0.01
assert cu_groups[0]["within_spec"] is True # AV≤15 → 合格
sections = skill.explain(result, svc)
assert "content_uniformity" in sections.sections
assert "AV" in sections.sections["content_uniformity"]
def test_content_uniformity_skips_mg_per_g_column():
"""mg/g 绝对含量列不应被当作 AV 判定对象。"""
skill = DescriptiveSummarySkill()
items = [
{"field": "含量mg/g", "value": v, "source_ref": f"含量mg/g {v}",
"group": {"strength": "20μg", "batch": "B", "attribute": "含量mg/g", "table": "含量均匀度"}}
for v in ("0.834", "0.83", "0.824")
]
raw = RawInput(goal="x", extra={"task_sheet": {"extracted_items": items}})
result = skill.compute(skill.extract(raw, _Svc()))
assert all(not g.get("acceptance_value") for g in result.summary["groups"])
def test_censored_value_through_task_sheet_path():
"""任务单/LLM 路径下的删失观测(耐折度>100、限度≥50)应被识别并正确判定。"""
skill = DescriptiveSummarySkill()
task_sheet = {
"extracted_items": [
{"field": "耐折度", "value": ">100", "source_ref": "耐折度 >100",
"group": {"strength": "20μg", "attribute": "耐折度"}},
{"field": "spec_limit", "value": "≥50", "source_ref": "限度 ≥50",
"group": {"attribute": "耐折度"}},
]
}
raw = RawInput(goal="梳理", extra={"task_sheet": task_sheet})
result = skill.compute(skill.extract(raw, _Svc()))
g = next(x for x in result.summary["groups"] if x["attribute"] == "耐折度")
assert g["censored_values"] == [">100"]
assert g["mean"] is None
assert g["within_spec"] is True # >100 ⟹ ≥50
def test_assay_percent_range_conformance_end_to_end():
"""assay 百分含量% 挂上 90~110% 区间限度后应判定符合。"""
skill = DescriptiveSummarySkill()
task_sheet = {
"extracted_items": [
{"field": "百分含量%", "value": "98.92", "source_ref": "百分含量% 98.92",
"group": {"strength": "20μg", "attribute": "百分含量%", "table": "含量"}},
{"field": "spec_limit", "value": "90.0~110.0%", "source_ref": "限度",
"group": {"attribute": "百分含量%"}},
]
}
raw = RawInput(goal="梳理", extra={"task_sheet": task_sheet})
result = skill.compute(skill.extract(raw, _Svc()))
g = next(x for x in result.summary["groups"] if x["attribute"] == "百分含量%")
assert g["within_spec"] is True
def test_humanized_group_has_no_internal_field_names():
"""喂给 LLM 的记录必须是本地化自然语言键值,绝不含内部字段名/布尔(防泄漏)。"""
skill = DescriptiveSummarySkill()
result = skill.compute(skill.extract(_cu_sheet_raw(), _Svc()))
g = next(x for x in result.summary["groups"] if x.get("acceptance_value"))
rec = skill._humanize_group(g, is_en=False)
blob = str(rec)
for forbidden in ("within_spec", "single_point", "n_numeric", "n_censored",
"mean_display", "rsd_display", "true", "false", "True", "False"):
assert forbidden not in blob
assert rec["符合性"] in ("符合", "不符合", "未判定")
assert "接受值" in rec # AV 以自然语言串呈现
|