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:
+180
-38
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user