feat(check): 响应方式识别(C12)和「这条要不要投标人回答」;评测集纠错
- checklist/response_mode.py:按类别(技术 / 商务)识别招标文件的响应方式(逐条响应 / 默认接受),规则优先; 两种写法冲突按逐条,认不出按逐条并写进覆盖范围。 - check/applicability.py:无效情形(★ / 斜体下划线标注的条款除外)、日期与递交、没要求附材料的评分标准、 无需提供证明或由采购人查询的资格,不逐条找响应,写进覆盖范围;要求附证明材料按原文判断,中标后才交的不算。 - 默认接受的要求没找到响应:一般要求记 implicit_accepted、不出问题;实质性要求记 implicit_review、提示级 人工核对;要附证明材料的不免(迁移 0006)。 - match.py:修同一行单元格又是别的条目候选句时的 KeyError(每条清单项单独记候选句,句子从总表里查)。 - 评测集:注入器同一要求的所有表格行一起改;散文、附件、格式文件里也响应了的要求不挂漏响应类问题; 错位响应必须是另一个参数;复核判不成立的单元不再挂;regen.py 按固定种子重新生成。 开发集 17 份:规矩投标文件报出的问题 639 → 229,误报 21.3 → 12.4 / 100,召回 68% → 65%,致命级 20 → 21 / 32 (docs/architecture/check-eval.md §4)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
56
server/src/bid_doctor/check/applicability.py
Normal file
56
server/src/bid_doctor/check/applicability.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""每条清单项要不要在投标文件里找响应、按什么响应方式判(决策 C12,`docs/architecture/check-eval.md` §2.2 第 ② 项)。
|
||||
|
||||
开发集上规矩的投标文件报出的问题,近一半落在不需要投标人回答的条目上(无效情形的列举、日期、评分标准、
|
||||
采购人自己查询的资格)。这些条目不逐条找响应,写进检查覆盖范围:
|
||||
- 无效情形、评审规定(只有「无效」类别、不是标注出来的条款):由规则检查(第 2 步)和人工核对覆盖;
|
||||
- 日期与递交:由规则检查(落款日期、有效期)和需人工核对覆盖;
|
||||
- 评分标准本身:没写要附什么材料的不找;写了的按证明材料找;
|
||||
- 资格条件里写明「无需提供」或由采购人查询(信用中国)的不找。
|
||||
其余按类别取招标文件的响应方式:技术参数用技术类,其余用商务类。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from bid_doctor.checklist.response_mode import Mode
|
||||
|
||||
# 要求投标时附证明材料(读标的 evidence_required 常常是空的,按原文判断);中标后 / 签合同时才交的不算
|
||||
EVIDENCE = re.compile(r"(提供|附|出具)[^。;]{0,20}(证明|报告|截图|复印件|扫描件|证书|合同|承诺|声明|社保|发票)")
|
||||
AWARD = re.compile(r"(中标后|成交后|签订合同(前|时)|合同签订(前|时))")
|
||||
NO_PROOF = re.compile(r"无需提供|不需提供|无须提供|采购人[^。;]{0,12}(查询|核查|查验)|信用中国|信用记录")
|
||||
SKIP_LABELS = {
|
||||
"condition": "无效情形和评审规定",
|
||||
"schedule": "日期与递交要求",
|
||||
"scoring": "评分标准(没要求附证明材料)",
|
||||
"no_proof": "资格条件(写明无需提供证明,或由采购人查询)",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Decision:
|
||||
match: bool
|
||||
mode: str = "explicit" # explicit / implicit
|
||||
skip: str | None = None # 不找响应的原因(SKIP_LABELS 的键)
|
||||
mode_status: str = "found"
|
||||
evidence: bool = False # 要求投标时附证明材料:默认接受也不能免(没附就是没附)
|
||||
|
||||
|
||||
def decide(item: dict[str, Any], modes: dict[str, Mode]) -> Decision:
|
||||
cats = set(item.get("categories") or [])
|
||||
text = f"{item.get('title') or ''}{item.get('requirement_text') or ''}"
|
||||
if cats == {"invalidity"} and item.get("importance_basis") != "marker":
|
||||
return Decision(False, skip="condition") # 「……的,投标无效」这类情形;★ / 斜体下划线标注的条款是要求,照常找
|
||||
if "schedule" in cats and not cats & {"tech_param", "qualification", "scoring"}:
|
||||
return Decision(False, skip="schedule")
|
||||
evidence = bool(item.get("evidence_required") or EVIDENCE.search(text)) and not AWARD.search(text)
|
||||
if cats <= {"scoring"} and not evidence:
|
||||
return Decision(False, skip="scoring")
|
||||
if "qualification" in cats and "tech_param" not in cats and NO_PROOF.search(text):
|
||||
return Decision(False, skip="no_proof")
|
||||
mode = modes.get("technical" if "tech_param" in cats else "commercial")
|
||||
if mode is None:
|
||||
return Decision(True, evidence=evidence)
|
||||
return Decision(True, mode=mode.mode, mode_status=mode.status, evidence=evidence)
|
||||
Reference in New Issue
Block a user