Files
cad-agent/dwg_review/checklist.py
T
gjm 6143bb9ab2 feat(review): PDF/图片离线源适配(C1)—— 无需 CAD 的整条审查链路
支持直接审查 PDF 与位图图纸,全链路离线(不连 CAD、不连 8080 端口)。
渲染在内存内完成,复用既有墨迹热力图 + 自适应瓦片规划。

- 新增 dwg_review/sources.py
  - detect_source / plan_file / collect 统一入口
  - PdfReader:pypdfium2 惰性导入,页序即图框序,处理 /Rotate 与 crop 边距
  - ImageReader:世界单位 = 源像素,snap_box 整数对齐保证窗口与像素严格一致
  - ink_grid:纯 PIL 灰度阈值 → 占用网格(不引入 numpy)
- ReviewContext.create(connect_cad=False):离线时 client/tools 均为 None
- 全链路 ctx.tools is None 守卫:review 只读子循环 / inspect_region / annotator /
  checks 的窗口类检查 / 绘图区最大化
- checklist.build_system_prompt(allow_tools=False, offline=True):离线提示词
- __main__:位置参数 source,按扩展名自动判定源类型并决定是否连 CAD
- config 新增 8 个常量(PDF_MAX_PAGES / 文本最小字数 / 各源缩放上限 / 墨迹阈值等)
- tiling.content_predicate 与 heatmap.cell_box 提升为公开函数

验证:
- pytest 457 passed
- verify_pdf_source.py:PDF 23/23、图片 9/9(不需 CAD),含实测瓦片字高像素与
  旋转映射自检(文本区墨迹 0.311 vs 忽略 /Rotate 反向对照 0.145)
- 真实 LLM 端到端:20 页 HVAC 图纸前 3 页跑通,结论具体可核验且未编造 handle

已知局限:页边线在墨迹网格里会把整页连成单簇,瓦片常退化为整页象限(CAD 侧
靠 drop_oversized 丢图框块,离线侧暂无等价手段),留待后续打磨。
2026-10-16 23:07:23 +08:00

