后端 - check 模块:检查预览、发起、进度、取消、记录;六个阶段(准备投标文件、响应匹配、规则、证据校验、 状态沿用、汇总);问题列表、处理、撤销、批量处理(只限提示级)。 - 每条清单项的检查结论 check_item_result(C9,迁移 0005):「已响应」也带投标原文出处; 没有校验通过的证据不能判已响应(C3)。级别从清单项算,不让模型定;说明不写整改建议。 - 响应匹配先只有第三层:内存 BM25 检索候选句 + check.match v1 提示词,检索不到不调模型, 漏答记 missing_answer,模型临时错误只影响那一批。 - 计费:免费版不记流水;专业版 24 小时内复查免扣(记一行 0);expected_charge 不一致时拒绝不扣点; 失败、取消退回;投标文件读不出文字按输入问题失败、不扣点。 - 投标分册:类型(volume_kind)和版本;电子投标平台加密格式在上传时拦下。 - 解析流水线前几个阶段拆到 document/pipeline/normalize.py,解析和检查共用。 - ops 模块:反馈接口;致命问题确认无误的说明自动进误报反馈(U4)。 - SK1:Valkey 客户端短超时、SSE 连接池上限。SK2:CI 加 Gotenberg、直接跑 init-roles.sh。 前端 - 上传投标文件并检查页、查标页(进度、覆盖范围、按级别筛选、逐条处理和行内说明、撤销、原文抽屉、 已处理、本次已解决);项目头抽成公共组件,加「查标」标签页;工作台「下一步」覆盖查标阶段。 契约、文档:openapi.yaml 加 item-results、volume_kind、coverage 字段;data-model、api-design §10、 架构 §8 / §13(SK14)、AGENTS.md 同步。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
4.0 KiB
Python
86 lines
4.0 KiB
Python
"""由每条清单项的检查结论生成问题(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)
|