feat(agent): 文档配图形态硬门禁——需求/设计文档及一切交付文档中的图必须invoke_model t2i/i2i生成真图(2026-09-15用户要求);新增diagram_gate确定性检测器(mermaid/plantuml围栏+箱线字符画++---+文本边框,豁免目录树条目/纯分隔线/markdown表格/代码块);deliver双路径(native+文本兜底)拦截伪图回填FAIL可行动改法;逃逸阀=文中如实标注「配图缺失:平台无可用文生图模型」;v1角色system prompt同步硬规定
This commit is contained in:
parent
313f32dd7b
commit
af69708dd0
@ -601,6 +601,7 @@ __TOOLS__
|
||||
|
||||
## 配图与媒体生成(有平台模型可用,禁止字符画凑数)
|
||||
产出文档/章节/方案时,遇到需要插图的内容(架构图/拓扑图/流程图示意图/效果图/封面图等),用 invoke_model 调用平台生成模型产出**真实图片**:
|
||||
- **需求/设计文档中的各种图必须真图(2026-09-15 硬规定)**:四张图(业务对象/生命周期/数据所有权/业务依赖)、系统架构图、模块依赖图、部署拓扑图、界面效果图等,一律 t2i/i2i 生成,**禁止 mermaid/plantuml 代码块或 ASCII/文本框线图代替**(QC 审查按此退回)。文字版要点可作图的补充说明,图本体必须是生成图片。
|
||||
- 不确定有什么模型时先 list_platform_models(capability=t2i 文生图 / i2v 图生视频 / tts 语音等)。
|
||||
- invoke_model 的 model 可留空(平台按任务自动匹配),task 写清画面内容(主体/风格/构图/文字要求)。
|
||||
- **完备性先于执行(铁律)**:生成类调用真花钱。调 invoke_model 前先自查该能力的必需输入是否齐备(文生图/视频要有画面描述;图生视频/图生图要有输入图;语音识别要有音频;缺媒体素材时先 ask_question 向人类要,拿到再调)。工具返回 QUESTION: 开头 = 平台门禁判定输入不完备、未发生调用,把追问原样转达给用户/按角色规则 ask_question,禁止自行编造素材或空参硬试。
|
||||
@ -2210,6 +2211,33 @@ _FORCE_PRODUCE_HINT = (
|
||||
)
|
||||
|
||||
|
||||
def _validate_diagram_form(role, deliverable, space_dir, written_files):
|
||||
"""交付件配图形态硬门禁(2026-09-15 用户要求:文档中的图必须 t2i/i2i 真图)。
|
||||
|
||||
检查交付摘要(result)与本任务 write_file 实写的 .md 文档:
|
||||
出现 mermaid/plantuml 代码块或 ASCII/文本框线「示意图」= 拒绝 deliver,
|
||||
回填可行动 FAIL 让 LLM 改用 invoke_model 生成真图后重交。
|
||||
确定性判定(diagram_gate),不靠 QC 的 LLM 自觉;平台无图像模型时产出中
|
||||
如实标注「配图缺失」即豁免(逃逸阀,不逼死无图环境)。
|
||||
返回 None=通过;字符串=拒绝原因。
|
||||
"""
|
||||
from .diagram_gate import find_fake_diagrams, gate_message
|
||||
texts = [(deliverable.get("result") or "", "交付摘要")]
|
||||
for f in dict.fromkeys(written_files or []):
|
||||
if not f.lower().endswith(('.md', '.markdown')):
|
||||
continue
|
||||
try:
|
||||
with open(f, encoding='utf-8', errors='ignore') as fh:
|
||||
texts.append((fh.read(), os.path.relpath(f, space_dir) if f.startswith(space_dir) else f))
|
||||
except Exception:
|
||||
pass
|
||||
for txt, where in texts:
|
||||
problems = find_fake_diagrams(txt)
|
||||
if problems:
|
||||
return gate_message(problems, scene=where)
|
||||
return None
|
||||
|
||||
|
||||
def _validate_deliverable_type(role, deliverable_type, allowed_types):
|
||||
"""deliver 类型守卫:角色声明了合法类型清单时,非法类型拒绝并返回清单(可行动报错)。
|
||||
|
||||
@ -2579,6 +2607,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _dt_err})
|
||||
logger.info(f"role_agent deliver type rejected: task={task_id} {_dt_err[:120]}")
|
||||
continue
|
||||
# 配图形态硬门禁(2026-09-15):文档中的图必须 t2i/i2i 真图,
|
||||
# mermaid/ASCII 伪图拒绝 deliver,回填 FAIL 让 LLM 改真图重交。
|
||||
_dg_err = _validate_diagram_form(role, params, space_dir, written_files)
|
||||
if _dg_err:
|
||||
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _dg_err})
|
||||
logger.info(f"role_agent deliver diagram rejected: task={task_id} {_dg_err[:160]}")
|
||||
continue
|
||||
deliverable = {"action": "deliver", **params}
|
||||
break
|
||||
if tool == "ask_question":
|
||||
@ -2628,6 +2663,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
msgs.append({"role": "user", "content": _dt_err})
|
||||
logger.info(f"role_agent deliver type rejected(text): task={task_id} {_dt_err[:120]}")
|
||||
continue
|
||||
# 配图形态硬门禁(文本兜底路径同款,2026-09-15)
|
||||
_dg_err = _validate_diagram_form(role, act, space_dir, written_files)
|
||||
if _dg_err:
|
||||
msgs.append({"role": "assistant", "content": raw})
|
||||
msgs.append({"role": "user", "content": _dg_err})
|
||||
logger.info(f"role_agent deliver diagram rejected(text): task={task_id} {_dg_err[:160]}")
|
||||
continue
|
||||
deliverable = act
|
||||
break
|
||||
elif act.get('action') == 'ask':
|
||||
|
||||
115
pipeline_service/diagram_gate.py
Normal file
115
pipeline_service/diagram_gate.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""pipeline_service.diagram_gate — 文档配图形态硬门禁(2026-09-15 用户要求)。
|
||||
|
||||
需求/设计文档(SDLC)与标书章节(投标)中的各种图**必须**用平台 t2i/i2i
|
||||
模型生成真实图片(invoke_model →  嵌入正文)。
|
||||
禁止 mermaid/plantuml 代码块、ASCII 字符画、文本框线拼「示意图」凑数。
|
||||
|
||||
判定是**确定性的**(花钱/质量闸门不靠 LLM 自觉,对齐 platform_model_tools
|
||||
完备性门禁的分层原则):
|
||||
- mermaid/plantuml/dot 围栏代码块 → 违规
|
||||
- 制表符画(│├└┌─… 箱线字符 + 同行有实际内容)≥2 行 → 违规
|
||||
- +---+ 文本边框 ≥2 行 → 违规
|
||||
纯分隔线(整行只有 ─ 和空白)不算图,避免误伤。
|
||||
|
||||
唯一逃逸阀:平台无任何图像生成模型时,产出中如实标注
|
||||
「配图缺失:平台无可用文生图模型」(DIAGRAM_MISSING_MARK),门禁放行。
|
||||
|
||||
消费方:
|
||||
- agent_loop.role_agent_run deliver 拦截(SDLC requirement/design 角色)
|
||||
- pipeline-bidding bid_write_capability.write_chapter / bid_review_capability.review_chapter
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# 如实标注配图缺失的豁免标记(包含即放行,不逼死无图像模型的环境)
|
||||
DIAGRAM_MISSING_MARK = "配图缺失"
|
||||
|
||||
# 需要真图的围栏代码块(画图 DSL 一律禁止)
|
||||
_FENCE_RE = re.compile(r"```[ \t]*(?:mermaid|plantuml|puml|dot|graphviz)\b", re.I)
|
||||
|
||||
# 箱线字符(制表符画/框图常用)
|
||||
_BOX_CHARS = set("│├└┌┐┘┬┴┤┼╔╗╚╝╠╣╦╩╬═║─━┃┏┓┗┛")
|
||||
|
||||
# 角/连接字符:出现即判定为箱线图边框行(纯 ─ 分隔线无角字符,不误伤)
|
||||
_CORNER_CHARS = set("┌┐└┘┏┓┗┛╔╗╚╝├┤┬┴┼╠╣╦╩╬")
|
||||
|
||||
# +----+ 风格文本边框
|
||||
_PLUS_BOX_RE = re.compile(r"\+[-=]{3,}\+")
|
||||
|
||||
|
||||
def _is_tree_entry(line):
|
||||
"""目录树条目行(├── src/main.py 这类):箱线字符集中在行首前缀,
|
||||
其后是非空的普通文本(无箱线字符穿插)→ 是文本清单不是图,豁免。
|
||||
整行全是箱线字符(┏━━━━┓)不是目录树条目(是图边框)。
|
||||
反例(判为图):├─组合─ 工单消息——箱线字符与内容穿插。"""
|
||||
s = line
|
||||
i = 0
|
||||
# 跳过行首的箱线字符与空白(目录树缩进前缀可含 │ + 空格,如 │ ├── name)
|
||||
while i < len(s) and (s[i] in _BOX_CHARS or s[i].isspace()):
|
||||
i += 1
|
||||
if i == 0:
|
||||
return False
|
||||
rest = s[i:]
|
||||
if not rest.strip():
|
||||
return False # 整行全是箱线字符/空白(如 ┏━━━━━━┓)→ 是图边框,非目录条目
|
||||
return not any(ch in _BOX_CHARS for ch in rest)
|
||||
|
||||
|
||||
def _is_box_line(line):
|
||||
"""该行是否是箱线画的组成部分(确定性判定,纯 ──── 分隔线不算图,防误伤):
|
||||
- 含任一角/连接字符(┌┐└┘┏┓┗┛├┤┬┴┼…)→ 是边框/树形线;或
|
||||
- ≥2 个箱线字符且除箱线/空白外还有 ≥2 个实际内容字符(如 ├─组合─ 工单)。
|
||||
豁免:目录树条目行(├── src/ 这类「箱线字符+单个词」)是文本清单不是图。"""
|
||||
n_box = 0
|
||||
n_other = 0
|
||||
for ch in line:
|
||||
if ch in _BOX_CHARS:
|
||||
n_box += 1
|
||||
elif not ch.isspace():
|
||||
n_other += 1
|
||||
if any(ch in _CORNER_CHARS for ch in line):
|
||||
# 目录树条目豁免:├── name / └── name/ / │ ├── name(箱线后只有普通文本)
|
||||
if _is_tree_entry(line):
|
||||
return False
|
||||
return True
|
||||
return n_box >= 2 and n_other >= 2
|
||||
|
||||
|
||||
def find_fake_diagrams(text):
|
||||
"""检测文本中的伪图,返回违规描述列表(空 = 通过)。确定性判定,无语义猜测。"""
|
||||
problems = []
|
||||
if not text:
|
||||
return problems
|
||||
if DIAGRAM_MISSING_MARK in text:
|
||||
# 如实标注平台无图像模型 → 逃逸阀放行(标注真实性由 QC/评审按平台模型清单核对)
|
||||
return problems
|
||||
if _FENCE_RE.search(text):
|
||||
problems.append("mermaid/plantuml 画图代码块")
|
||||
lines = text.splitlines()
|
||||
box_lines = sum(1 for ln in lines if _is_box_line(ln))
|
||||
if box_lines >= 2:
|
||||
problems.append("ASCII/箱线字符画示意图(%d 行)" % box_lines)
|
||||
plus_lines = sum(1 for ln in lines if _PLUS_BOX_RE.search(ln))
|
||||
if plus_lines >= 2:
|
||||
problems.append("+---+ 文本边框示意图(%d 行)" % plus_lines)
|
||||
return problems
|
||||
|
||||
|
||||
def gate_message(problems, scene="文档"):
|
||||
"""统一的可行动拒绝话术(说清怎么改,不是只说不行)。"""
|
||||
return ("FAIL: %s中的图必须用平台 t2i/i2i 模型生成真实图片(2026-09-15 硬规定)。"
|
||||
"发现伪图:%s。请逐图改用 invoke_model 生成"
|
||||
"(model 留空自动选型;task 写清图的主体/层次/风格——商务风、白底、含中文标注——"
|
||||
"及图中文字要求;改造已有图带输入图 URL 走 i2i),"
|
||||
"产物 URL 以  嵌入正文,删除 mermaid/ASCII 字符画后重新提交。"
|
||||
"仅当平台确无任何图像生成模型时,才可在文中如实标注「%s:平台无可用文生图模型」。"
|
||||
% (scene, ";".join(sorted(set(problems))), DIAGRAM_MISSING_MARK))
|
||||
|
||||
|
||||
def count_image_embeds(text):
|
||||
"""统计 markdown 图片嵌入行数(,排除空 URL)。正向门禁用:
|
||||
要求含四张图/架构图的文档至少有一张真图。"""
|
||||
if not text:
|
||||
return 0
|
||||
return len([m for m in re.finditer(r"!\[[^\]]*\]\(([^)\s]+)\)", text)
|
||||
if m.group(1).strip()])
|
||||
Loading…
x
Reference in New Issue
Block a user