# 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 ``` 1. **Stage 0** `pdf_processor` — PDF pages to base64 JPEGs (100 DPI, 2400px cap). 2. **Stage 1** `extractor` — one vision call per sheet extracts discrete, **grounded** assertions (each carries verbatim `source_text` and a `location_key`). A grounding guard drops any assertion whose number isn't in its own source text. 3. **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. 4. **Stage 3** `conflict_checker` — one reasoning call per cluster (with the relevant sheet images) decides whether the disciplines genuinely contradict, and classifies it. 5. **Stage 4** `report` — `conflicts.json` + a readable `report.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) ```bash 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): ```bash 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 ```bash 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: ```bash 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 1. **Enable Actions** on the repo: Settings → Actions → Enable Repository Actions. 2. **Runner** — an `act_runner` registered against `gitea.scoutitsystems.com` with the `self-hosted` label and access to a Docker daemon (typically `/var/run/docker.sock` mounted into the runner). This instance uses `sits-docker-runner` (`self-hosted`, `linux`, `docker`). 3. **Registry secret** — create a Personal Access Token on Gitea with at least `write:package` (and `write:release` if you use version tags). Add it as a repository secret named **`REGISTRY_TOKEN`** (Settings → Actions → Secrets). ### What runs automatically | Trigger | Result | |---------|--------| | Push to `main` | Image pushed as `:latest` and `:sha-` | | Push tag `v*` (e.g. `v0.1.0`) | Image tagged with the release + Gitea Release created | Image location: ```text gitea.scoutitsystems.com/woogi/conflict-checker: ``` Workflow file: `.gitea/workflows/docker-release.yml` ### Local dev → deploy loop ```bash # 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): ```bash 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: ```bash 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 `/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=) -> 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//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`): ```bash 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): ```bash 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//assertions.json` to see what Stage 1 extracted, and `out//clusters.json` to 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.json` for 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.