Add per-job run logs and separate vision/text model selection.
Docker Release / build-and-push (push) Successful in 1m27s
Docker Release / release (push) Skipped

Capture pipeline stdout into job.log + API/UI so failed runs can be reviewed, and let users pick OpenRouter vision vs text models independently.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-31 14:56:48 -05:00
co-authored by Cursor
parent e30522af9a
commit a6b0c8fdfa
8 changed files with 586 additions and 44 deletions
+37 -4
View File
@@ -14,11 +14,12 @@ import tempfile
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, Response
from fastapi.responses import HTMLResponse, JSONResponse, Response, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from backend import config
from backend.jobs import create_job, get_job
from backend.jobs import create_job, get_job, get_job_log
from backend.models_catalog import list_models
from backend.pipeline.pdf_processor import render_page_jpeg
app = FastAPI(title=config.APP_TITLE, version=config.APP_VERSION)
@@ -29,10 +30,17 @@ _FRONTEND_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "
@app.get("/health")
def health():
return {"status": "ok", "model": config.MODEL,
"text_model": config.TEXT_MODEL,
"key_configured": bool(config.AI_API_KEY),
"email_configured": bool(config.SMTP_HOST and config.SMTP_USER and config.SMTP_PASSWORD)}
@app.get("/models")
def models():
"""Vision vs text OpenRouter model lists for the UI dropdowns."""
return JSONResponse(list_models())
@app.post("/check")
async def check(
file: UploadFile = File(...),
@@ -42,6 +50,8 @@ async def check(
occupancy: Optional[str] = Form(None),
work_type: Optional[str] = Form(None),
text_local: bool = Form(False),
vision_model: Optional[str] = Form(None),
text_model: Optional[str] = Form(None),
):
"""
Accept a PDF, start a background conflict check, and return a job_id
@@ -50,6 +60,9 @@ async def check(
Optional intake fields (project_name/address/occupancy/work_type) feed the
Stage 0 jurisdiction profile; anything left blank is derived from the cover
sheet.
vision_model / text_model override the configured defaults for this run
(vision always OpenRouter; text follows the OpenRouter vs hybrid choice).
"""
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Please upload a PDF.")
@@ -69,8 +82,17 @@ async def check(
}.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)
v_model = (vision_model or "").strip() or None
t_model = (text_model or "").strip() or None
job_id = create_job(
tmp_path,
source_filename=file.filename,
email=email,
project_input=project_input,
text_local=text_local,
vision_model=v_model,
text_model=t_model,
)
return JSONResponse({"job_id": job_id, "status": "queued", "email": email})
@@ -82,6 +104,17 @@ def job_status(job_id: str):
return JSONResponse(job)
@app.get("/jobs/{job_id}/log")
def job_log(job_id: str, plain: bool = False):
"""Full captured run log (also on disk as outputs/<job_id>/job.log)."""
lines = get_job_log(job_id)
if lines is None:
raise HTTPException(status_code=404, detail="Job not found")
if plain:
return PlainTextResponse("\n".join(lines) + ("\n" if lines else ""))
return JSONResponse({"job_id": job_id, "lines": lines, "text": "\n".join(lines)})
@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)."""