"""contimp-app: FastAPI service exposing the task registry + static frontend.""" from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from app import config, engine from app.tasks import REGISTRY app = FastAPI(title="contimp-app", version=config.APP_VERSION) @app.middleware("http") async def passcode_gate(request: Request, call_next): needs_gate = ( config.APP_PASSCODE and request.url.path.startswith("/api/") and request.url.path != "/api/health" ) if needs_gate and request.headers.get("x-contimp-passcode") != config.APP_PASSCODE: from fastapi.responses import JSONResponse return JSONResponse({"detail": "bad or missing passcode"}, status_code=401) return await call_next(request) def get_task(task_id: str): task = REGISTRY.get(task_id) if task is None: raise HTTPException(404, f"unknown task {task_id!r}") return task @app.get("/api/health") def health() -> dict: return {"ok": True, "version": config.APP_VERSION, "model": config.LLM_MODEL} @app.get("/api/tasks") def list_tasks() -> list[dict]: return [task.meta() for task in REGISTRY.values()] @app.post("/api/tasks/{task_id}/sample") def sample(task_id: str) -> dict: s = get_task(task_id).sample() return {"input_id": s.input_id, "text": s.text} class RunRequest(BaseModel): text: str input_id: str | None = None user: str = "anonymous" session_id: str = "unknown" source: str = "human" @app.post("/api/tasks/{task_id}/run") def run(task_id: str, body: RunRequest) -> dict: task = get_task(task_id) if not body.text.strip(): raise HTTPException(422, "empty input") return engine.run_task( task, text=body.text, input_id=body.input_id, user_id=body.user, session_id=body.session_id, source="synthetic" if body.source == "synthetic" else "human", ) class FeedbackRequest(BaseModel): trace_id: str value: int | None = None # 1 = up, 0 = down; None = note-only comment: str | None = None @app.post("/api/feedback") def feedback(body: FeedbackRequest) -> dict: if body.value is None and not (body.comment or "").strip(): raise HTTPException(422, "feedback needs a value or a comment") value = None if body.value is None else (1 if body.value else 0) engine.record_feedback(body.trace_id, value, body.comment) return {"ok": True} @app.get("/") def index() -> FileResponse: return FileResponse(config.STATIC_DIR / "index.html") app.mount("/static", StaticFiles(directory=config.STATIC_DIR), name="static")