"""由每条清单项的检查结论生成问题(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": "模型服务暂时不可用", "over_budget": "超出本次检查的预算", "response_mismatch": "响应格写的是别的内容", } @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: out = _spec(result, item) note = result.detail.get("qualification_note") if out is None or not note: return out return Spec(out.type, out.severity, out.title, f"{out.explanation}{note}", out.detail) def _spec(result: Result, item: dict[str, Any]) -> Spec | None: if result.detail.get("superseded_by"): return None # 资质匹配出了更具体的问题(check/qualification.py),扫描件兜底那条不再单独出 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 in ("responded", "implicit_accepted"): return None if result.verdict == "implicit_review": text = ( "招标文件规定未列入偏离表的视为接受,投标文件里没有找到对这条要求的明确响应。" "这是实质性要求,请人工核对所投内容确实满足。" ) return Spec("manual_review", "hint", f"请人工核对:{name}", text, {**base, "mode": "implicit"}) 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" and result.detail.get("mismatch"): text = ( f"投标文件照抄了这条要求,但同一行的响应格写的是「{result.detail['mismatch'][:40]}」," "和这条要求对不上,像是别的条目的内容。请核对。" ) return Spec("unresponded", sev, f"响应可能对不上:{name}", text, base) if result.verdict == "not_found": text = f"在投标文件({scope})中没有找到对这条要求的响应。" if scope else "没有找到对这条要求的响应。" return Spec("unresponded", sev, f"未找到响应:{name}", text, base) if result.verdict == "negative_deviation" and result.detail.get("deviation_unclear"): text = ( f"这一行的偏离栏写的是「{result.detail.get('direct_read')}」,不是明确的「无偏离 / 满足」," "请核对是否满足这条要求。" ) return Spec("negative_deviation", "important" if sev == "fatal" else sev, f"偏离栏需核对:{name}", text, base) if result.verdict == "negative_deviation": cell = result.detail.get("direct_read") text = ( f"投标文件这一行的偏离 / 响应栏写着「{cell}」,没有找到说明。" if cell else "投标文件写明了偏离或不满足,没有找到说明。" ) return Spec("negative_deviation", sev, f"负偏离未说明:{name}", text, 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": total = result.detail.get("scanned_total") or 0 where = result.detail.get("scanned_where") # 只说扫描件在哪几页,不说「材料可能在扫描件里」:扫描件上是不是这份材料,没有 OCR 分不出来 text = ( f"投标文件文字部分没有找到这份材料。{where} 是扫描件(共 {total} 页),系统读不出上面的文字;" "材料如果附在这些页上,请人工核对,没有附就是缺了。" if where else f"投标文件文字部分没有找到这份材料。投标文件里有 {total} 页扫描件,系统读不出上面的文字," "材料可能在扫描件页上,请人工核对。" ) return Spec("manual_review", "hint", 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)