- SheetExtractorAgent accepts top-level array responses as the objects array instead of discarding them (recovered the failure mode behind 16/38 failed sheets on job 475a6f184dd1) - Second-chance compact retry per page before declaring extraction failed - call_json: reasoning_effort param (cloud-only extra_body), finish_reason capture + explicit max_tokens log line, finish_reason in raw dumps, cache key covers reasoning_effort - EXTRACT_MAX_TOKENS default 16384 -> 32768 (Gemini thinking tokens count against the cap); new EXTRACT_REASONING_EFFORT=low default for extract - tests: 5 new fallback-ladder tests
Conflict Checker
Cross-discipline design contradiction detector for construction drawing sets. Built for architects to back-check a set (Arch / Struct / Mech / Elec / Plumb / FP / …) before it goes out to a GC for bid. It flags information that disagrees between disciplines — e.g. the RCP ceiling height vs the mechanical duct elevation, a door schedule count vs the floor plan, a column that lands in a corridor.
This is not the IronBid scope-ownership conflict checker (who-owns-what for bidding). That stays in IronBid; this tool is a separate, design-QA pipeline.
How it works (hybrid extract-then-compare)
PDF -> page images -> per-sheet grounded facts -> cluster by location -> reason -> report
Stage 0 Stage 1 (vision) Stage 2 (no LLM) Stage 3 Stage 4
- Stage 0
pdf_processor— PDF pages to base64 JPEGs (100 DPI, 2400px cap). - Stage 1
extractor— one vision call per sheet extracts discrete, grounded assertions (each carries verbatimsource_textand alocation_key). A grounding guard drops any assertion whose number isn't in its own source text. - Stage 2
clusterer— deterministic: groups assertions that refer to the same grid / room / tag across ≥2 disciplines (or a schedule-vs-plan within one). This keeps Stage 3 calls small and few. - Stage 3
conflict_checker— one reasoning call per cluster (with the relevant sheet images) decides whether the disciplines genuinely contradict, and classifies it. - Stage 4
report—conflicts.json+ a readablereport.md.
The model is google/gemini-2.5-pro via OpenRouter (configurable). All prompt
tuning lives in backend/prompts.py — the system prompts, the attribute vocabulary,
and the conflict taxonomy.
Setup
Docker (recommended)
cp backend/.env.example backend/.env # set AI_API_KEY (and SMTP if you want email)
docker compose up -d --build
# -> http://localhost:8099
Check health: curl http://localhost:8099/health
Logs: docker compose logs -f app
CLI inside the container (mount your PDF read-only):
docker compose run --rm \
-v "$(pwd)/samples/your_set.pdf:/data/set.pdf:ro" \
app python cli/run_check.py /data/set.pdf --out /app/backend/outputs/cli-run
Persistent data lives in Docker volumes (uploads, outputs, llm_cache). To bind
mount host directories instead, replace the named volumes in docker-compose.yml.
For hybrid mode (local vLLM on the host), set LOCAL_BASE_URL=http://host.docker.internal:8000/v1
in backend/.env. Compose already maps host.docker.internal to the host gateway on Linux.
Local Python
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt # needs system poppler-utils for pdf2image
cp backend/.env.example backend/.env # then set AI_API_KEY to your OpenRouter key
Repository (Gitea)
Source of truth is Scout IT's Gitea instance:
| Remote | https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git |
| Web UI | https://gitea.scoutitsystems.com/woogi/Conflict_Checker |
Clone:
git clone https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git
cd Conflict_Checker
This repo's git config uses a dedicated credential store at
~/.config/git/gitea-credentials (not your global GitHub credentials). If push/pull
prompts for auth, log in via the Gitea web UI and create a personal access token, then
store it there or run one authenticated push so the helper saves it.
CI/CD (Gitea Actions → Container Registry)
Develop locally, push to Gitea, and let a runner build/publish the Docker image.
One-time setup
- Enable Actions on the repo: Settings → Actions → Enable Repository Actions.
- Runner — an
act_runnerregistered againstgitea.scoutitsystems.comwith theself-hostedlabel and access to a Docker daemon (typically/var/run/docker.sockmounted into the runner). This instance usessits-docker-runner(self-hosted,linux,docker). - Registry secret — create a Personal Access Token on Gitea with at least
write:package(andwrite:releaseif you use version tags). Add it as a repository secret namedREGISTRY_TOKEN(Settings → Actions → Secrets).
What runs automatically
| Trigger | Result |
|---|---|
Push to main |
Image pushed as :latest and :sha-<commit> |
Push tag v* (e.g. v0.1.0) |
Image tagged with the release + Gitea Release created |
Image location:
gitea.scoutitsystems.com/woogi/conflict-checker:<tag>
Workflow file: .gitea/workflows/docker-release.yml
Local dev → deploy loop
# 1. Develop and test locally (venv or docker compose build)
git add -A && git commit -m "your change"
git push origin main # CI builds :latest
# 2. Cut a release when ready
git tag v0.1.0 && git push origin v0.1.0
# 3. On the deploy host — pull and run the published image
docker login gitea.scoutitsystems.com
IMAGE_TAG=v0.1.0 docker compose -f docker-compose.prod.yml up -d
# or: IMAGE_TAG=latest for the newest main-branch build
Copy backend/.env to the deploy host separately (never commit it). Use
docker-compose.prod.yml for production; keep docker-compose.yml for local
builds from source.
Run
CLI (the fast tuning loop — also dumps assertions.json / clusters.json for inspection):
python cli/run_check.py samples/your_set.pdf --out out/your_set
# -> out/your_set/report.md + conflicts.json
The Classic pipeline remains the recommended default. The experimental Agent fork runs in the same image and can be selected in the web UI or from the CLI:
python cli/run_check.py samples/your_set.pdf --mode agent --out out/agent-run
Agent mode uses OpenRouter for every model call and runs bounded specialist
waves: one-sheet extraction, sheet-index/jurisdiction orientation, semantic
linkers partitioned by level and object family, per-cluster conflict critics,
batched code review, cluster-scoped constructability, summary-only completeness,
central Brain consolidation, and one-finding RFI writers. It returns the same
conflicts, validated_issues, rfis, and summary fields as Classic.
Agent artifacts are also written under <output>/agent/, including wave
snapshots and the final Project Memory. summary.agent_stats,
summary.cost_by_stage, and summary.models_used are job-local, so concurrent
Agent jobs do not share accounting.
Optional AGENT_*_MODEL variables select an OpenRouter model per specialist.
The AGENT_*_CONCURRENCY and scope-cap variables in backend/.env.example
bound fan-out and prompt size. Agent mode intentionally ignores the hybrid/local
text option in v1.
Agent mode: required human review
By default (AGENT_REQUIRE_REVIEW=true) an Agent run stops after the Brain
consolidation wave and waits for a human before anything ships:
Brain merge -> needs_review -> review UI (/?job=<id>) -> finalize -> final report
The job lifecycle adds review states: needs_review (queue built, waiting),
reviewing (decisions submitted), finalizing (targeted reruns + RFI writers
running), then done — or finalization_error if finalization fails. Open the
job in the web UI to work the queue: blocking items (high/critical severity,
low confidence, sensitive categories) must be decided; clean-cluster items are
non-blocking spot-checks.
Email is two-phase: a "review required" notice goes out when the job enters
needs_review (with a link to the review UI); the final conflict report email
is only sent after finalization completes. The unreviewed report never leaves
the server.
Privacy boundary: all review artifacts (queue, decisions, final report) are
job-local under outputs/<job_id>/review/. Cross-job review-feedback
aggregation, when built, excludes verbatim source_text, images, and comments
unless REVIEW_AGGREGATE_INCLUDE_TEXT=true.
Config knobs (see backend/.env.example):
| Key | Default | Effect |
|---|---|---|
AGENT_REQUIRE_REVIEW |
true |
false = Agent jobs skip the gate entirely (old behavior: RFIs, final report, one email) |
AGENT_REVIEW_AUDIT_SAMPLE |
5 |
Max clean clusters added to the queue as spot-checks |
REVIEW_AGGREGATE_INCLUDE_TEXT |
false |
Allow future aggregate feedback to include source text/images/comments |
From the CLI, --no-review bypasses the gate for that run (it overrides
AGENT_REQUIRE_REVIEW=true):
python cli/run_check.py samples/your_set.pdf --mode agent --no-review --out out/agent-run
Deployment note: the review endpoints (/jobs/{id}/review-decisions,
/jobs/{id}/finalize-review) are state-changing and sensitive — they accept
human decisions that alter the final report. Do not expose the UI/API
publicly without reverse-proxy auth or a shared access token in front of it.
Web UI (upload + view):
uvicorn backend.main:app --reload --port 8099 # open http://127.0.0.1:8099
Or use Docker: docker compose up -d (see Setup above).
The standard Docker image contains both pipelines; no additional queue,
database, or model service is required. Set AI_API_KEY in backend/.env and
choose Agent mode per request. Treat Agent output as experimental and compare it
against a reviewed golden set before using it for issuance decisions.
Conflict categories
dimensional_disagreement, elevation_disagreement, location_mismatch,
missing_element, schedule_vs_plan_mismatch, tag_or_reference_inconsistency,
spatial_clash, note_or_spec_contradiction.
Tuning
- Edit prompts/vocab/taxonomy in
backend/prompts.py. - Inspect
out/<set>/assertions.jsonto see what Stage 1 extracted, andout/<set>/clusters.jsonto see what got compared. Most false negatives are a clustering/extraction miss; most false positives are a Stage-3 prompt issue. - Build a golden set: hand-verify
conflicts.jsonfor a known set and diff future runs.
Known limitations (v1)
- A location appearing in only one discipline never forms a cluster, so pure "missing element" gaps aren't caught deterministically (Stage 3 still catches missing counterparts when the location is co-located).
- Stage 3 currently sends full pages, not crops. If accuracy/cost needs it, add crop-by-grid later.