Gokulavanan
Deploy toxic comment classifier
030f249
Raw
History Blame Contribute Delete
4.29 kB
import os
import sys
import time
import logging
from datetime import datetime
from typing import List
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, validator
from logging.handlers import RotatingFileHandler
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
# Add project root
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.predict import ToxicityPredictor
# ============================
# CONFIG
# ============================
MODEL_DIR = "model_output"
MAX_INPUT_CHARS = 500
LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)
API_LOG_FILE = os.path.join(LOG_DIR, "api.log")
# Rate limit: 30 requests per minute per IP
RATE_LIMIT = "30/minute"
# ============================
# LOGGING SETUP
# ============================
logger = logging.getLogger("toxicity_api")
logger.setLevel(logging.INFO)
file_handler = RotatingFileHandler(
API_LOG_FILE,
maxBytes=10_000_000,
backupCount=5,
encoding="utf-8"
)
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# ============================
# FASTAPI INIT
# ============================
app = FastAPI(
title="Toxic Content Classifier API",
version="2.0.0"
)
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
# ============================
# EXCEPTION HANDLERS
# ============================
@app.exception_handler(RateLimitExceeded)
def rate_limit_handler(request: Request, exc):
return JSONResponse(
status_code=429,
content={"detail": "Rate limit exceeded"}
)
@app.exception_handler(Exception)
def global_exception_handler(request: Request, exc):
logger.error(f"Unhandled error: {str(exc)}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
# ============================
# MODEL LOADING
# ============================
predictor = None
@app.on_event("startup")
def load_model():
global predictor
logger.info("Loading model...")
predictor = ToxicityPredictor(MODEL_DIR)
logger.info("Model loaded successfully.")
# ============================
# REQUEST / RESPONSE MODELS
# ============================
class TextRequest(BaseModel):
text: str
@validator("text")
def validate_text(cls, v):
if not v or not v.strip():
raise ValueError("Text must be non-empty.")
if len(v) > MAX_INPUT_CHARS:
raise ValueError("Input too long.")
return v.strip()
class PredictionResponse(BaseModel):
prediction: str
raw_prediction: str
confidence: float
process_time_ms: float
timestamp: str
# ============================
# ENDPOINTS
# ============================
@app.get("/health")
def health():
return {"status": "healthy"}
@app.get("/ready")
def readiness():
if predictor is None:
raise HTTPException(status_code=503, detail="Model not ready")
return {"status": "ready"}
@app.post("/predict", response_model=PredictionResponse)
@limiter.limit(RATE_LIMIT)
def predict(request: Request, payload: TextRequest):
if predictor is None:
raise HTTPException(status_code=503, detail="Model not ready")
start_time = time.time()
result = predictor.predict(payload.text)
process_time = (time.time() - start_time) * 1000
timestamp = datetime.utcnow().isoformat()
# Structured logging (NO RAW TEXT)
logger.info(
f"prediction={result['prediction']} "
f"confidence={round(result['confidence'],4)} "
f"time_ms={round(process_time,2)}"
)
return {
"original_comment" : payload.text,
"prediction": result["prediction"],
"raw_prediction": result["raw_prediction"],
"confidence": round(result["confidence"], 4),
"process_time_ms": round(process_time, 2),
"timestamp": timestamp
}
# ============================
# LOCAL RUN (DEV ONLY)
# ============================
if __name__ == "__main__":
import uvicorn
uvicorn.run("src.api:app", host="0.0.0.0", port=8000)