coderound / backend /main.py
ketannnn's picture
feat: slider max dynamically set from CSV row count (header excluded)
7770c5f
import os
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PayloadSchemaType
from src.config import get_settings
from src.models import JobDescription, Candidate, MatchResult, Session
from src.routers import jds, candidates, matching, sessions, admin
# Configure root logger so ALL module loggers (stage2, reranker, etc.)
# emit to stdout — critical for error visibility on Hugging Face
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler()],
)
logger = logging.getLogger(__name__)
settings = get_settings()
_qdrant_client: QdrantClient | None = None
_qdrant_ready: bool = False
def get_qdrant() -> QdrantClient:
return _qdrant_client
@asynccontextmanager
async def lifespan(app: FastAPI):
global _qdrant_client, _qdrant_ready
_qdrant_client = QdrantClient(url=settings.qdrant_url, api_key=settings.qdrant_api_key)
try:
existing = [c.name for c in _qdrant_client.get_collections().collections]
if settings.collection_name not in existing:
_qdrant_client.create_collection(
collection_name=settings.collection_name,
vectors_config=VectorParams(size=settings.vector_size, distance=Distance.COSINE),
)
# Create indexing for the session_id to allow fast filtering
_qdrant_client.create_payload_index(
collection_name=settings.collection_name,
field_name="session_id",
field_schema=PayloadSchemaType.UUID,
)
# Create indexing for years_of_experience for range filtering
_qdrant_client.create_payload_index(
collection_name=settings.collection_name,
field_name="years_of_experience",
field_schema=PayloadSchemaType.FLOAT,
)
_qdrant_ready = True
logger.info("Qdrant connected — collection '%s' ready", settings.collection_name)
except Exception as exc:
_qdrant_ready = False
logger.warning(
"Qdrant unavailable at startup (%s). "
"The API will start but vector search will fail until Qdrant is reachable.",
exc,
)
app.state.qdrant = _qdrant_client
app.state.qdrant_ready = _qdrant_ready
# Pre-load the lightweight CrossEncoder (~80MB) eagerly at startup so the
# first matching request doesn't pay the cold-start download cost.
try:
import asyncio
from src.ml.reranker import _get_reranker
logger.info("Warming up Neural CrossEncoder reranker...")
await asyncio.to_thread(_get_reranker)
logger.info("Neural CrossEncoder loaded and ready!")
except Exception as warm_exc:
# Log but don't crash — matching will attempt lazy-load on first request
logger.warning(f"Reranker warm-up failed (will retry on first request): {warm_exc}")
yield
_qdrant_client.close()
app = FastAPI(
title="TalentPulse — AI Candidate Matching",
description="Two-stage retrieval + reranking pipeline for matching JDs against candidate sessions",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(sessions.router, prefix="/api/sessions", tags=["Sessions"])
app.include_router(jds.router, prefix="/api/jds", tags=["Job Descriptions"])
app.include_router(candidates.router, prefix="/api/candidates", tags=["Candidates"])
app.include_router(matching.router, prefix="/api/match", tags=["Matching"])
app.include_router(admin.router, prefix="/api/admin", tags=["Admin"])
@app.get("/health")
async def health(request: "Request"):
qdrant_ok = getattr(request.app.state, "qdrant_ready", False)
return {
"status": "ok",
"version": "1.0.0",
"qdrant": "connected" if qdrant_ok else "unavailable",
}
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.isdir(static_dir):
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")