AshenClock commited on
Commit
b1d4913
·
verified ·
1 Parent(s): 177b586

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -150
app.py CHANGED
@@ -12,97 +12,107 @@ import re
12
  # CONFIGURAZIONE LOGGING
13
  # ---------------------------------------------------------------------------
14
  logging.basicConfig(
15
- level=logging.DEBUG, # DEBUG per un log più dettagliato
16
  format="%(asctime)s - %(levelname)s - %(message)s",
17
  handlers=[logging.FileHandler("app.log"), logging.StreamHandler()]
18
  )
19
  logger = logging.getLogger(__name__)
20
 
21
- # Categorie di zero-shot classification
22
- CANDIDATE_LABELS = ["domanda_museo", "small_talk", "fuori_contesto"]
 
 
23
  HF_API_KEY = os.getenv("HF_API_KEY")
24
- HF_MODEL = "meta-llama/Llama-3.3-70B-Instruct" # modello per query SPARQL e risposte
25
- ZERO_SHOT_MODEL = "facebook/bart-large-mnli" # modello per zero-shot classification
26
-
27
  if not HF_API_KEY:
 
28
  logger.error("HF_API_KEY non impostata.")
29
  raise EnvironmentError("HF_API_KEY non impostata.")
30
 
 
 
 
 
 
 
 
 
 
31
  # ---------------------------------------------------------------------------
32
- # INIZIALIZZIAMO IL CLIENT PER ZERO-SHOT
33
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
34
  try:
35
- logger.info("Inizializzazione del client per Zero-Shot Classification.")
36
- client_cls = InferenceClient(
37
  token=HF_API_KEY,
38
- model=ZERO_SHOT_MODEL
 
 
 
 
 
39
  )
40
- logger.info("Client zero-shot creato con successo.")
41
  except Exception as ex:
42
- logger.error(f"Errore nell'inizializzazione del client zero-shot: {ex}")
43
- raise ex
44
 
45
  # ---------------------------------------------------------------------------
46
- # FUNZIONE DI CLASSIFICAZIONE
47
  # ---------------------------------------------------------------------------
48
- def classify_message_inference_api(text: str) -> str:
49
- """
50
- Usa client_cls.zero_shot_classification(...) per classificare
51
- 'domanda_museo', 'small_talk' o 'fuori_contesto'.
52
- Restituisce la label top.
53
- """
54
- try:
55
- hypothesis_template = "Questa domanda è inerente all'arte o all'ontologia di un museo ({}), oppure no?"
56
-
57
- # multi_label=False => elegge UNA sola label top
58
- results = client_cls.zero_shot_classification(
59
- text=text,
60
- candidate_labels=CANDIDATE_LABELS,
61
- multi_label=False,
62
- hypothesis_template=hypothesis_template
63
- )
64
- # results è una lista di ZeroShotClassificationOutputElement
65
- # es: [ZeroShotClassificationOutputElement(label='domanda_museo', score=0.85), ...]
66
- top_label = results[0].label
67
- top_score = results[0].score
68
- logger.info(f"[ZeroShot] top_label={top_label}, score={top_score}")
69
- return top_label
70
- except Exception as e:
71
- logger.error(f"Errore nella zero-shot classification: {e}")
72
- return "fuori_contesto" # fallback in caso di errore
73
-
74
- # Inizializziamo la nostra ontologia
75
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
76
- RDF_FILE = os.path.join(BASE_DIR, "Ontologia_corretto.rdf")
77
- client_cls = InferenceClient(token=HF_API_KEY)
78
  ontology_graph = rdflib.Graph()
79
  try:
80
- # L'ontologia è in formato RDF/XML
81
  logger.info(f"Caricamento ontologia da file: {RDF_FILE}")
 
82
  ontology_graph.parse(RDF_FILE, format="xml")
83
  logger.info("Ontologia RDF caricata correttamente (formato XML).")
84
  except Exception as e:
85
  logger.error(f"Errore nel caricamento dell'ontologia: {e}")
86
  raise e
87
-
88
  # ---------------------------------------------------------------------------
89
- # DEFINIZIONE DELL'APP FASTAPI
90
  # ---------------------------------------------------------------------------
91
- app = FastAPI()
92
-
93
- # Modello di request
94
  class AssistantRequest(BaseModel):
 
 
 
 
 
 
 
95
  message: str
96
  max_tokens: int = 512
97
  temperature: float = 0.5
98
 
99
  # ---------------------------------------------------------------------------
100
- # FUNZIONI DI SUPPORTO (Prompts, validazione SPARQL, correzione)
101
  # ---------------------------------------------------------------------------
 
102
  def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
103
  """
104
- PRIMO PROMPT DI SISTEMA molto prolisso e stringente sulle regole SPARQL,
105
- con i vari esempi (1-10) inclusi.
 
 
 
 
 
 
 
 
106
  """
