Containerize the app for local and production deploys, and publish images to the Gitea container registry on main pushes and version tags. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
6.6 KiB
Markdown
179 lines
6.6 KiB
Markdown
# 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-<commit>` |
|
|
| 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:<tag>
|
|
```
|
|
|
|
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/<set>/assertions.json` to see what Stage 1 extracted, and
|
|
`out/<set>/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.
|