BhavyaSoni21 commited on
Commit
f1f4ccb
Β·
verified Β·
1 Parent(s): 4af361a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -38
app.py CHANGED
@@ -1,13 +1,31 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel
 
 
 
 
 
 
3
  import joblib
4
  import numpy as np
 
5
  from fastapi.middleware.cors import CORSMiddleware
6
- import pandas as pd # You might need this if you decide to preprocess raw input in the API
7
 
8
- app = FastAPI(title="Multi-Class Web Data Classifier API")
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- # Allow browser requests
11
  app.add_middleware(
12
  CORSMiddleware,
13
  allow_origins=["*"],
@@ -16,52 +34,237 @@ app.add_middleware(
16
  allow_headers=["*"],
17
  )
18
 
19
- # The number of features MUST match the X_preprocessed.shape[1] from training
20
- # Our X_preprocessed had 5 columns: 'feature1', 'id', 'type_defacement', 'type_malware', 'type_phishing'
 
 
 
21
  EXPECTED_FEATURES = 5
22
 
23
- # Request schema for the preprocessed features
24
- # IMPORTANT: The client calling this API must preprocess their data
25
- # (impute, scale numerical, one-hot encode categorical) to match the format
26
- # of X_preprocessed used during model training.
 
 
 
 
 
 
 
 
27
  class InputData(BaseModel):
28
- features: list[float] # Expecting 5 preprocessed float values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- # Load model and label encoder
31
- try:
32
- model = joblib.load("trained_lightgbm_model.pkl")
33
- label_encoder = joblib.load("label_encoder.pkl")
34
- print("Model and Label Encoder loaded successfully.")
35
- except Exception as e:
36
- raise RuntimeError(f"Error loading model or label encoder: {e}")
37
 
38
  @app.get("/")
39
- def home():
40
- return {"message": "Multi-Class Web Data Classifier API running"}
 
 
 
 
 
 
 
41
 
42
  @app.get("/health")
43
- def health():
44
- return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- @app.post("/predict")
47
- def predict(data: InputData):
48
- if len(data.features) != EXPECTED_FEATURES:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  raise HTTPException(
50
  status_code=400,
51
- detail=f"Expected {EXPECTED_FEATURES} features, but received {len(data.features)}"
 
 
 
 
 
 
 
 
52
  )
53
 
54
- # Convert input list to numpy array and reshape for prediction
55
- # This assumes input features are ALREADY preprocessed as per training
56
- input_array = np.array(data.features).reshape(1, -1)
57
 
58
- # Make prediction
59
- prediction_encoded = model.predict(input_array)[0]
 
60
 
61
- # Inverse transform to get the original label string
62
- prediction_label = label_encoder.inverse_transform([prediction_encoded])[0]
63
 
64
- return {
65
- "predicted_label": prediction_label,
66
- "raw_prediction_encoded": int(prediction_encoded)
67
- }
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
  import joblib
10
  import numpy as np
11
+ from fastapi import FastAPI, HTTPException
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from pydantic import BaseModel, Field
14
 
15
+ # Configure logging
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='[%(asctime)s] %(name)s β€” %(levelname)s: %(message)s'
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ app = FastAPI(
23
+ title="CyHub Model 4 β€” Domain Classification",
24
+ description="Multi-class web data classifier (Normal/Adult/Betting/Malware)",
25
+ version="1.0.0"
26
+ )
27
 
28
+ # CORS configuration
29
  app.add_middleware(
30
  CORSMiddleware,
31
  allow_origins=["*"],
 
34
  allow_headers=["*"],
35
  )
36
 
37
+ # ─────────────────────────────────────────────────────────────────
38
+ # Configuration
39
+ # ─────────────────────────────────────────────────────────────────
40
+
41
+ # Expected feature count (must match training data)
42
  EXPECTED_FEATURES = 5
43
 
44
+ # Model file paths (support both relative and absolute)
45
+ MODEL_DIR = Path(os.getenv("MODEL_DIR", "."))
46
+ MODEL_PATH = MODEL_DIR / "trained_lightgbm_model.pkl"
47
+ ENCODER_PATH = MODEL_DIR / "label_encoder.pkl"
48
+
49
+ logger.info(f"Looking for model at: {MODEL_PATH.absolute()}")
50
+ logger.info(f"Looking for encoder at: {ENCODER_PATH.absolute()}")
51
+
52
+ # ─────────────────────────────────────────────────────────────────
53
+ # Request/Response Models
54
+ # ─────────────────────────────────────────────────────────────────
55
+
56
  class InputData(BaseModel):
57
+ """Input data for model prediction.
58
+
59
+ Must be preprocessed (scaled, imputed, encoded) to match training data.
60
+ """
61
+ features: List[float] = Field(
62
+ ...,
63
+ min_items=EXPECTED_FEATURES,
64
+ max_items=EXPECTED_FEATURES,
65
+ description=f"Exactly {EXPECTED_FEATURES} preprocessed float values"
66
+ )
67
+
68
+
69
+ class PredictionResponse(BaseModel):
70
+ """Model prediction response."""
71
+ predicted_label: str
72
+ raw_prediction_encoded: int
73
+
74
+
75
+ # ─────────────────────────────────────────────────────────────────
76
+ # Model Loading
77
+ # ─────────────────────────────────────────────────────────────────
78
+
79
+ model = None
80
+ label_encoder = None
81
+ model_loaded = False
82
+
83
+ def load_models():
84
+ """Load LightGBM model and label encoder."""
85
+ global model, label_encoder, model_loaded
86
+
87
+ try:
88
+ # Check if files exist
89
+ if not MODEL_PATH.exists():
90
+ raise FileNotFoundError(f"Model file not found: {MODEL_PATH.absolute()}")
91
+ if not ENCODER_PATH.exists():
92
+ raise FileNotFoundError(f"Encoder file not found: {ENCODER_PATH.absolute()}")
93
+
94
+ # Load model
95
+ logger.info(f"Loading model from {MODEL_PATH.absolute()}...")
96
+ model = joblib.load(str(MODEL_PATH))
97
+ logger.info("βœ“ Model loaded successfully")
98
+
99
+ # Load label encoder
100
+ logger.info(f"Loading label encoder from {ENCODER_PATH.absolute()}...")
101
+ label_encoder = joblib.load(str(ENCODER_PATH))
102
+ logger.info("βœ“ Label encoder loaded successfully")
103
+
104
+ # Verify encoder has classes
105
+ if not hasattr(label_encoder, 'classes_'):
106
+ raise ValueError("Label encoder missing 'classes_' attribute")
107
+
108
+ logger.info(f"Label classes: {label_encoder.classes_.tolist()}")
109
+ model_loaded = True
110
+
111
+ return True
112
+
113
+ except FileNotFoundError as e:
114
+ logger.error(f"File error: {e}")
115
+ logger.warning(f"Make sure model files are in: {MODEL_DIR.absolute()}")
116
+ return False
117
+ except Exception as e:
118
+ logger.error(f"Error loading models: {e}")
119
+ import traceback
120
+ traceback.print_exc()
121
+ return False
122
+
123
+
124
+ # ─────────────────────────────────────────────────────────────────
125
+ # Startup/Shutdown Events
126
+ # ─────────────────────────────────────────────────────────────────
127
+
128
+ @app.on_event("startup")
129
+ async def startup_event():
130
+ """Initialize models on startup."""
131
+ global model_loaded
132
+ logger.info("Starting up CyHub Model 4 API...")
133
+
134
+ if not load_models():
135
+ logger.error("Failed to load models. API will return 503 until models are available.")
136
+ model_loaded = False
137
+ else:
138
+ logger.info("Model 4 API ready!")
139
+
140
+
141
+ @app.on_event("shutdown")
142
+ async def shutdown_event():
143
+ """Cleanup on shutdown."""
144
+ logger.info("Shutting down CyHub Model 4 API...")
145
 
146
+
147
+ # ─────────────────────────────────────────────────────────────────
148
+ # API Endpoints
149
+ # ─────────────────────────────────────────────────────────────────
 
 
 
150
 
151
  @app.get("/")
152
+ async def root():
153
+ """Root endpoint β€” health check."""
154
+ return {
155
+ "service": "CyHub Model 4 β€” Domain Classification",
156
+ "status": "healthy",
157
+ "model_loaded": model_loaded,
158
+ "version": "1.0.0"
159
+ }
160
+
161
 
162
  @app.get("/health")
163
+ async def health_check():
164
+ """Health check endpoint."""
165
+ if not model_loaded:
166
+ raise HTTPException(
167
+ status_code=503,
168
+ detail="Model not loaded. Check server logs."
169
+ )
170
+
171
+ return {
172
+ "status": "healthy",
173
+ "model_status": "ready",
174
+ "expected_features": EXPECTED_FEATURES
175
+ }
176
+
177
+
178
+ @app.post("/predict", response_model=PredictionResponse)
179
+ async def predict(data: InputData) -> PredictionResponse:
180
+ """Predict domain classification from preprocessed features.
181
+
182
+ Args:
183
+ data: InputData with exactly 5 preprocessed features
184
 
185
+ Returns:
186
+ PredictionResponse with predicted_label and raw_prediction_encoded
187
+
188
+ Raises:
189
+ HTTPException: If model not loaded or feature count incorrect
190
+ """
191
+ # Check if model is loaded
192
+ if not model_loaded or model is None or label_encoder is None:
193
+ logger.error("Model not loaded, cannot make prediction")
194
+ raise HTTPException(
195
+ status_code=503,
196
+ detail="Model not loaded. Check server logs."
197
+ )
198
+
199
+ try:
200
+ # Validate feature count
201
+ if len(data.features) != EXPECTED_FEATURES:
202
+ raise ValueError(
203
+ f"Expected {EXPECTED_FEATURES} features, got {len(data.features)}"
204
+ )
205
+
206
+ # Validate all features are float/convertible
207
+ features_array = np.array(data.features, dtype=float)
208
+
209
+ # Check for NaN or Inf
210
+ if not np.all(np.isfinite(features_array)):
211
+ raise ValueError("Features contain NaN or Inf values")
212
+
213
+ # Reshape for prediction (1 sample, 5 features)
214
+ input_array = features_array.reshape(1, -1)
215
+
216
+ logger.debug(f"Input features: {data.features}")
217
+
218
+ # Make prediction
219
+ prediction_encoded = model.predict(input_array)[0]
220
+ logger.debug(f"Raw prediction: {prediction_encoded}")
221
+
222
+ # Ensure prediction is valid
223
+ if prediction_encoded not in label_encoder.classes_:
224
+ logger.warning(f"Unexpected prediction value: {prediction_encoded}")
225
+
226
+ # Decode prediction to label
227
+ try:
228
+ prediction_label = label_encoder.inverse_transform([prediction_encoded])[0]
229
+ except Exception as e:
230
+ logger.error(f"Error decoding prediction: {e}")
231
+ raise ValueError(f"Could not decode prediction {prediction_encoded}")
232
+
233
+ logger.info(f"Prediction: {prediction_label}")
234
+
235
+ return PredictionResponse(
236
+ predicted_label=prediction_label,
237
+ raw_prediction_encoded=int(prediction_encoded)
238
+ )
239
+
240
+ except ValueError as e:
241
+ logger.error(f"Validation error: {e}")
242
  raise HTTPException(
243
  status_code=400,
244
+ detail=str(e)
245
+ )
246
+ except Exception as e:
247
+ logger.error(f"Prediction error: {e}")
248
+ import traceback
249
+ traceback.print_exc()
250
+ raise HTTPException(
251
+ status_code=500,
252
+ detail=f"Prediction failed: {str(e)}"
253
  )
254
 
 
 
 
255
 
256
+ # ─────────────────────────────────────────────────────────────────
257
+ # Development Entry Point
258
+ # ─────────────────────────────────────────────────────────────────
259
 
260
+ if __name__ == "__main__":
261
+ import uvicorn
262
 
263
+ # For development/testing
264
+ logger.info("Starting Model 4 API in development mode...")
265
+ uvicorn.run(
266
+ app,
267
+ host="0.0.0.0",
268
+ port=7860, # Default HuggingFace Spaces port
269
+ log_level="info"
270
+ )