"""FastAPI app — JSON API for ProcureMind (Hugging Face Space / local dev).""" from __future__ import annotations import os from contextlib import asynccontextmanager from typing import Any from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from server.agent import coerce_payload, run_agent from server.catalog import connect, get_commodity, summarize_row from server.form_schema import ensure_form_schema_table, get_or_create_schema from server.pr_lines import build_pr_rows @asynccontextmanager async def lifespan(_: FastAPI): from pathlib import Path p = os.environ.get("UNSPSC_DB_PATH", "") if p and not Path(p).exists(): print(f"WARNING: catalogue database path does not exist: {p}") try: conn = connect() ensure_form_schema_table(conn) conn.close() except Exception as ex: print(f"WARNING: could not init auxiliary tables: {ex}") yield app = FastAPI(title="ProcureMind API", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class ChatRequest(BaseModel): message: str = "" selected_commodity_code: int | None = Field( default=None, description="Skip LLM and lock this catalogue commodity (disambiguation choice)", ) class BuildPrRequest(BaseModel): commodity_code: int dynamic_values: dict[str, Any] = Field(default_factory=dict) deliveries: int = 4 interval: str = "Quarterly" other_spec: str = "" year: int = 2026 @app.get("/api/health") def health(): return {"status": "ok"} def _enrich_found(conn, payload: dict) -> dict: if payload.get("status") != "found": return payload code = payload.get("commodity_code") if code is None or payload.get("selected_details"): return payload row = get_commodity(conn, int(code)) if row: payload["selected_details"] = summarize_row(row) return payload @app.get("/api/form-schema/{commodity_code}") def form_schema(commodity_code: int): conn = connect() try: return get_or_create_schema(conn, commodity_code) finally: conn.close() @app.post("/api/build-pr") def build_pr(req: BuildPrRequest): conn = connect() try: schema = get_or_create_schema(conn, req.commodity_code) fields = schema.get("fields") or [] rows = build_pr_rows( mat_grp=req.commodity_code, dynamic_fields_ordered=fields, dynamic_values=req.dynamic_values, deliveries=req.deliveries, interval=req.interval, other_spec=req.other_spec, year=req.year, ) return {"rows": rows} finally: conn.close() @app.post("/api/chat") def chat(req: ChatRequest): msg = (req.message or "").strip() if not msg and req.selected_commodity_code is None: return JSONResponse( {"detail": "Provide message or selected_commodity_code"}, status_code=400, ) conn = connect() try: payload = run_agent( msg, conn=conn, selected_code=req.selected_commodity_code, ) payload = _enrich_found(conn, payload) return coerce_payload(payload) finally: conn.close()