loanguard / src /api /main.py
adwitiyashukla's picture
update repo
44cd54e
Raw
History Blame Contribute Delete
4.8 kB
"""FastAPI application - real-time + batch scoring with Prometheus metrics."""
from __future__ import annotations
import time
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import (
Counter,
Gauge,
Histogram,
generate_latest,
CONTENT_TYPE_LATEST,
)
from ..utils.logging import get_logger, setup_logging
from .schemas import (
BatchScoreRequest,
BatchScoreResponse,
HealthResponse,
LoanApplication,
ScoreResponse,
)
from .service import ScoringService
# ---------------------------------------------------------------------- #
# Metrics
# ---------------------------------------------------------------------- #
PREDICT_REQUESTS = Counter(
"loanguard_predict_requests_total",
"Number of scoring requests",
["endpoint", "decision"],
)
PREDICT_LATENCY = Histogram(
"loanguard_predict_latency_seconds",
"Latency of scoring requests",
["endpoint"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
)
SCORE_HIST = Histogram(
"loanguard_fraud_score",
"Distribution of fraud scores",
buckets=tuple(i / 20 for i in range(21)),
)
FEATURE_PSI = Gauge(
"loanguard_feature_psi",
"Population stability index for a feature vs. training distribution",
["feature"],
)
# ---------------------------------------------------------------------- #
# Lifespan
# ---------------------------------------------------------------------- #
service = ScoringService()
boot_time = time.time()
log = get_logger("api")
@asynccontextmanager
async def lifespan(app: FastAPI): # noqa: D401
setup_logging()
log.info("LoanGuard API booting...")
service.load()
yield
log.info("LoanGuard API shutting down")
# ---------------------------------------------------------------------- #
# App
# ---------------------------------------------------------------------- #
app = FastAPI(
title="LoanGuard Fraud Scoring API",
description="Real-time fraud risk scoring for loan applications.",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------------------------------------------------- #
# Routes
# ---------------------------------------------------------------------- #
@app.get("/", tags=["meta"])
def root():
return {
"service": "loanguard",
"version": app.version,
"endpoints": ["/health", "/score", "/score/batch", "/metrics", "/docs"],
}
@app.get("/health", response_model=HealthResponse, tags=["meta"])
def health():
status = "ok" if service.is_ready else "degraded"
return HealthResponse(
status=status, # type: ignore[arg-type]
model_loaded=service.is_ready,
model_version=service.model_version if service.is_ready else None,
uptime_seconds=time.time() - boot_time,
)
@app.post("/score", response_model=ScoreResponse, tags=["scoring"])
def score(app_in: LoanApplication):
if not service.is_ready:
raise HTTPException(status_code=503, detail="Model not loaded yet.")
with PREDICT_LATENCY.labels(endpoint="score").time():
try:
result = service.score_one(app_in)
except Exception as exc:
log.exception("Scoring failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc
PREDICT_REQUESTS.labels(endpoint="score", decision=result.decision).inc()
SCORE_HIST.observe(result.fraud_score)
return result
@app.post("/score/batch", response_model=BatchScoreResponse, tags=["scoring"])
def score_batch(req: BatchScoreRequest):
if not service.is_ready:
raise HTTPException(status_code=503, detail="Model not loaded yet.")
if len(req.applications) > 5000:
raise HTTPException(status_code=413, detail="Batch too large (max 5000).")
with PREDICT_LATENCY.labels(endpoint="score_batch").time():
results = service.score_many(req.applications)
for r in results:
PREDICT_REQUESTS.labels(endpoint="score_batch", decision=r.decision).inc()
SCORE_HIST.observe(r.fraud_score)
return BatchScoreResponse(
scored_at=datetime.now(timezone.utc),
model_version=service.model_version,
results=results,
)
@app.post("/reload", tags=["meta"])
def reload_artifacts():
"""Reload model artifacts (e.g. after a new training run)."""
service.load()
return {"reloaded": True, "ready": service.is_ready}
@app.get("/metrics", tags=["meta"])
def metrics():
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)