fix(binding): 空绑定语义补齐+要求类型纠正通道(部署前逻辑走查自纠)——①显式空绑定[]不回退编号匹配但仍带全局规则(format/seal/copies),否则修复引入回归:绑定分支丢全局格式要求;②chapter_detail空绑定明示'无章节级要求,勿套用他章';③update_doc_requirement加new_req_type(与定位键req_type分开,否则改类型时按新值定位不到原条目),给全局规则误标structure的纠正通道;④check_requirement_binding区分'显式空绑定(已裁决结论)'与'未绑定(缺口)'

This commit is contained in:
yumoqing 2026-09-16 18:35:47 +08:00
parent f9cbfdc625
commit 9305e9550c
3 changed files with 53 additions and 11 deletions

View File

@ -462,13 +462,17 @@ async def add_doc_requirement(project_id, requirement, req_type="structure",
async def update_doc_requirement(project_id, record_id="", chapter_no="", req_type="", async def update_doc_requirement(project_id, record_id="", chapter_no="", req_type="",
requirement="", source_ref="", page_limit="", requirement="", source_ref="", page_limit="",
chapter_title="", delete=False, who=None, agent_id=None): chapter_title="", delete=False, new_req_type="",
who=None, agent_id=None):
"""更新/删除既有投标文件要求条目(2026-09-03,根治 add 只增不改)。 """更新/删除既有投标文件要求条目(2026-09-03,根治 add 只增不改)。
定位:record_id 优先;否则 chapter_no+req_type 唯一定位。 定位:record_id 优先;否则 chapter_no+req_type 唯一定位。
- delete=True → 删除该条(编造/无出处项的处置); - delete=True → 删除该条(编造/无出处项的处置);
- requirement/source_ref/page_limit/chapter_title 非空 → 回写原条目 - requirement/source_ref/page_limit/chapter_title 非空 → 回写原条目
(纠正必须回写,不得只追加新条目形成矛盾对)。 (纠正必须回写,不得只追加新条目形成矛盾对);
- new_req_type(2026-09-16)→ 纠正条目类型:全局规则误标 structure 会占用
章节编号匹配位(chapter_detail 按编号回退时递错要求),应改 other/format。
与定位键 req_type 分开传,否则改类型时按新值定位不到原条目。
""" """
try: try:
project_id = await resolve_project_id(project_id) project_id = await resolve_project_id(project_id)
@ -510,8 +514,17 @@ async def update_doc_requirement(project_id, record_id="", chapter_no="", req_ty
sets.append("source_ref=${sr}$"); p["sr"] = str(source_ref)[:480] sets.append("source_ref=${sr}$"); p["sr"] = str(source_ref)[:480]
if page_limit: if page_limit:
sets.append("page_limit=${pl}$"); p["pl"] = to_int(page_limit, 0) or None sets.append("page_limit=${pl}$"); p["pl"] = to_int(page_limit, 0) or None
# req_type 纠正走独立参数 new_req_type(req_type 是定位键:按 chapter_no+req_type
# 定位时传新值会定位不到——2026-09-16)。全局规则条目误标 structure 会占用章节
# 编号匹配位,导致写者按编号拿到错的要求(如"总体构成与编制顺序"应改 other)。
if new_req_type:
rt = str(new_req_type).strip()[:30]
if rt not in ("structure", "format", "seal", "copies", "deadline", "other"):
return False, ("new_req_type 非法: %s(structure/format/seal/copies/"
"deadline/other)" % rt)
sets.append("req_type=${nrt}$"); p["nrt"] = rt
if not sets: if not sets:
return False, "无可更新字段(requirement/source_ref/page_limit 至少传一个)" return False, "无可更新字段(requirement/source_ref/page_limit/req_type 至少传一个)"
sets.append("updated_at=NOW()") sets.append("updated_at=NOW()")
await sor.sqlExe("UPDATE bid_doc_requirements SET " + ", ".join(sets) + await sor.sqlExe("UPDATE bid_doc_requirements SET " + ", ".join(sets) +
" WHERE id=${r}$", p) " WHERE id=${r}$", p)
@ -551,9 +564,10 @@ async def check_requirement_binding(project_id):
rq_by_no.setdefault(r["chapter_no"], r) rq_by_no.setdefault(r["chapter_no"], r)
bound_rq = set() bound_rq = set()
lines = ["## 章节↔编写要求绑定体检(%d 章 / %d 条要求)" % (len(chapters), len(reqs)), ""] lines = ["## 章节↔编写要求绑定体检(%d 章 / %d 条要求)" % (len(chapters), len(reqs)), ""]
suspicious, unbound_ch, mismatched = [], [], [] suspicious, unbound_ch, mismatched, empty_bound = [], [], [], []
for c in chapters: for c in chapters:
ids = json_loads(c.get("doc_requirement_ids"), []) raw = c.get("doc_requirement_ids")
ids = json_loads(raw, [])
cno = str(c.get("chapter_no") or "") cno = str(c.get("chapter_no") or "")
ctitle = str(c.get("title") or "") ctitle = str(c.get("title") or "")
if ids: if ids:
@ -562,6 +576,10 @@ async def check_requirement_binding(project_id):
if bad: if bad:
suspicious.append("%s %s: 绑定含失效id %s" % (cno, ctitle[:18], bad[:2])) suspicious.append("%s %s: 绑定含失效id %s" % (cno, ctitle[:18], bad[:2]))
continue continue
if raw is not None:
# 显式空绑定:已语义裁决"本章无章节级要求",是结论不是缺口
empty_bound.append("%s %s" % (cno, ctitle[:18]))
continue
fb = rq_by_no.get(cno) fb = rq_by_no.get(cno)
if fb: if fb:
rt = str(fb.get("chapter_title") or "") rt = str(fb.get("chapter_title") or "")
@ -582,6 +600,9 @@ async def check_requirement_binding(project_id):
if unbound_ch: if unbound_ch:
lines += ["### 无同号要求的章节(%d)——核对是否漏抽要求或确属无单独要求" lines += ["### 无同号要求的章节(%d)——核对是否漏抽要求或确属无单独要求"
% len(unbound_ch), *["- " + m for m in unbound_ch], ""] % len(unbound_ch), *["- " + m for m in unbound_ch], ""]
if empty_bound:
lines += ["### 显式空绑定(%d)——已语义裁决本章无章节级要求(如招标书明文无需提供)"
% len(empty_bound), *[("- " + m) for m in empty_bound], ""]
if orphan: if orphan:
lines += ["### 未被任何章节引用的 structure 要求(%d)——可能是全局规则误标 structure," lines += ["### 未被任何章节引用的 structure 要求(%d)——可能是全局规则误标 structure,"
"应 update_doc_requirement 改 req_type=other/format,或补绑定" % len(orphan)] "应 update_doc_requirement 改 req_type=other/format,或补绑定" % len(orphan)]

View File

@ -64,16 +64,32 @@ async def chapter_detail(chapter_id, with_content="1", length=12000):
# 未绑定时按编号回退,但用标题互含做词匹配触发器:不互含→输出⚠可疑警告 # 未绑定时按编号回退,但用标题互含做词匹配触发器:不互含→输出⚠可疑警告
# (词匹配只报可疑,语义裁决权在 agent;判错用 list_doc_requirements + # (词匹配只报可疑,语义裁决权在 agent;判错用 list_doc_requirements +
# patch_chapters(doc_requirements_by_no) 落绑定后再写)。 # patch_chapters(doc_requirements_by_no) 落绑定后再写)。
rq_ids = json_loads(d.get("doc_requirement_ids"), []) rq_raw = d.get("doc_requirement_ids")
rq_ids = json_loads(rq_raw, [])
rq_bound = False rq_bound = False
if rq_raw is not None:
# 显式绑定(含裁定为空绑定 "[]":语义裁决后确认本章无对应要求,
# 禁止编号回退再递错——2026-09-16 甘肃 1.9 实测:招标书明文
# "供应商无需提供信用记录查询结果",同号要求是别的章节的)
rq_bound = True
if rq_ids: if rq_ids:
inlist = ",".join("'" + str(x).replace("'", "") + "'" for x in rq_ids) inlist = ",".join("'" + str(x).replace("'", "") + "'" for x in rq_ids)
rrecs = await sor.sqlExe( rrecs = await sor.sqlExe(
"SELECT req_type, chapter_no, chapter_title, requirement, page_limit, " "SELECT req_type, chapter_no, chapter_title, requirement, page_limit, "
"source_ref FROM bid_doc_requirements WHERE id IN (" + inlist + ")", {}) "source_ref FROM bid_doc_requirements WHERE id IN (" + inlist + ") "
"OR (project_id=${pid}$ AND req_type IN ('format','seal','copies')) "
"ORDER BY order_no", {"pid": d.get("project_id")})
await sor.sqlExe("COMMIT", {})
reqs = rows_to_dicts(rrecs)
elif rq_bound:
# 显式空绑定:无章节级要求,但全局规则(format/seal/copies)仍适用
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 req_type IN ('format','seal','copies') ORDER BY order_no",
{"pid": d.get("project_id")})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
reqs = rows_to_dicts(rrecs) reqs = rows_to_dicts(rrecs)
rq_bound = True
else: else:
rrecs = await sor.sqlExe( rrecs = await sor.sqlExe(
"SELECT req_type, chapter_no, chapter_title, requirement, page_limit, " "SELECT req_type, chapter_no, chapter_title, requirement, page_limit, "
@ -84,6 +100,7 @@ async def chapter_detail(chapter_id, with_content="1", length=12000):
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
reqs = rows_to_dicts(rrecs) reqs = rows_to_dicts(rrecs)
rq_warn = "" rq_warn = ""
rq_empty = rq_bound and not rq_ids # 显式空绑定(已裁定本章无章节级要求)
if not rq_bound: if not rq_bound:
ctitle = (d.get("title") or "").strip() ctitle = (d.get("title") or "").strip()
mismatched = [] mismatched = []
@ -100,6 +117,9 @@ async def chapter_detail(chapter_id, with_content="1", length=12000):
% ";".join(mismatched[:3])) % ";".join(mismatched[:3]))
elif reqs: elif reqs:
rq_warn = "(本章要求为编号匹配、未落显式绑定;标题核对一致,可用)" rq_warn = "(本章要求为编号匹配、未落显式绑定;标题核对一致,可用)"
elif rq_empty:
rq_warn = ("(本章经裁决为空绑定:无章节级编写要求,仅列全局规则——按章节 outline "
"与招标原文相关条款编写,勿套用其他章节的要求)")
out = { out = {
"id": d.get("id"), "chapter_no": d.get("chapter_no"), "title": d.get("title"), "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"), "section": d.get("section"), "status": d.get("status"), "version": d.get("version"),

View File

@ -134,11 +134,12 @@ BID_ROLE_TOOL_SCHEMAS = {
}, },
"update_doc_requirement": { "update_doc_requirement": {
"module": f"{_M}.bid_analysis_capability", "module": f"{_M}.bid_analysis_capability",
"description": "更新/删除既有投标文件要求条目(纠正必须回写原条目,不得只追加新条目形成矛盾对;编造/无出处项用 delete 删除)。add_doc_requirement 只增不改", "description": "更新/删除既有投标文件要求条目(纠正必须回写原条目,不得只追加新条目形成矛盾对;编造/无出处项用 delete 删除)。add_doc_requirement 只增不改。new_req_type 纠正条目类型:全局规则误标 structure 会占用章节编号匹配位(写者按编号拿到错的要求),应改 other/format",
"params": {"project_id": "项目ID(可选,默认当前)", "record_id": "条目ID(优先)", "params": {"project_id": "项目ID(可选,默认当前)", "record_id": "条目ID(优先)",
"chapter_no": "章节号(与req_type组合定位)", "req_type": "类型(structure/format/seal/copies/deadline/other)", "chapter_no": "章节号(与req_type组合定位)", "req_type": "定位用类型(structure/format/seal/copies/deadline/other)",
"requirement": "回写的新正文(可选)", "source_ref": "回写原文出处(可选)", "requirement": "回写的新正文(可选)", "source_ref": "回写原文出处(可选)",
"page_limit": "页数(可选)", "delete": "true=删除该条(可选)"}, "page_limit": "页数(可选)", "delete": "true=删除该条(可选)",
"new_req_type": "纠正条目类型(可选,与定位键req_type分开传)"},
"required": [], "required": [],
}, },
"list_scoring_items": { "list_scoring_items": {