107
  prompt = f"""SEI UN GENERATORE DI QUERY SPARQL PER L'ONTOLOGIA DI UN MUSEO.
108
  DEVI GENERARE SOLO UNA QUERY SPARQL (IN UNA SOLA RIGA) SE LA DOMANDA RIGUARDA INFORMAZIONI NELL'ONTOLOGIA.
@@ -155,12 +165,13 @@ FINE ONTOLOGIA.
155
  """
156
  logger.debug("[create_system_prompt_for_sparql] Prompt generato con ESEMPI e regole SPARQL.")
157
  return prompt
158
- from huggingface_hub import InferenceClient
159
 
160
  def classify_and_translate(question_text: str, model_answer_text: str) -> str:
161
  """
162
  Classifica la lingua della domanda e della risposta, quindi traduce la risposta
163
- nella lingua della domanda se sono diverse.
 
164
 
165
  Parametri:
166
  - question_text: Testo della domanda dell'utente.
@@ -169,17 +180,12 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
169
  Restituisce:
170
  - La risposta tradotta nella lingua della domanda o la risposta originale
171
  se entrambe le lingue coincidono.
172
- """
173
- # Costanti
174
- LANG_DETECT_MODEL = "papluca/xlm-roberta-base-language-detection" # Modello per rilevamento lingua
175
- TRANSLATOR_MODEL_PREFIX = "Helsinki-NLP/opus-mt" # Prefisso dei modelli di traduzione
176
-
177
- # Crea il client per il rilevamento delle lingue
178
- lang_detect_client = InferenceClient(
179
- token=HF_API_KEY,
180
- model=LANG_DETECT_MODEL
181
- )
182
 
 
 
 
 
 
183
  # Rileva la lingua della domanda
184
  try:
185
  question_lang_result = lang_detect_client.text_classification(text=question_text)
@@ -187,7 +193,7 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
187
  logger.info(f"[LangDetect] Lingua della domanda: {question_lang}")
188
  except Exception as e:
189
  logger.error(f"Errore nel rilevamento della lingua della domanda: {e}")
190
- question_lang = "en" # Default fallback
191
 
192
  # Rileva la lingua della risposta
193
  try:
@@ -196,95 +202,104 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
196
  logger.info(f"[LangDetect] Lingua della risposta: {answer_lang}")
197
  except Exception as e:
198
  logger.error(f"Errore nel rilevamento della lingua della risposta: {e}")
199
- answer_lang = "en" # Default fallback
200
 
201
- # Se le lingue sono uguali, non tradurre
202
  if question_lang == answer_lang:
203
- logger.info("[Translate] Lingue uguali, nessuna traduzione necessaria.")
204
  return model_answer_text
205
 
206
- # Prepara il modello di traduzione
 
207
  translator_model = f"{TRANSLATOR_MODEL_PREFIX}-{answer_lang}-{question_lang}"
208
-
209
- # Crea il client per la traduzione
210
  translator_client = InferenceClient(
211
  token=HF_API_KEY,
212
  model=translator_model
213
  )
214
 
215
- # Traduci la risposta
216
  try:
217
  translation_result = translator_client.translation(text=model_answer_text)
218
  translated_answer = translation_result["translation_text"]
219
  logger.info("[Translate] Risposta tradotta con successo.")
220
  except Exception as e:
221
- logger.error(f"Errore nella traduzione {answer_lang}->{question_lang}: {e}")
222
- translated_answer = model_answer_text # Fallback alla risposta originale
 
223
 
224
  return translated_answer
225
 
226
 
227
  def create_system_prompt_for_guide() -> str:
228
  """
229
- SECONDO PROMPT DI SISTEMA:
230
- - Risponde in stile "guida museale" in modo breve (max ~50 parole).
231
- - Se c'è una query e risultati, descrive brevemente.
232
- - Se non c'è query o non ci sono risultati, prova comunque a dare una risposta.
233
  """
234
  prompt = (
235
- "SEI UNA GUIDA MUSEALE VIRTUALE. "
236
- "RISPONDI IN MODO BREVE (~50 PAROLE), SENZA SALUTI O INTRODUZIONI PROLISSE. "
237
- "SE HAI RISULTATI SPARQL, USALI. "
238
- "SE NON HAI RISULTATI O NON HAI UNA QUERY, RISPONDI COMUNQUE CERCANDO DI RIARRANGIARE LE TUE CONOSCENZE."
239
- )
240
  logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
241
  return prompt
242
 
243
 
244
  def correct_sparql_syntax_advanced(query: str) -> str:
245
  """
246
- Corregge in maniera più complessa gli errori sintattici comuni generati dal modello
247
- nelle query SPARQL, tramite euristiche:
248
- - Spazi dopo SELECT, WHERE
249
- - Rimozione di '?autore' attaccato a 'progettoMuseo:autoreOpera?autore'
250
- - Aggiunta di PREFIX se assente
251
- - Rimozione newline (una riga)
252
- - Aggiunta di '.' se manca a fine tripla
253
- - Pulizia di spazi doppi
 
 
 
 
 
 
 
 
254
  """
255
  original_query = query
