feat(qualification): 资质库和资质匹配(查标第 4 步,规则集 2026.11-r4)
- 资质库(FR-M6-08):增删改、证书扫描件(PDF/JPG/PNG,企业级密钥加密、签名下载)、 late_for 按进行中项目的投标截止日提醒、审计;前端资质库页和对话框,侧栏入口 - 资质匹配(FR-M3-05):证书类别词表(16 类,产品证书、人员证书不进匹配), QUAL.EXPIRED / LEVEL_LOW / NOT_COVERED;投标文字层按清单项定级,只有资质库记录的降一级, 出处显示「依据:资质库记录」(查标页、报告模板 v2);一类证书一个问题;评分项不逐条找响应的也查 - 响应错位规则不再把资质表的证书名称格当成照抄的要求 - 评测:qual_defects.py 派生资质情况表,D12 3/3、D18 2/2、规矩的资质表 0 误报;开发集不退化 - 决策 QL1–QL7;方案和落地记录 docs/architecture/qualification-plan.md,评测 check-eval.md §13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,13 +25,14 @@ from bid_doctor.ai import prompts
|
||||
from bid_doctor.ai import service as ai_service
|
||||
from bid_doctor.ai.providers.base import near_limit
|
||||
from bid_doctor.ai.service import OutputIncomplete, provider
|
||||
from bid_doctor.check import applicability, deviation, findings, match, rules
|
||||
from bid_doctor.check import applicability, deviation, findings, match, qualification, rules
|
||||
from bid_doctor.checklist import response_mode
|
||||
from bid_doctor.checklist import price
|
||||
from bid_doctor.checklist.extract import amount_yuan
|
||||
from bid_doctor.core.config import get_settings
|
||||
from bid_doctor.core.db import TenantContext
|
||||
from bid_doctor.document.pipeline import text_layer
|
||||
from bid_doctor.qualification.service import Record
|
||||
|
||||
EVALS = pathlib.Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(EVALS))
|
||||
@@ -175,6 +176,19 @@ def facts(tender: str, run: str, items: dict[UUID, dict[str, Any]]) -> rules.Fac
|
||||
)
|
||||
|
||||
|
||||
def library(kind: str) -> list[Record]:
|
||||
"""模拟的资质库(qualification-plan §5):empty 空库;valid 每类一条、有效、等级最高;partial 只有 ISO 9001;
|
||||
expired 每类一条、2020 年就过期。软著不进模拟(不查有效期)。"""
|
||||
best = {"asc": "5级", "cn": "一级", "jia": "甲级"}
|
||||
fams = [f for f in qualification.FAMILIES if f.key != "softcopy"]
|
||||
if kind == "partial":
|
||||
fams = [qualification.BY_KEY["iso9001"]]
|
||||
if kind == "empty":
|
||||
return []
|
||||
to = dt.date(2020, 1, 1) if kind == "expired" else dt.date(2030, 12, 31)
|
||||
return [Record(uuid4(), f.label, "certificate", best.get(f.levels or ""), "SIM-001", to) for f in fams]
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("bid")
|
||||
@@ -187,6 +201,7 @@ async def main() -> None:
|
||||
ap.add_argument("--no-direct", action="store_true", help="不做偏离栏直读(对比用)")
|
||||
ap.add_argument("--no-mismatch", action="store_true", help="不做响应错位判断(对比用)")
|
||||
ap.add_argument("--modes", help="人工确认后的响应方式,如 technical=implicit,commercial=implicit")
|
||||
ap.add_argument("--library", default="empty", choices=["empty", "valid", "partial", "expired"], help="模拟的资质库")
|
||||
args = ap.parse_args()
|
||||
d = OUT / args.bid
|
||||
manifest = json.loads((d / "manifest.json").read_text())
|
||||
@@ -204,9 +219,8 @@ async def main() -> None:
|
||||
[s for p in pages for s in p.sentences if s.location == "header_footer"],
|
||||
)
|
||||
)
|
||||
hits, rules_cov = rules.run(
|
||||
facts(manifest["tender"], args.checklist, items), docs, gold.sentences(manifest["tender"])
|
||||
)
|
||||
project_facts = facts(manifest["tender"], args.checklist, items)
|
||||
hits, rules_cov = rules.run(project_facts, docs, gold.sentences(manifest["tender"]))
|
||||
|
||||
def rule_rows(covered: set[tuple[str, int, str]]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
@@ -261,6 +275,7 @@ async def main() -> None:
|
||||
if decisions:
|
||||
match.apply_modes(results, items, decisions)
|
||||
match.scanned_fallback(results, items, decisions, docs)
|
||||
qual_hits, qual_cov = qualification.run(all_items, results, docs, library(args.library), project_facts.deadline)
|
||||
rows = []
|
||||
for item_id, item in all_items.items():
|
||||
if item_id not in items:
|
||||
@@ -308,7 +323,24 @@ async def main() -> None:
|
||||
}
|
||||
(out / "results.json").write_text(
|
||||
json.dumps(
|
||||
{"summary": summary, "items": rows, "rules": rule_rows(covered), "rules_coverage": rules_cov},
|
||||
{
|
||||
"summary": summary,
|
||||
"items": rows,
|
||||
"rules": rule_rows(covered),
|
||||
"rules_coverage": rules_cov,
|
||||
"qualification": [
|
||||
{
|
||||
"code": h.code,
|
||||
"severity": h.severity,
|
||||
"title": h.title,
|
||||
"explanation": h.explanation,
|
||||
"source": h.detail["source"],
|
||||
"evidence": [{"volume": doc.label, "p": s.page_no, "text": s.text} for doc, s in h.evidence],
|
||||
}
|
||||
for h in qual_hits
|
||||
],
|
||||
"qualification_coverage": {"library": args.library, **qual_cov.to_api()},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=1,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user