ai-helpdesk-api / services /classifier_service.py
github-actions
deploy: sync from github main
6d32f12
Raw
History Blame Contribute Delete
5.92 kB
"""
Classifier Service — Loads the trained DistilBert sequence classifier and predicts.
The model outputs combined "Category | SubCategory" labels.
Priority and other fields are derived from the category mapping.
"""
import os
import json
import torch
import torch.nn.functional as F
from transformers import DistilBertTokenizerFast, DistilBertForSequenceClassification
SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "models", "classifier")
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
MAX_LEN = 128
# Priority mapping based on sub-category severity
PRIORITY_MAP = {
"Blue Screen": "Critical", "Overheating": "Critical", "Data Loss": "Critical",
"Hardware Failure": "Critical", "Application Crash": "High",
"Login Failure": "High", "Password Reset": "High", "VPN Connection": "High",
"Firewall Block": "High", "DNS Problem": "High", "MFA Problem": "High",
"Account Expired": "High", "Permission Issue": "Medium", "Access Request": "Medium",
"Software Install": "Medium", "Update Problem": "Medium", "Compatibility": "Medium",
"Configuration": "Medium", "License Issue": "Medium", "Performance": "Medium",
"Internet Slow": "Medium", "WiFi Issue": "Medium", "Remote Access": "Medium",
"Proxy Error": "Medium", "Network Drive": "Medium", "Role Change": "Medium",
"Account Unlock": "Low", "Keyboard/Mouse": "Low", "Monitor Problem": "Low",
"Printer Error": "Low", "Battery Issue": "Low", "Laptop Issue": "Low",
}
# Team assignment based on category
TEAM_MAP = {
"Access": "IAM Team",
"Network": "Network Support",
"Software": "Application Support",
"Hardware": "Hardware Support",
}
# Auto-resolve: simple issues that can be auto-resolved
AUTO_RESOLVE_SUBS = {
"Password Reset", "Account Unlock", "Software Install",
"WiFi Issue", "Printer Error", "Monitor Problem",
}
class ClassifierService:
def __init__(self):
self.model = None
self.tokenizer = None
self.id2label = None
self.label2id = None
self._loaded = False
def load(self):
"""Load model, tokenizer, and label mappings from disk."""
if self._loaded:
return
abs_dir = os.path.abspath(SAVE_DIR)
if not os.path.exists(os.path.join(abs_dir, "model.safetensors")):
raise FileNotFoundError(
f"Classifier model not found at {abs_dir}. "
"Please ensure model files are present."
)
# Load label mappings
with open(os.path.join(abs_dir, "id2label.json"), "r") as f:
self.id2label = json.load(f)
with open(os.path.join(abs_dir, "label2id.json"), "r") as f:
self.label2id = json.load(f)
# Load tokenizer
self.tokenizer = DistilBertTokenizerFast.from_pretrained(abs_dir)
# Load model
self.model = DistilBertForSequenceClassification.from_pretrained(abs_dir)
self.model.to(DEVICE)
self.model.eval()
self._loaded = True
print("Classifier loaded successfully")
def predict(self, text: str) -> dict:
"""
Predict category, subcategory, priority, auto_resolve, assigned_team, and confidence.
"""
self.load()
encoding = self.tokenizer(
text,
truncation=True,
padding="max_length",
max_length=MAX_LEN,
return_tensors="pt",
)
input_ids = encoding["input_ids"].to(DEVICE)
attention_mask = encoding["attention_mask"].to(DEVICE)
with torch.no_grad():
outputs = self.model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits
probs = F.softmax(logits, dim=1)
confidence, pred_idx = torch.max(probs, dim=1)
pred_idx = pred_idx.item()
confidence = round(confidence.item(), 4)
# Decode the combined label "Category | SubCategory"
combined_label = self.id2label.get(str(pred_idx), "Unknown | Unknown")
parts = combined_label.split(" | ", 1)
category = parts[0].strip() if len(parts) > 0 else "Unknown"
subcategory = parts[1].strip() if len(parts) > 1 else "Unknown"
# Derive priority
priority = PRIORITY_MAP.get(subcategory, "Medium")
# Derive assigned team
assigned_team = TEAM_MAP.get(category, "General Support")
# Derive auto_resolve
auto_resolve = subcategory in AUTO_RESOLVE_SUBS
# --- Regex Override Layer (Boost for Technical Keywords) ---
tech_keywords = {
"Network": ["IP address", "hostname", "connection", "network", "bandwidth", "DNS", "firewall", "VPN", "Connectivity", "Latency", "Routing", "Spikes"],
"Software": ["crash", "load", "website", "application", "error", "bug", "failing", "software", "SQL", "Cluster", "Database", "Production", "Latency"],
"Access": ["login", "password", "access", "authentication", "account", "permission", "MFA", "OAuth"]
}
lower_text = text.lower()
for cat, keywords in tech_keywords.items():
if any(k.lower() in lower_text for k in keywords):
# If current prediction is generic, or we have a high-value technical keyword
if category == "General" or confidence < 0.9:
category = cat
assigned_team = TEAM_MAP.get(cat, "General Support")
# Boost confidence significantly for verified technical signals
confidence = max(confidence, 0.92)
break
return {
"category": category,
"subcategory": subcategory,
"priority": priority,
"auto_resolve": auto_resolve,
"assigned_team": assigned_team,
"confidence": confidence,
}