- 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).
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
"""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)
|