pankaj
Add per-request LLM provider toggle (OpenAI / Gemini) + React frontend
fe74a7d
"""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": "<JD text, NL query, or URL>",
"provider": "openai" | "gemini" | null (optional; defaults to env)
"model": "<model name>" (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)