Spaces:
Running
Running
| """ | |
| app/classifiers/hf2_classifier.py | |
| =================================== | |
| Phase 3 (HF2 Ultra) runtime classifier — loads and serves AegisMTModel. | |
| Returns an extended predict dict (backward-compatible with Phase 1/2): | |
| { | |
| # Existing keys (all classifiers) | |
| "label": "benign" | "malicious", | |
| "malicious_prob": float, | |
| "benign_prob": float, | |
| # Extended keys (hf2 only) | |
| "threat_category_probs": { | |
| "prompt_injection": float, | |
| "jailbreak": float, | |
| "data_exfiltration": float, | |
| "canary_leak": float, | |
| "pii_leak": float, | |
| "harmful_content": float, | |
| "none": float, | |
| }, | |
| "threat_category": str, # argmax of threat_category_probs | |
| "classifier_stage": "hf2", | |
| "preprocessing_flags": dict, # from TextPreprocessor | |
| } | |
| The temperature scaling scalar T* is read from config.json and applied | |
| before converting logits to probabilities. | |
| TextPreprocessor (Unicode normalization + invisible char detection) runs | |
| internally before every inference call. | |
| Lazy imports: torch/transformers are only imported when .load() is called, | |
| so the rest of the service starts without the ML stack. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| from pathlib import Path | |
| from typing import Any | |
| logger = logging.getLogger(__name__) | |
| class HF2Classifier: | |
| """ | |
| Wraps a trained AegisMTModel for runtime inference. | |
| The model directory must contain: | |
| model.pt — PyTorch state dict (from AegisMTModel.save_pretrained) | |
| config.json — config with temperature_scaling + aegis_* metadata | |
| tokenizer files — tokenizer.json / spiece.model | |
| """ | |
| def __init__(self, model_path: str) -> None: | |
| self.model_path = model_path | |
| self._module = None | |
| self._tokenizer = None | |
| self._temperature: float = 1.0 | |
| self._device = None | |
| self._loaded = False | |
| self._preprocessor = None | |
| # ── Lifecycle ───────────────────────────────────────────────────────────── | |
| def _probe_device(torch) -> int: | |
| """Return GPU device index (0) if available and functional, else -1 (CPU).""" | |
| if not torch.cuda.is_available(): | |
| return -1 | |
| try: | |
| t = torch.zeros(4).cuda() | |
| torch.isfinite(t) | |
| return 0 | |
| except RuntimeError as exc: | |
| logger.warning( | |
| "GPU detected but not functional (%s) — falling back to CPU.", exc | |
| ) | |
| return -1 | |
| def load(self) -> None: | |
| """Load the AegisMTModel. Heavy imports deferred to here.""" | |
| import torch | |
| from transformers import AutoTokenizer | |
| from training.phase3_hf2.model import AegisMTModel | |
| from app.classifiers.text_preprocessor import TextPreprocessor | |
| path = Path(self.model_path) | |
| if not path.exists(): | |
| raise FileNotFoundError( | |
| f"HF2 model not found at {path}. " | |
| "Run 'python -m training.phase3_hf2.train' first." | |
| ) | |
| device_id = self._probe_device(torch) | |
| device_label = "CUDA/ROCm" if device_id == 0 else "CPU" | |
| logger.info("Loading HF2 classifier from %s on %s", path, device_label) | |
| aegis_model, temperature = AegisMTModel.from_pretrained(path) | |
| self._module = aegis_model.module | |
| self._temperature = temperature | |
| device = torch.device(f"cuda:{device_id}" if device_id >= 0 else "cpu") | |
| self._module.to(device) | |
| self._module.eval() | |
| self._device = device | |
| self._tokenizer = AutoTokenizer.from_pretrained(str(path)) | |
| self._preprocessor = TextPreprocessor() | |
| self._loaded = True | |
| logger.info( | |
| "HF2 classifier loaded successfully (T=%.4f).", self._temperature | |
| ) | |
| def is_loaded(self) -> bool: | |
| return self._loaded | |
| # ── Inference ────────────────────────────────────────────────────────────── | |
| async def predict(self, text: str) -> dict[str, Any]: | |
| """ | |
| Async prediction wrapper. | |
| Returns the extended predict dict (see module docstring). | |
| """ | |
| if not self._loaded or self._module is None: | |
| raise RuntimeError("HF2Classifier is not loaded. Call .load() first.") | |
| return await asyncio.to_thread(self._predict_sync, text) | |
| def _predict_sync(self, text: str) -> dict[str, Any]: | |
| """Synchronous inference — called inside a thread pool worker.""" | |
| import torch | |
| import torch.nn.functional as F | |
| from training.phase3_hf2.model import ID2THREAT, THREAT_CATEGORIES | |
| assert self._module is not None | |
| assert self._tokenizer is not None | |
| assert self._preprocessor is not None | |
| # ── Preprocessing (Unicode normalization + invisible char detection) ── | |
| cleaned_text, preprocess_flags = self._preprocessor.preprocess(text) | |
| # ── Tokenise ────────────────────────────────────────────────────────── | |
| inputs = self._tokenizer( | |
| cleaned_text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=512, | |
| padding=True, | |
| ) | |
| inputs = {k: v.to(self._device) for k, v in inputs.items()} | |
| # ── Inference ───────────────────────────────────────────────────────── | |
| with torch.no_grad(): | |
| outputs = self._module(**inputs) | |
| binary_logits = outputs.binary_logits | |
| threat_logits = outputs.threat_logits | |
| # Apply temperature scaling to binary logits | |
| binary_probs = F.softmax(binary_logits / self._temperature, dim=-1)[0] | |
| threat_probs = F.softmax(threat_logits, dim=-1)[0] | |
| malicious_prob = float(binary_probs[1].item()) | |
| benign_prob = float(binary_probs[0].item()) | |
| label = "malicious" if malicious_prob >= 0.5 else "benign" | |
| # Build threat_category_probs dict | |
| threat_probs_dict = { | |
| THREAT_CATEGORIES[i]: float(threat_probs[i].item()) | |
| for i in range(len(THREAT_CATEGORIES)) | |
| } | |
| threat_category = max(threat_probs_dict, key=threat_probs_dict.get) | |
| return { | |
| "label": label, | |
| "malicious_prob": malicious_prob, | |
| "benign_prob": benign_prob, | |
| "threat_category_probs": threat_probs_dict, | |
| "threat_category": threat_category, | |
| "classifier_stage": "hf2", | |
| "preprocessing_flags": preprocess_flags, | |
| } | |