File size: 4,792 Bytes
9d9e4e8
1df4bc7
1ade648
 
 
 
 
9d9e4e8
 
 
1df4bc7
 
 
 
 
 
 
 
1ade648
 
 
9d9e4e8
 
 
 
 
 
 
 
1ade648
1df4bc7
 
 
ab97cfb
 
 
 
 
 
 
 
1ade648
ab97cfb
 
 
 
 
 
 
 
 
 
1ade648
ab97cfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1df4bc7
 
 
 
 
 
 
 
 
9d9e4e8
 
 
 
1ade648
 
 
 
 
9d9e4e8
 
 
 
 
 
 
 
 
 
1ade648
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d9e4e8
 
 
 
1ade648
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from fastapi import FastAPI, HTTPException, Header
from contextlib import asynccontextmanager
import pandas as pd
import os
import joblib
import time
import requests
from huggingface_hub import hf_hub_download
from prometheus_client import Counter, Histogram, Gauge, generate_latest

# Global variables
model = None
encoders = {}
scaler = None
feature_columns = []
categorical_columns = []
boolean_columns = []
loaded = False

# Environment variables
API_KEY = os.getenv("API_KEY", "test-key-123")
HF_MODEL_REPO = os.getenv("HF_MODEL_REPO")
PROM_PUSHGATEWAY = os.getenv("PROM_PUSHGATEWAY")

# Prometheus metrics
REQS = Counter("pred_requests_total", "Total prediction requests")
LAT = Histogram("pred_request_latency_seconds", "Request latency")
LATEST = Gauge("latest_prediction", "Last predicted value")


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    global model, encoders, scaler, feature_columns, categorical_columns, boolean_columns, loaded
    
    print("πŸš€ Starting FastAPI application...")
    print(f"API_KEY: {'Set' if API_KEY else 'Not set'}")
    print(f"HF_MODEL_REPO: {HF_MODEL_REPO}")
    
    if not HF_MODEL_REPO:
        print("⚠️  WARNING: HF_MODEL_REPO not set. Using mock mode.")
        loaded = False
    else:
        try:
            print(f" Downloading model from {HF_MODEL_REPO}...")
            
            m = hf_hub_download(repo_id=HF_MODEL_REPO, filename="best_model.joblib")
            e = hf_hub_download(repo_id=HF_MODEL_REPO, filename="models/encoders.joblib")
            s = hf_hub_download(repo_id=HF_MODEL_REPO, filename="models/scaler.joblib")
            f = hf_hub_download(repo_id=HF_MODEL_REPO, filename="models/feature_columns.joblib")
            c = hf_hub_download(repo_id=HF_MODEL_REPO, filename="models/categorical_columns.joblib")
            b = hf_hub_download(repo_id=HF_MODEL_REPO, filename="models/boolean_columns.joblib")

            print(" Loading artifacts...")
            model = joblib.load(m)
            encoders = joblib.load(e)
            scaler = joblib.load(s)
            feature_columns = joblib.load(f)
            categorical_columns = joblib.load(c)
            boolean_columns = joblib.load(b)
            loaded = True
            
            print(" Model loaded successfully!")
            print(f"   Features: {len(feature_columns)}")
        except Exception as ex:
            print(f" Model load error: {ex}")
            loaded = False
    
    print(" FastAPI app initialized and ready!")
    
    yield  # Server runs here
    
    # Shutdown (cleanup if needed)
    print("πŸ›‘ Shutting down...")


app = FastAPI(title="Loan Approval API", version="1.0", lifespan=lifespan)


@app.get("/")
def health():
    return {
        "status": "ok",
        "model_loaded": loaded,
        "features": feature_columns if loaded else []
    }


@app.post("/predict")
def predict(payload: dict, x_api_key: str = Header(None)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API key")

    if not loaded:
        raise HTTPException(status_code=503, detail="Model not loaded")

    try:
        df = pd.DataFrame([payload])
        
        for col in feature_columns:
            if col not in df.columns:
                df[col] = 0
        
        for col in boolean_columns:
            if col in df.columns:
                if df[col].dtype == bool:
                    df[col] = df[col].astype(int)
                elif df[col].dtype == 'object':
                    df[col] = df[col].map({
                        'True': 1, 'true': 1, True: 1, 1: 1,
                        'False': 0, 'false': 0, False: 0, 0: 0
                    }).fillna(0).astype(int)
        
        for col in categorical_columns:
            if col in df.columns and col in encoders:
                try:
                    df[col] = encoders[col].transform(df[col])
                except:
                    df[col] = 0
        
        df = df[feature_columns]
        df_scaled = scaler.transform(df)
        
        start = time.time()
        pred = model.predict(df_scaled)[0]
        latency = time.time() - start
        
        LAT.observe(latency)
        REQS.inc()
        LATEST.set(pred)

        if PROM_PUSHGATEWAY:
            try:
                requests.post(f"{PROM_PUSHGATEWAY}/metrics/job/loan_model", data=generate_latest(), timeout=2)
            except:
                pass

        return {
            "prediction": int(pred),
            "prediction_label": "Approved" if pred == 1 else "Rejected",
            "latency_seconds": round(latency, 4)
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/metrics")
def metrics():
    return generate_latest()