feat: refocus on drawings — code/ADA gated off, drawing-integrity wave, Brain-directed clarification
- ENABLE_CODE_REVIEW flag (default off): skips code/ADA/jurisdiction review path in both pipelines; nothing deleted, one env flag to restore. - Per-sheet Drawing Integrity QA wave (agent + classic, default on): dangling refs, on-sheet contradictions, dimension sanity, missing sheet essentials, tag hygiene. New DrawingIntegrityAgent + classic stage. - Broadened conflict critic: intra-sheet + same-discipline contradictions, not just cross-discipline. - Wave 6.5 Brain-directed clarification (bounded hub-and-spoke): Brain names uncertain findings, verify_evidence requests route through the wave-5b verifier; refuted findings suppressed. One planning call + capped verifies, single iteration. Shared _build_verify_scopes across 5b and 6.5. - Config knobs, .env.example, frontend copy, tests (182 passing).
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
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
|
||||
@@ -39,6 +39,7 @@ from backend.agents.disputes import annotate_clusters
|
||||
from backend.pipeline.conflict_checker import check_conflicts
|
||||
from backend.pipeline.qaqc_review import senior_review
|
||||
from backend.pipeline.code_review import code_review
|
||||
from backend.pipeline.drawing_integrity import drawing_integrity_review
|
||||
from backend.pipeline.constructability import constructability_review
|
||||
from backend.pipeline.validator import dedup_validate
|
||||
from backend.pipeline.risk import score_and_prioritize
|
||||
@@ -158,15 +159,23 @@ def _run_stages(
|
||||
stage("Full-set QAQC review")
|
||||
qaqc_issues = senior_review(sheets, clusters, conflicts, sheet_index)
|
||||
|
||||
stage("Code / ADA review")
|
||||
code_issues = code_review(jurisdiction, sheets, sheet_index)
|
||||
if config.ENABLE_CODE_REVIEW:
|
||||
stage("Code / ADA review")
|
||||
code_issues = code_review(jurisdiction, sheets, sheet_index)
|
||||
else:
|
||||
print("[Code] code/ADA review disabled (ENABLE_CODE_REVIEW=0)")
|
||||
code_issues = []
|
||||
|
||||
stage("Drawing integrity (per-sheet QA)")
|
||||
integrity_issues = drawing_integrity_review(sheets, pages)
|
||||
|
||||
stage("Constructability review")
|
||||
construct_issues = constructability_review(sheets, clusters, conflicts)
|
||||
|
||||
stage("Validate & deduplicate")
|
||||
conflict_issues = [v for v in (validate_issue(c, "conflict") for c in conflicts) if v]
|
||||
all_issues = conflict_issues + qaqc_issues + code_issues + construct_issues
|
||||
all_issues = (conflict_issues + integrity_issues + qaqc_issues
|
||||
+ code_issues + construct_issues)
|
||||
validated = dedup_validate(all_issues)
|
||||
|
||||
stage("Risk scoring & prioritization")
|
||||
@@ -189,6 +198,7 @@ def _run_stages(
|
||||
"conflicts": len(conflicts),
|
||||
"qaqc": len(qaqc_issues),
|
||||
"code": len(code_issues),
|
||||
"drawing_integrity": len(integrity_issues),
|
||||
"constructability": len(construct_issues),
|
||||
"validated": len(validated),
|
||||
"rfis": len(rfis),
|
||||
@@ -213,6 +223,7 @@ def _run_stages(
|
||||
_dump(out_dir, "project_intelligence.json", project_intel)
|
||||
_dump(out_dir, "qaqc_issues.json", qaqc_issues)
|
||||
_dump(out_dir, "code_issues.json", code_issues)
|
||||
_dump(out_dir, "drawing_integrity.json", integrity_issues)
|
||||
_dump(out_dir, "constructability.json", construct_issues)
|
||||
_dump(out_dir, "validated_issues.json", prioritized)
|
||||
_dump(out_dir, "rfis.json", rfis)
|
||||
|
||||
Reference in New Issue
Block a user