256
  logger.debug(f"[correct_sparql_syntax_advanced] Query originaria:\n{original_query}")
257
 
258
- # 1) Rimuoviamo newline e forziamo un'unica riga
259
  query = query.replace('\n', ' ').replace('\r', ' ')
260
 
261
- # 2) Se manca il PREFIX, lo aggiungiamo in testa (solo se notiamo che non c'è "PREFIX progettoMuseo:")
262
  if 'PREFIX progettoMuseo:' not in query:
263
  logger.debug("[correct_sparql_syntax_advanced] Aggiungo PREFIX progettoMuseo.")
264
- query = ("PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> "
265
- + query)
 
 
266
 
267
- # 3) Spazio dopo SELECT se manca
268
  query = re.sub(r'(SELECT)(\?|\*)', r'\1 \2', query, flags=re.IGNORECASE)
269
 
270
- # 4) Spazio dopo WHERE se manca
271
  query = re.sub(r'(WHERE)\{', r'\1 {', query, flags=re.IGNORECASE)
272
 
273
- # 5) Correggiamo i punti interrogativi attaccati alle proprietà:
274
- # "progettoMuseo:autoreOpera?autore" => "progettoMuseo:autoreOpera ?autore"
275
  query = re.sub(r'(progettoMuseo:\w+)\?(\w+)', r'\1 ?\2', query)
276
 
277
  # 6) Rimuoviamo spazi multipli
278
  query = re.sub(r'\s+', ' ', query).strip()
279
 
280
- # 7) Aggiungiamo '.' a fine tripla prima del '}' se manca
281
  query = re.sub(r'(\?\w+)\s*\}', r'\1 . }', query)
282
 
283
- # 8) Se manca la clausola WHERE, proviamo ad aggiungerla
284
  if 'WHERE' not in query.upper():
285
  query = re.sub(r'(SELECT\s+[^\{]+)\{', r'\1 WHERE {', query, flags=re.IGNORECASE)
286
 
287
- # 9) Pulizia finale di spazi
288
  query = re.sub(r'\s+', ' ', query).strip()
289
 
290
  logger.debug(f"[correct_sparql_syntax_advanced] Query dopo correzioni:\n{query}")
@@ -292,7 +307,10 @@ def correct_sparql_syntax_advanced(query: str) -> str:
292
 
293
 
294
  def is_sparql_query_valid(query: str) -> bool:
295
- """Verifica la sintassi SPARQL tramite rdflib."""
 
 
 
296
  logger.debug(f"[is_sparql_query_valid] Validazione SPARQL: {query}")
297
  try:
298
  parseQuery(query)
@@ -303,50 +321,63 @@ def is_sparql_query_valid(query: str) -> bool:
303
  return False
304
 
305
  # ---------------------------------------------------------------------------
306
- # ENDPOINT UNICO
307
  # ---------------------------------------------------------------------------
308
  @app.post("/assistant")
309
  def assistant_endpoint(req: AssistantRequest):
310
  """
311
- Endpoint UNICO con due step interni:
312
- 1) Genera la query SPARQL (prompt prolisso).
313
- 2) Esegue la query (se valida) e fornisce una risposta breve stile "guida museale",
314
- anche se i risultati sono vuoti o la query non esiste.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  """
316
  logger.info("Ricevuta chiamata POST su /assistant")
 
 
317
  user_message = req.message
318
  max_tokens = req.max_tokens
319
  temperature = req.temperature
320
- label = classify_message_inference_api(user_message)
321
- logger.info(label)
322
  logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
323
- # STEP 1: Generazione SPARQL
 
 
 
324
  try:
325
- logger.debug("Serializzazione dell'ontologia in formato Turtle per contesto nel prompt.")
326
  ontology_turtle = ontology_graph.serialize(format="xml")
327
  logger.debug("Ontologia serializzata con successo (XML).")
328
  except Exception as e:
329
- logger.warning(f"Impossibile serializzare l'ontologia in Turtle: {e}")
330
  ontology_turtle = ""
 
 
331
  system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
332
- # Inizializziamo client Hugging Face
333
- try:
334
- logger.debug(f"Inizializzazione InferenceClient con modello='{HF_MODEL}'.")
335
- hf_client = InferenceClient(model=HF_MODEL, token=HF_API_KEY)
336
- except Exception as ex:
337
- logger.error(f"Errore inizializzazione HF client: {ex}")
338
- raise HTTPException(status_code=500, detail="Impossibile inizializzare il modello Hugging Face.")
339
 
340
- # Chiediamo al modello la query SPARQL (fase interna 1)
341
  try:
342
  logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
343
- gen_sparql_output = hf_client.chat.completions.create(
344
  messages=[
345
  {"role": "system", "content": system_prompt_sparql},
346
  {"role": "user", "content": user_message}
347
  ],
348
- max_tokens=512,
349
- temperature=0
350
  )
351
  possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
352
  logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
@@ -355,22 +386,24 @@ def assistant_endpoint(req: AssistantRequest):
355
  # Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
356
  possible_query = "NO_SPARQL"