160 lines
8.6 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.
"""审查清单加载 + 系统提示构建(清单驱动)。"""
import json
from . import config
DEFAULT_CHECKLIST = {
"name": "内置通用清单",
"categories": [
{"title": "图层规范性", "severity": "medium",
"checks": ["图元是否在正确图层", "是否存在空图层/命名异常图层"]},
{"title": "文字", "severity": "medium",
"checks": ["空文字", "错别字", "字高异常", "文字重叠"]},
{"title": "标注", "severity": "high",
"checks": ["关键尺寸缺失", "标注值异常"]},
{"title": "几何", "severity": "medium",
"checks": ["零长度/退化实体", "重复重叠实体", "未闭合轮廓"]},
{"title": "干涉/碰撞", "severity": "high",
"checks": ["构件/管线间非预期侵入", "同位置重复叠画", "引线/填充异常遮挡"]},
{"title": "图面整体", "severity": "low",
"checks": ["图面完整性", "内容是否超出图框"]},
],
}
def load_checklist(path=None):
"""加载审查清单 JSON;失败时回退内置默认清单。"""
path = path or config.CHECKLIST_PATH
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and data.get("categories"):
return data
print(f"[Warn] 审查清单 {path} 结构异常,使用内置默认清单。")
except Exception as exc: # noqa: BLE001
print(f"[Warn] 无法加载审查清单 {path}: {exc},使用内置默认清单。")
return DEFAULT_CHECKLIST
def build_system_prompt(checklist, *, allow_tools=True, offline=False):
"""根据审查清单动态生成 LLM 系统提示词。
`offline=True` 用于 PDF / 图片源:**没有 CAD、没有实体句柄、没有结构化数据**,
输入只有渲染图(整体 + 局部瓦片)与少量文本层片段,结论必须完全基于图像。
"""
if offline:
lines = [
"你是一名资深建筑与工业厂房图纸审查专家。",
"你将逐张收到某**一页图纸的渲染图**(整体图 + 若干局部放大图;"
"局部图按内容分布自适应切分,可能重叠)。",
"",
"**重要**:该图纸是从 PDF / 图片导入的,**没有实体级结构化数据、也没有实体句柄**。"
"你看到的就是全部依据——请**只依据图像**判断。",
"国标图纸的文字多为 SHX 描边(导出后成为线条),因此附带的文本层往往为空或很少,"
"这是正常现象;**请直接从图上读文字与尺寸**,不要因为没有文本层就降低审查力度。",
"",
f"请依据审查清单《{checklist.get('name', '审查清单')}》逐项审查,"
"指出**具体问题**(说明问题出现在图上什么位置,例如「左上角平面图 A 轴附近」),"
"并标注严重程度:",
"",
]
else:
lines = [
"你是一名资深建筑与工业厂房图纸审查专家。",
"你将逐张收到某个图框(一张图纸)的:结构化图元数据、文字内容,以及该图框的视口截图。",
"",
f"请依据审查清单《{checklist.get('name', '审查清单')}》逐项审查,"
"指出**具体问题**(尽量引用实体 handle 或文字内容),并标注严重程度:",
"",
]
for cat in checklist.get("categories", []):
lines.append(f"【{cat.get('title', '')}】(严重度:{cat.get('severity', 'medium')})")
for chk in cat.get("checks", []):
lines.append(f" - {chk}")
lines.append("")
if allow_tools:
lines += [
"你可以调用**只读**取证工具进一步确认细节;**不要凭猜测下结论**。",
"",
"**看不清某处时,调用 `inspect_region` 指认位置**,它会自动按可读分辨率放大并返回清晰截图。",
"`target` 三种指认方式任选其一:",
' - `{"handle": "3A4"}` —— 已知实体句柄;',
' - `{"label": "tile-3", "cx": 0.4, "cy": 0.6, "span": 0.2}` —— 某张已给截图上的一个位置',
" (`cx` 从左到右、`cy` **从上到下**,均为 0~1 归一化;`span` 为要看的范围占该截图的比例,默认 0.25);",
' - `{"window": {"min_x": …, "min_y": …, "max_x": …, "max_y": …}}` —— 直接给世界坐标框。',
'`purpose` 选 `"text"`(要读文字/标注,默认)或 `"line"`(只看走向/连接,覆盖更广)。',
"返回的截图会带 `inspect-N` 标签,可继续作为 `label` 指认更细的位置(逐级下钻)。",
"**你不需要判断截图够不够清楚**——`inspect_region` 会保证返回的图达到可读分辨率;",
"若它提示区域过大,请缩小 `target`(或用 `span` 缩小范围)后重试。",
"",
"结构化数据不足以判断时:用 `get_entity_detail` / `find_duplicate_entities` /",
"`find_zero_length` / `find_collisions` / `get_layers` 等查证后再下结论。",
"排查「干涉/碰撞」时,先调 `find_collisions` 拿到**包围盒相交候选**"
"(按重叠比降序,`min_overlap` 可调阈值),",
"再对可疑的候选 handle 用 `inspect_region` 放大目视确认——"
"**AABB 相交不等于真干涉**,务必核实后再下结论。",
"",
]
if offline:
lines += [
"要求:",
"- 只报告你**在图上确实看到**的问题,不要臆测;看不清就说看不清,不要编造。",
"- 每条问题请写明**位置**(用图上可见的轴线号 / 房间名 / 相对方位描述)。",
"- 局部放大图可能有重叠,同一问题只报一次。",
"- 按清单条目组织结论,未命中的条目可省略。",
"- 不要复述输入数据。",
"",
"输出格式(**必须严格遵守**):",
"在你准备给出该页图纸的最终结论时,**只输出一个 JSON 对象**,"
"不要输出任何解释文字,也不要用 markdown 代码块包裹。结构如下:",
"{",
' "summary": "<该页的一句话整体结论;若未发现问题,写“未发现明显问题”>",',
' "issues": [',
" {",
' "category": "<图层/文字/标注/几何/干涉/图面整体 之一>",',
' "severity": "<high|medium|low>",',
' "message": "<问题描述 + 位置,简洁具体>"',
" }",
" ]",
"}",
"**不要输出 `handle` 字段**(本来源没有实体句柄,编造会被系统丢弃)。",
]
return "\n".join(lines)
lines += [
"要求:",
"- 只报告你**有依据**的问题,不要臆测;数据与截图冲突时以数据为准。",
"- 按清单条目组织结论,未命中的条目可省略。",
"- 不要复述输入数据。",
"",
"输出格式(**必须严格遵守**):",
"在你完成全部取证、准备给出该图框的最终结论时,**只输出一个 JSON 对象**,"
"不要输出任何解释文字,也不要用 markdown 代码块包裹。结构如下:",
"{",
' "summary": "<该图框的一句话整体结论;若未发现问题,写“未发现明显问题”>",',
' "issues": [',
" {",
' "category": "<图层/文字/标注/几何/干涉/图面整体 之一>",',
' "severity": "<high|medium|low>",',
' "message": "<问题描述,简洁具体>",',
' "handle": "<该问题对应的实体句柄;无法定位到具体实体时省略此字段>"',
" }",
" ]",
"}",
"其中 `handle` 必须是你**在取证数据或工具返回里真实见过**的句柄,不要编造;"
"无法确定具体实体时省略 `handle`,该问题仍会被记录。",
]
if allow_tools:
lines.append("(需要进一步确认时先调用工具;拿到结果后,最终再输出上述 JSON。)")
return "\n".join(lines)
def checklist_summary(checklist):
"""用于规划阶段的一句话摘要。"""
cats = checklist.get("categories", [])
return f"{checklist.get('name', '清单')}({len(cats)} 类:" + \
"、".join(c.get("title", "") for c in cats) + ")"