From f9cbfdc6259b5d7f6b6ddc51663d9f853923d365 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 16 Sep 2026 18:23:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(binding):=20=E7=AB=A0=E8=8A=82=E2=86=94?= =?UTF-8?q?=E7=BC=96=E5=86=99=E8=A6=81=E6=B1=82=E6=98=BE=E5=BC=8F=E7=BB=91?= =?UTF-8?q?=E5=AE=9Adoc=5Frequirement=5Fids(2026-09-16=E7=BC=96=E5=8F=B7?= =?UTF-8?q?=E9=94=99=E4=BD=8D=E5=AE=9E=E6=B5=8B=E6=A0=B9=E5=9B=A0)?= =?UTF-8?q?=E2=80=94=E2=80=94requirements=E6=8C=89=E6=8B=9B=E6=A0=87?= =?UTF-8?q?=E4=B9=A6=E7=9B=AE=E5=BD=95=E7=BC=96=E5=8F=B7/chapters=E6=8C=89?= =?UTF-8?q?=E8=A3=85=E8=AE=A2=E7=BC=96=E5=8F=B7=E4=B8=A4=E5=A5=97=E7=BC=96?= =?UTF-8?q?=E5=8F=B7=E6=BC=82=E7=A7=BB,chapter=5Fdetail=E7=AD=89=E5=80=BCJ?= =?UTF-8?q?OIN=E9=9D=99=E9=BB=98=E9=80=92=E9=94=99=E8=A6=81=E6=B1=82(?= =?UTF-8?q?=E5=86=99=E8=80=85=E6=8B=BF'=E8=90=A5=E4=B8=9A=E6=89=A7?= =?UTF-8?q?=E7=85=A7'=E8=A6=81=E6=B1=82=E5=86=99=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E5=A7=94=E6=89=98=E4=B9=A6,4=E7=AB=A0=E9=94=99=E4=BD=8D+5?= =?UTF-8?q?=E7=AB=A0=E6=97=A0=E8=A6=81=E6=B1=82)=E3=80=82=E6=9C=BA?= =?UTF-8?q?=E5=88=B6:create=5Fchapter=5Foutline=E8=87=AA=E5=8A=A8=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=90=BD=E7=BB=91=E5=AE=9A;patch=5Fchapters=E6=94=AF?= =?UTF-8?q?=E6=8C=81doc=5Frequirement=5Fids/doc=5Frequirements=5Fby=5Fno(i?= =?UTF-8?q?d=E6=A0=A1=E9=AA=8C);chapter=5Fdetail=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E4=BC=98=E5=85=88+=E7=BC=96=E5=8F=B7=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E5=B8=A6=E6=A0=87=E9=A2=98=E4=BA=92=E5=90=AB=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=99=A8(=E8=AF=8D=E5=8C=B9=E9=85=8D=E5=8F=AA=E6=8A=A5?= =?UTF-8?q?=E5=8F=AF=E7=96=91,=E8=AF=AD=E4=B9=89=E8=A3=81=E5=86=B3?= =?UTF-8?q?=E5=9C=A8agent);=E6=96=B0=E5=B7=A5=E5=85=B7check=5Frequirement?= =?UTF-8?q?=5Fbinding=E4=BD=93=E6=A3=80=E6=8A=A5=E5=91=8A(=E9=94=99?= =?UTF-8?q?=E4=BD=8D/=E5=A4=B1=E6=95=88/=E5=AD=A4=E5=84=BF);QC=5FTYPE=5FOU?= =?UTF-8?q?TLINE=E5=8A=A0=E7=AC=AC5=E7=BB=B4=E7=BB=91=E5=AE=9A=E5=AF=B9?= =?UTF-8?q?=E9=BD=90;models+json2ddl=E5=AE=9E=E8=B7=91=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- json/bid_chapters.json | 5 +- models/bid_chapters.json | 5 + pipeline_bidding/bid_analysis_capability.py | 114 +++++++++++++++++++- pipeline_bidding/bid_qc_capability.py | 5 +- pipeline_bidding/bid_write_capability.py | 54 ++++++++-- pipeline_bidding/role_tool_schemas.py | 10 +- 6 files changed, 180 insertions(+), 13 deletions(-) diff --git a/json/bid_chapters.json b/json/bid_chapters.json index a4ef90c..39ed57f 100644 --- a/json/bid_chapters.json +++ b/json/bid_chapters.json @@ -11,7 +11,8 @@ "exclouded": [ "content", "outline", - "scoring_item_ids" + "scoring_item_ids", + "doc_requirement_ids" ] }, "editexclouded": [ @@ -47,4 +48,4 @@ ] } } -} +} \ No newline at end of file diff --git a/models/bid_chapters.json b/models/bid_chapters.json index 4174ebd..1a4dd35 100644 --- a/models/bid_chapters.json +++ b/models/bid_chapters.json @@ -56,6 +56,11 @@ "title": "对应评分项(JSON)", "type": "text" }, + { + "name": "doc_requirement_ids", + "title": "绑定投标文件要求ID(JSON数组)", + "type": "text" + }, { "name": "outline", "title": "章节要点", diff --git a/pipeline_bidding/bid_analysis_capability.py b/pipeline_bidding/bid_analysis_capability.py index 8734e09..8edddf9 100644 --- a/pipeline_bidding/bid_analysis_capability.py +++ b/pipeline_bidding/bid_analysis_capability.py @@ -521,6 +521,78 @@ async def update_doc_requirement(project_id, record_id="", chapter_no="", req_ty return True, "已更新条目 %s" % rid +async def check_requirement_binding(project_id): + """体检「章节↔投标文件要求」绑定对齐(2026-09-16:编号错位实测根因)。 + + 输出报告:每章的绑定状态(explicit/fallback/none)、编号回退时标题不互含的可疑 + 错位、未被任何章节绑定的 structure 要求清单。词匹配只报可疑(触发器), + 语义裁决与绑定修正走 patch_chapters(doc_requirement_ids/doc_requirements_by_no)。 + """ + try: + project_id = await resolve_project_id(project_id) + except ValueError as e: + return "ERROR: 项目上下文缺失(%s)。请显式传 project_id 后重试。" % e + db, dbname = get_db() + async with db.sqlorContext(dbname) as sor: + chs = await sor.sqlExe( + "SELECT id, chapter_no, title, doc_requirement_ids FROM bid_chapters " + "WHERE project_id=${pid}$ ORDER BY order_no, chapter_no", {"pid": project_id}) + await sor.sqlExe("COMMIT", {}) + rqs = await sor.sqlExe( + "SELECT id, req_type, chapter_no, chapter_title FROM bid_doc_requirements " + "WHERE project_id=${pid}$ ORDER BY order_no", {"pid": project_id}) + await sor.sqlExe("COMMIT", {}) + chapters = rows_to_dicts(chs, limit=500) + reqs = rows_to_dicts(rqs, limit=500) + rq_by_id = {r["id"]: r for r in reqs if r.get("id")} + rq_by_no = {} + for r in reqs: + if r.get("req_type") == "structure" and r.get("chapter_no"): + rq_by_no.setdefault(r["chapter_no"], r) + bound_rq = set() + lines = ["## 章节↔编写要求绑定体检(%d 章 / %d 条要求)" % (len(chapters), len(reqs)), ""] + suspicious, unbound_ch, mismatched = [], [], [] + for c in chapters: + ids = json_loads(c.get("doc_requirement_ids"), []) + cno = str(c.get("chapter_no") or "") + ctitle = str(c.get("title") or "") + if ids: + bound_rq.update(str(x) for x in ids) + bad = [x for x in ids if str(x) not in rq_by_id] + if bad: + suspicious.append("%s %s: 绑定含失效id %s" % (cno, ctitle[:18], bad[:2])) + continue + fb = rq_by_no.get(cno) + if fb: + rt = str(fb.get("chapter_title") or "") + if rt and ctitle and (rt not in ctitle and ctitle not in rt): + mismatched.append("%s 章节《%s》↔同号要求《%s》不互含(疑似错位)" + % (cno, ctitle[:18], rt[:18])) + bound_rq.add(fb["id"]) + else: + unbound_ch.append("%s %s" % (cno, ctitle[:18])) + orphan = ["%s %s" % (r.get("chapter_no"), (r.get("chapter_title") or "")[:22]) + for r in reqs + if r.get("req_type") == "structure" and r["id"] not in bound_rq] + if mismatched: + lines += ["### ⚠ 编号错位可疑(%d)——LLM 语义裁决后 patch_chapters 落绑定" % len(mismatched)] + lines += ["- " + m for m in mismatched] + [""] + if suspicious: + lines += ["### ✗ 绑定失效(%d)" % len(suspicious), *["- " + m for m in suspicious], ""] + if unbound_ch: + lines += ["### 无同号要求的章节(%d)——核对是否漏抽要求或确属无单独要求" + % len(unbound_ch), *["- " + m for m in unbound_ch], ""] + if orphan: + lines += ["### 未被任何章节引用的 structure 要求(%d)——可能是全局规则误标 structure," + "应 update_doc_requirement 改 req_type=other/format,或补绑定" % len(orphan)] + lines += ["- " + m for m in orphan] + [""] + if not (mismatched or suspicious or unbound_ch or orphan): + lines.append("✓ 全部章节要求绑定对齐,无错位、无孤儿要求。") + lines += ["---", "处置:错位/补绑 → patch_chapters [{chapter_no, doc_requirements_by_no:[{chapter_no:要求条目号}]}];" + "要求本身错 → update_doc_requirement 回写原条目。"] + return "\n".join(lines) + + async def _upsert_tender_facts(project_id, facts, who=None, agent_id=None): """把投标要素写入 bid_tenders(source=tender_doc,复用原招标信息表承载商务要素)。 @@ -1073,7 +1145,8 @@ async def create_chapter_outline(project_id, chapters="", who=None, agent_id=Non async with db.sqlorContext(dbname) as sor: if not items: recs = await sor.sqlExe( - "SELECT chapter_no, chapter_title, requirement, page_limit FROM bid_doc_requirements " + "SELECT id, chapter_no, chapter_title, requirement, page_limit " + "FROM bid_doc_requirements " "WHERE project_id=${pid}$ AND req_type='structure' ORDER BY order_no", {"pid": project_id}) await sor.sqlExe("COMMIT", {}) @@ -1086,6 +1159,9 @@ async def create_chapter_outline(project_id, chapters="", who=None, agent_id=Non "title": d.get("chapter_title") or "", "outline": (d.get("requirement") or "")[:4000], "section": _infer_section(d.get("chapter_title") or ""), + # 显式绑定来源要求(2026-09-16:骨架从 structure 要求生成时 + # 天然携带其 id——写者/评审按绑定取要求,不再靠编号巧合) + "doc_requirement_ids": [d.get("id")] if d.get("id") else [], }) if not items: return False, ("无章节来源:请先抽取 req_type=structure 的投标文件要求," @@ -1125,6 +1201,10 @@ async def create_chapter_outline(project_id, chapters="", who=None, agent_id=Non "chapter_no": cno, "parent_no": str(it.get("parent_no") or "")[:30], "title": title, "section": sec, "scoring_item_ids": json.dumps(matched, ensure_ascii=False), + "doc_requirement_ids": (json.dumps( + [str(x) for x in (it.get("doc_requirement_ids") or []) if x], + ensure_ascii=False) + if (it.get("doc_requirement_ids") or []) else None), "outline": str(it.get("outline") or "")[:20000], "status": CH_PENDING, "version": 1, "revise_count": 0, "max_score": msum or None, @@ -1213,6 +1293,38 @@ async def patch_chapters(project_id, patches="", who=None, agent_id=None): p["si"] = json.dumps(ids, ensure_ascii=False) if it.get("max_score") is not None: sets.append("max_score=${ms}$"); p["ms"] = to_float(it["max_score"], 0) + # ── 要求绑定修正(2026-09-16:编号错位实测——写者按 chapter_no 等值 JOIN + # 取到别的章节的编写要求。绑定是落库事实,LLM 语义裁决后经此回写)── + # doc_requirement_ids: 直接覆盖(真实 id 数组,逐个校验存在于本项目) + # doc_requirements_by_no: [{"chapter_no":"1.3"}] 按要求条目的 chapter_no 反查 id + if it.get("doc_requirement_ids") is not None or it.get("doc_requirements_by_no"): + rq_ids = [str(x) for x in (it.get("doc_requirement_ids") or []) if x] + if it.get("doc_requirements_by_no"): + rq_ids = [] + for n in (it.get("doc_requirements_by_no") or []): + rno = str(n.get("chapter_no") if isinstance(n, dict) else (n or "")).strip() + if not rno: + continue + rr = await sor.sqlExe( + "SELECT id FROM bid_doc_requirements WHERE project_id=${pid}$ " + "AND chapter_no=${rn}$", + {"pid": project_id, "rn": rno[:30]}) + await sor.sqlExe("COMMIT", {}) + for x in (rr or []): + rq_ids.append(getattr(x, "id", "")) + rq_ids = [x for x in rq_ids if x] + if rq_ids: + inlist = ",".join("'" + x.replace("'", "") + "'" for x in rq_ids) + chk = await sor.sqlExe( + "SELECT COUNT(*) AS c FROM bid_doc_requirements WHERE id IN (" + inlist + + ") AND project_id=${pid}$", {"pid": project_id}) + await sor.sqlExe("COMMIT", {}) + n_ok = to_int(getattr(chk[0], "c", 0) if chk else 0) + if n_ok != len(rq_ids): + return False, ("章节 %s 的要求绑定含不存在/非本项目的 id(%d/%d 有效)," + "先用 list_doc_requirements 取真实 id" % (cno, n_ok, len(rq_ids))) + sets.append("doc_requirement_ids=${dri}$") + p["dri"] = json.dumps(rq_ids, ensure_ascii=False) or None if not sets: continue sets.append("updated_at=NOW()") diff --git a/pipeline_bidding/bid_qc_capability.py b/pipeline_bidding/bid_qc_capability.py index 45a8d27..031f86e 100644 --- a/pipeline_bidding/bid_qc_capability.py +++ b/pipeline_bidding/bid_qc_capability.py @@ -52,7 +52,10 @@ QC_DIMENSIONS = { "1. 章节与 req_type=structure 的要求一一对应,无缺章、无多余章;\n" "2. 章节号/标题/顺序与投标文件要求一致;\n" "3. 评分项挂载正确:每个评分项能落到对应章节(target_chapter_no/名称匹配),无错挂漏挂;\n" - "4. 章节要点来自评分标准原文枚举的子维度,不是通用目录。" + "4. 章节要点来自评分标准原文枚举的子维度,不是通用目录;\n" + "5. 编写要求绑定对齐:先调 check_requirement_binding 体检,逐条裁决报告中的错位可疑项" + "(章节号与要求号是两套编号会漂移,同号≠同章),确认后督促/自行用 patch_chapters" + "(doc_requirements_by_no) 落显式绑定;绑定错位=写者拿错编写要求=整章按错标准写。" ), QC_TYPE_COST: ( "投标成本收益分析契合度审核(对照招标文件商务条款与预算/保证金原文):\n" diff --git a/pipeline_bidding/bid_write_capability.py b/pipeline_bidding/bid_write_capability.py index 24086d1..b6bb89a 100644 --- a/pipeline_bidding/bid_write_capability.py +++ b/pipeline_bidding/bid_write_capability.py @@ -57,12 +57,49 @@ async def chapter_detail(chapter_id, with_content="1", length=12000): "FROM bid_scoring_items WHERE id IN (" + inlist + ")", {}) await sor.sqlExe("COMMIT", {}) items = rows_to_dicts(srecs) - reqs = await sor.sqlExe( - "SELECT req_type, requirement, page_limit FROM bid_doc_requirements " - "WHERE project_id=${pid}$ AND (chapter_no=${cno}$ OR req_type IN ('format','seal','copies')) " - "ORDER BY order_no", - {"pid": d.get("project_id"), "cno": d.get("chapter_no")}) - await sor.sqlExe("COMMIT", {}) + # ── 编写要求:显式绑定优先,编号匹配回退+错位触发器(2026-09-16)── + # 根因实测:bid_doc_requirements 与 bid_chapters 两套 chapter_no 会漂移 + # (抽取按招标书目录编号,骨架按投标文件装订编号),等值 JOIN 静默递错要求—— + # 写者拿「营业执照」的要求写授权委托书。绑定(doc_requirement_ids)是正本; + # 未绑定时按编号回退,但用标题互含做词匹配触发器:不互含→输出⚠可疑警告 + # (词匹配只报可疑,语义裁决权在 agent;判错用 list_doc_requirements + + # patch_chapters(doc_requirements_by_no) 落绑定后再写)。 + rq_ids = json_loads(d.get("doc_requirement_ids"), []) + rq_bound = False + if rq_ids: + inlist = ",".join("'" + str(x).replace("'", "") + "'" for x in rq_ids) + rrecs = await sor.sqlExe( + "SELECT req_type, chapter_no, chapter_title, requirement, page_limit, " + "source_ref FROM bid_doc_requirements WHERE id IN (" + inlist + ")", {}) + await sor.sqlExe("COMMIT", {}) + reqs = rows_to_dicts(rrecs) + rq_bound = True + else: + rrecs = await sor.sqlExe( + "SELECT req_type, chapter_no, chapter_title, requirement, page_limit, " + "source_ref FROM bid_doc_requirements " + "WHERE project_id=${pid}$ AND (chapter_no=${cno}$ OR req_type IN " + "('format','seal','copies')) ORDER BY order_no", + {"pid": d.get("project_id"), "cno": d.get("chapter_no")}) + await sor.sqlExe("COMMIT", {}) + reqs = rows_to_dicts(rrecs) + rq_warn = "" + if not rq_bound: + ctitle = (d.get("title") or "").strip() + mismatched = [] + for r in reqs: + if r.get("req_type") != "structure": + continue + rt = (r.get("chapter_title") or "").strip() + if rt and ctitle and (rt not in ctitle and ctitle not in rt): + mismatched.append("%s≠%s" % (rt[:20], ctitle[:20])) + if mismatched: + rq_warn = ("⚠ 编号匹配可疑(要求标题与章节标题不互含:%s)——按编号取到的编写要求" + "可能属于其他章节,动笔前先 list_doc_requirements 语义核对," + "确属错位请 patch_chapters(doc_requirements_by_no=[…]) 落绑定。" + % ";".join(mismatched[:3])) + elif reqs: + rq_warn = "(本章要求为编号匹配、未落显式绑定;标题核对一致,可用)" out = { "id": d.get("id"), "chapter_no": d.get("chapter_no"), "title": d.get("title"), "section": d.get("section"), "status": d.get("status"), "version": d.get("version"), @@ -70,8 +107,11 @@ async def chapter_detail(chapter_id, with_content="1", length=12000): "max_score": d.get("max_score"), "review_score": d.get("review_score"), "review_comment": (d.get("review_comment") or "")[:6000], "scoring_items": items, - "doc_requirements": rows_to_dicts(reqs), + "doc_requirements": reqs, + "doc_requirements_binding": "explicit" if rq_bound else "fallback_by_no", } + if rq_warn: + out["doc_requirements_warning"] = rq_warn if str(with_content) in ("1", "true", "True"): out["content"] = (d.get("content") or "")[:to_int(length, 12000)] return json.dumps(out, ensure_ascii=False, default=str) diff --git a/pipeline_bidding/role_tool_schemas.py b/pipeline_bidding/role_tool_schemas.py index bba1826..fbc3398 100644 --- a/pipeline_bidding/role_tool_schemas.py +++ b/pipeline_bidding/role_tool_schemas.py @@ -127,9 +127,9 @@ BID_ROLE_TOOL_SCHEMAS = { }, "patch_chapters": { "module": f"{_M}.bid_analysis_capability", - "description": "按 chapter_no 增量更新已有章节(改标题/outline/挂载评分项/满分)。create_chapter_outline 只增不改,修正既有骨架必须用它。scoring_items_by_name 按名称反查绑定真实 id", + "description": "按 chapter_no 增量更新已有章节(改标题/outline/挂载评分项/满分/绑定编写要求)。create_chapter_outline 只增不改,修正既有骨架必须用它。scoring_items_by_name 按名称反查绑定真实 id;doc_requirements_by_no=[{chapter_no:要求条目号}] 按 bid_doc_requirements 的 chapter_no 反查 id 落显式绑定(章节号与要求号错位时必用)", "params": {"project_id": "项目ID(可选,默认当前)", - "patches": "JSON数组[{chapter_no,title?,outline?,section?,scoring_item_ids?,scoring_items_by_name?,max_score?}]"}, + "patches": "JSON数组[{chapter_no,title?,outline?,section?,scoring_item_ids?,scoring_items_by_name?,max_score?,doc_requirement_ids?,doc_requirements_by_no?}]"}, "required": ["patches"], }, "update_doc_requirement": { @@ -153,6 +153,12 @@ BID_ROLE_TOOL_SCHEMAS = { "params": {"project_id": "项目ID(可选,默认当前)", "req_type": "类型(可选)"}, "required": [], }, + "check_requirement_binding": { + "module": f"{_M}.bid_analysis_capability", + "description": "体检「章节↔编写要求」绑定对齐:报告编号错位可疑章节/绑定失效/无要求章节/未被引用的structure要求。词匹配只报可疑,语义裁决后用patch_chapters(doc_requirements_by_no)落绑定或update_doc_requirement改正", + "params": {"project_id": "项目ID(可选,默认当前)"}, + "required": [], + }, "mark_file_extracted": { "module": f"{_M}.bid_analysis_capability", "description": "标记招标文件抽取完成",