357
 
358
- # Verifica se la query è NO_SPARQL
359
  if possible_query.upper().startswith("NO_SPARQL"):
360
  generated_query = None
361
- logger.debug("[assistant_endpoint] Modello indica 'NO_SPARQL', nessuna query generata.")
362
  else:
363
- # Correggiamo in modo avanzato
364
  advanced_corrected = correct_sparql_syntax_advanced(possible_query)
365
- # Dopo la correzione, verifichiamo se è valida
366
  if is_sparql_query_valid(advanced_corrected):
367
  generated_query = advanced_corrected
368
  logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
369
  else:
370
- logger.debug("[assistant_endpoint] Query SPARQL non valida dopo correzione avanzata. La ignoriamo.")
371
  generated_query = None
372
 
373
- # STEP 2: Esecuzione query (se presente) e risposta guida
 
 
374
  results = []
375
  if generated_query:
376
  logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
@@ -381,17 +414,17 @@ def assistant_endpoint(req: AssistantRequest):
381
  except Exception as ex:
382
  logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
383
  results = []
384
-
385
- # Creiamo il prompt di sistema "guida museale"
 
 
386
  system_prompt_guide = create_system_prompt_for_guide()
 
387
  if generated_query and results:
388
- # Abbiamo query + risultati
389
  # Convertiamo i risultati in una stringa più leggibile
390
  results_str = "\n".join(
391
- f"{idx+1}) " + ", ".join(
392
- f"{var}={row[var]}"
393
- for var in row.labels
394
- )
395
  for idx, row in enumerate(results)
396
  )
397
  second_prompt = (
@@ -402,17 +435,19 @@ def assistant_endpoint(req: AssistantRequest):
402
  "Rispondi in modo breve (max ~50 parole)."
403
  )
404
  logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
 
405
  elif generated_query and not results:
406
- # Query valida ma 0 risultati
407
  second_prompt = (
408
  f"{system_prompt_guide}\n\n"
409
  f"Domanda utente: {user_message}\n"
410
  f"Query generata: {generated_query}\n"
411
  "Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
412
  )
413
- logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma nessun risultato.")
 
414
  else:
415
- # Nessuna query generata
416
  second_prompt = (
417
  f"{system_prompt_guide}\n\n"
418
  f"Domanda utente: {user_message}\n"
@@ -420,42 +455,56 @@ def assistant_endpoint(req: AssistantRequest):
420
  )
421
  logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
422
 
423
- # Ultima chiamata al modello per la risposta finale
424
  try:
425
- logger.debug("[assistant_endpoint] Chiamata HF per la risposta guida museale...")
426
- final_output = hf_client.chat.completions.create(
427
  messages=[
428
  {"role": "system", "content": second_prompt},
429
  {"role": "user", "content": "Fornisci la risposta finale."}
430
  ],
431
- max_tokens=512,
432
- temperature=0.2
433
  )
434
  final_answer = final_output["choices"][0]["message"]["content"].strip()
435
  logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
436
  except Exception as ex:
437
  logger.error(f"Errore nella generazione della risposta finale: {ex}")
438
  raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
 
 
 
 
439
  final_ans = classify_and_translate(user_message, final_answer)
440
- # Risposta JSON
441
- logger.debug("[assistant_endpoint] Fine elaborazione. Restituzione risposta.")
 
 
 
442
  return {
443
  "query": generated_query,
444
  "response": final_ans
445
  }
446
 
447
  # ---------------------------------------------------------------------------
448
- # ENDPOINT DI TEST
449
  # ---------------------------------------------------------------------------
450
  @app.get("/")
451
  def home():
 
 
 
452
  logger.debug("Chiamata GET su '/' - home.")
453
  return {
454
- "message": "Endpoint con ESEMPI di query SPARQL + correzione avanzata + risposta guida museale."
455
  }
456
 
457
  # ---------------------------------------------------------------------------
458
  # MAIN
459
  # ---------------------------------------------------------------------------
460
  if __name__ == "__main__":
461
- logger.info("Avvio dell'applicazione FastAPI.")
 
 
 
 
 
12
  # CONFIGURAZIONE LOGGING
13
  # ---------------------------------------------------------------------------
14
  logging.basicConfig(
15
+ level=logging.DEBUG, # Utilizziamo il livello DEBUG per un log più dettagliato
16
  format="%(asctime)s - %(levelname)s - %(message)s",
17
  handlers=[logging.FileHandler("app.log"), logging.StreamHandler()]
18
  )
19
  logger = logging.getLogger(__name__)
20
 
21
+ # ---------------------------------------------------------------------------
22
+ # COSTANTI / CHIAVI / MODELLI
23
+ # ---------------------------------------------------------------------------
24
+ # Nota: HF_API_KEY deve essere impostata a una chiave valida di Hugging Face.
25
  HF_API_KEY = os.getenv("HF_API_KEY")
 
 
 
26
  if not HF_API_KEY:
27
+ # Se la chiave API non è impostata, solleva un errore
28
  logger.error("HF_API_KEY non impostata.")
