Spaces:
Sleeping
Sleeping
| """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 ------------------------------------------------------------- | |
| def health() -> dict[str, str]: | |
| """Liveness probe. Returns {"status": "healthy"} per Appendix 2.""" | |
| return {"status": "healthy"} | |
| 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) | |