Eklavya73 commited on
Commit
55332e2
·
verified ·
1 Parent(s): d9c67f1

Upload 2 files

Browse files
Files changed (3) hide show
  1. .gitattributes +1 -0
  2. BG_Img-2.png +3 -0
  3. app.py +870 -0
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  Data/Domain-A_Dataset_Clean.csv filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  Data/Domain-A_Dataset_Clean.csv filter=lfs diff=lfs merge=lfs -text
37
+ BG_Img-2.png filter=lfs diff=lfs merge=lfs -text
BG_Img-2.png ADDED

Git LFS Details

  • SHA256: ed1a640a1b5482210bcf07b34a464853c5f704bd3248db3596da8d86b7c904a0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.55 MB
app.py ADDED
@@ -0,0 +1,870 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Intelligent Ticket Auto-Routing System - Hugging Face Spaces App
3
+ ================================================================
4
+ Converts support tickets into structured routing decisions:
5
+ - Multi-label tag classification
6
+ - Department routing (hybrid: tag-voting + semantic similarity)
7
+ - Priority prediction
8
+ - Duplicate detection via FAISS
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import csv
15
+ import os
16
+ import tempfile
17
+ import time
18
+ import uuid
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ import gradio as gr
23
+ import joblib
24
+ import numpy as np
25
+ from sentence_transformers import SentenceTransformer
26
+
27
+ from calibration_utils import (
28
+ calibrate_probabilities,
29
+ load_temperature_scaler,
30
+ )
31
+
32
+ from duplicate_detection_utils import CachedDuplicateDetectionEngine
33
+
34
+ from hybrid_routing_utils import (
35
+ DEFAULT_TAG_TO_DEPARTMENT,
36
+ assert_valid_routing_label_policy,
37
+ compute_department_hybrid_scores,
38
+ load_routing_label_policy,
39
+ )
40
+
41
+ from review_policy_utils import (
42
+ apply_controlled_review,
43
+ load_review_policy,
44
+ )
45
+
46
+ from runtime_utils import (
47
+ load_model_config,
48
+ load_routing_config,
49
+ resolve_dataset_file,
50
+ resolve_model_dir,
51
+ resolve_model_reference,
52
+ )
53
+
54
+ APP_DIR = Path(__file__).resolve().parent
55
+ MODEL_DIR = resolve_model_dir(APP_DIR)
56
+
57
+ ROUTING_CONFIG, ROUTING_CONFIG_PATH = load_routing_config(APP_DIR)
58
+ DEFAULT_DEPARTMENT = str(
59
+ ROUTING_CONFIG.get("default_department", "Human_Review")
60
+ )
61
+ PRIORITY_ESCALATION = {
62
+ str(priority).lower(): department
63
+ for priority, department in (ROUTING_CONFIG.get("priority_escalation") or {}).items()
64
+ }
65
+ LOG_PATH = os.path.join(tempfile.gettempdir(), "routing_evaluation_log.csv")
66
+
67
+
68
+ print("Loading SBERT model...")
69
+ model_config = load_model_config(APP_DIR)
70
+ routing_sbert_model_name = resolve_model_reference(
71
+ model_config.get("sbert_model", "Eklavya73/sbert_finetuned"),
72
+ base_dir=APP_DIR,
73
+ model_dir=MODEL_DIR,
74
+ )
75
+ duplicate_sbert_model_name = resolve_model_reference(
76
+ model_config.get("duplicate_sbert_model", "Eklavya73/duplicate_sbert"),
77
+ base_dir=APP_DIR,
78
+ model_dir=MODEL_DIR,
79
+ default="all-mpnet-base-v2",
80
+ )
81
+ routing_sbert = SentenceTransformer(routing_sbert_model_name)
82
+ duplicate_sbert = (
83
+ routing_sbert
84
+ if duplicate_sbert_model_name == routing_sbert_model_name
85
+ else SentenceTransformer(duplicate_sbert_model_name)
86
+ )
87
+
88
+ print("Loading classifiers...")
89
+ tag_model = joblib.load(MODEL_DIR / "sbert_classifier.pkl")
90
+ tag_calibrators = joblib.load(MODEL_DIR / "tag_calibrators.pkl")
91
+ temperature_scaler = load_temperature_scaler(MODEL_DIR / "tag_temperature_scaler.pkl")
92
+ review_policy = load_review_policy(MODEL_DIR / "routing_review_policy.pkl")
93
+
94
+ DEMO_REVIEW_THRESHOLD_CAP = 0.30
95
+ review_policy = dict(review_policy)
96
+ review_policy["percentile_threshold"] = min(
97
+ float(review_policy.get("percentile_threshold", 0.55)),
98
+ DEMO_REVIEW_THRESHOLD_CAP,
99
+ )
100
+ review_policy["fallback_threshold"] = min(
101
+ float(review_policy.get("fallback_threshold", 0.55)),
102
+ DEMO_REVIEW_THRESHOLD_CAP,
103
+ )
104
+ review_policy["effective_threshold"] = max(
105
+ review_policy["percentile_threshold"],
106
+ review_policy["fallback_threshold"],
107
+ )
108
+
109
+ priority_bundle = joblib.load(MODEL_DIR / "tuned_priority_model.pkl")
110
+ priority_model = (
111
+ priority_bundle["model"]
112
+ if isinstance(priority_bundle, dict) and "model" in priority_bundle
113
+ else priority_bundle
114
+ )
115
+ priority_encoder = joblib.load(MODEL_DIR / "priority_encoder.pkl")
116
+ hf_scaler = joblib.load(MODEL_DIR / "hf_scaler.pkl")
117
+
118
+ tag_binarizer = joblib.load(MODEL_DIR / "mlb_tag_binarizer.pkl")
119
+ tag_list = list(tag_binarizer.classes_)
120
+
121
+ dept_prototypes = joblib.load(MODEL_DIR / "department_prototypes.pkl")
122
+ routing_label_policy = load_routing_label_policy(
123
+ MODEL_DIR / "routing_label_policy.pkl",
124
+ fallback_tag_to_department=ROUTING_CONFIG.get(
125
+ "departments",
126
+ DEFAULT_TAG_TO_DEPARTMENT,
127
+ ),
128
+ valid_tags=tag_list,
129
+ valid_departments=dept_prototypes.keys(),
130
+ default_department=DEFAULT_DEPARTMENT,
131
+ )
132
+ tag_to_department = routing_label_policy["tag_to_department"]
133
+ assert_valid_routing_label_policy(
134
+ routing_label_policy,
135
+ valid_tags=tag_list,
136
+ valid_departments=dept_prototypes.keys(),
137
+ )
138
+
139
+ print("Loading duplicate detection index...")
140
+ duplicate_engine = CachedDuplicateDetectionEngine(APP_DIR)
141
+
142
+ print(f"[OK] Tags: {len(tag_list)}, Departments: {len(dept_prototypes)}")
143
+ print(f"[OK] Routing label policy: {len(tag_to_department)} active mappings")
144
+ print(
145
+ "[OK] Routing config: "
146
+ f"{ROUTING_CONFIG_PATH if ROUTING_CONFIG_PATH is not None else 'defaults'}"
147
+ )
148
+ print(f"[OK] Default human-review department: {DEFAULT_DEPARTMENT}")
149
+ print(f"[OK] Routing SBERT model: {routing_sbert_model_name}")
150
+ print(f"[OK] Duplicate SBERT model: {duplicate_sbert_model_name}")
151
+ print(f"[OK] Duplicate index: {duplicate_engine.index_size} vectors")
152
+ print(f"[OK] Duplicate threshold: {duplicate_engine.duplicate_threshold:.4f}")
153
+ print(f"[OK] Temperature scaler: T={temperature_scaler.get('temperature', 1.0):.3f}")
154
+ print(
155
+ "[OK] Review policy: "
156
+ f"target={review_policy.get('target_review_fraction', 0.15):.0%}, "
157
+ f"percentile_threshold={review_policy.get('percentile_threshold', 0.55):.3f}, "
158
+ f"fallback_threshold={review_policy.get('fallback_threshold', 0.55):.3f}"
159
+ )
160
+
161
+
162
+ def encode_ticket_embedding(text, encoder):
163
+ emb = np.asarray(encoder.encode(text), dtype=float).reshape(-1)
164
+ emb_norm = np.linalg.norm(emb)
165
+ if emb_norm == 0.0:
166
+ return emb
167
+ return emb / emb_norm
168
+
169
+
170
+ def predict_tags(text, emb):
171
+ raw_probs = np.asarray(tag_model.predict_proba([emb])[0], dtype=float)
172
+ calibrated = calibrate_probabilities(
173
+ raw_probs,
174
+ tag_calibrators=tag_calibrators,
175
+ temperature_scaler=temperature_scaler,
176
+ )
177
+ top_idx = calibrated.argsort()[-5:][::-1]
178
+ return top_idx, calibrated[top_idx], calibrated, raw_probs
179
+
180
+
181
+ def extract_features(text):
182
+ words = text.split()
183
+ return [
184
+ len(text),
185
+ len(words),
186
+ len(set(words)) / (len(words) + 1),
187
+ np.mean([len(word) for word in words]) if words else 0,
188
+ sum(word in text.lower() for word in ["urgent", "critical", "down"]),
189
+ sum(word in text.lower() for word in ["not", "cannot", "no"]),
190
+ ]
191
+
192
+
193
+ def predict_priority(text, emb, return_confidence=False):
194
+ features = extract_features(text)
195
+ features_scaled = hf_scaler.transform([features])
196
+ x = np.hstack([emb.reshape(1, -1), features_scaled])
197
+ pred_idx = int(priority_model.predict(x)[0])
198
+ priority_label = str(priority_encoder.classes_[pred_idx])
199
+ priority_confidence = float("nan")
200
+
201
+ if hasattr(priority_model, "predict_proba"):
202
+ try:
203
+ probs = np.asarray(
204
+ priority_model.predict_proba(x)[0],
205
+ dtype=float,
206
+ ).reshape(-1)
207
+ if probs.size:
208
+ priority_confidence = float(probs[pred_idx])
209
+ except Exception:
210
+ priority_confidence = float("nan")
211
+
212
+ if return_confidence:
213
+ return priority_label, priority_confidence
214
+ return priority_label
215
+
216
+
217
+ HYBRID_CLASSIFIER_WEIGHT = 0.7
218
+ HYBRID_SIMILARITY_WEIGHT = 0.3
219
+ HYBRID_FLOOR = 0.45
220
+ FLAGGED_HYBRID_FLOOR = 0.30
221
+ MARGIN_THRESHOLD = 0.15
222
+ ENTROPY_THRESHOLD = 1.8
223
+
224
+
225
+ def compute_confidence_metrics(calibrated_probs):
226
+ probs = np.asarray(calibrated_probs, dtype=float).reshape(-1)
227
+ if probs.size == 0:
228
+ return 0.0, float("inf")
229
+
230
+ sorted_probs = np.sort(probs)[::-1]
231
+ top1 = float(sorted_probs[0])
232
+ top2 = float(sorted_probs[1]) if len(sorted_probs) > 1 else 0.0
233
+ margin = top1 - top2
234
+
235
+ p = np.clip(probs, 1e-12, None)
236
+ total = float(p.sum())
237
+ if total == 0.0:
238
+ p = np.full_like(p, 1.0 / len(p))
239
+ else:
240
+ p = p / total
241
+ entropy = float(-np.sum(p * np.log(p)))
242
+ return margin, entropy
243
+
244
+
245
+ def decide_routing_mode(hybrid_confidence, calibrated_probs):
246
+ margin, entropy = compute_confidence_metrics(calibrated_probs)
247
+
248
+ if hybrid_confidence < HYBRID_FLOOR:
249
+ return "HUMAN_REVIEW", True, margin, entropy
250
+
251
+ if (margin >= MARGIN_THRESHOLD) or (entropy <= ENTROPY_THRESHOLD):
252
+ return "AUTO_ROUTE", False, margin, entropy
253
+
254
+ if hybrid_confidence >= FLAGGED_HYBRID_FLOOR:
255
+ return "AUTO_ROUTE_FLAGGED", True, margin, entropy
256
+
257
+ return "HUMAN_REVIEW", True, margin, entropy
258
+
259
+
260
+ def route_ticket(emb, text):
261
+ _, _, calibrated_probs, _ = predict_tags(text, emb)
262
+ best_dept, hybrid_confidence, department_details, top_tag_votes = (
263
+ compute_department_hybrid_scores(
264
+ calibrated_probs,
265
+ emb,
266
+ dept_prototypes,
267
+ tag_to_department=tag_to_department,
268
+ tag_names=tag_list,
269
+ classifier_weight=HYBRID_CLASSIFIER_WEIGHT,
270
+ similarity_weight=HYBRID_SIMILARITY_WEIGHT,
271
+ top_k=5,
272
+ )
273
+ )
274
+ priority, priority_confidence = predict_priority(
275
+ text,
276
+ emb,
277
+ return_confidence=True,
278
+ )
279
+ base_mode, _, margin, entropy = decide_routing_mode(
280
+ hybrid_confidence,
281
+ calibrated_probs,
282
+ )
283
+
284
+ recommended_department = best_dept
285
+ routed_department = recommended_department
286
+ escalation_note = ""
287
+
288
+ if not top_tag_votes or best_dept is None:
289
+ review_decision = {
290
+ "base_mode": "HUMAN_REVIEW",
291
+ "final_mode": "HUMAN_REVIEW",
292
+ "forced_human_review": False,
293
+ "percentile_threshold": float(
294
+ review_policy.get("percentile_threshold", 0.55)
295
+ ),
296
+ "fallback_threshold": float(
297
+ review_policy.get("fallback_threshold", 0.55)
298
+ ),
299
+ "reason": "No valid tag votes or department resolved. Requires human review.",
300
+ }
301
+ return {
302
+ "mode": "HUMAN_REVIEW",
303
+ "department": DEFAULT_DEPARTMENT,
304
+ "recommended_department": None,
305
+ "priority": priority,
306
+ "priority_confidence": priority_confidence,
307
+ "hybrid_confidence": hybrid_confidence,
308
+ "review": True,
309
+ "margin": margin,
310
+ "entropy": entropy,
311
+ "best_details": {},
312
+ "top_tag_votes": [],
313
+ "review_decision": review_decision,
314
+ "note": review_decision["reason"],
315
+ }
316
+
317
+ escalation_department = PRIORITY_ESCALATION.get(str(priority).lower())
318
+ if base_mode != "HUMAN_REVIEW" and escalation_department:
319
+ routed_department = str(escalation_department)
320
+ escalation_note = (
321
+ f" Priority escalation override applied after gate: "
322
+ f"{priority} -> {routed_department}."
323
+ )
324
+
325
+ mode, review, review_decision = apply_controlled_review(
326
+ base_mode,
327
+ hybrid_confidence,
328
+ review_policy=review_policy,
329
+ )
330
+
331
+ if review_decision.get("forced_human_review", False):
332
+ final_department = DEFAULT_DEPARTMENT
333
+ note = (
334
+ f"{review_decision.get('reason', '')} "
335
+ f"Recommended department before override: {routed_department}."
336
+ f"{escalation_note}"
337
+ ).strip()
338
+ elif mode == "AUTO_ROUTE":
339
+ final_department = routed_department
340
+ note = (
341
+ f"Stage 2 pass: hybrid_confidence={hybrid_confidence:.4f}, "
342
+ f"margin={margin:.4f}, entropy={entropy:.4f}."
343
+ f"{escalation_note}"
344
+ )
345
+ elif mode == "AUTO_ROUTE_FLAGGED":
346
+ final_department = routed_department
347
+ note = (
348
+ f"Stage 2 flagged: hybrid_confidence={hybrid_confidence:.4f}, "
349
+ f"margin={margin:.4f}, entropy={entropy:.4f}."
350
+ f"{escalation_note}"
351
+ )
352
+ elif hybrid_confidence < HYBRID_FLOOR:
353
+ final_department = DEFAULT_DEPARTMENT
354
+ note = (
355
+ f"Stage 1 reject: hybrid_confidence {hybrid_confidence:.4f} "
356
+ f"< HYBRID_FLOOR {HYBRID_FLOOR}."
357
+ )
358
+ else:
359
+ final_department = DEFAULT_DEPARTMENT
360
+ note = (
361
+ f"Stage 2 reject: hybrid_confidence={hybrid_confidence:.4f}, "
362
+ f"margin={margin:.4f}, entropy={entropy:.4f}."
363
+ )
364
+
365
+ best_details = department_details.get(recommended_department, {})
366
+ return {
367
+ "mode": mode,
368
+ "department": final_department,
369
+ "recommended_department": recommended_department,
370
+ "priority": priority,
371
+ "priority_confidence": priority_confidence,
372
+ "hybrid_confidence": hybrid_confidence,
373
+ "review": review,
374
+ "margin": margin,
375
+ "entropy": entropy,
376
+ "best_details": best_details,
377
+ "top_tag_votes": top_tag_votes,
378
+ "review_decision": review_decision,
379
+ "note": note.strip(),
380
+ }
381
+
382
+
383
+ LOG_COLUMNS = [
384
+ "ticket_id",
385
+ "timestamp",
386
+ "ticket_text",
387
+ "duplicate_flag",
388
+ "duplicate_score",
389
+ "routing_mode",
390
+ "department",
391
+ "base_routing_mode",
392
+ "requires_review",
393
+ "controlled_review_applied",
394
+ "department_confidence",
395
+ "classifier_confidence",
396
+ "semantic_similarity",
397
+ "raw_semantic_similarity",
398
+ "priority",
399
+ "priority_confidence",
400
+ "selected_tags",
401
+ "routing_score",
402
+ "hybrid_confidence",
403
+ "margin",
404
+ "entropy",
405
+ "review_percentile_threshold",
406
+ "review_fallback_threshold",
407
+ "prediction_latency_ms",
408
+ "explanation",
409
+ ]
410
+
411
+
412
+ def _ensure_log_header():
413
+ if not os.path.exists(LOG_PATH):
414
+ with open(LOG_PATH, "w", newline="", encoding="utf-8") as handle:
415
+ csv.writer(handle).writerow(LOG_COLUMNS)
416
+ return
417
+
418
+ with open(LOG_PATH, "r", newline="", encoding="utf-8") as handle:
419
+ existing_header = next(csv.reader(handle), [])
420
+
421
+ if existing_header != LOG_COLUMNS:
422
+ with open(LOG_PATH, "w", newline="", encoding="utf-8") as handle:
423
+ csv.writer(handle).writerow(LOG_COLUMNS)
424
+
425
+
426
+ def _append_log(row_dict):
427
+ _ensure_log_header()
428
+ with open(LOG_PATH, "a", newline="", encoding="utf-8") as handle:
429
+ csv.writer(handle).writerow([row_dict.get(column, "") for column in LOG_COLUMNS])
430
+
431
+
432
+ def process_ticket(text):
433
+ t0 = time.time()
434
+ ticket_id = str(uuid.uuid4())[:8]
435
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
436
+
437
+ routing_emb = encode_ticket_embedding(text, routing_sbert)
438
+ duplicate_emb = encode_ticket_embedding(text, duplicate_sbert)
439
+
440
+ best_match = duplicate_engine.find_best_match(duplicate_emb, k=20)
441
+ dup_score = (
442
+ float(best_match["similarity"])
443
+ if best_match is not None
444
+ else 0.0
445
+ )
446
+ dup_text = best_match.get("matched_text") if best_match is not None else None
447
+ is_dup = bool(
448
+ best_match is not None
449
+ and dup_score >= float(duplicate_engine.duplicate_threshold)
450
+ )
451
+
452
+ routing = route_ticket(routing_emb, text)
453
+ latency_ms = round((time.time() - t0) * 1000, 2)
454
+
455
+ mode = routing["mode"]
456
+ dept = routing["department"]
457
+ priority = routing["priority"]
458
+ priority_confidence = routing["priority_confidence"]
459
+ hybrid_confidence = routing["hybrid_confidence"]
460
+ review = routing["review"]
461
+ margin = routing["margin"]
462
+ entropy = routing["entropy"]
463
+ best_details = routing["best_details"]
464
+ top_tag_votes = routing["top_tag_votes"]
465
+ review_decision = routing["review_decision"]
466
+ note = routing["note"]
467
+
468
+ classifier_confidence = float(best_details.get("classifier_confidence", 0.0))
469
+ semantic_similarity = float(best_details.get("semantic_similarity", 0.0))
470
+ raw_semantic_similarity = float(best_details.get("raw_semantic_similarity", 0.0))
471
+ base_mode = str(review_decision.get("base_mode", mode))
472
+ review_reason = str(review_decision.get("reason", note))
473
+ percentile_threshold = float(
474
+ review_decision.get(
475
+ "percentile_threshold",
476
+ review_policy.get("percentile_threshold", 0.55),
477
+ )
478
+ )
479
+ fallback_threshold = float(
480
+ review_decision.get(
481
+ "fallback_threshold",
482
+ review_policy.get("fallback_threshold", 0.55),
483
+ )
484
+ )
485
+ controlled_review_applied = bool(
486
+ review_decision.get("forced_human_review", False)
487
+ )
488
+ recommended_department = routing.get("recommended_department")
489
+ tag_summary = ", ".join(
490
+ f"{vote['tag']} ({vote['score']:.2f})"
491
+ for vote in top_tag_votes[:3]
492
+ )
493
+
494
+ recommended_text = (
495
+ f" Recommended department before final policy: {recommended_department}."
496
+ if recommended_department and recommended_department != dept
497
+ else ""
498
+ )
499
+
500
+ if is_dup:
501
+ explanation = (
502
+ f"Duplicate detected (score={dup_score:.4f}). "
503
+ f"Original: {str(dup_text)[:100]}. "
504
+ f"Routing mode: {mode} (base_mode={base_mode}), "
505
+ f"final_department={dept}, hybrid_confidence={hybrid_confidence:.3f}, "
506
+ f"classifier_confidence={classifier_confidence:.3f}, "
507
+ f"semantic_similarity={semantic_similarity:.3f} "
508
+ f"(raw={raw_semantic_similarity:.3f}), margin={margin:.3f}, "
509
+ f"entropy={entropy:.3f}, controlled_review_applied={controlled_review_applied}, "
510
+ f"review_thresholds=(percentile={percentile_threshold:.3f}, "
511
+ f"fallback={fallback_threshold:.3f}).{recommended_text} {note}"
512
+ )
513
+ result = {
514
+ "ticket_id": ticket_id,
515
+ "status": "DUPLICATE",
516
+ "route": mode,
517
+ "department": dept,
518
+ "priority": priority,
519
+ "confidence": round(float(hybrid_confidence), 3),
520
+ "review": review,
521
+ "tags": tag_summary,
522
+ "message": (
523
+ f"Duplicate of: {str(dup_text)[:200]} (similarity={dup_score:.3f}). "
524
+ f"{note}"
525
+ ).strip(),
526
+ "latency": latency_ms,
527
+ }
528
+ else:
529
+ explanation = (
530
+ f"Ticket processed with final department {dept}. "
531
+ f"Predicted tags [{tag_summary}] produced routing mode {mode} "
532
+ f"(base_mode={base_mode}), hybrid_confidence={hybrid_confidence:.3f}, "
533
+ f"classifier_confidence={classifier_confidence:.3f}, "
534
+ f"semantic_similarity={semantic_similarity:.3f} "
535
+ f"(raw={raw_semantic_similarity:.3f}), margin={margin:.3f}, "
536
+ f"entropy={entropy:.3f}, controlled_review_applied={controlled_review_applied}, "
537
+ f"review_thresholds=(percentile={percentile_threshold:.3f}, "
538
+ f"fallback={fallback_threshold:.3f}).{recommended_text} {review_reason}"
539
+ )
540
+ result = {
541
+ "ticket_id": ticket_id,
542
+ "status": "NOT DUPLICATE",
543
+ "route": mode,
544
+ "department": dept,
545
+ "priority": priority,
546
+ "confidence": round(float(hybrid_confidence), 3),
547
+ "review": review,
548
+ "tags": tag_summary,
549
+ "message": note if note else "Ticket processed successfully",
550
+ "latency": latency_ms,
551
+ }
552
+
553
+ duplicate_engine.add_ticket(ticket_id, text, embedding=duplicate_emb)
554
+ _append_log(
555
+ {
556
+ "ticket_id": ticket_id,
557
+ "timestamp": timestamp,
558
+ "ticket_text": text,
559
+ "duplicate_flag": is_dup,
560
+ "duplicate_score": round(float(dup_score), 4),
561
+ "routing_mode": mode,
562
+ "department": dept,
563
+ "department_confidence": round(float(hybrid_confidence), 4),
564
+ "base_routing_mode": base_mode,
565
+ "requires_review": bool(review),
566
+ "controlled_review_applied": controlled_review_applied,
567
+ "classifier_confidence": round(float(classifier_confidence), 4),
568
+ "semantic_similarity": round(float(semantic_similarity), 4),
569
+ "raw_semantic_similarity": round(float(raw_semantic_similarity), 4),
570
+ "priority": priority,
571
+ "priority_confidence": (
572
+ round(float(priority_confidence), 4)
573
+ if np.isfinite(priority_confidence)
574
+ else ""
575
+ ),
576
+ "selected_tags": tag_summary,
577
+ "routing_score": round(float(hybrid_confidence), 4),
578
+ "hybrid_confidence": round(float(hybrid_confidence), 4),
579
+ "margin": round(float(margin), 4),
580
+ "entropy": round(float(entropy), 4),
581
+ "review_percentile_threshold": round(float(percentile_threshold), 4),
582
+ "review_fallback_threshold": round(float(fallback_threshold), 4),
583
+ "prediction_latency_ms": latency_ms,
584
+ "explanation": explanation,
585
+ }
586
+ )
587
+
588
+ return result
589
+
590
+
591
+ def ui_process(text):
592
+ if not text or not text.strip():
593
+ return ("Please enter ticket text", "", "", "", "", "", "", "", "")
594
+
595
+ result = process_ticket(text.strip())
596
+ conf_pct = int(result["confidence"] * 100)
597
+
598
+ if result["route"] == "HUMAN_REVIEW":
599
+ review_badge = "Human review required"
600
+ elif result["route"] == "AUTO_ROUTE_FLAGGED":
601
+ review_badge = "QA review required"
602
+ else:
603
+ review_badge = "No"
604
+
605
+ priority_map = {
606
+ "critical": "Critical",
607
+ "high": "High",
608
+ "medium": "Medium",
609
+ "low": "Low",
610
+ }
611
+ priority_display = priority_map.get(
612
+ result["priority"].lower(),
613
+ result["priority"],
614
+ )
615
+
616
+ route_map = {
617
+ "AUTO_ROUTE": "Auto-Routed",
618
+ "AUTO_ROUTE_FLAGGED": "Auto-Routed + Flagged",
619
+ "HUMAN_REVIEW": "Human Review Required",
620
+ }
621
+ route_display = route_map.get(result["route"], result["route"])
622
+ dept_display = result["department"].replace("_", " ")
623
+
624
+ return (
625
+ result["status"],
626
+ result["ticket_id"],
627
+ route_display,
628
+ dept_display,
629
+ priority_display,
630
+ f"{conf_pct}%",
631
+ result["tags"],
632
+ review_badge,
633
+ result["message"],
634
+ )
635
+
636
+
637
+ _BG_IMAGE_PATH = APP_DIR / "BG_Img-2.png"
638
+ _BG_DATA_URI = (
639
+ f"data:image/png;base64,{base64.b64encode(_BG_IMAGE_PATH.read_bytes()).decode('ascii')}"
640
+ if _BG_IMAGE_PATH.exists()
641
+ else ""
642
+ )
643
+
644
+ _BG_CSS = (
645
+ f"""
646
+ body, gradio-app {{
647
+ background-image: url('{_BG_DATA_URI}') !important;
648
+ background-size: cover !important;
649
+ background-position: center center !important;
650
+ background-attachment: fixed !important;
651
+ background-repeat: no-repeat !important;
652
+ }}
653
+ """
654
+ if _BG_DATA_URI
655
+ else ""
656
+ )
657
+
658
+ CSS = _BG_CSS + """
659
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
660
+
661
+ * { font-family: 'Inter', sans-serif !important; }
662
+
663
+ .gradio-container {
664
+ max-width: 960px !important;
665
+ margin: 0 auto !important;
666
+ background: transparent !important;
667
+ }
668
+
669
+ .app-header {
670
+ text-align: center;
671
+ padding: 1.5rem 1rem;
672
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%);
673
+ border-radius: 16px;
674
+ margin-bottom: 1.5rem;
675
+ box-shadow: 0 8px 32px rgba(79, 70, 229, 0.3);
676
+ }
677
+ .app-header h1 {
678
+ color: white !important;
679
+ font-size: 1.75rem !important;
680
+ font-weight: 700 !important;
681
+ margin: 0 !important;
682
+ letter-spacing: -0.02em;
683
+ }
684
+ .app-header p {
685
+ color: rgba(255,255,255,0.85) !important;
686
+ font-size: 0.95rem !important;
687
+ margin: 0.4rem 0 0 0 !important;
688
+ }
689
+
690
+ .result-card {
691
+ background: linear-gradient(145deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02));
692
+ border: 1px solid rgba(255,255,255,0.1);
693
+ border-radius: 12px;
694
+ padding: 0.25rem;
695
+ }
696
+
697
+ .status-box textarea, .status-box input {
698
+ font-weight: 600 !important;
699
+ font-size: 1rem !important;
700
+ }
701
+
702
+ .submit-btn {
703
+ background: linear-gradient(135deg, #4f46e5, #7c3aed) !important;
704
+ border: none !important;
705
+ color: white !important;
706
+ font-weight: 600 !important;
707
+ font-size: 1rem !important;
708
+ padding: 0.75rem 2rem !important;
709
+ border-radius: 10px !important;
710
+ box-shadow: 0 4px 16px rgba(79, 70, 229, 0.4) !important;
711
+ transition: all 0.3s ease !important;
712
+ }
713
+ .submit-btn:hover {
714
+ transform: translateY(-2px) !important;
715
+ box-shadow: 0 6px 24px rgba(79, 70, 229, 0.5) !important;
716
+ }
717
+
718
+ .clear-btn {
719
+ border: 1px solid rgba(255,255,255,0.2) !important;
720
+ border-radius: 10px !important;
721
+ font-weight: 500 !important;
722
+ }
723
+
724
+ .stats-row {
725
+ text-align: center;
726
+ padding: 0.75rem;
727
+ background: rgba(79, 70, 229, 0.08);
728
+ border-radius: 10px;
729
+ margin-top: 0.5rem;
730
+ font-size: 0.85rem;
731
+ color: #a5b4fc;
732
+ }
733
+
734
+ footer { display: none !important; }
735
+ """
736
+
737
+
738
+ EXAMPLES = [
739
+ [
740
+ "My laptop screen is flickering and sometimes goes completely black. "
741
+ "I've tried restarting but the issue persists after login."
742
+ ],
743
+ [
744
+ "I cannot access the company VPN from my home network. It keeps showing "
745
+ "authentication failed error even though my password is correct."
746
+ ],
747
+ [
748
+ "We need to upgrade our database server as the current one is running out "
749
+ "of storage space and response times have increased significantly."
750
+ ],
751
+ [
752
+ "I was charged twice for my last month's subscription. Please process a "
753
+ "refund for the duplicate charge."
754
+ ],
755
+ [
756
+ "The email server has been down since this morning. No one in the office "
757
+ "can send or receive emails. This is critical!"
758
+ ],
759
+ [
760
+ "Can you provide training materials for the new CRM software that was "
761
+ "deployed last week?"
762
+ ],
763
+ ]
764
+
765
+
766
+ with gr.Blocks(
767
+ css=CSS,
768
+ theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
769
+ title="Ticket Auto-Routing System",
770
+ ) as app:
771
+ gr.HTML(
772
+ """
773
+ <div class="app-header">
774
+ <h1>Intelligent Ticket Auto-Routing System</h1>
775
+ <p>Domain-Adaptive Multi-Label and Duplicate-Aware Ticket Auto-Routing Framework</p>
776
+ </div>
777
+ """
778
+ )
779
+
780
+ with gr.Row():
781
+ with gr.Column(scale=1):
782
+ ticket_input = gr.Textbox(
783
+ label="Ticket Description",
784
+ placeholder="Describe the support issue in detail...",
785
+ lines=6,
786
+ max_lines=12,
787
+ )
788
+ with gr.Row():
789
+ submit_btn = gr.Button(
790
+ "Process Ticket",
791
+ variant="primary",
792
+ elem_classes=["submit-btn"],
793
+ )
794
+ clear_btn = gr.ClearButton(
795
+ value="Clear",
796
+ elem_classes=["clear-btn"],
797
+ )
798
+
799
+ gr.Examples(
800
+ examples=EXAMPLES,
801
+ inputs=ticket_input,
802
+ label="Try these examples",
803
+ )
804
+
805
+ with gr.Column(scale=1):
806
+ with gr.Group(elem_classes=["result-card"]):
807
+ dup_status = gr.Textbox(
808
+ label="Duplicate Status",
809
+ interactive=False,
810
+ elem_classes=["status-box"],
811
+ )
812
+ ticket_id = gr.Textbox(label="Ticket ID", interactive=False)
813
+
814
+ with gr.Group(elem_classes=["result-card"]):
815
+ with gr.Row():
816
+ route_mode = gr.Textbox(
817
+ label="Routing Mode",
818
+ interactive=False,
819
+ )
820
+ department = gr.Textbox(
821
+ label="Department",
822
+ interactive=False,
823
+ )
824
+ with gr.Row():
825
+ priority = gr.Textbox(label="Priority", interactive=False)
826
+ confidence = gr.Textbox(
827
+ label="Hybrid Confidence",
828
+ interactive=False,
829
+ )
830
+
831
+ with gr.Group(elem_classes=["result-card"]):
832
+ tags = gr.Textbox(label="Predicted Tags", interactive=False)
833
+ needs_review = gr.Textbox(label="Needs Review", interactive=False)
834
+ message = gr.Textbox(
835
+ label="Details",
836
+ interactive=False,
837
+ lines=2,
838
+ )
839
+
840
+ gr.HTML(
841
+ f"""
842
+ <div class="stats-row">
843
+ Database: <strong>{duplicate_engine.index_size:,}</strong> tickets indexed
844
+ &nbsp;|&nbsp;
845
+ <strong>{len(tag_list)}</strong> tag categories
846
+ &nbsp;|&nbsp;
847
+ <strong>{len(dept_prototypes)}</strong> departments
848
+ </div>
849
+ """
850
+ )
851
+
852
+ outputs = [
853
+ dup_status,
854
+ ticket_id,
855
+ route_mode,
856
+ department,
857
+ priority,
858
+ confidence,
859
+ tags,
860
+ needs_review,
861
+ message,
862
+ ]
863
+
864
+ submit_btn.click(fn=ui_process, inputs=ticket_input, outputs=outputs)
865
+ ticket_input.submit(fn=ui_process, inputs=ticket_input, outputs=outputs)
866
+ clear_btn.add([ticket_input] + outputs)
867
+
868
+
869
+ if __name__ == "__main__":
870
+ app.launch()