"""MuleGuard scoring API. Run: .venv/bin/uvicorn src.api.main:app --port 8000 """ from __future__ import annotations from datetime import datetime, timezone from typing import Any import pandas as pd from fastapi import FastAPI from pydantic import BaseModel, Field from src import config from src.models.scoring import explain_row, load_artifacts, narrative, score_frame app = FastAPI(title="MuleGuard — Mule Account Risk Scoring", version="1.0") class ScoreRequest(BaseModel): account_id: str = Field(..., description="Account identifier") features: dict[str, Any] = Field(..., description="Raw feature map (F1..F3923); missing allowed") class ScoreResponse(BaseModel): account_id: str risk_score: float risk_tier: str decision: str probability: float reason_codes: list[dict] narrative: str timestamp: str @app.on_event("startup") def _warmup() -> None: load_artifacts() # load model + explainers once @app.get("/health") def health() -> dict: art = load_artifacts() return {"status": "ok", "n_features": len(art.features), "threshold": art.threshold} @app.get("/model-info") def model_info() -> dict: art = load_artifacts() m = art.metadata return { "model": m["model"], "n_features": m["n_features"], "leakage_excluded": m["leakage_excluded"], "cv_metrics": m["cv_metrics"], "test_metrics": m["test_metrics"], "threshold": art.threshold, } @app.post("/score", response_model=ScoreResponse) def score(req: ScoreRequest) -> ScoreResponse: row = pd.DataFrame([req.features]) scored = score_frame(row).iloc[0] reasons = explain_row(row) return ScoreResponse( account_id=req.account_id, risk_score=float(scored["risk_score"]), risk_tier=str(scored["risk_tier"]), decision=str(scored["decision"]), probability=float(scored["probability"]), reason_codes=reasons, narrative=narrative(float(scored["risk_score"]), reasons), timestamp=datetime.now(timezone.utc).isoformat(), )