29
  raise EnvironmentError("HF_API_KEY non impostata.")
30
 
31
+ # Nome del modello Hugging Face per generare query SPARQL e risposte finali
32
+ HF_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
33
+
34
+ # Nome del modello Hugging Face per rilevamento lingua
35
+ LANG_DETECT_MODEL = "papluca/xlm-roberta-base-language-detection"
36
+
37
+ # Prefisso per i modelli di traduzione su Hugging Face
38
+ TRANSLATOR_MODEL_PREFIX = "Helsinki-NLP/opus-mt"
39
+
40
  # ---------------------------------------------------------------------------
41
+ # INIZIALIZZAZIONE CLIENT HUGGING FACE (una volta sola)
42
  # ---------------------------------------------------------------------------
43
+ """
44
+ Qui inizializziamo i client necessari. In questo modo, evitiamo di istanziare
45
+ continuamente nuovi oggetti InferenceClient a ogni chiamata delle funzioni.
46
+
47
+ - hf_generation_client: per generare query SPARQL e risposte stile "guida museale"
48
+ - lang_detect_client: per rilevare la lingua della domanda e della risposta
49
+ """
50
  try:
51
+ logger.info("[Startup] Inizializzazione client HF per generazione (modello di LLM).")
52
+ hf_generation_client = InferenceClient(
53
  token=HF_API_KEY,
54
+ model=HF_MODEL
55
+ )
56
+ logger.info("[Startup] Inizializzazione client HF per rilevamento lingua.")
57
+ lang_detect_client = InferenceClient(
58
+ token=HF_API_KEY,
59
+ model=LANG_DETECT_MODEL
60
  )
 
61
  except Exception as ex:
62
+ logger.error(f"Errore inizializzazione dei client Hugging Face: {ex}")
63
+ raise HTTPException(status_code=500, detail="Impossibile inizializzare i modelli Hugging Face.")
64
 
65
  # ---------------------------------------------------------------------------
66
+ # CARICAMENTO ONTOLOGIA
67
  # ---------------------------------------------------------------------------
68
+ """
69
+ Carichiamo il file RDF/XML contenente l'ontologia del museo. Questo file è
70
+ fondamentale per l'esecuzione di query SPARQL, in quanto definisce le classi,
71
+ le proprietà e le istanze presenti nell'ontologia del museo.
72
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
74
+ RDF_FILE = os.path.join(BASE_DIR, "Ontologia_corretto-2.rdf")
75
+
76
  ontology_graph = rdflib.Graph()
77
  try:
 
78
  logger.info(f"Caricamento ontologia da file: {RDF_FILE}")
79
+ # Indichiamo che l'ontologia è in formato RDF/XML
80
  ontology_graph.parse(RDF_FILE, format="xml")
81
  logger.info("Ontologia RDF caricata correttamente (formato XML).")
82
  except Exception as e:
83
  logger.error(f"Errore nel caricamento dell'ontologia: {e}")
84
  raise e
 
85
  # ---------------------------------------------------------------------------
86
+ # Pydantic Model per la richiesta
87
  # ---------------------------------------------------------------------------
 
 
 
88
  class AssistantRequest(BaseModel):
89
+ """
90
+ Questo modello Pydantic definisce lo schema della richiesta che
91
+ riceverà l'endpoint /assistant. Contiene:
92
+ - message: la domanda del visitatore
93
+ - max_tokens: max di token per le risposte (di default 512)
94
+ - temperature: temperatura di generazione (di default 0.5)
95
+ """
96
  message: str
97
  max_tokens: int = 512
98
  temperature: float = 0.5
99
 
100
  # ---------------------------------------------------------------------------
101
+ # FUNZIONI DI SUPPORTO (Prompts, validazione SPARQL, correzioni, ecc.)
102
  # ---------------------------------------------------------------------------
103
+
104
  def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
105
  """
106
+ Genera il testo di prompt che istruisce il modello su come costruire
107
+ SOLO UNA query SPARQL, in un'unica riga, o in alternativa 'NO_SPARQL'
108
+ se la domanda non è pertinente all'ontologia. Il prompt include regole
109
+ di formattazione e alcuni esempi di domanda-risposta SPARQL.
110
+
111
+ Parametri:
112
+ - ontology_turtle: una stringa con l'ontologia in formato Turtle (o simile).
113
+
114
+ Ritorna:
115
+ - Il testo da usare come "system prompt" per il modello generativo.
116
  """
117
  prompt = f"""SEI UN GENERATORE DI QUERY SPARQL PER L'ONTOLOGIA DI UN MUSEO.
118
  DEVI GENERARE SOLO UNA QUERY SPARQL (IN UNA SOLA RIGA) SE LA DOMANDA RIGUARDA INFORMAZIONI NELL'ONTOLOGIA.
 
165
  """
166
  logger.debug("[create_system_prompt_for_sparql] Prompt generato con ESEMPI e regole SPARQL.")
167
  return prompt
168
+
169
 
170
  def classify_and_translate(question_text: str, model_answer_text: str) -> str:
171
  """
