feat(binding): 章节↔编写要求显式绑定doc_requirement_ids(2026-09-16编号错位实测根因)——requirements按招标书目录编号/chapters按装订编号两套编号漂移,chapter_detail等值JOIN静默递错要求(写者拿'营业执照'要求写授权委托书,4章错位+5章无要求)。机制:create_chapter_outline自动生成落绑定;patch_chapters支持doc_requirement_ids/doc_requirements_by_no(id校验);chapter_detail绑定优先+编号回退带标题互含触发器(词匹配只报可疑,语义裁决在agent);新工具check_requirement_binding体检报告(错位/失效/孤儿);QC_TYPE_OUTLINE加第5维绑定对齐;models+json2ddl实跑验证
This commit is contained in:
parent
598578d288
commit
f9cbfdc625
@ -11,7 +11,8 @@
|
||||
"exclouded": [
|
||||
"content",
|
||||
"outline",
|
||||
"scoring_item_ids"
|
||||
"scoring_item_ids",
|
||||
"doc_requirement_ids"
|
||||
]
|
||||
},
|
||||
"editexclouded": [
|
||||
@ -47,4 +48,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,11 @@
|
||||
"title": "对应评分项(JSON)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "doc_requirement_ids",
|
||||
"title": "绑定投标文件要求ID(JSON数组)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "outline",
|
||||
"title": "章节要点",
|
||||
|
||||
@ -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()")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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": "标记招标文件抽取完成",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user