Files
cad-agent/dwg_review/evidence_model.py
T
gjm b757892c67 feat(review): agent 侧虚拟工具 inspect_region —— 按可读分辨率放大指定区域
解决三个缺口:① 放大没复用标定/瓦片数学(LLM 一次 zoom 只出一张,宽区域仍读不清);
②「看清没」由 VLM 自评(易过度自信/幻觉);③ 只能按 handle / 手算 bbox 指认,
图上看到但没 handle 的东西指认不了。

核心思路:把「放大」升级成一个语义化工具——LLM 只指认「要看哪里」,由 agent 复用
tiling.py 的标定数学算出该切几张、每张覆盖多大,用构造保证可读。

新增 dwg_review/inspect.py(agent 侧,不编 DLL):
- TOOL_SCHEMA:inspect_region(target, purpose);target 三形态 {handle} / {window} /
  {label,cx,cy,span},purpose = text(8px) / line(6px)。
- InspectState:单图框内「截图标签 → 世界可见范围」登记表,支持逐级下钻
  (返回的 inspect-N 可继续作为 label 指认)。
- resolve_target / _point_box:label+cx+cy → WCS。cy 是图像坐标(上→下),
  世界 y 必须翻转(y = y2 - cy*H)——单测专门钉住。
- plan_tiles:复用 world_per_px + tiles_for_box + subdivide;超 INSPECT_MAX_TILES(4)
  则硬失败并给出「缩小 target / 分多次请求」的指引(不静默降级);拿不到比例/像素
  则退回单张并如实告知。

关键几何:CAD 的 _.ZOOM _W 是等比适配(不拉伸),故「截图上的归一化坐标」必须按
max(W/ew, H/eh) 反算真实可见范围(新增 tiling.visible_box)——直接用送进去的窗口会偏。
zoom_to_window 返回的 view 是「加过 margin 的窗口」,不是可见范围,不能混用。

证据层配套:evidence_model.make_image(..., window=) 记录每张图的可见世界范围;
capture_images 用 view_px(overall 图原始像素)+ visible_box 逐张登记。

子循环接入:_run_readonly_tool 在只读白名单校验之前拦截 inspect_region(它不在
READ_ONLY_TOOLS 里,内部只调白名单内的只读 MCP 工具);_review_tools 启用时用
inspect_region 取代三个裸视口工具;inspect_region 计入 _VIEW_TOOLS → 结束复位视口。
配置:INSPECT_ENABLED(True) / INSPECT_MAX_TILES(4)。

端到端实测(真实图纸 1 图框):取证与审查两阶段均在最大化态、结束精确还原(4 项 PASS);
LLM 的 11 次取证调用里 2 次是 inspect_region,且都显式给 window + purpose:"text"
(x[20000,115000] y[-6500,3000],即 A0 图框底部横条=标题栏所在)。
验收口径达成:报告报出标题栏乱码 ???????????????,而 get_text_content 对全图返回 0 条文字
(本图文字全在块/自定义实体内)→ 块内文字是被视觉放大读出来的。
pytest 343 passed。
2026-10-16 19:15:32 +08:00

98 lines
3.2 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.
"""统一证据模型:多来源(CAD / PDF / 图片)证据的字段常量与访问器。
设计要点:
- evidence 是跨阶段 + 落盘的「线格式」数据,统一用 dict 承载(不用 dataclass)。
- 图片统一为 `images: [{"b64","mime","label"}]`;访问器兼容旧的 `image_b64/image_mime`
单图字段(历史 evidence.json 与旧测试构造仍可读)。
- 本模块不含任何 CAD / 工具逻辑,可被 phases、findings、__main__、tests 共同引用。
"""
SOURCE_CAD = "cad"
SOURCE_PDF = "pdf"
SOURCE_IMAGE = "image"
# 结构化证据键(CAD 侧各查询工具产出的信封);下游 handle 收集 / 派生检查统一引用,
# 避免在多处硬编码同一组键名。
STRUCTURED_SOURCES = ("entities", "texts", "dimensions", "blocks", "polylines")
def make_image(b64, mime="image/png", label=None, window=None):
"""构造 images 列表元素。
`window` 为该截图**实际可见的世界范围** `(min_x,min_y,max_x,max_y)`(可选)。
审查阶段的 `inspect_region` 靠它把「截图上的归一化坐标」映射回 WCS(逐级下钻)。
"""
img = {"b64": b64, "mime": mime or "image/png", "label": label}
if window is not None:
img["window"] = window
return img
def source_type_of(container):
"""读取来源类型,缺省视为 CAD。
container 可以是审查单元(unit)/ 图框 / evidence 等任何 dict。
"""
if isinstance(container, dict):
value = container.get("source_type")
if isinstance(value, str) and value:
return value
return SOURCE_CAD
def evidence_images(evidence):
"""规范化证据里的图片列表(单点归一)。
优先读 `images`;否则回退旧的 `image_b64` / `image_mime` 单图字段。
统一返回 `[{"b64","mime","label","window"}]`,过滤掉空 b64。
"""
if not isinstance(evidence, dict):
return []
images = []
raw = evidence.get("images")
if isinstance(raw, list):
for item in raw:
if not isinstance(item, dict):
continue
b64 = item.get("b64")
if not b64:
continue
images.append(
{
"b64": b64,
"mime": item.get("mime") or "image/png",
"label": item.get("label"),
"window": item.get("window"),
}
)
if images:
return images
# 回退:旧的单图字段
b64 = evidence.get("image_b64")
if b64:
return [
{
"b64": b64,
"mime": evidence.get("image_mime") or "image/png",
"label": None,
"window": None,
}
]
return []
def primary_image(evidence):
"""返回首图(label == "overall" 优先),无图返回 None。"""
images = evidence_images(evidence)
if not images:
return None
for img in images:
if img.get("label") == "overall":
return img
return images[0]
def image_labels(evidence):
"""返回图片标签列表(用于日志 / payload 说明)。"""
return [img.get("label") for img in evidence_images(evidence)]