172
  Classifica la lingua della domanda e della risposta, quindi traduce la risposta
173
+ se la lingua è diversa da quella della domanda. L'idea è di restituire una
174
+ risposta nella stessa lingua dell'utente.
175
 
176
  Parametri:
177
  - question_text: Testo della domanda dell'utente.
 
180
  Restituisce:
181
  - La risposta tradotta nella lingua della domanda o la risposta originale
182
  se entrambe le lingue coincidono.
 
 
 
 
 
 
 
 
 
 
183
 
184
+ NB: Qui l'oggetto 'lang_detect_client' (per rilevamento lingua) è già
185
+ stato inizializzato all'avvio dell'app. Mentre il 'translator_client'
186
+ viene creato 'al volo' poiché la direzione di traduzione dipende
187
+ dalle due lingue effettive.
188
+ """
189
  # Rileva la lingua della domanda
190
  try:
191
  question_lang_result = lang_detect_client.text_classification(text=question_text)
 
193
  logger.info(f"[LangDetect] Lingua della domanda: {question_lang}")
194
  except Exception as e:
195
  logger.error(f"Errore nel rilevamento della lingua della domanda: {e}")
196
+ question_lang = "en" # Fallback se non riusciamo a rilevare la lingua
197
 
198
  # Rileva la lingua della risposta
199
  try:
 
202
  logger.info(f"[LangDetect] Lingua della risposta: {answer_lang}")
203
  except Exception as e:
204
  logger.error(f"Errore nel rilevamento della lingua della risposta: {e}")
205
+ answer_lang = "it" # Fallback se non riusciamo a rilevare la lingua
206
 
207
+ # Se domanda e risposta sono nella stessa lingua, non traduciamo
208
  if question_lang == answer_lang:
209
+ logger.info("[Translate] Nessuna traduzione necessaria: stessa lingua.")
210
  return model_answer_text
211
 
212
+ # Altrimenti, costruiamo "al volo" il modello di traduzione appropriato
213
+ # (es: "Helsinki-NLP/opus-mt-en-it", "Helsinki-NLP/opus-mt-fr-en", ecc.)
214
  translator_model = f"{TRANSLATOR_MODEL_PREFIX}-{answer_lang}-{question_lang}"
 
 
215
  translator_client = InferenceClient(
216
  token=HF_API_KEY,
217
  model=translator_model
218
  )
219
 
220
+ # Traduzione della risposta
221
  try:
222
  translation_result = translator_client.translation(text=model_answer_text)
223
  translated_answer = translation_result["translation_text"]
224
  logger.info("[Translate] Risposta tradotta con successo.")
225
  except Exception as e:
226
+ logger.error(f"Errore nella traduzione {answer_lang} -> {question_lang}: {e}")
227
+ # Se fallisce, restituiamo la risposta originale come fallback
228
+ translated_answer = model_answer_text
229
 
230
  return translated_answer
231
 
232
 
233
  def create_system_prompt_for_guide() -> str:
234
  """
235
+ Genera un testo di prompt che istruisce il modello a rispondere
236
+ come "guida museale virtuale", in modo breve (~50 parole), riassumendo
237
+ i risultati SPARQL (se presenti) o fornendo comunque una risposta
238
+ in base alle conoscenze pregresse.
239
  """
240
  prompt = (
241
+ "SEI UNA GUIDA MUSEALE VIRTUALE. "
242
+ "RISPONDI IN MODO BREVE (~50 PAROLE), SENZA SALUTI O INTRODUZIONI PROLISSE. "
243
+ "SE HAI RISULTATI SPARQL, USALI. "
244
+ "SE NON HAI RISULTATI O NON HAI UNA QUERY, RISPONDI COMUNQUE CERCANDO DI RIARRANGIARE LE TUE CONOSCENZE."
245
+ )
246
  logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
247
  return prompt
248
 
249
 
250
  def correct_sparql_syntax_advanced(query: str) -> str:
251
  """
