GaetanoParente commited on
Commit
96bc481
·
1 Parent(s): a4a884a

migliorata visualizzazione BPO Dispatcher

Browse files
.gitignore CHANGED
@@ -1 +1,2 @@
1
  __pycache__/
 
 
1
  __pycache__/
2
+ .venv
app.py CHANGED
@@ -128,7 +128,7 @@ def bpo_dispatch_logic(text, request: gr.Request):
128
  start_time = time.time()
129
 
130
  try:
131
- intent, urgency, entities = predict_bpo_ticket(text)
132
 
133
  if intent is None:
134
  raise gr.Error("Errore nel modello BPO. Verifica i log.")
@@ -145,6 +145,11 @@ def bpo_dispatch_logic(text, request: gr.Request):
145
 
146
  html_output = utils.render_ner_html(entities)
147
 
 
 
 
 
 
148
  elapsed_time = time.time() - start_time
149
  logger.log_interaction(
150
  request=request,
@@ -154,7 +159,7 @@ def bpo_dispatch_logic(text, request: gr.Request):
154
  execution_time=elapsed_time
155
  )
156
 
157
- return intent, urgency, action, html_output
158
 
159
  except Exception as e:
160
  raise gr.Error(f"Errore nell'analisi: {str(e)}")
@@ -190,8 +195,6 @@ with gr.Blocks(title="NGT AI Platform", theme=theme, css_paths="style.css") as d
190
  gr.Image(value="data/icon.png", show_label=False, show_download_button=False, show_share_button=False, container=False, show_fullscreen_button=False, interactive=False, height=80, width=80)
191
  with gr.Column(scale=1, elem_classes="header-text-col"):
192
  gr.Markdown("""<h1>AI Platform</h1><div class='subheader'>Advanced Machine Learning Solutions</div>""")
193
-
194
- # --- BPO INTELLIGENT DISPATCHER ---
195
  with gr.Tab("🧩 BPO Dispatcher") as tab_bpo:
