pipeline-bidding/pipeline_bidding/bid_write_capability.py

151 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""章节编写能力(agent.bid_writer):查章节、看要求、写正文、提交评审。
对应业务:按照投标文件要求分章节编写投标文件。
一章一任务(任务 params.chapter_id),写完置 written 由评审角色接手(reconciler 自动派评审任务)。
"""
import json
import logging
from .bid_common import (
get_db, rec_to_dict, rows_to_dicts, json_loads, to_int, record,
resolve_project_id, CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
)
logger = logging.getLogger("pipeline.bidding.write")
TABLE = "bid_chapters"
MIN_CONTENT_LEN = 200 # 章节正文最小长度门槛(防空交付)
async def list_chapters(project_id, status=""):
"""列出章节(不含正文,避免上下文爆炸)。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, chapter_no, title, section, status, version, revise_count, "
"word_count, review_score, max_score, assignee_role "
"FROM " + TABLE + " WHERE project_id=${pid}$")
p = {"pid": project_id}
if status:
sql += " AND status=${st}$"
p["st"] = status
sql += " ORDER BY order_no, chapter_no"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def chapter_detail(chapter_id, with_content="1", length=12000):
"""章节详情:要点、对应评分项及得分规则、上一轮评审改进意见、当前正文。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe("SELECT * FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
sids = json_loads(d.get("scoring_item_ids"), [])
items = []
if sids:
inlist = ",".join("'" + str(x).replace("'", "") + "'" for x in sids)
srecs = await sor.sqlExe(
"SELECT item_no, item_name, max_score, is_veto, scoring_rule, source_ref "
"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", {})
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"),
"revise_count": d.get("revise_count"), "outline": (d.get("outline") or "")[:6000],
"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),
}
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)
async def write_chapter(chapter_id, content, summary="", who=None, agent_id=None):
"""写入/覆盖章节正文:pending/rejected/writing → writing,版本+1,字数统计。"""
if not chapter_id:
return False, "缺少 chapter_id"
txt = content or ""
if len(txt.strip()) < MIN_CONTENT_LEN:
return False, ("章节正文过短(%d 字符 < %d),拒绝写入。请按招标文件要求与评分规则"
"写到操作层面(量化指标/流程步骤/责任人/表单),不要写空话。"
% (len(txt.strip()), MIN_CONTENT_LEN))
# ── 配图形态硬门禁(2026-09-15 用户要求:标书章节的图必须 t2i/i2i 真图)──
# 确定性判定(复用 pipeline_service.diagram_gate),mermaid/ASCII 伪图拒绝写入;
# 平台无图像模型时正文如实标注「配图缺失」即豁免。
try:
from pipeline_service.diagram_gate import find_fake_diagrams, gate_message
_dg = find_fake_diagrams(txt)
if _dg:
return False, gate_message(_dg, scene="标书章节")
except ImportError:
pass # 宿主未装 pipeline-service 时放行(逃逸阀,与产线零接触分层一致)
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, status, version, title FROM " + TABLE + " WHERE id=${cid}$",
{"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
if d.get("status") == CH_APPROVED:
return False, "章节已评审通过(approved),不可覆盖;如需修改请先由评审/评分角色退回"
ver = to_int(d.get("version"), 1)
await sor.sqlExe(
"UPDATE " + TABLE + " SET content=${c}$, word_count=${w}$, status=${st}$, "
"version=${v}$, updated_at=NOW() WHERE id=${cid}$",
{"c": txt, "w": len(txt), "st": CH_WRITING, "v": ver + 1, "cid": chapter_id})
await sor.sqlExe("COMMIT", {})
await record(sor, d.get("project_id"), TABLE, chapter_id, "write",
from_state=d.get("status"), to_state=CH_WRITING,
who=who, agent_id=agent_id, detail=(summary or "")[:480])
return True, ("章节「%s」正文已保存(%d 字符,v%d)。确认完成后调用 submit_chapter 提交评审。"
% (d.get("title"), len(txt), ver + 1))
async def submit_chapter(chapter_id, note="", who=None, agent_id=None):
"""提交章节评审:writing → written(reconciler 会自动派发评审任务)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, status, title, content FROM " + TABLE + " WHERE id=${cid}$",
{"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
if len((d.get("content") or "").strip()) < MIN_CONTENT_LEN:
return False, "章节正文为空或过短,请先 write_chapter 写入正文"
if d.get("status") not in (CH_WRITING, CH_PENDING, CH_REJECTED):
return False, "当前状态 %s 不可提交(需 writing/pending/rejected)" % d.get("status")
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, updated_at=NOW() WHERE id=${cid}$ "
"AND status=${old}$",
{"st": CH_WRITTEN, "old": d.get("status"), "cid": chapter_id})
chk = await sor.sqlExe("SELECT status FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
cur = getattr(chk[0], "status", "") if chk else ""
if cur != CH_WRITTEN:
return False, "提交失败(CAS):当前状态 %s" % cur
await record(sor, d.get("project_id"), TABLE, chapter_id, "submit",
from_state=d.get("status"), to_state=CH_WRITTEN,
who=who, agent_id=agent_id, detail=(note or "")[:480])
return True, "章节「%s」已提交评审" % d.get("title")