# 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 ``` 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). ## 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.