-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
151 lines (123 loc) · 6.03 KB
/
Copy pathapp.py
File metadata and controls
151 lines (123 loc) · 6.03 KB
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
150
151
import asyncio
from quart import Quart, request, jsonify
from inference.model import ThreatClassifier
from inference.schemas import PredictionRequest, PredictionResponse
from cockatoo_ml.registry import APIConfig
from cockatoo_ml.logger.context import inference_api_server_logger as logger
from cockatoo_ml.logger.context import request_logger as inference_request_logger
app = Quart(__name__)
classifier = None
@app.before_serving
async def initialize():
# init the model into global scope so it can be reused
global classifier
classifier = ThreatClassifier()
logger.info("Model loaded and ready for inference")
@app.route("/health", methods=["GET"])
async def health():
# server health check
origin = request.remote_addr
inference_request_logger.info(f"Health check received from {origin}")
return jsonify({"status": "ok", "model": APIConfig.MODEL_NAME})
@app.route("/predict", methods=["POST"])
async def predict():
# inference endpoint for single text input
try:
data = await request.get_json()
origin = request.remote_addr
text = data.get("text", "")
threshold = data.get("threshold", "default")
inference_request_logger.info(f"Prediction request from {origin} - text length: {len(text)}, threshold: {threshold}")
req = PredictionRequest(**data)
# run blocking inference in thread pool
result = await asyncio.to_thread(classifier.predict, req.text)
if "error" in result:
inference_request_logger.error(f"Prediction error: {result['error']}")
return jsonify({"error": result["error"]}), 400
# determine thresholds: use provided or fall back to per-label defaults
if req.threshold is None:
# use per-label thresholds from column_mapping
thresholds = classifier.get_label_thresholds()
elif isinstance(req.threshold, dict):
# use provided per-label thresholds
thresholds = req.threshold
else:
# use single threshold for all labels
thresholds = {label: req.threshold for label in result["predictions"].keys()}
positive_labels = [
label for label, score in result["predictions"].items()
if float(score) >= thresholds.get(label, APIConfig.DEFAULT_THRESHOLD)
]
# call into the inference helper to get predictions and format response
# return all labels and confidence
response = PredictionResponse(
text=req.text,
predictions=result["predictions"],
positive_labels=positive_labels,
top_label=result["top_label"],
max_score=float(result["max_score"])
)
inference_request_logger.info(f"Prediction response - all labels: {list(response.predictions.keys())}, positive_labels: {positive_labels}")
return jsonify(response.model_dump())
except ValueError as e:
inference_request_logger.error(f"Validation error: {str(e)}")
return jsonify({"error": f"Validation error: {str(e)}"}), 422
except Exception as e:
# catch errors
inference_request_logger.error(f"Prediction endpoint error: {str(e)}")
return jsonify({"error": str(e)}), 500
@app.route("/batch", methods=["POST"])
async def batch_predict():
try:
data = await request.get_json()
texts = data.get("texts", [])
threshold = data.get("threshold", None)
origin = request.remote_addr
inference_request_logger.info(f"Batch prediction request from {origin} - count: {len(texts)}, threshold: {threshold or 'default'}, sample texts: {[t[:50] for t in texts[:3]]}")
if not texts or not isinstance(texts, list):
inference_request_logger.warning(f"Invalid batch request from {origin}: texts is not a non-empty list")
return jsonify({"error": "texts must be a non-empty list"}), 400
# determine thresholds: use provided or fall back to per-label defaults
if threshold is None:
# will use per-label thresholds from column_mapping
use_per_label = True
thresholds = classifier.get_label_thresholds()
elif isinstance(threshold, dict):
# use provided per-label thresholds
use_per_label = True
thresholds = threshold
else:
# use single threshold for all labels
use_per_label = False
single_threshold = threshold
results = []
for text in texts:
result = await asyncio.to_thread(classifier.predict, text)
if "error" not in result:
if use_per_label:
positive_labels = [
label for label, score in result["predictions"].items()
if score >= thresholds.get(label, APIConfig.DEFAULT_THRESHOLD)
]
else:
positive_labels = [
label for label, score in result["predictions"].items()
if score >= single_threshold
]
# return all labels
results.append({
"text": text,
"predictions": result["predictions"],
"positive_labels": positive_labels,
"top_label": result["top_label"],
"max_score": result["max_score"]
})
inference_request_logger.info(f"Batch prediction response - processed: {len(results)}/{len(texts)} with all labels returned")
return jsonify({"count": len(results), "results": results})
except Exception as e:
inference_request_logger.error(f"Batch prediction endpoint error: {str(e)}")
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
# run with: hypercorn app:app --bind 0.0.0.0:8000
# development server (not for production use)
app.run(debug=APIConfig.DEBUG, host=APIConfig.HOST, port=APIConfig.PORT)