File size: 4,410 Bytes
c1dbdc6 12fa3c2 c1dbdc6 12fa3c2 c1dbdc6 12fa3c2 96fe8d8 c1dbdc6 12fa3c2 022fb5a c1dbdc6 7770c5f 12fa3c2 c1dbdc6 7770c5f 96fe8d8 c1dbdc6 96fe8d8 c835700 c1dbdc6 96fe8d8 c1dbdc6 12fa3c2 96fe8d8 12fa3c2 96fe8d8 12fa3c2 96fe8d8 12fa3c2 96fe8d8 12fa3c2 c1dbdc6 96fe8d8 7770c5f 20ae104 c1dbdc6 96fe8d8 c1dbdc6 12fa3c2 c1dbdc6 12fa3c2 c1dbdc6 022fb5a c1dbdc6 12fa3c2 96fe8d8 12fa3c2 96fe8d8 12fa3c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 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")
|