252
+ Applica correzioni sintattiche (euristiche) su una query SPARQL eventualmente
253
+ mal formattata, generata dal modello.
254
+ Passi:
255
+ 1. Rimuove newline.
256
+ 2. Verifica l'esistenza di 'PREFIX progettoMuseo:' e lo aggiunge se mancante.
257
+ 3. Inserisce spazi dopo SELECT, WHERE (se mancanti).
258
+ 4. Se c'è 'progettoMuseo:autoreOpera?autore' lo trasforma in 'progettoMuseo:autoreOpera ?autore'.
259
+ 5. Rimuove spazi multipli.
260
+ 6. Aggiunge '.' prima di '}' se manca.
261
+ 7. Aggiunge la clausola WHERE se non presente.
262
+
263
+ Parametri:
264
+ - query: stringa con la query SPARQL potenzialmente mal formattata.
265
+
266
+ Ritorna:
267
+ - La query SPARQL corretta se possibile, in singola riga.
268
  """
269
  original_query = query
270
  logger.debug(f"[correct_sparql_syntax_advanced] Query originaria:\n{original_query}")
271
 
272
+ # 1) Rimuoviamo newline e normalizziamo a una singola riga
273
  query = query.replace('\n', ' ').replace('\r', ' ')
274
 
275
+ # 2) Se manca il PREFIX museo, lo aggiungiamo in testa
276
  if 'PREFIX progettoMuseo:' not in query:
277
  logger.debug("[correct_sparql_syntax_advanced] Aggiungo PREFIX progettoMuseo.")
278
+ query = (
279
+ "PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> "
280
+ + query
281
+ )
282
 
283
+ # 3) Spazio dopo SELECT se manca (SELECT?autore => SELECT ?autore)
284
  query = re.sub(r'(SELECT)(\?|\*)', r'\1 \2', query, flags=re.IGNORECASE)
285
 
286
+ # 4) Spazio dopo WHERE se manca (WHERE{ => WHERE {)
287
  query = re.sub(r'(WHERE)\{', r'\1 {', query, flags=re.IGNORECASE)
288
 
289
+ # 5) Correggiamo le incollature: progettoMuseo:autoreOpera?autore => progettoMuseo:autoreOpera ?autore
 
290
  query = re.sub(r'(progettoMuseo:\w+)\?(\w+)', r'\1 ?\2', query)
291
 
292
  # 6) Rimuoviamo spazi multipli
293
  query = re.sub(r'\s+', ' ', query).strip()
294
 
295
+ # 7) Aggiungiamo '.' prima di '}' se manca
296
  query = re.sub(r'(\?\w+)\s*\}', r'\1 . }', query)
297
 
298
+ # 8) Se manca la clausola WHERE, la aggiungiamo
299
  if 'WHERE' not in query.upper():
300
  query = re.sub(r'(SELECT\s+[^\{]+)\{', r'\1 WHERE {', query, flags=re.IGNORECASE)
301
 
302
+ # 9) Pulizia spazi superflui
303
  query = re.sub(r'\s+', ' ', query).strip()
304
 
305
  logger.debug(f"[correct_sparql_syntax_advanced] Query dopo correzioni:\n{query}")
 
307
 
308
 
309
  def is_sparql_query_valid(query: str) -> bool:
310
+ """
311
+ Verifica la validità sintattica di una query SPARQL usando rdflib.
312
+ Ritorna True se la query è sintatticamente corretta, False altrimenti.
313
+ """
314
  logger.debug(f"[is_sparql_query_valid] Validazione SPARQL: {query}")
315
  try:
316
  parseQuery(query)
 
321
  return False
322
 
323
  # ---------------------------------------------------------------------------
324
+ # ENDPOINT UNICO: /assistant
325
  # ---------------------------------------------------------------------------
326
  @app.post("/assistant")
327
  def assistant_endpoint(req: AssistantRequest):
328
  """
