Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mlops_nlp.config import load_config
from mlops_nlp.logging_config import configure_logging, get_logger
from mlops_nlp.pipelines.inference_pipeline import InferencePipeline
from mlops_nlp.schemas import PredictionRequest, PredictionResponse
from mlops_nlp.schemas import PredictionRequest, PredictionResponse, BatchPredictionRequest, BatchPredictionResponse
from mlops_nlp.utils.drift import log_inference

APP_CONFIG = load_config()
Expand Down Expand Up @@ -66,6 +66,7 @@ def root() -> JSONResponse:
"docs": "/docs",
"health": "/health",
"predict": "/predict",
"predict_batch": "/predict/batch",
"metrics": "/metrics" if PROMETHEUS_ENABLED else "disabled",
}
)
Expand Down Expand Up @@ -120,6 +121,41 @@ def predict(
)


@app.post("/predict/batch", response_model=BatchPredictionResponse)
def predict_batch(
payload: BatchPredictionRequest,
_api_key: str = Depends(get_api_key)
) -> BatchPredictionResponse:
if pipeline is None:
raise HTTPException(status_code=503, detail="Model is not loaded. Train model first.")

results = pipeline.predict_batch(payload.texts)
responses = []

for text, (prediction, confidence) in zip(payload.texts, results):
if PROMETHEUS_ENABLED:
PREDICTION_COUNT.labels(prediction=prediction).inc()

# Log for drift detection
log_inference(
log_path=APP_CONFIG.monitoring.inference_log_path,
text=text,
prediction=prediction,
confidence=confidence,
model_version=pipeline.metadata["model_version"],
)

responses.append(
PredictionResponse(
prediction=prediction,
confidence=confidence,
model_version=pipeline.metadata["model_version"]
)
)

return BatchPredictionResponse(predictions=responses)


@app.get("/metrics")
def metrics() -> PlainTextResponse:
if not PROMETHEUS_ENABLED:
Expand Down
11 changes: 11 additions & 0 deletions src/mlops_nlp/pipelines/inference_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ def predict(self, text: str) -> tuple[str, float]:

return str(prediction), confidence

def predict_batch(self, texts: list[str]) -> list[tuple[str, float]]:
cleaned_texts = [clean_text(t) for t in texts]
features = self.vectorizer.transform(cleaned_texts)
predictions = self.model.predict(features)
probabilities = self.model.predict_proba(features)

results = []
for pred, proba in zip(predictions, probabilities):
results.append((str(pred), float(max(proba))))
return results

9 changes: 9 additions & 0 deletions src/mlops_nlp/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ class PredictionResponse(BaseModel):
confidence: float
model_version: str


class BatchPredictionRequest(BaseModel):
texts: list[str] = Field(..., min_length=1, max_length=100)


class BatchPredictionResponse(BaseModel):
model_config = ConfigDict(protected_namespaces=())
predictions: list[PredictionResponse]

35 changes: 35 additions & 0 deletions tests/test_batch_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

import pytest
from fastapi.testclient import TestClient
from api.main import app

client = TestClient(app)

def test_predict_batch_no_auth():
# Similar to test_api_auth.py, we assume auth is handled correctly if it works for single predict
# This test focuses on functionality.

# Mocking the pipeline if necessary, but we can just run a real test if a model exists
import api.main
if api.main.pipeline is None:
# Try to initialize it if possible (requires trained model)
from mlops_nlp.pipelines.inference_pipeline import InferencePipeline
try:
api.main.pipeline = InferencePipeline()
except Exception:
pytest.skip("No trained model available for integration test")

texts = ["win cash now", "hello how are you"]
response = client.post("/predict/batch", json={"texts": texts})

# If auth is required, this will be 403, which is also a valid check
if response.status_code == 403:
return

assert response.status_code == 200
data = response.json()
assert "predictions" in data
assert len(data["predictions"]) == 2
assert data["predictions"][0]["prediction"] in ["spam", "ham"]
assert isinstance(data["predictions"][0]["confidence"], float)
Loading