196
  gr.Markdown("""
197
  # 🧩 Intelligent Ticket Routing & NER
@@ -205,24 +208,32 @@ with gr.Blocks(title="NGT AI Platform", theme=theme, css_paths="style.css") as d
205
  analyze_btn_bpo = gr.Button("⚡ Analizza Richiesta", variant="primary")
206
  gr.HTML("""
207
  <div class='model-card'>
208
- <strong>🛠️ Model Architecture:</strong> NGT-BERT-Custom (DistilBERT)<br>
209
- <strong>📚 Training Data:</strong> Synthetic BPO Dataset (2025)<br>
210
- <strong>🎯 Tasks:</strong> Intent Classification (Multi-class), Entity Extraction (NER)
211
  </div>
212
  """)
213
-
214
  # OUTPUT
215
  with gr.Column(scale=1):
216
  with gr.Group():
217
  gr.Markdown("#### 📋 Analisi Processata", elem_classes="h4-margin")
218
- bpo_intent_output = gr.Label(num_top_classes=3, label="Intento Rilevato")
 
 
219
  with gr.Row():
220
  bpo_urgency_output = gr.Textbox(label="Livello Urgenza", scale=1)
221
  bpo_action_output = gr.Textbox(label="Azione Consigliata (Auto)", scale=1)
222
 
 
 
 
 
 
 
223
  gr.Markdown("#### 🔍 Dati Estratti (NER)", elem_classes="h4-margin")
224
  bpo_ner_output = gr.HTML(label="Visualizzazione Entità")
225
-
226
  gr.Examples(
227
  examples=[
228
  ["Buongiorno, vi scrivo perché la fattura n. 99283 del mese scorso è sbagliata. Non ho consumato così tanto. Il mio codice cliente è 4599201. Attendo rettifica urgente."],
@@ -235,7 +246,7 @@ with gr.Blocks(title="NGT AI Platform", theme=theme, css_paths="style.css") as d
235
  analyze_btn_bpo.click(
236
  bpo_dispatch_logic,
237
  inputs=bpo_input,
238
- outputs=[bpo_intent_output, bpo_urgency_output, bpo_action_output, bpo_ner_output]
239
  )
240
 
241
  # --- AI FORECASTER ---
@@ -468,25 +479,26 @@ with gr.Blocks(title="NGT AI Platform", theme=theme, css_paths="style.css") as d
468
  return (
469
  None, # 1. bpo_input
470
  None, # 2. bpo_intent
471
- None, # 3. bpo_urgency
472
- None, # 4. bpo_action
473
- None, # 5. bpo_ner
474
- None, # 6. forecast_file
475
- None, # 7. forecast_plot
476
- None, # 8. forecast_stats
477
- None, # 9. image_input
478
- None, # 10. output_label
479
- None, # 11. image_input_dr
480
- None, # 12. output_dr_diag
481
- None, # 13. output_dr_prob
482
- None, # 14. multi_input
483
- None, # 15. multi_output
484
- None, # 16. sentiment_input
485
- None # 17. sentiment_output
 
486
  )
487
 
488
  reset_outputs = [
489
- bpo_input, bpo_intent_output, bpo_urgency_output, bpo_action_output, bpo_ner_output,
490
  forecast_file, forecast_plot, forecast_stats,
491
  image_input, output_label,
492
  image_input_dr, output_dr_diagnosis, output_dr_prob,
@@ -545,6 +557,6 @@ with gr.Blocks(title="NGT AI Platform", theme=theme, css_paths="style.css") as d
545
  if __name__ == "__main__":
546
  demo.launch(
547
  server_name="0.0.0.0",
548
- server_port=7860,
549
  allowed_paths=["data"]
550
  )
 
128
  start_time = time.time()
129
 
130
  try:
131
+ intent, urgency, entities, sentiment = predict_bpo_ticket(text)
132
 
133
  if intent is None:
134
  raise gr.Error("Errore nel modello BPO. Verifica i log.")
 
145
 
146
  html_output = utils.render_ner_html(entities)
147
 
148
+ sentiment_formatted = {
149
+ "Positivo": sentiment.get("positive", 0.0),
150
+ "Negativo": sentiment.get("negative", 0.0)
151
+ }
152
+
153
  elapsed_time = time.time() - start_time
154
  logger.log_interaction(
155
  request=request,
 
159
  execution_time=elapsed_time
160
  )
161
 
162
+ return intent, sentiment_formatted, urgency, action, html_output
163
 
164
  except Exception as e:
165
  raise gr.Error(f"Errore nell'analisi: {str(e)}")
 
195
  gr.Image(value="data/icon.png", show_label=False, show_download_button=False, show_share_button=False, container=False, show_fullscreen_button=False, interactive=False, height=80, width=80)
196
  with gr.Column(scale=1, elem_classes="header-text-col"):
197
  gr.Markdown("""<h1>AI Platform</h1><div class='subheader'>Advanced Machine Learning Solutions</div>""")
 
 
198
  with gr.Tab("🧩 BPO Dispatcher") as tab_bpo:
199
  gr.Markdown("""
200
  # 🧩 Intelligent Ticket Routing & NER
 
208
  analyze_btn_bpo = gr.Button("⚡ Analizza Richiesta", variant="primary")
209
  gr.HTML("""
210
  <div class='model-card'>
211
+ <strong>🛠️ Model Architecture:</strong> NGT-BERT-Custom (DistilBERT) + NGT Sentiment Classifier<br>
212
+ <strong>📚 Training Data:</strong> Synthetic BPO Dataset & Helpdesk Sentiment Dataset<br>
213
+ <strong>🎯 Tasks:</strong> Intent Classification, Entity Extraction, & Sentiment-Driven Urgency Routing
214
  </div>
215
  """)
216
+
217
  # OUTPUT
218
  with gr.Column(scale=1):
219
  with gr.Group():
220
  gr.Markdown("#### 📋 Analisi Processata", elem_classes="h4-margin")
221
+ with gr.Row():
222
+ bpo_intent_output = gr.Label(num_top_classes=3, label="Intento Rilevato", scale=1)
223
+ bpo_sentiment_output = gr.Label(num_top_classes=2, label="Sentiment Rilevato (Sentiment Analysis)", scale=1)
224
  with gr.Row():
225
  bpo_urgency_output = gr.Textbox(label="Livello Urgenza", scale=1)
226
  bpo_action_output = gr.Textbox(label="Azione Consigliata (Auto)", scale=1)
227
 
228
+ gr.HTML("""
229
+ <div class='sentiment-info-badge'>
230
+ 💡 <strong>Integrazione Sentiment Analysis:</strong> L'urgenza e l'assegnazione automatica del ticket sono calibrate in tempo reale integrando il modello di <em>Sentiment Analysis</em> per rilevare lo stato emotivo del cliente (es. minacce legali o toni irritati).
231
+ </div>
232
+ """)
233
+
234
  gr.Markdown("#### 🔍 Dati Estratti (NER)", elem_classes="h4-margin")
235
  bpo_ner_output = gr.HTML(label="Visualizzazione Entità")
236
+
237
  gr.Examples(
238
  examples=[
239
  ["Buongiorno, vi scrivo perché la fattura n. 99283 del mese scorso è sbagliata. Non ho consumato così tanto. Il mio codice cliente è 4599201. Attendo rettifica urgente."],
 
246
  analyze_btn_bpo.click(
247
  bpo_dispatch_logic,
248
  inputs=bpo_input,
249
+ outputs=[bpo_intent_output, bpo_sentiment_output, bpo_urgency_output, bpo_action_output, bpo_ner_output]
250
  )
251
 
252
  # --- AI FORECASTER ---
 
479
  return (
480
  None, # 1. bpo_input
481
  None, # 2. bpo_intent
482
+ None, # 3. bpo_sentiment
483
+ None, # 4. bpo_urgency
484
+ None, # 5. bpo_action
485
+ None, # 6. bpo_ner
486
+ None, # 7. forecast_file
487
+ None, # 8. forecast_plot
488
+ None, # 9. forecast_stats
489
+ None, # 10. image_input
490
+ None, # 11. output_label
491
+ None, # 12. image_input_dr
492
+ None, # 13. output_dr_diag
493
+ None, # 14. output_dr_prob
494
+ None, # 15. multi_input
495
+ None, # 16. multi_output
496
+ None, # 17. sentiment_input
497
+ None # 18. sentiment_output
498
  )
499
 
500
  reset_outputs = [
501
+ bpo_input, bpo_intent_output, bpo_sentiment_output, bpo_urgency_output, bpo_action_output, bpo_ner_output,
502
  forecast_file, forecast_plot, forecast_stats,
503
  image_input, output_label,
504
  image_input_dr, output_dr_diagnosis, output_dr_prob,
 
557
  if __name__ == "__main__":
558
  demo.launch(
559
  server_name="0.0.0.0",
560
+ server_port=7862,
561
  allowed_paths=["data"]
562
  )
data/logs/access_logs.csv CHANGED
@@ -0,0 +1,2 @@
 
 
 
1
+ timestamp,session_id,module,action,ip_address,user_agent,language,input_size,input_text,processing_time
2
+ 2026-06-05T12:50:51.155066+02:00,59c1bbc1,BPO Dispatcher,Prediction,127.0.0.1,"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",it-IT,166 chars,"Buongiorno, vi scrivo perché la fattura n. 99283 del mese scorso è sbagliata. Non ho consumato così tanto. Il mio codice cliente è 4599201. Attendo rettifica urgente.",2.6220s
modules/bpo_dispatcher.py CHANGED
@@ -121,7 +121,7 @@ class BPODispatcher:
121
  add_entity(ent.text, "AZIENDA", ent.start_char, ent.end_char)
122
  return entities
123
 
124
- def _calculate_smart_urgency(self, text, intent_label):
125
  """
126
  MATRICE DI URGENZA (Intent + Sentiment)
127
  Combina la gravità del problema con lo stato d'animo del cliente.
@@ -129,18 +129,6 @@ class BPODispatcher:
129
  urgency = "Bassa"
130
  text_lower = text.lower()
131
 
132
- # Analisi Sentiment
133
- sentiment_score_neg = 0.0
134
- sentiment_score_pos = 0.0
135
-
136
- if binary_classification:
137
- try:
138
- sent_result = binary_classification(text)
139
- sentiment_score_neg = float(sent_result.get('negative', 0.0))
140
- sentiment_score_pos = float(sent_result.get('positive', 0.0))
141
- except Exception:
142
- sentiment_score_neg = 0.5 # Fallback neutro
143
-
144
  # CASO CHURN (Disdetta) -> Sempre Critico
145
  # Indipendentemente dal tono, se uno vuole andare via è priorità assoluta.
146
  if intent_label == "Retention / Churn Risk":
@@ -187,8 +175,8 @@ class BPODispatcher:
187
  return urgency
188
 
189
  def predict(self, text):
190
- if self.model is None: return None, "Errore", []
191
- if not text.strip(): return None, "Vuoto", []
192
 
193
  # Intent Classification (BERT)
194
  inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
@@ -202,13 +190,29 @@ class BPODispatcher:
202
  top_idx = torch.max(probs, dim=-1)[1].item()
203
  predicted_label = LABELS_MAP[top_idx]
204
 
 
 
 
 
 
 
 
 
 
 
 
205
  # Urgenza (AI + Sentiment + Rules)
206
- urgency = self._calculate_smart_urgency(text, predicted_label)
207
 
208
  # NER Extraction
209
  entities = self._extract_smart_entities(text)
210
 
211
- return label_output, urgency, entities
 
 
 
 
 
212
 
213
  dispatcher = BPODispatcher()
214
  def predict_bpo_ticket(text): return dispatcher.predict(text)
 
121
  add_entity(ent.text, "AZIENDA", ent.start_char, ent.end_char)
122
  return entities
123
 
124
+ def _calculate_smart_urgency(self, text, intent_label, sentiment_score_neg, sentiment_score_pos):
125
  """
126
  MATRICE DI URGENZA (Intent + Sentiment)
127
  Combina la gravità del problema con lo stato d'animo del cliente.
 
129
  urgency = "Bassa"
130
  text_lower = text.lower()
131
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  # CASO CHURN (Disdetta) -> Sempre Critico
133
  # Indipendentemente dal tono, se uno vuole andare via è priorità assoluta.
134
  if intent_label == "Retention / Churn Risk":
 
175
  return urgency
176
 
177
  def predict(self, text):
178
+ if self.model is None: return None, "Errore", [], {}
179
+ if not text.strip(): return None, "Vuoto", [], {}
180
 
181
  # Intent Classification (BERT)
182
  inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
 
190
  top_idx = torch.max(probs, dim=-1)[1].item()
191
  predicted_label = LABELS_MAP[top_idx]
192
 
193
+ # Analisi Sentiment
194
+ sentiment_score_neg = 0.5
195
+ sentiment_score_pos = 0.5
196
+ if binary_classification:
197
+ try:
198
+ sent_result = binary_classification(text)
199
+ sentiment_score_neg = float(sent_result.get('negative', 0.5))
200
+ sentiment_score_pos = float(sent_result.get('positive', 0.5))
201
+ except Exception:
202
+ pass
203
+
204
  # Urgenza (AI + Sentiment + Rules)
205
+ urgency = self._calculate_smart_urgency(text, predicted_label, sentiment_score_neg, sentiment_score_pos)
206
 
207
  # NER Extraction
208
  entities = self._extract_smart_entities(text)
209
 
210
+ sentiment_output = {
211
+ "negative": sentiment_score_neg,
212
+ "positive": sentiment_score_pos
213
+ }
214
+
215
+ return label_output, urgency, entities, sentiment_output
216
 
217
  dispatcher = BPODispatcher()
218
  def predict_bpo_ticket(text): return dispatcher.predict(text)
modules/image_classification.py CHANGED
@@ -30,13 +30,13 @@ def load_resources():
30
  except Exception as e:
31
  print(f"❌ Errore caricamento modello X-Ray: {e}")
32
 
33
- # Inizializziamo subito
34
- load_resources()
35
-
36
  def image_classification(image_array):
37
  """
38
  Analizza un'immagine radiografica (numpy array) e restituisce le probabilità.
39
  """
 
 
 
40
  if model is None:
41
  return {"Errore": "Modello non disponibile (Verifica il percorso file)"}
42
  if image_array is None:
 
30
  except Exception as e:
31
  print(f"❌ Errore caricamento modello X-Ray: {e}")
32
 
 
 
 
33
  def image_classification(image_array):
34
  """
35
  Analizza un'immagine radiografica (numpy array) e restituisce le probabilità.
36
  """
37
+ global model
38
+ if model is None:
39
+ load_resources()
40
  if model is None:
41
  return {"Errore": "Modello non disponibile (Verifica il percorso file)"}
42
  if image_array is None:
modules/multilabel_classification.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import json
3
  from keras.models import load_model
4
  from keras_preprocessing.sequence import pad_sequences
5
- from keras.preprocessing.text import tokenizer_from_json
6
 
7
  BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Risale alla root
8
  MODEL_PATH = os.path.join(BASE_DIR, 'data', 'model', 'multi-classification.h5')
@@ -12,9 +12,10 @@ CLASS_NAMES = ['Economia', 'Politica', 'Scienza_e_tecnica', 'Sport', 'Storia']
12
 
13
  # Caricamento Singleton (lo carichiamo una volta sola)
14
  model = None
 
15
 
16
  def load_resources():
17
- global model
18
  if model is None and os.path.exists(MODEL_PATH):
19
  try:
20
  # Carica Tokenizer
@@ -29,10 +30,10 @@ def load_resources():
29
  print(f"Errore caricamento risorse MultiLabel: {e}")
30
  return None, None
31
 
32
- # Carichiamo una volta sola all'avvio (Singleton) per velocità
33
- model, tokenizer = load_resources()
34
-
35
  def multi_classification(text):
 
 
 
36
  if model is None or tokenizer is None:
37
  return {"Errore": "Modello non caricato"}
38
  try:
 
2
  import json
3
  from keras.models import load_model
4
  from keras_preprocessing.sequence import pad_sequences
5
+ from keras_preprocessing.text import tokenizer_from_json
6
 
7
  BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Risale alla root
8
  MODEL_PATH = os.path.join(BASE_DIR, 'data', 'model', 'multi-classification.h5')
 
12
 
13
  # Caricamento Singleton (lo carichiamo una volta sola)
14
  model = None
15
+ tokenizer = None
16
 
17
  def load_resources():
18
+ global model, tokenizer
19
  if model is None and os.path.exists(MODEL_PATH):
20
  try:
21
  # Carica Tokenizer
 
30
  print(f"Errore caricamento risorse MultiLabel: {e}")
31
  return None, None
32
 
 
 
 
33
  def multi_classification(text):
34
+ global model, tokenizer
35
+ if model is None or tokenizer is None:
36
+ load_resources()
37
  if model is None or tokenizer is None:
38
  return {"Errore": "Modello non caricato"}
39
  try:
modules/retina.py CHANGED
@@ -19,11 +19,11 @@ def load_resources():
19
  except Exception as e:
20
  print(f"❌ Errore caricamento modello Retina: {e}")
21
 
22
- # Caricamento all'avvio
23
- load_resources()
24
-
25
  def predict_diabetic_retinopathy(image_array):
26
  # Restituiamo sempre DUE valori (Diagnosi, Percentuale) anche in caso di errore
 
 
 
27
  if model is None:
28
  return "❌ Errore: Modello non trovato", "0%"
29
 
 
19
  except Exception as e:
20
  print(f"❌ Errore caricamento modello Retina: {e}")
21
 
 
 
 
22
  def predict_diabetic_retinopathy(image_array):
23
  # Restituiamo sempre DUE valori (Diagnosi, Percentuale) anche in caso di errore
24
+ global model
25
+ if model is None:
26
+ load_resources()
27
  if model is None:
28
  return "❌ Errore: Modello non trovato", "0%"
29
 
modules/utilities/logger.py CHANGED
@@ -42,14 +42,27 @@ if not LOG_FILE.exists() or LOG_FILE.stat().st_size == 0:
42
  "ip_address", "user_agent", "language", "input_size", "input_text" ,"processing_time"
43
  ])
44
 
45
- scheduler = CommitScheduler(
46
- repo_id=DATASET_REPO_ID,
47
- repo_type="dataset",
48
- folder_path=LOG_DIR,
49
- path_in_repo="logs",
50
- every=TIME,
51
- token=HF_TOKEN
52
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  def log_interaction(request, module_name, action, input_data=None, execution_time=0.0):
55
  try:
 
42
  "ip_address", "user_agent", "language", "input_size", "input_text" ,"processing_time"
43
  ])
44
 
45
+ class DummyScheduler:
46
+ def __init__(self):
47
+ class Lock:
48
+ def __enter__(self): pass
49
+ def __exit__(self, exc_type, exc_val, exc_tb): pass
50
+ self.lock = Lock()
51
+
52
+ try:
53
+ scheduler = CommitScheduler(
54
+ repo_id=DATASET_REPO_ID,
55
+ repo_type="dataset",
56
+ folder_path=LOG_DIR,
57
+ path_in_repo="logs",
58
+ every=TIME,
59
+ token=HF_TOKEN
60
+ )
61
+ print("✅ CommitScheduler caricato con successo.")
62
+ except Exception as e:
63
+ print(f"⚠️ Impossibile inizializzare CommitScheduler: {e}")
64
+ print("ℹ️ I log verranno salvati solo in locale.")
65
+ scheduler = DummyScheduler()
66
 
67
  def log_interaction(request, module_name, action, input_data=None, execution_time=0.0):
68
  try:
modules/utilities/utils.py CHANGED
@@ -1,8 +1,8 @@
1
  import json
2
  import gradio as gr
3
  import os
4
- from keras.preprocessing.text import Tokenizer
5
- from keras.preprocessing.text import tokenizer_from_json
6
 
7
  def load_doc(filename):
8
  # open the file as read only
 
1
  import json
2
  import gradio as gr
3
  import os
4
+ from keras_preprocessing.text import Tokenizer
5
+ from keras_preprocessing.text import tokenizer_from_json
6
 
7
  def load_doc(filename):
8
  # open the file as read only
style.css CHANGED
@@ -180,4 +180,21 @@ button.primary:hover { filter: brightness(1.1); box-shadow: 0 4px 15px rgba(139,
180
  }
181
 
182
  }
183
- footer {visibility: hidden}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
  }
183
+ footer {visibility: hidden}
184
+
185
+ /* Sentiment info badge for BPO Dispatcher */
186
+ .sentiment-info-badge {
187
+ background: #f0fdf4;
188
+ border: 1px solid #bbf7d0;
189
+ border-radius: 8px;
190
+ padding: 12px;
191
+ font-size: 0.9em;
192
+ color: #166534;
193
+ margin-top: 15px;
194
+ margin-bottom: 15px;
195
+ line-height: 1.4;
196
+ }
197
+
198
+ .sentiment-info-badge strong {
199
+ color: #14532d !important;
200
+ }