Keep NOTES.md current for new sessions after the job-log and dual-model UI work. Co-authored-by: Cursor <cursoragent@cursor.com>
8.4 KiB
Session Notes — Conflict Checker
Orientation for a new coding session. Setup and Docker details live in README.md. This file tracks what the code actually does and what tends to waste time.
Last updated: 2026-07-31 · tip a6b0c8f on Gitea main
What this is
Cross-discipline design contradiction / senior-architect QAQC for construction drawing PDFs (Arch, Struct, Mech, Elec, Plumb, FP, etc.). Flags disagreements between disciplines before a set goes to bid/permit.
Not IronBid’s scope-ownership conflict checker.
Source of truth: Scout IT Gitea — gitea.scoutitsystems.com/woogi/Conflict_Checker.
Stack
| Layer | Detail |
|---|---|
| API | Python 3.12, FastAPI, Uvicorn (backend/main.py) |
| UI | Single static file frontend/index.html, served by FastAPI |
| Pipeline | Shared by web + CLI: backend/pipeline/runner.py |
| LLM | OpenRouter via openai SDK; default google/gemini-2.5-pro. Vision always OpenRouter; text stages can use local vLLM |
pdf2image + system poppler-utils → JPEG page images |
|
| Jobs | In-memory threads (backend/jobs.py) — no Redis/DB |
| Deploy | Docker Compose; app on port 8099 |
Live pipeline (authoritative)
README still describes an older 5-stage extract-then-compare loop. Trust runner.py. Actual flow:
PDF → images → extract → sheet index → jurisdiction
→ normalize → project intelligence (GOIDs)
→ cluster → conflict reason
→ QAQC / code / constructability
→ dedup-validate → risk → RFIs → report
| Runner stage | Module | Notes |
|---|---|---|
| PDF → images | pdf_processor |
Rasterize |
| Extract assertions | extractor |
Vision, per sheet |
| Classify sheet index | sheet_index |
LLM |
| Jurisdiction profile | jurisdiction |
After cover meta |
| Normalize | normalizer |
LLM batches |
| Project intelligence | normalizer.build_project_intelligence |
GOIDs + relationships |
| Cluster | llm_clusterer or clusterer |
Default CLUSTERER=llm |
| Conflicts | conflict_checker |
Per-cluster vision reason |
| QAQC / code / constructability | qaqc_review, code_review, constructability |
Full-set / batched |
| Validate & dedup | validator |
Merges conflict + QAQC + code + construct issues |
| Risk / RFIs | risk, rfi |
Text-only |
| Report | report |
conflicts.json + report.md (+ stage JSON dumps when out_dir set) |
Design notes for Stage 2/3 engines also live under Changes/*.docx.
HTTP API (current)
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Liveness + default model / text_model + key/email flags |
| GET | /models |
OpenRouter catalog split into vision[] / text[] + defaults (cached ~1h) |
| POST | /check |
Upload PDF; returns {job_id} immediately |
| GET | /jobs/{id} |
Status poll. Running: stage + log_tail. Done/error: report and/or error + full log |
| GET | /jobs/{id}/log |
Full run log JSON (lines, text); ?plain=1 for text/plain |
| GET | /jobs/{id}/sheet-image/{page} |
JPEG of source PDF page for the sheet viewer |
| GET | / |
Serves frontend/index.html |
/check form fields
- Required:
file(PDF) - Optional:
notification_email,project_name,address,occupancy,work_type - Compute:
text_local(true= hybrid local text) - Models:
vision_model,text_model(OpenRouter ids; blank = config defaults)
Job logs
Pipeline print() is teed for the job thread (backend/job_log.py):
- Live:
GET /jobs/{id}→log_tail(last 80 lines) - Done/error: same payload includes full
log - Disk:
backend/outputs/<job_id>/job.log(survives restart; status registry does not) - API:
GET /jobs/{id}/logor?plain=1 - UI: “Run log” panel updates while running; stays visible after finish/fail
Vision vs text models
Two models, not one:
| Role | Config | Stages | Backend |
|---|---|---|---|
| Vision | MODEL |
Extract, conflict reason (images) | Always OpenRouter |
| Text | TEXT_MODEL (falls back to MODEL) |
Sheet index, jurisdiction, normalize, cluster(LLM), QAQC, code, construct, validate, risk, RFI | OpenRouter, or local when hybrid |
UI: two dropdowns filled from GET /models (backend/models_catalog.py). Per-run picks go through set_model_overrides() in backend/llm.py; runner clears them in finally. Hybrid: text dropdown also names the local model override and OpenRouter fallback.
Where to change what
| Concern | File |
|---|---|
| Prompts, vocab, conflict taxonomy | backend/prompts.py |
| Env knobs | backend/config.py, backend/.env.example |
| HTTP API | backend/main.py |
| UI (upload, models, live log, results) | frontend/index.html |
| CLI tuning loop | cli/run_check.py |
| LLM client, cache, cost, model overrides | backend/llm.py |
| OpenRouter vision/text model lists | backend/models_catalog.py |
| Job registry + stdout tee log | backend/jobs.py, backend/job_log.py |
| Stage helpers (prompt render, issue validate) | backend/pipeline/_stage.py |
| Code text corpus (Stage 7) | backend/code_corpus/ |
| Hybrid local LLM helper | scripts/setup_vllm.sh |
Older prompt snapshot: backend/prompts.py.v1.
Gotchas
- Prompt placeholders — Use
str.replacevia_stage.render, neverstr.format. Prompts contain literal{JSON braces. - Clustering — Default is LLM (
CLUSTERER=llm); empty LLM result falls back to deterministic. Deterministic clusters need ≥2 disciplines (or schedule-vs-plan); single-discipline “missing” gaps are a known limit. - Jobs are in-memory — Process restart clears job status;
outputs/<job_id>/(report +job.log+source.pdf) still reload via disk fallback. - Dependency pin —
httpx==0.27.2withopenai==1.51.0. httpx ≥0.28 breaks openai’sproxies=kwarg. - Code corpus licensing — Only
ada_2010.txtis shipped. Do not paste IBC/IFC/IECC without a license (seebackend/code_corpus/README.md). - Dual assertion schema — Newer
{sheet, objects[]}is mapped to legacy{assertions[]}withattribute/valuefor older stages. - Grounding guard — Extractor drops objects whose numeric claims are not in
source_text(graphical-only objects allowed). - Module globals — LLM cost counters, stdout tee, and model overrides are process-global; overlapping jobs can interleave (single-user tool assumption).
- Samples —
samples/*.pdfare gitignored; drop PDFs locally for CLI runs. - README drift — Treat README for setup/CI; treat this file +
runner.pyfor pipeline truth.prompts.pyheader may still say some prompts are unwired — they are wired through the runner. - Git identity — This box has no
user.name/user.email; commits needGIT_AUTHOR_*/GIT_COMMITTER_*env vars (do notgit config). Remote push to Gitea works. - No local Python deps on host — App is meant to run in Docker; bare
python3imports may missdotenv/httpx. Preferdocker compose.
Quick start pointers
- Full setup: README.md (
docker compose up -d --build→ http://localhost:8099). - Local CLI:
python cli/run_check.py samples/your_set.pdf --out out/your_set. - Prompt iteration: set
LLM_CACHE=trueinbackend/.envso unchanged stages replay for free; clear withrm -rf backend/.llm_cache. - Artifacts per job:
assertions.json,clusters.json, per-stage JSON,conflicts.json,report.md,job.log,source.pdfunderbackend/outputs/<job_id>/. - No automated test suite; validate via CLI dumps and golden-set diffs (described in README).
Recent work (2026-07-31)
Shipped on main as a6b0c8f:
- Per-job run log (tee + disk + API + UI)
- Separate vision/text model dropdowns backed by OpenRouter
/models - Session notes file (this doc)
Conflict categories (taxonomy)
Defined in backend/prompts.py: dimensional_disagreement, elevation_disagreement, location_mismatch, missing_element, schedule_vs_plan_mismatch, detail_vs_plan_mismatch, tag_or_reference_inconsistency, spatial_clash, note_or_spec_contradiction.