329
+ Endpoint che gestisce l'intera pipeline:
330
+ 1) Genera una query SPARQL dal messaggio dell'utente (prompt dedicato).
331
+ 2) Verifica la validità della query e, se valida, la esegue sull'ontologia RDF.
332
+ 3) Crea un "prompt da guida museale" e genera una risposta finale breve (max ~50 parole).
333
+ 4) Eventualmente, traduce la risposta nella lingua dell'utente.
334
+
335
+ Parametri:
336
+ - req (AssistantRequest): un oggetto contenente:
337
+ - message (str): la domanda dell'utente
338
+ - max_tokens (int, opzionale): numero massimo di token per la generazione
339
+ - temperature (float, opzionale): temperatura per la generazione
340
+
341
+ Ritorna:
342
+ - Un JSON con:
343
+ {
344
+ "query": <la query SPARQL generata o None>,
345
+ "response": <la risposta finale in linguaggio naturale>
346
+ }
347
  """
348
  logger.info("Ricevuta chiamata POST su /assistant")
349
+
350
+ # Estraggo i campi dal body della richiesta
351
  user_message = req.message
352
  max_tokens = req.max_tokens
353
  temperature = req.temperature
354
+
 
355
  logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
356
+
357
+ # -----------------------------------------------------------------------
358
+ # STEP 1: Generazione della query SPARQL
359
+ # -----------------------------------------------------------------------
360
  try:
361
+ # Serializziamo l'ontologia in XML per fornirla al prompt (anche se si chiama 'turtle' va bene così).
362
  ontology_turtle = ontology_graph.serialize(format="xml")
363
  logger.debug("Ontologia serializzata con successo (XML).")
364
  except Exception as e:
365
+ logger.warning(f"Impossibile serializzare l'ontologia in formato XML: {e}")
366
  ontology_turtle = ""
367
+
368
+ # Creiamo il prompt di sistema per la generazione SPARQL
369
  system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
 
 
 
 
 
 
 
370
 
371
+ # Chiamata al modello per generare la query SPARQL
372
  try:
373
  logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
374
+ gen_sparql_output = hf_generation_client.chat.completions.create(
375
  messages=[
376
  {"role": "system", "content": system_prompt_sparql},
377
  {"role": "user", "content": user_message}
378
  ],
379
+ max_tokens=512, # max_tokens per la generazione della query
380
+ temperature=0.2 # temperatura bassa per avere risposte più "deterministiche"
381
  )
382
  possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
383
  logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
 
386
  # Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
387
  possible_query = "NO_SPARQL"
388
 
389
+ # Verifichiamo se la query è "NO_SPARQL"
390
  if possible_query.upper().startswith("NO_SPARQL"):
391
  generated_query = None
392
+ logger.debug("[assistant_endpoint] Modello indica 'NO_SPARQL', quindi nessuna query generata.")
393
  else:
394
+ # Applichiamo la correzione avanzata
395
  advanced_corrected = correct_sparql_syntax_advanced(possible_query)
396
+ # Verifichiamo la validità della query
397
  if is_sparql_query_valid(advanced_corrected):
398
  generated_query = advanced_corrected
399
  logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
400
  else:
401
+ logger.debug("[assistant_endpoint] Query SPARQL non valida. Verrà ignorata.")
402
  generated_query = None
403
 
404
+ # -----------------------------------------------------------------------
405
+ # STEP 2: Esecuzione della query, se disponibile
406
+ # -----------------------------------------------------------------------
407
  results = []
408
  if generated_query:
409
  logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
 
414
  except Exception as ex:
415
  logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
416
  results = []
417
+
418
+ # -----------------------------------------------------------------------
419
+ # STEP 3: Generazione della risposta finale stile "guida museale"
420
+ # -----------------------------------------------------------------------
421
  system_prompt_guide = create_system_prompt_for_guide()
422
+
423
  if generated_query and results:
424
+ # Caso: query generata + risultati SPARQL
425
  # Convertiamo i risultati in una stringa più leggibile
426
  results_str = "\n".join(
427
+ f"{idx+1}) " + ", ".join(f"{var}={row[var]}" for var in row.labels)
 
 
 
428
  for idx, row in enumerate(results)
429
  )
430
  second_prompt = (
 
435
  "Rispondi in modo breve (max ~50 parole)."
436
  )
437
  logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
438
+
439
  elif generated_query and not results:
440
+ # Caso: query valida ma 0 risultati
441
  second_prompt = (
442
  f"{system_prompt_guide}\n\n"
443
  f"Domanda utente: {user_message}\n"
444
  f"Query generata: {generated_query}\n"
445
  "Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
446
  )
447
+ logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma senza risultati.")
448
+
449
  else:
450
+ # Caso: nessuna query generata
451
  second_prompt = (
452
  f"{system_prompt_guide}\n\n"
453
  f"Domanda utente: {user_message}\n"
 
455
  )
456
  logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
457
 
458
+ # Chiamata finale al modello per la risposta "guida museale"
459
  try:
460
+ logger.debug("[assistant_endpoint] Chiamata HF per generare la risposta finale...")
461
+ final_output = hf_generation_client.chat.completions.create(
462
  messages=[
463
  {"role": "system", "content": second_prompt},
464
  {"role": "user", "content": "Fornisci la risposta finale."}
465
  ],
466
+ max_tokens=max_tokens,
467
+ temperature=temperature
468
  )
469
  final_answer = final_output["choices"][0]["message"]["content"].strip()
470
  logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
471
  except Exception as ex:
472
  logger.error(f"Errore nella generazione della risposta finale: {ex}")
473
  raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
474
+
475
+ # -----------------------------------------------------------------------
476
+ # STEP 4: Traduzione (se necessario)
477
+ # -----------------------------------------------------------------------
478
  final_ans = classify_and_translate(user_message, final_answer)
479
+ final_ans = final_ans.replace('\\"', "").replace('\"', "")
480
+ # -----------------------------------------------------------------------
481
+ # Restituzione in formato JSON
482
+ # -----------------------------------------------------------------------
483
+ logger.debug("[assistant_endpoint] Fine elaborazione, restituzione risposta JSON.")
484
  return {
485
  "query": generated_query,
486
  "response": final_ans
487
  }
488
 
489
  # ---------------------------------------------------------------------------
490
+ # ENDPOINT DI TEST / HOME
491
  # ---------------------------------------------------------------------------
492
  @app.get("/")
493
  def home():
494
+ """
495
+ Endpoint di test per verificare se l'applicazione è in esecuzione.
496
+ """
497
  logger.debug("Chiamata GET su '/' - home.")
498
  return {
499
+ "message": "Endpoint attivo. Esempio di backend per generare query SPARQL e risposte guida museale."
500
  }
501
 
502
  # ---------------------------------------------------------------------------
503
  # MAIN
504
  # ---------------------------------------------------------------------------
505
  if __name__ == "__main__":
506
+ """
507
+ Avvio dell'applicazione FastAPI sulla porta 8000,
508
+ utile se eseguito come script principale.
509
+ """
510
+ logger.info("Avvio dell'applicazione FastAPI.")