feat: refocus on drawings — code/ADA gated off, drawing-integrity wave, Brain-directed clarification
Docker Release / build-and-push (push) Successful in 1m8s
Docker Release / release (push) Skipped

- 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:
2026-08-20 15:10:32 -05:00
parent bae608a505
commit d37ac8c1c7
22 changed files with 1917 additions and 63 deletions
+72 -1
View File
@@ -6,7 +6,12 @@ from typing import Dict, List, Tuple
from backend import config
from backend.agents.base import AgentUsage
from backend.agents.prompts import BRAIN_SYSTEM_PROMPT, BRAIN_USER_PROMPT
from backend.agents.prompts import (
BRAIN_CLARIFY_SYSTEM_PROMPT,
BRAIN_CLARIFY_USER_PROMPT,
BRAIN_SYSTEM_PROMPT,
BRAIN_USER_PROMPT,
)
from backend.llm import call_json
from backend.pipeline._stage import collect_list, validate_issue
@@ -127,3 +132,69 @@ class BrainAgent:
issues.sort(key=lambda item: -int(item.get("risk_score") or 0))
decisions = parsed.get("decisions") or []
return issues, [item for item in decisions if isinstance(item, dict)]
def plan_clarifications(self, prioritized: List[Dict]) -> List[Dict]:
"""Wave 6.5 planning call: name kept findings the Brain wants to
double-check before publishing, as typed clarification requests.
Returns a capped list of {issue_id, request_type, reason}. Only
findings that carry an issue_id and do NOT already have a verification
result are offered to the model; anything the model names outside that
set, or with an unknown request_type, is dropped by the caller/router.
Never raises — a failed/empty plan just yields no requests.
"""
max_requests = config.BRAIN_CLARIFY_MAX_REQUESTS
if not prioritized or max_requests <= 0:
return []
candidates = [
{
"issue_id": f.get("issue_id"),
"severity": f.get("severity"),
"confidence": f.get("confidence"),
"source_stage": f.get("source_stage"),
"description": (f.get("description") or "")[:400],
"evidence": f.get("evidence") or [],
"already_verified": bool(f.get("verification")),
}
for f in prioritized
if f.get("issue_id") and not f.get("verification")
]
if not candidates:
return []
instruction = (
BRAIN_CLARIFY_USER_PROMPT
.replace("{max_requests}", str(max_requests))
.replace("{findings}", json.dumps(candidates, ensure_ascii=True))
)
try:
parsed = call_json(
system_prompt=BRAIN_CLARIFY_SYSTEM_PROMPT,
user_text=instruction,
max_tokens=config.BRAIN_CLARIFY_MAX_TOKENS,
model=config.AGENT_BRAIN_MODEL,
usage_tracker=self.usage,
usage_stage="agent.brain_clarify",
)
except Exception:
return []
raw = parsed.get("requests") if isinstance(parsed, dict) else parsed
if not isinstance(raw, list):
return []
valid_ids = {c["issue_id"] for c in candidates}
requests: List[Dict] = []
seen: set = set()
for item in raw:
if not isinstance(item, dict):
continue
issue_id = item.get("issue_id")
if issue_id not in valid_ids or issue_id in seen:
continue
requests.append({
"issue_id": issue_id,
"request_type": (item.get("request_type") or "verify_evidence").strip(),
"reason": (item.get("reason") or "").strip(),
})
seen.add(issue_id)
if len(requests) >= max_requests:
break
return requests
+111
View File
@@ -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)
+53 -1
View File
@@ -12,6 +12,42 @@ Sheet index: {sheet_index}
Aggregate sheet summaries: {sheet_summaries}
Cluster summary: {cluster_summary}"""
DRAWING_INTEGRITY_SYSTEM_PROMPT = """You are a Senior Architect performing a single-sheet QAQC back-check of ONE construction drawing before the set is issued for bid, permit, or construction.
You are given the extracted construction objects for this one sheet, plus the sheet image and its deterministic PDF text layer.
Your job is to find problems INTERNAL TO THIS SHEET - defects a human checker would red-line on this drawing by itself, without needing any other sheet.
You are NOT performing code review. You are NOT checking ADA/accessibility. You are NOT doing cross-sheet coordination (a separate reviewer handles conflicts between sheets). You are NOT estimating cost. You are NOT redesigning anything.
What IS a drawing-integrity issue on this sheet:
- Dangling reference: a detail callout, section marker, elevation marker, keynote, or sheet reference that points to a target that does not exist on this sheet AND is not resolved by an explicit off-sheet reference (e.g. "SIM 5/A501" when this is A501 and it has no detail 5; a keynote number called out in the plan but absent from the keynote legend on the same sheet).
- On-sheet contradiction: the plan disagrees with a schedule or legend printed on the SAME sheet; two notes on the sheet contradict each other; a tag in the plan is not in the sheet's own schedule/legend (or vice versa); the title block discipline/level disagrees with the drawing content.
- Dimension sanity: a dimension string whose segments do not sum to the stated overall; an overall dimension that contradicts a repeated/typical dimension on the same sheet; obviously impossible or missing critical dimensions on a dimensioned plan.
- Missing sheet essentials: no scale, no north arrow on a plan that needs one, missing sheet number/title in the title block, a schedule with header columns but no rows, a legend referenced but not present.
- Label/tag hygiene: duplicate tags that should be unique on this sheet (two different doors both tagged 101A), a room shown with no room number/name where the sheet otherwise numbers rooms, inconsistent tag formatting that breaks a reference.
What is NOT a drawing-integrity issue:
- Anything requiring another sheet to judge (that is cross-sheet coordination, handled elsewhere).
- A code, ADA, or accessibility requirement.
- A design preference or cost concern.
- A value simply not repeated where repetition is optional.
- Anything you cannot support with text or a clear visual from THIS sheet.
Be conservative and evidence-bound:
- Only flag defects you can point to with verbatim source_text from this sheet or a clear description of what the image shows.
- Trust the TEXT LAYER for alphanumeric content (numbers, tags, note text, dimensions); use the image for geometry, symbols, linework, and whether a referenced target actually appears.
- When a value is marked DISPUTED (possible extraction misread), verify against the image before relying on it.
- If the sheet is internally clean, return an empty issues array.
Severity (use exactly one of critical, high, medium, low):
- high = a defect that would cause rework, a wrong build, or a stop at permit/bid if issued as-is (missing critical dimension, dangling reference to a nonexistent detail that drives construction).
- medium = a real drawing defect needing correction before issue.
- low = minor cleanup/clarification.
Use plain ASCII only. Respond only with valid JSON."""
DRAWING_INTEGRITY_USER_PROMPT = """Back-check this single sheet for internal drawing-integrity defects.
Respond ONLY with a valid JSON object - no markdown fences, no explanation:
{"issues":[{"issue_id":"string","source_stage":"drawing_integrity","category":"dangling_reference | on_sheet_contradiction | dimension_error | missing_sheet_essential | tag_or_label_error | other","severity":"critical | high | medium | low","confidence":"high | medium | low","location":"where on the sheet, e.g. 'Room 124 / detail callout 5' or 'door schedule'","disciplines":["string"],"sheets":["this sheet number"],"description":"senior architect explanation of the defect and why it matters","evidence":[{"discipline":"string","sheet":"string","source_text":"verbatim text from this sheet","asserted_value":"string"}],"recommended_resolution":"coordinate drawing | correct dimension | add missing detail | issue RFI | verify with architect | verify with engineer","code_reference":null}]}
If the sheet is internally clean, return {"issues":[]}.
Sheet: {sheet_meta}
Extracted objects on this sheet: {assertions}
TEXT LAYER (deterministic page text - authoritative for alphanumeric content):
{text_layer}"""
BRAIN_SYSTEM_PROMPT = """You are the central decision layer for a construction drawing
review. Merge duplicate specialist findings, reject vague or unsupported findings,
preserve verbatim evidence, and prioritize the kept issues. Do not create new issues.
@@ -19,7 +55,23 @@ Conflicts need drawing evidence; completeness findings may instead cite an expli
missing item from the sheet index. Return only valid JSON."""
BRAIN_USER_PROMPT = """Judge and consolidate these scoped specialist findings.
Return {"issues":[{"issue_id":"string","source_stage":"conflict | qaqc | code | constructability","category":"string","severity":"critical | high | medium | low","confidence":"high | medium | low","location":"string","disciplines":["string"],"sheets":["string"],"description":"string","evidence":[{"discipline":"string","sheet":"string","source_text":"string","asserted_value":"string"}],"recommended_resolution":"string","code_reference":"string or null","risk_score":1,"recommended_priority":"immediate | before_bid | before_permit | before_construction | track_only"}],"decisions":[{"finding_refs":["string"],"action":"kept | merged | dropped","reason":"string","kept_issue_id":"string or null"}]}.
Return {"issues":[{"issue_id":"string","source_stage":"conflict | drawing_integrity | qaqc | code | constructability","category":"string","severity":"critical | high | medium | low","confidence":"high | medium | low","location":"string","disciplines":["string"],"sheets":["string"],"description":"string","evidence":[{"discipline":"string","sheet":"string","source_text":"string","asserted_value":"string"}],"recommended_resolution":"string","code_reference":"string or null","risk_score":1,"recommended_priority":"immediate | before_bid | before_permit | before_construction | track_only"}],"decisions":[{"finding_refs":["string"],"action":"kept | merged | dropped","reason":"string","kept_issue_id":"string or null"}]}.
Sheet index: {sheet_index}
Jurisdiction summary: {jurisdiction}
Specialist findings: {findings}"""
BRAIN_CLARIFY_SYSTEM_PROMPT = """You are the central decision layer for a construction drawing review, deciding which of your kept findings you are NOT yet confident enough to publish.
You have already merged and prioritized the findings. Now, for the borderline ones, you may request ONE targeted clarification each before the report is finalized.
Request a clarification only when a finding's evidence is thin, ambiguous, possibly a misread of the drawing, or internally inconsistent - the kind of finding a senior reviewer would double-check against the sheet before signing off. Do NOT request clarification for findings that are already clearly supported by verbatim evidence, and do NOT re-request a finding that already carries a verification result.
The only request type available right now is:
- verify_evidence: re-check this finding's quoted evidence against the actual sheet images and deterministic text layer (catches wave-1 vision misreads such as "(2)" vs "(5)" and dangling references that do not actually appear on the sheet).
Be selective. Requesting everything wastes the budget and slows the review; request only the findings where a second look would actually change your decision.
Use plain ASCII only. Respond only with valid JSON."""
BRAIN_CLARIFY_USER_PROMPT = """Decide which of these kept findings you want to double-check before publishing.
You may request at most {max_requests} clarifications. Choose the findings where a second look at the sheet would most likely change your keep/drop/severity decision.
Respond ONLY with a valid JSON object - no markdown fences, no explanation:
{"requests":[{"issue_id":"the issue_id of the finding to check","request_type":"verify_evidence","reason":"one sentence: why this finding is uncertain"}]}
If every finding is already well supported, return {"requests":[]}.
Findings (each shows issue_id, severity, confidence, evidence, and whether it already has a verification result):
{findings}"""
+180 -38
View File
@@ -18,6 +18,9 @@ from backend.agents.extractors import (
SheetExtractorAgent,
SheetIndexAgent,
)
from backend.agents.integrity_agent import (
DrawingIntegrityAgent, build_integrity_scopes,
)
from backend.agents.linker import LinkerAgent, build_link_scopes, build_object_graph
from backend.agents.memory import ProjectMemory
from backend.agents.orchestrator import Orchestrator
@@ -174,11 +177,23 @@ def run_agent_pipeline(
memory.extend("findings", conflict_findings)
orchestrator.stage("Agent wave 5: scoped specialists")
code_results = orchestrator.run_scopes(
CodeAgent(usage),
build_code_scopes(sheets, jurisdiction, sheet_index),
config.AGENT_SPECIALIST_CONCURRENCY,
)
if config.ENABLE_CODE_REVIEW:
code_results = orchestrator.run_scopes(
CodeAgent(usage),
build_code_scopes(sheets, jurisdiction, sheet_index),
config.AGENT_SPECIALIST_CONCURRENCY,
)
else:
orchestrator.stage("[wave 5] code/ADA review disabled (ENABLE_CODE_REVIEW=0)")
code_results = []
if config.ENABLE_DRAWING_INTEGRITY:
integrity_results = orchestrator.run_scopes(
DrawingIntegrityAgent(usage),
build_integrity_scopes(sheets, page_to_b64, page_to_text),
config.AGENT_INTEGRITY_CONCURRENCY,
)
else:
integrity_results = []
construct_results = orchestrator.run_scopes(
ConstructabilityAgent(usage),
build_construct_scopes(clusters, conflict_findings),
@@ -197,7 +212,8 @@ def run_agent_pipeline(
)
specialist_findings = [
artifact
for result in code_results + construct_results + completeness_results
for result in (code_results + integrity_results
+ construct_results + completeness_results)
for artifact in result.artifacts
]
@@ -210,38 +226,12 @@ def run_agent_pipeline(
severities=config.AGENT_VERIFY_SEVERITIES,
)
target_indexes = {id(f): i for i, f in enumerate(specialist_findings)}
verify_scopes = []
for finding in verify_targets:
cited_pages = [
sheet_to_page[str(name)]
for name in (finding.get("sheets") or [])
if sheet_to_page.get(str(name)) in page_to_b64
]
images = [
page_to_b64[p]
for p in cited_pages[:config.AGENT_CONFLICT_MAX_IMAGES]
]
if not images:
continue # never judge evidence against images we could not load
# Text oracle: concatenated text layer of the cited sheets, capped.
excerpt = "\n\n".join(
f"--- Page {p} ---\n{page_to_text[p]}"
for p in cited_pages
if page_to_text.get(p)
)[:config.VERIFY_TEXT_MAX_CHARS]
if config.VERIFY_HI_DPI_CROPS:
images = _evidence_crops(finding, cited_pages, sheet_to_page,
page_words, page_to_b64, pdf_path,
fallback=images)
verify_scopes.append(AgentScope(
scope_id=f"verify:{target_indexes[id(finding)]}",
payload={
"finding_index": target_indexes[id(finding)],
"finding": finding,
"images_b64": images,
"text_layer_excerpt": excerpt,
},
))
verify_scopes = _build_verify_scopes(
verify_targets,
index_for=lambda f: target_indexes[id(f)],
sheet_to_page=sheet_to_page, page_to_b64=page_to_b64,
page_to_text=page_to_text, page_words=page_words, pdf_path=pdf_path,
)
verify_results = orchestrator.run_scopes(
EvidenceVerifierAgent(usage), verify_scopes, config.AGENT_VERIFY_CONCURRENCY)
suppressed = apply_verdicts(specialist_findings, verify_results)
@@ -286,6 +276,18 @@ def run_agent_pipeline(
1 for decision in decisions if decision.get("action") == "merged"
)
# Wave 6.5 — Brain-directed clarification (bounded hub-and-spoke). The Brain
# names kept findings it is unsure about; verify_evidence requests route
# back through the wave-5b verifier (fresh images + text-layer oracle).
# Refuted findings are demoted, dropped from `prioritized`, and moved to
# memory["suppressed"]. One planning call, one bounded verify wave, no loop.
if config.ENABLE_BRAIN_CLARIFY and prioritized:
prioritized = _brain_clarification_pass(
orchestrator, usage, memory, prioritized,
sheet_to_page=sheet_to_page, page_to_b64=page_to_b64,
page_to_text=page_to_text, page_words=page_words, pdf_path=pdf_path,
)
if require_review:
orchestrator.stage("Agent review gate: build human-review queue")
memory_snapshot = memory.snapshot()
@@ -331,6 +333,10 @@ def run_agent_pipeline(
1 for item in specialist_findings
if item.get("source_stage") == "code"
),
"drawing_integrity": sum(
1 for item in specialist_findings
if item.get("source_stage") == "drawing_integrity"
),
"constructability": sum(
1 for item in specialist_findings
if item.get("source_stage") == "constructability"
@@ -404,6 +410,10 @@ def run_agent_pipeline(
1 for item in specialist_findings
if item.get("source_stage") == "code"
),
"drawing_integrity": sum(
1 for item in specialist_findings
if item.get("source_stage") == "drawing_integrity"
),
"constructability": sum(
1 for item in specialist_findings
if item.get("source_stage") == "constructability"
@@ -440,6 +450,138 @@ def _dump(out_dir: str, name: str, value) -> None:
json.dump(value, f, indent=2)
def _brain_clarification_pass(
orchestrator,
usage,
memory,
prioritized,
sheet_to_page,
page_to_b64,
page_to_text,
page_words,
pdf_path,
):
"""Wave 6.5: let the Brain request targeted clarifications, execute the
verify_evidence ones through the wave-5b verifier, and prune refuted
findings out of `prioritized` into memory["suppressed"].
Bounded and non-looping: one Brain planning call, at most
BRAIN_CLARIFY_MAX_REQUESTS verifications, a single pass. Returns the
(possibly shortened) prioritized list. Any request type other than
verify_evidence is logged as planned-but-not-executed and left untouched.
"""
requests = BrainAgent(usage).plan_clarifications(prioritized)
if not requests:
return prioritized
by_id = {f.get("issue_id"): f for f in prioritized}
verify_findings = []
unsupported = 0
for req in requests:
if req.get("request_type") != "verify_evidence":
unsupported += 1
continue
finding = by_id.get(req.get("issue_id"))
if finding is not None and finding not in verify_findings:
verify_findings.append(finding)
orchestrator.stage(
f"Agent wave 6.5: Brain-directed clarification "
f"({len(verify_findings)} verify, {unsupported} other)"
)
if unsupported:
for req in requests:
if req.get("request_type") != "verify_evidence":
orchestrator.stats.failed_scopes.append(
f"brain_clarify:{req.get('issue_id')}: "
f"request_type '{req.get('request_type')}' planned, "
f"not executed (v1 supports verify_evidence only)"
)
if not verify_findings:
return prioritized
index_of = {id(f): i for i, f in enumerate(prioritized)}
verify_scopes = _build_verify_scopes(
verify_findings,
index_for=lambda f: index_of[id(f)],
sheet_to_page=sheet_to_page, page_to_b64=page_to_b64,
page_to_text=page_to_text, page_words=page_words, pdf_path=pdf_path,
)
if not verify_scopes:
return prioritized
verify_results = orchestrator.run_scopes(
EvidenceVerifierAgent(usage), verify_scopes,
config.AGENT_VERIFY_CONCURRENCY,
)
suppressed = apply_verdicts(prioritized, verify_results)
if suppressed:
suppressed_ids = {id(f) for f in suppressed}
prioritized = [f for f in prioritized if id(f) not in suppressed_ids]
existing = memory.snapshot().get("suppressed") or []
memory.replace("suppressed", existing + suppressed)
memory.extend("decisions", [
{
"finding_refs": [f.get("issue_id")],
"action": "dropped",
"reason": "Brain-directed clarification: evidence refuted on re-check",
"kept_issue_id": None,
}
for f in suppressed
])
return prioritized
def _build_verify_scopes(
targets,
index_for,
sheet_to_page,
page_to_b64,
page_to_text,
page_words,
pdf_path,
):
"""Build EvidenceVerifierAgent scopes for a set of findings.
Shared by wave 5b (severity-gated) and wave 6.5 (Brain-directed): each
finding's cited sheets are mapped to page images (hi-DPI evidence crops
when enabled, else full pages) plus a capped text-layer oracle. Findings
whose sheets resolve to NO loadable image are skipped (I2 guard) — never
judge evidence against images we could not load. index_for(finding) yields
the finding_index the verifier echoes back for apply_verdicts alignment.
"""
scopes = []
for finding in targets:
cited_pages = [
sheet_to_page[str(name)]
for name in (finding.get("sheets") or [])
if sheet_to_page.get(str(name)) in page_to_b64
]
images = [
page_to_b64[p]
for p in cited_pages[:config.AGENT_CONFLICT_MAX_IMAGES]
]
if not images:
continue # never judge evidence against images we could not load
# Text oracle: concatenated text layer of the cited sheets, capped.
excerpt = "\n\n".join(
f"--- Page {p} ---\n{page_to_text[p]}"
for p in cited_pages
if page_to_text.get(p)
)[:config.VERIFY_TEXT_MAX_CHARS]
if config.VERIFY_HI_DPI_CROPS:
images = _evidence_crops(finding, cited_pages, sheet_to_page,
page_words, page_to_b64, pdf_path,
fallback=images)
finding_index = index_for(finding)
scopes.append(AgentScope(
scope_id=f"verify:{finding_index}",
payload={
"finding_index": finding_index,
"finding": finding,
"images_b64": images,
"text_layer_excerpt": excerpt,
},
))
return scopes
def _evidence_crops(
finding: Dict,
cited_pages: list,
+9
View File
@@ -36,6 +36,15 @@ def _valid_verdict(item):
def _status(verdicts):
"""Roll per-evidence verdicts up to a finding-level status.
NOTE on "corrected": it is deliberately NON-confirming. The canonical case
(job 959e16407573) is evidence quoting "(2) 2x6 STUD PACK" against a sheet
that reads "(5)" — the text exists but the VALUE the finding rests on was a
wave-1 misread, so the finding's basis is gone. Hence refuted = zero
CONFIRMED verdicts, not zero not_found ones. Do not "fix" this to treat
corrected as supporting; see tests/agents/test_verifier.py.
"""
if not verdicts:
return "unverified"
confirmed = sum(1 for v in verdicts if v["verdict"] == "confirmed")