Files
Conflict_Checker/backend/agents/integrity_agent.py
T
woogi d37ac8c1c7
Docker Release / build-and-push (push) Successful in 1m8s
Docker Release / release (push) Skipped
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).
2026-08-20 15:10:32 -05:00

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)