Add per-job run logs and separate vision/text model selection.
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:
+37
-4
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user