"""由每条清单项的检查结论生成问题(FR-M3-03、m3-plan §2.3)。 - 级别从清单项算,不让模型定(参考项目让模型定级别,模型可以把高危项降成低危); - 说明只写事实和出处,不写整改建议(报价类问题的「建议」会写出数字,红线 6); - 「已响应」不生成问题,结论留在 check_item_result 里。 """ from __future__ import annotations from dataclasses import dataclass from typing import Any from bid_doctor.check.match import Result FATAL_CATEGORIES = {"qualification", "invalidity"} REASONS = { "evidence_missing": "没有给出投标原文", "sid_format": "引用的句子编号写法不对", "sid_unknown": "引用的句子不在候选范围内", "quote_not_original": "引文和投标原文对不上", "quote_too_short": "引文太短,无法核对", "missing_answer": "这一条没有得到判断结果", "model_error": "模型服务暂时不可用", } @dataclass(frozen=True, slots=True) class Spec: type: str severity: str title: str explanation: str detail: dict[str, Any] def severity(item: dict[str, Any]) -> str: """★ 条款、法规列明或原文写明无效的、资格条件 → 致命;▲ 参数、评分项(缺证明材料)→ 重要;其余 → 提示。""" cats = set(item.get("categories") or []) if item.get("importance") == "substantive" or cats & FATAL_CATEGORIES: return "fatal" if item.get("importance") == "important" or "scoring" in cats: return "important" return "hint" def _pages(pages: list[int]) -> str: return "、".join(f"第 {p} 页" for p in pages) def spec(result: Result, item: dict[str, Any]) -> Spec | None: sev = severity(item) name = str(item.get("title") or "") base = {"importance_basis": item.get("importance_basis"), **result.detail} scope = "、".join(result.detail.get("search_scope") or []) if result.verdict == "responded": return None if result.verdict == "not_found" and result.evidence_status == "unverified": claimed = result.detail.get("claimed_pages") or [] where = f"{_pages(claimed)}有响应" if claimed else "有响应" why = REASONS.get(result.reason_code or "", result.reason_code or "") text = f"模型认为投标文件{where},但出处没能核对({why}),按「未找到响应」处理。请人工核对。" return Spec("unresponded", sev, f"未找到响应:{name}", text, {**base, "evidence_status": "unverified"}) if result.verdict == "not_found": text = f"在投标文件({scope})中没有找到对这条要求的响应。" if scope else "没有找到对这条要求的响应。" return Spec("unresponded", sev, f"未找到响应:{name}", text, base) if result.verdict == "negative_deviation": return Spec( "negative_deviation", sev, f"负偏离未说明:{name}", "投标文件写明了偏离或不满足,没有找到说明。", base ) if result.verdict == "suspected_unmet": value = result.detail.get("response_value") text = ( f"投标文件给出的值({value})按要求值判断可能不满足。" if value else "投标文件给出的值按要求值判断可能不满足。" ) return Spec("suspected_unmet", sev, f"疑似不满足:{name}", text, base) if result.verdict == "no_value": text = "投标文件只写了满足或响应,没有给出要求里要的具体值或材料。" return Spec("suspected_unmet", "hint", f"未给出具体值:{name}", text, {**base, "kind": "no_value"}) if result.verdict == "scanned": text = "候选内容所在的页面是扫描件,系统读不出文字,请人工核对。" return Spec("manual_review", sev, f"请人工核对:{name}", text, base) why = REASONS.get(result.reason_code or "", result.reason_code or "") return Spec("manual_review", sev, f"未判定:{name}", f"这条要求本次没有完成判断({why}),请人工核对。", base)