"""SHL Assessment Recommender — public API. Implements the two endpoints required by Appendix 2 of the spec: GET /health → {"status": "healthy"} POST /recommend → {"recommended_assessments": [ {url, name, ...}, ...]} Method 3 of the system (hybrid retrieval + LLM rerank over the concept-enriched index) is what's served here — our best-performing configuration on the train set. Run locally: uvicorn api:app --host 0.0.0.0 --port 8000 Test: curl http://localhost:8000/health curl -X POST http://localhost:8000/recommend \\ -H "Content-Type: application/json" \\ -d '{"query":"hire java developers under 40 minutes"}' """ from __future__ import annotations import os from typing import Literal, Optional from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field try: from dotenv import load_dotenv load_dotenv() except Exception: pass # Import the retrieval entrypoint we'll serve. from recsys.hybrid import search_with_rag app = FastAPI( title="SHL Assessment Recommender", description=( "Recommends SHL assessments for a hiring query, JD text, or JD URL. " "Backed by hybrid retrieval (BGE-large + BM25 + RRF) over a " "concept-enriched index, with an LLM reranker for final ordering." ), version="1.0", ) # CORS so the browser-based frontend can hit this API from a different origin. app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"], ) # -------- request / response schemas -------------------------------------------- class RecommendRequest(BaseModel): query: str = Field( ..., min_length=1, description="Natural-language hiring query, JD text, or JD URL.", ) # Optional per-request LLM overrides. If omitted, the server falls back # to the LLM_PROVIDER / LLM_MODEL env vars set in the deployment. provider: Optional[Literal["openai", "gemini"]] = Field( default=None, description="Override the LLM provider used for the rerank step.", ) model: Optional[str] = Field( default=None, description="Override the LLM model name within the chosen provider.", ) class Assessment(BaseModel): url: str name: str | None = None adaptive_support: str description: str duration: int remote_support: str test_type: list[str] class RecommendResponse(BaseModel): recommended_assessments: list[Assessment] # -------- helpers --------------------------------------------------------------- def _shape_hit(h: dict) -> dict: """Convert a pipeline hit into the Appendix 2 response shape.""" # test_type is stored as a comma-joined string in Chroma metadata. raw_tt = h.get("test_type") or "" if isinstance(raw_tt, str): tt_list = [t.strip() for t in raw_tt.split(",") if t.strip()] else: tt_list = list(raw_tt) # Duration is -1 (or null) for "untimed/variable" products. The spec # types this as Integer — emit 0 in that case. dur = h.get("duration") dur_int = int(dur) if isinstance(dur, int) and dur > 0 else 0 return { "url": h.get("url", ""), "name": h.get("name"), "adaptive_support": h.get("adaptive_support", "No"), "description": h.get("description") or "", "duration": dur_int, "remote_support": h.get("remote_support", "No"), "test_type": tt_list, } # -------- endpoints ------------------------------------------------------------- @app.get("/health") def health() -> dict[str, str]: """Liveness probe. Returns {"status": "healthy"} per Appendix 2.""" return {"status": "healthy"} @app.post("/recommend", response_model=RecommendResponse) def recommend(req: RecommendRequest) -> RecommendResponse: """Return up to 10 most relevant SHL assessments for the given query. Body: { "query": "", "provider": "openai" | "gemini" | null (optional; defaults to env) "model": "" (optional; defaults to env) } """ try: hits = search_with_rag( req.query, k=10, fanout=50, provider=req.provider, model=req.model, ) except Exception as e: raise HTTPException(status_code=500, detail=f"retrieval failed: {e}") from e items = [_shape_hit(h) for h in hits] return RecommendResponse(recommended_assessments=items)