""" drawing_integrity.py - Per-sheet Drawing Integrity QA (LLM, classic pipeline). The drawing-focused pass: reads ONE sheet's own extracted objects + sheet image + deterministic text layer and flags defects internal to that single sheet (dangling detail/callout/keynote references, schedule-vs-plan/legend disagreements on the same sheet, dimension strings that do not sum, missing title-block/scale essentials, duplicate/inconsistent tags). It complements the cross-sheet conflict checker; it never does code/ADA or cross-sheet coordination. Emits the canonical issue schema. Returns [] on failure. Gated by config.ENABLE_DRAWING_INTEGRITY. Runs sheets concurrently, one call per sheet, skipping sheets below INTEGRITY_MIN_ASSERTIONS. """ from concurrent.futures import ThreadPoolExecutor from typing import Dict, List from backend import config from backend.agents.prompts import ( DRAWING_INTEGRITY_SYSTEM_PROMPT, DRAWING_INTEGRITY_USER_PROMPT, ) from backend.pipeline._serialize import dumps from backend.pipeline._stage import call_stage, collect_list, validate_issue def _sheet_meta(sheet: Dict) -> Dict: return { "sheet_number": sheet.get("sheet_number"), "sheet_title": sheet.get("sheet_title"), "discipline": sheet.get("discipline"), "drawing_type": sheet.get("drawing_type"), "level": sheet.get("level"), "scale": sheet.get("scale"), } def _review_sheet(sheet: Dict, page_to_b64: Dict, page_to_text: Dict) -> List[Dict]: page_number = sheet.get("page_number") assertions = (sheet.get("assertions") or [])[ :config.AGENT_INTEGRITY_MAX_ASSERTIONS ] text_layer = (page_to_text.get(page_number) or "")[ :config.TEXT_LAYER_MAX_CHARS ] image = page_to_b64.get(page_number) images = [image][:config.AGENT_INTEGRITY_MAX_IMAGES] if image else [] parsed = call_stage( DRAWING_INTEGRITY_SYSTEM_PROMPT, DRAWING_INTEGRITY_USER_PROMPT, subs={ "sheet_meta": dumps(_sheet_meta(sheet)), "assertions": dumps(assertions), "text_layer": text_layer, }, images_b64=images, max_tokens=config.INTEGRITY_MAX_TOKENS, ) issues = collect_list( parsed, "issues", lambda c: validate_issue(c, "drawing_integrity") ) sheet_number = sheet.get("sheet_number") for issue in issues: if not issue.get("sheets") and sheet_number: issue["sheets"] = [sheet_number] return issues def drawing_integrity_review(sheets: List[Dict], pages: List[Dict]) -> List[Dict]: """One LLM call per non-sparse sheet, run concurrently.""" if not config.ENABLE_DRAWING_INTEGRITY: return [] page_to_b64 = {p["page_number"]: p.get("base64") for p in pages} page_to_text = {p["page_number"]: p.get("text_layer") for p in pages} targets = [ s for s in sheets if len(s.get("assertions") or []) >= config.INTEGRITY_MIN_ASSERTIONS ] issues: List[Dict] = [] if targets: with ThreadPoolExecutor(max_workers=config.AGENT_INTEGRITY_CONCURRENCY) as pool: for res in pool.map( lambda s: _review_sheet(s, page_to_b64, page_to_text), targets ): issues.extend(res) print(f"[DrawingIntegrity] {len(issues)} issue(s) across {len(targets)} sheet(s)") return issues