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,111 @@
|
||||
"""Per-sheet Drawing Integrity QA agent.
|
||||
|
||||
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/north
|
||||
essentials, and duplicate/inconsistent tags. This is the drawing-focused pass
|
||||
that complements the cross-sheet conflict critic; it never does code/ADA or
|
||||
cross-sheet coordination.
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from backend import config
|
||||
from backend.agents.base import AgentResult, AgentScope, AgentUsage, failure
|
||||
from backend.agents.prompts import (
|
||||
DRAWING_INTEGRITY_SYSTEM_PROMPT,
|
||||
DRAWING_INTEGRITY_USER_PROMPT,
|
||||
)
|
||||
from backend.llm import call_json
|
||||
from backend.pipeline._serialize import dumps
|
||||
from backend.pipeline._stage import collect_list, validate_issue
|
||||
|
||||
|
||||
def _sheet_meta(sheet: Dict) -> Dict:
|
||||
"""Compact title-block-ish descriptor of the sheet (no raw assertions)."""
|
||||
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 build_integrity_scopes(
|
||||
sheets: List[Dict], page_to_b64: Dict, page_to_text: Dict
|
||||
) -> List[AgentScope]:
|
||||
"""One scope per sheet that carries enough objects to judge internal
|
||||
consistency. Sheets below INTEGRITY_MIN_ASSERTIONS are skipped as too
|
||||
sparse for a meaningful single-sheet back-check."""
|
||||
scopes: List[AgentScope] = []
|
||||
for sheet in sheets:
|
||||
assertions = sheet.get("assertions") or []
|
||||
if len(assertions) < config.INTEGRITY_MIN_ASSERTIONS:
|
||||
continue
|
||||
page_number = sheet.get("page_number")
|
||||
scopes.append(AgentScope(
|
||||
scope_id=f"integrity:{page_number}",
|
||||
payload={
|
||||
"sheet": sheet,
|
||||
"page_number": page_number,
|
||||
"image_b64": page_to_b64.get(page_number),
|
||||
"text_layer": page_to_text.get(page_number) or "",
|
||||
},
|
||||
))
|
||||
return scopes
|
||||
|
||||
|
||||
class DrawingIntegrityAgent:
|
||||
name = "drawing_integrity"
|
||||
|
||||
def __init__(self, usage: AgentUsage) -> None:
|
||||
self.usage = usage
|
||||
|
||||
def run(self, scope: AgentScope) -> AgentResult:
|
||||
try:
|
||||
sheet = dict(scope.payload["sheet"])
|
||||
assertions = (
|
||||
sheet.get("assertions") or []
|
||||
)[:config.AGENT_INTEGRITY_MAX_ASSERTIONS]
|
||||
text_layer = (scope.payload.get("text_layer") or "")[
|
||||
:config.TEXT_LAYER_MAX_CHARS
|
||||
]
|
||||
image_b64 = scope.payload.get("image_b64")
|
||||
images = [image_b64] if image_b64 else []
|
||||
images = images[:config.AGENT_INTEGRITY_MAX_IMAGES]
|
||||
|
||||
instruction = DRAWING_INTEGRITY_USER_PROMPT
|
||||
for key, value in {
|
||||
"sheet_meta": dumps(_sheet_meta(sheet)),
|
||||
"assertions": dumps(assertions),
|
||||
"text_layer": text_layer,
|
||||
}.items():
|
||||
instruction = instruction.replace("{" + key + "}", value)
|
||||
|
||||
parsed = call_json(
|
||||
system_prompt=DRAWING_INTEGRITY_SYSTEM_PROMPT,
|
||||
user_text=instruction,
|
||||
images_b64=images,
|
||||
max_tokens=config.INTEGRITY_MAX_TOKENS,
|
||||
model=config.AGENT_INTEGRITY_MODEL,
|
||||
usage_tracker=self.usage,
|
||||
usage_stage="agent.drawing_integrity",
|
||||
reasoning_effort=config.EXTRACT_REASONING_EFFORT or None,
|
||||
reasoning_max_tokens=config.EXTRACT_REASONING_MAX_TOKENS or None,
|
||||
)
|
||||
findings = collect_list(
|
||||
parsed, "issues",
|
||||
lambda item: validate_issue(item, "drawing_integrity"),
|
||||
)
|
||||
sheet_number = sheet.get("sheet_number")
|
||||
for finding in findings:
|
||||
finding.update(agent=self.name, scope_id=scope.scope_id)
|
||||
# Anchor the finding to this sheet if the model left it blank.
|
||||
if not finding.get("sheets") and sheet_number:
|
||||
finding["sheets"] = [sheet_number]
|
||||
return AgentResult(scope_id=scope.scope_id, artifacts=findings)
|
||||
except Exception as exc:
|
||||
return failure(scope, exc)
|
||||
Reference in New Issue
Block a user