Files
Conflict_Checker/backend/main.py
T
woogiandClaude Opus 4.8 1d248a8808 Initial commit: Conflict Checker
Cross-discipline design-contradiction checker for construction drawing
sets. Standalone tool broken out from Iron_Bid; a pipeline stage may
later fold back into Iron_Bid.

Pipeline: PDF->images -> per-sheet assertion extraction -> deterministic
clustering by location -> per-cluster reasoning -> report.
Includes CLI (cli/run_check.py) and web UI (backend/main.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:22:02 +00:00

113 lines
4.0 KiB
Python

"""
main.py - Thin FastAPI app: upload a PDF, run the pipeline, return the report.
Run from the project root:
uvicorn backend.main:app --reload
The pipeline is synchronous and can take minutes on a large set; for a single
architect checking one set at a time that's fine. Move to a job queue if this
ever needs concurrency.
"""
import os
import tempfile
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from backend import config
from backend.jobs import create_job, get_job
from backend.pipeline.pdf_processor import render_page_jpeg
app = FastAPI(title=config.APP_TITLE, version=config.APP_VERSION)
_FRONTEND_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "frontend")
@app.get("/health")
def health():
return {"status": "ok", "model": config.MODEL,
"key_configured": bool(config.AI_API_KEY),
"email_configured": bool(config.SMTP_HOST and config.SMTP_USER and config.SMTP_PASSWORD)}
@app.post("/check")
async def check(
file: UploadFile = File(...),
notification_email: Optional[str] = Form(None),
project_name: Optional[str] = Form(None),
address: Optional[str] = Form(None),
occupancy: Optional[str] = Form(None),
work_type: Optional[str] = Form(None),
text_local: bool = Form(False),
):
"""
Accept a PDF, start a background conflict check, and return a job_id
immediately. The client can poll GET /jobs/{id} or just wait for the email.
Optional intake fields (project_name/address/occupancy/work_type) feed the
Stage 0 jurisdiction profile; anything left blank is derived from the cover
sheet.
"""
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Please upload a PDF.")
os.makedirs(config.UPLOAD_DIR, exist_ok=True)
suffix = "_" + os.path.basename(file.filename)
fd, tmp_path = tempfile.mkstemp(suffix=suffix, dir=config.UPLOAD_DIR)
with os.fdopen(fd, "wb") as f:
f.write(await file.read())
email = (notification_email or "").strip() or None
project_input = {
k: v.strip()
for k, v in {
"project_name": project_name, "address": address,
"occupancy": occupancy, "work_type": work_type,
}.items()
if v and v.strip()
}
job_id = create_job(tmp_path, source_filename=file.filename, email=email,
project_input=project_input, text_local=text_local)
return JSONResponse({"job_id": job_id, "status": "queued", "email": email})
@app.get("/jobs/{job_id}")
def job_status(job_id: str):
job = get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
return JSONResponse(job)
@app.get("/jobs/{job_id}/sheet-image/{page}")
def sheet_image(job_id: str, page: int):
"""Render one page of a completed job's source PDF as JPEG (sheet viewer)."""
pdf = os.path.join(config.OUTPUT_DIR, job_id, "source.pdf")
if not os.path.isfile(pdf):
raise HTTPException(status_code=404, detail="Source PDF not found for this job")
try:
data = render_page_jpeg(pdf, page)
except IndexError:
raise HTTPException(status_code=404, detail=f"Page {page} out of range")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Could not render page: {e}")
return Response(content=data, media_type="image/jpeg",
headers={"Cache-Control": "max-age=3600"})
@app.get("/", response_class=HTMLResponse)
def index():
index_path = os.path.join(_FRONTEND_DIR, "index.html")
if os.path.isfile(index_path):
with open(index_path) as f:
return f.read()
return "<h1>Conflict Checker</h1><p>frontend/index.html not found.</p>"
# Serve any other static assets (none required for the single-file UI).
if os.path.isdir(_FRONTEND_DIR):
app.mount("/static", StaticFiles(directory=_FRONTEND_DIR), name="static")