Files
cad-agent/dwg_review_agent.py
T

310 lines
11 KiB
Python
Raw Normal View History

"""
dwg_review_agent.py
====================
逐图框图纸审查 Agent(联调脚本)
流程:
1. detect_drawing_frames 识别图纸中的所有图框
2. 对每个图框:
a. zoom_to_window 缩放到该图框范围
b. query_entities 取图框内图元摘要(按 window 过滤)
c. get_text_content 取图框内文字
d. get_viewport_screenshot 截取当前视口
e. 交给 LLM 审查,产出该图框的审查意见
3. 汇总所有图框意见,生成总报告
依赖:
pip install openai
环境变量:
DEEPSEEK_API_KEY (不设置时回退到脚本内默认值)
DEEPSEEK_MODEL (默认 deepseek-chat)
使用前提:
CAD 已打开并加载了 cad_mcp_frame.arx,8080 端口在监听。
"""
import os
import sys
import json
import time
import socket
from openai import OpenAI
HOST = "127.0.0.1"
PORT = 8080
# 与仓库既有脚本保持一致;生产环境请改为从环境变量注入
API_KEY = os.environ.get("DEEPSEEK_API_KEY", "sk-420190f448fe41158c4e2ccff90e35ce")
BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
MODEL_NAME = os.environ.get("DEEPSEEK_MODEL", "deepseek-flash") # 该模型已确认支持视觉
# 每个图框审查时,取数据的条数上限
FRAME_ENTITY_LIMIT = int(os.environ.get("FRAME_ENTITY_LIMIT", "200"))
# 最多审查多少个图框(0 = 全部)
MAX_FRAMES = int(os.environ.get("MAX_FRAMES", "0"))
# 审查清单文件路径
CHECKLIST_PATH = os.environ.get("REVIEW_CHECKLIST", "review_checklist.json")
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
# ---------------------------------------------------------------------------
# MCP / TCP 通信
# ---------------------------------------------------------------------------
def call_tool(tool_name, arguments, recv_timeout=30.0):
"""调用 CAD 基座的 tools/call,返回解析后的 JSON-RPC 响应(失败返回 None)"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(recv_timeout)
sock.connect((HOST, PORT))
req = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": tool_name, "arguments": arguments},
}
sock.sendall(json.dumps(req).encode("utf-8"))
data = b""
while True:
chunk = sock.recv(65536)
if not chunk:
break
data += chunk
# 基座是单次 send 返回整包;收到后尝试解析即可结束
try:
json.loads(data.decode("utf-8"))
break
except json.JSONDecodeError:
continue
sock.close()
return json.loads(data.decode("utf-8"))
except Exception as e:
print(f"[Error] 调用工具 {tool_name} 失败: {e}")
return None
def mcp_text(resp):
"""从 MCP 响应中取出文本内容;若文本是 JSON 则解析为对象返回"""
if not resp or "result" not in resp:
return None
for item in resp["result"].get("content", []):
if item.get("type") == "text":
text = item.get("text", "")
try:
return json.loads(text)
except (json.JSONDecodeError, TypeError):
return text
return None
def mcp_image(resp):
"""从 MCP 响应中取出图片,返回 (base64, mimeType)"""
if not resp or "result" not in resp:
return None, None
for item in resp["result"].get("content", []):
if item.get("type") == "image":
return item.get("data"), item.get("mimeType", "image/png")
return None, None
def mcp_error(resp):
"""若响应是错误,返回错误信息字符串,否则返回 None"""
if resp and "error" in resp:
err = resp["error"]
return f"code={err.get('code')} message={err.get('message')}"
return None
def bbox_to_window(bbox):
"""[minx,miny,maxx,maxy] -> window 对象"""
return {"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3]}
# ---------------------------------------------------------------------------
# LLM 审查(审查清单可配置)
# ---------------------------------------------------------------------------
def load_checklist(path):
"""加载审查清单 JSON;失败时回退到内置通用清单"""
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 e:
print(f"[Warn] 无法加载审查清单 {path}: {e},使用内置默认清单。")
return {
"name": "内置通用清单",
"categories": [
{"title": "图层规范性", "severity": "medium",
"checks": ["图元是否在正确图层", "是否存在空图层/命名异常图层"]},
{"title": "文字", "severity": "medium",
"checks": ["空文字", "错别字", "字高异常", "文字重叠"]},
{"title": "标注", "severity": "high",
"checks": ["关键尺寸缺失", "标注值异常"]},
{"title": "几何", "severity": "medium",
"checks": ["零长度/退化实体", "重复重叠实体", "未闭合轮廓"]},
{"title": "图面整体", "severity": "low",
"checks": ["图面完整性", "内容是否超出图框"]},
],
}
def build_system_prompt(checklist):
"""根据审查清单动态生成 LLM 系统提示词"""
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("")
lines += [
"要求:",
"- 只报告你**有依据**的问题,不要臆测;数据与截图冲突时以数据为准。",
"- 按清单条目组织结论,未命中的条目可省略。",
"- 若该图框无任何明显问题,明确说“未发现明显问题”。",
"- 不要复述输入数据。",
]
return "\n".join(lines)
def review_frame(idx, frame, entities, texts, img_b64, mime, system_prompt):
"""对单个图框调用 LLM 审查,返回审查意见文本"""
sheet = frame.get("sheet_size") or "未知图幅"
kind = frame.get("kind")
layer = frame.get("layer")
summary = {
"图框序号": idx,
"图幅": sheet,
"形态": kind,
"图层": layer,
"包围盒": frame.get("bbox"),
"图元总数": (entities or {}).get("total") if isinstance(entities, dict) else None,
"图元(截断)": (entities or {}).get("items") if isinstance(entities, dict) else None,
"文字": (texts or {}).get("items") if isinstance(texts, dict) else None,
}
user_content = [
{"type": "text", "text": "以下是该图框的审查输入数据(JSON):\n" +
json.dumps(summary, ensure_ascii=False, indent=2)}
]
if img_b64:
user_content.append({
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{img_b64}"},
})
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
]
try:
resp = client.chat.completions.create(model=MODEL_NAME, messages=messages)
return resp.choices[0].message.content
except Exception as e:
return f"[LLM 审查失败] {e}"
# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
def main():
print("=== 🏗️ 逐图框图纸审查 Agent 启动 ===")
# 0. 载入可配置审查清单
checklist = load_checklist(CHECKLIST_PATH)
system_prompt = build_system_prompt(checklist)
print(f"[Init] 审查清单:{checklist.get('name')}"
f"({len(checklist.get('categories', []))} 类),模型:{MODEL_NAME}\n")
# 1. 识别图框
resp = call_tool("detect_drawing_frames", {"min_confidence": 0.3})
err = mcp_error(resp)
if err:
print(f"[Error] 识别图框失败: {err}")
return
frames_data = mcp_text(resp)
items = frames_data.get("items", []) if isinstance(frames_data, dict) else []
if not items:
print("未识别到任何图框,请确认图纸中存在图框,或调整 detect_drawing_frames 参数。")
return
total = len(items)
if MAX_FRAMES > 0:
items = items[:MAX_FRAMES]
print(f"识别到 {total} 个图框,本次审查 {len(items)} 个。\n")
findings = []
# 2. 逐图框审查
for idx, frame in enumerate(items, 1):
bbox = frame.get("bbox")
if not bbox or len(bbox) != 4:
print(f"[跳过] 图框 {idx} 缺少有效包围盒")
continue
print(f"--- 图框 {idx}/{len(items)} kind={frame.get('kind')} "
f"sheet={frame.get('sheet_size')} bbox={bbox}")
win = bbox_to_window(bbox)
# a. 缩放到图框
call_tool("zoom_to_window", dict(win, margin=0.02))
time.sleep(0.6) # 给 CAD 渲染留出时间
# b/c. 取图框内数据
entities = mcp_text(call_tool("query_entities", {
"window": win, "limit": FRAME_ENTITY_LIMIT
}))
texts = mcp_text(call_tool("get_text_content", {
"window": win, "limit": FRAME_ENTITY_LIMIT
}))
# d. 截图
shot = call_tool("get_viewport_screenshot", {})
img_b64, mime = mcp_image(shot)
print(f" 图元 {(entities or {}).get('total')} 个,"
f"文字 {(texts or {}).get('total')} 条,"
f"截图 {'已获取' if img_b64 else '无'}")
# e. LLM 审查
finding = review_frame(idx, frame, entities, texts, img_b64, mime, system_prompt)
findings.append({"frame": idx, "sheet": frame.get("sheet_size"), "finding": finding})
print(f" 审查结论:{finding}\n")
# 3. 汇总总报告
print("=== 📋 汇总总报告 ===")
report_input = "\n\n".join(
f"【图框 {f['frame']}(图幅 {f['sheet']})】\n{f['finding']}" for f in findings
)
try:
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content":
"你是图纸审查负责人。请把下面各图框的审查意见汇总为一份"
"结构化的审查报告:先给整体结论,再按图框列出问题,最后给出整改建议。"
"语言简洁、条目化。"},
{"role": "user", "content": report_input},
],
)
print(resp.choices[0].message.content)
except Exception as e:
print(f"[汇总失败] {e}")
print(report_input)
if __name__ == "__main__":
main()