AshenClock commited on
Commit
cc035bf
·
verified ·
1 Parent(s): c4fa90c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -377
app.py CHANGED
@@ -1,438 +1,395 @@
1
  import os
2
  import logging
3
- from typing import List
4
- from pydantic import BaseModel
5
  from fastapi import FastAPI, HTTPException
 
6
  import rdflib
7
- from rdflib import RDF, RDFS, OWL, URIRef
8
- from sentence_transformers import SentenceTransformer
9
- import faiss
10
- import json
11
- import numpy as np
12
- from dotenv import load_dotenv
13
  from huggingface_hub import InferenceClient
 
14
 
15
- # Carica le variabili d'ambiente
16
- load_dotenv()
17
-
18
- # Configura il logging
19
  logging.basicConfig(
20
- level=logging.INFO, # Mantiene INFO per ambiente di produzione
21
  format="%(asctime)s - %(levelname)s - %(message)s",
22
  handlers=[logging.FileHandler("app.log"), logging.StreamHandler()]
23
  )
24
  logger = logging.getLogger(__name__)
25
 
26
- # Recupera la chiave API
 
27
  HF_API_KEY = os.getenv("HF_API_KEY")
 
 
 
28
  if not HF_API_KEY:
29
  logger.error("HF_API_KEY non impostata.")
30
  raise EnvironmentError("HF_API_KEY non impostata.")
31
 
32
- # Definisci i percorsi dei file
33
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
34
- RDF_FILE = os.path.join(BASE_DIR, "Ontologia.rdf")
35
- HF_MODEL = "Qwen/Qwen2.5-72B-Instruct" # Modello ottimizzato per seguire istruzioni
36
-
37
- # Percorsi dei file generati
38
- DOCUMENTS_FILE = os.path.join(BASE_DIR, "data", "documents.json")
39
- FAISS_INDEX_FILE = os.path.join(BASE_DIR, "data", "faiss.index")
40
-
41
- # Carica il modello di embedding una sola volta per migliorare le prestazioni
42
  try:
43
- embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
44
- logger.info("Modello SentenceTransformer caricato con successo.")
45
- except Exception as e:
46
- logger.error(f"Errore nel caricamento del modello SentenceTransformer: {e}")
47
- raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- # Inizializza il client di Hugging Face
 
 
 
 
50
  try:
51
- client = InferenceClient(model=HF_MODEL, token=HF_API_KEY)
52
- logger.info("InferenceClient inizializzato correttamente.")
 
 
53
  except Exception as e:
54
- logger.error(f"Errore nell'inizializzazione di InferenceClient: {e}")
55
  raise e
56
 
57
- def create_data_directory():
58
- """Crea la directory 'data/' se non esiste."""
59
- os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True)
60
- logger.info("Directory 'data/' creata o già esistente.")
61
 
62
- def extract_lines(rdf_file: str, output_file: str):
63
- """
64
- Estrae ogni riga dell'ontologia RDF e la salva in un file JSON.
65
- Questo permette di indicizzare ogni riga singolarmente.
66
- """
67
- logger.info(f"Inizio estrazione delle linee dall'ontologia da {rdf_file}.")
68
- try:
69
- with open(rdf_file, "r", encoding="utf-8") as f:
70
- lines = f.readlines()
71
- # Rimuovi spazi vuoti e newline
72
- lines = [line.strip() for line in lines if line.strip()]
73
- # Salva come lista di documenti
74
- with open(output_file, "w", encoding="utf-8") as f:
75
- json.dump({"lines": lines}, f, ensure_ascii=False, indent=2)
76
- logger.info(f"Linee estratte e salvate in {output_file}")
77
- except Exception as e:
78
- logger.error(f"Errore nell'estrazione delle linee: {e}")
79
- raise e
80
 
81
- def create_faiss_index(documents_file: str, index_file: str, embedding_model_instance: SentenceTransformer):
 
 
 
82
  """
83
- Crea un indice FAISS a partire dalle linee estratte.
 
84
  """
85
- logger.info(f"Inizio creazione dell'indice FAISS da {documents_file}.")
86
- try:
87
- # Carica il documento
88
- with open(documents_file, "r", encoding="utf-8") as f:
89
- document = json.load(f)
90
- lines = document['lines']
91
- logger.info(f"{len(lines)} linee caricate da {documents_file}.")
92
-
93
- # Genera embedding
94
- embeddings = embedding_model_instance.encode(lines, convert_to_numpy=True, show_progress_bar=True)
95
- logger.info("Embedding generati con SentenceTransformer.")
96
-
97
- # Crea l'indice FAISS
98
- dimension = embeddings.shape[1]
99
- index = faiss.IndexFlatL2(dimension)
100
- index.add(embeddings)
101
- logger.info(f"Indice FAISS creato con dimensione: {dimension}.")
102
-
103
- # Salva l'indice
104
- faiss.write_index(index, index_file)
105
- logger.info(f"Indice FAISS salvato in {index_file}.")
106
- except Exception as e:
107
- logger.error(f"Errore nella creazione dell'indice FAISS: {e}")
108
- raise e
109
-
110
- def prepare_retrieval(embedding_model_instance: SentenceTransformer):
111
- """Prepara i file necessari per l'approccio RAG."""
112
- logger.info("Inizio preparazione per il retrieval.")
113
- create_data_directory()
114
-
115
- # Verifica se Ontologia.rdf esiste
116
- if not os.path.exists(RDF_FILE):
117
- logger.error(f"File RDF non trovato: {RDF_FILE}")
118
- raise FileNotFoundError(f"File RDF non trovato: {RDF_FILE}")
119
- else:
120
- logger.info(f"File RDF trovato: {RDF_FILE}")
121
 
122
- # Verifica se documents.json esiste, altrimenti generarlo
123
- if not os.path.exists(DOCUMENTS_FILE):
124
- logger.info(f"File {DOCUMENTS_FILE} non trovato. Estrazione delle linee dell'ontologia.")
125
- try:
126
- extract_lines(RDF_FILE, DOCUMENTS_FILE)
127
- except Exception as e:
128
- logger.error(f"Errore nell'estrazione delle linee: {e}")
129
- raise e
130
- else:
131
- logger.info(f"File {DOCUMENTS_FILE} trovato.")
132
 
133
- # Verifica se faiss.index esiste, altrimenti crearlo
134
- if not os.path.exists(FAISS_INDEX_FILE):
135
- logger.info(f"File {FAISS_INDEX_FILE} non trovato. Creazione dell'indice FAISS.")
136
- try:
137
- create_faiss_index(DOCUMENTS_FILE, FAISS_INDEX_FILE, embedding_model_instance)
138
- except Exception as e:
139
- logger.error(f"Errore nella creazione dell'indice FAISS: {e}")
140
- raise e
141
- else:
142
- logger.info(f"File {FAISS_INDEX_FILE} trovato.")
143
 
144
- def retrieve_relevant_lines(query: str, top_k: int = 5, embedding_model_instance: SentenceTransformer = None):
145
- """Recupera le linee rilevanti usando FAISS."""
146
- logger.info(f"Recupero delle linee rilevanti per la query: {query}")
147
- try:
148
- # Carica il documento
149
- with open(DOCUMENTS_FILE, "r", encoding="utf-8") as f:
150
- document = json.load(f)
151
- lines = document['lines']
152
- logger.info(f"{len(lines)} linee caricate da {DOCUMENTS_FILE}.")
153
-
154
- # Carica l'indice FAISS
155
- index = faiss.read_index(FAISS_INDEX_FILE)
156
- logger.info(f"Indice FAISS caricato da {FAISS_INDEX_FILE}.")
157
-
158
- # Genera embedding della query
159
- if embedding_model_instance is None:
160
- embedding_model_instance = SentenceTransformer('all-MiniLM-L6-v2')
161
- logger.info("Modello SentenceTransformer caricato per l'embedding della query.")
162
-
163
- query_embedding = embedding_model_instance.encode([query], convert_to_numpy=True)
164
- logger.info("Embedding della query generati.")
165
-
166
- # Ricerca nell'indice
167
- distances, indices = index.search(query_embedding, top_k)
168
- logger.info(f"Ricerca FAISS completata. Risultati ottenuti: {len(indices[0])}")
169
-
170
- # Recupera le linee rilevanti
171
- relevant_texts = [lines[idx] for idx in indices[0] if idx < len(lines)]
172
- retrieved_docs = "\n".join(relevant_texts)
173
- logger.info(f"Linee rilevanti recuperate: {len(relevant_texts)}")
174
- return retrieved_docs
175
- except Exception as e:
176
- logger.error(f"Errore nel recupero delle linee rilevanti: {e}")
177
- raise e
178
 
179
- def create_system_message(retrieved_docs: str) -> str:
180
- """
181
- Prompt di sistema robusto, con regole su query in una riga e
182
- informazioni recuperate tramite RAG.
183
- """
184
- return f"""### Istruzioni ###
185
- Sei un assistente museale esperto in ontologie RDF. Utilizza le informazioni fornite per generare query SPARQL precise e pertinenti.
186
-
187
- ### Ontologia ###
188
- {retrieved_docs}
189
- ### FINE Ontologia ###
190
-
191
- ### Regole Stringenti ###
192
- 1) Se l'utente chiede informazioni su questa ontologia, genera SEMPRE una query SPARQL in UNA SOLA RIGA, con prefix:
193
- PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
194
- 2) La query SPARQL deve essere precisa e cercare esattamente le entità specificate dall'utente. Ad esempio, se l'utente chiede "Chi ha creato l'opera 'Amore e Psiche'?", la query dovrebbe cercere l'opera esattamente con quel nome.
195
- 3) Se la query produce 0 risultati o fallisce, ritenta con un secondo tentativo.
196
- 4) Se la domanda è generica (tipo 'Ciao, come stai?'), rispondi brevemente.
197
- 5) Se trovi risultati, la risposta finale deve essere la query SPARQL (una sola riga).
198
- 6) Se non trovi nulla, rispondi con 'Nessuna info.'
199
- 7) Non multiline. Esempio: PREFIX base: <...> SELECT ?x WHERE {{ ... }}.
200
- Esempio:
201
- Utente: Chi ha creato l'opera 'Amore e Psiche'?
202
- Risposta: PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?creatore WHERE {{ ?opera base:hasName "Amore e Psiche" . ?opera base:creatoDa ?creatore . }}
203
- ### FINE REGOLE ###
204
-
205
- ### Conversazione ###
206
- """
207
 
208
- def create_explanation_prompt(results_str: str) -> str:
209
- """Prompt per generare una spiegazione museale dei risultati SPARQL."""
210
- return f"""Ho ottenuto questi risultati SPARQL:
211
- {results_str}
212
- Ora fornisci una breve spiegazione museale (massimo ~10 righe), senza inventare oltre i risultati.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  """
 
 
214
 
215
- async def call_hf_model(prompt: str, temperature: float = 0.5, max_tokens: int = 150, stream: bool = False) -> str:
216
- """Chiama il modello Hugging Face tramite InferenceClient e gestisce la risposta."""
217
- logger.debug("Chiamo HF con il seguente prompt:")
218
- content_preview = (prompt[:300] + '...') if len(prompt) > 300 else prompt
219
- logger.debug(f"PROMPT => {content_preview}")
220
 
221
- try:
222
- # Costruisci i messaggi per il modello
223
- # Poiché stiamo usando text_generation, il prompt deve essere una stringa continua
224
- full_prompt = prompt
225
-
226
- # Esegui la text_generation
227
- response = client.text_generation(
228
- full_prompt,
229
- max_new_tokens=max_tokens,
230
- temperature=temperature,
231
- top_p=0.7,
232
- stream=stream
233
- )
 
 
234
 
235
- logger.debug(f"Risposta completa dal modello: {response}")
236
 
237
- if stream:
238
- # Gestisci lo stream
239
- generated_text = ""
240
- async for token in response:
241
- if 'generated_text' in token:
242
- generated_text += token['generated_text']
243
- print(token['generated_text'], end="")
244
- return generated_text.strip()
245
- else:
246
- # Risposta non in streaming
247
- # Verifica la struttura della risposta
248
- # La risposta dovrebbe contenere 'generated_text'
249
- if isinstance(response, list) and len(response) > 0 and "generated_text" in response[0]:
250
- generated_text = response[0]["generated_text"]
251
- elif isinstance(response, dict) and "generated_text" in response:
252
- generated_text = response["generated_text"]
253
- else:
254
- raise ValueError("Nessun campo 'generated_text' nella risposta.")
255
-
256
- # Forza la risposta su una singola linea se multilinea
257
- single_line = " ".join(generated_text.splitlines())
258
- logger.debug(f"Risposta HF single-line: {single_line}")
259
- return single_line.strip()
260
- except Exception as e:
261
- logger.error(f"Errore nella chiamata all'API Hugging Face tramite InferenceClient: {e}")
262
- raise HTTPException(status_code=500, detail=str(e))
263
 
264
- # Variabile globale per le etichette delle entità
265
- entity_labels: List[str] = []
266
 
267
- def load_entity_labels(rdf_file: str):
268
- """Carica le etichette delle entità dall'ontologia RDF."""
269
- global entity_labels
270
- try:
271
- g = rdflib.Graph()
272
- g.parse(rdf_file, format="xml")
273
- entities = set()
274
- for s in g.subjects(RDF.type, OWL.NamedIndividual):
275
- label = g.value(s, RDFS.label, default=str(s))
276
- if isinstance(label, URIRef):
277
- label = label.split('#')[-1].replace('_', ' ')
278
- else:
279
- label = str(label)
280
- entities.add(label.lower())
281
- entity_labels = list(entities)
282
- logger.info(f"Elenco delle etichette delle entità caricato: {entity_labels}")
283
- except Exception as e:
284
- logger.error(f"Errore nel caricamento delle etichette delle entità: {e}")
285
- entity_labels = []
286
-
287
- def is_ontology_related(query: str) -> bool:
288
- """Determina se la domanda è pertinente all'ontologia."""
289
- query_lower = query.lower()
290
- keywords = ["opera", "museo", "stanza", "tour", "visitatore", "biglietto", "guida", "evento", "agente"]
291
- if any(keyword in query_lower for keyword in keywords):
292
- return True
293
- if any(entity in query_lower for entity in entity_labels):
294
- return True
295
- return False
296
 
297
- app = FastAPI()
 
298
 
299
- class QueryRequest(BaseModel):
300
- message: str
301
- max_tokens: int = 512 # Aumentato per risposte più dettagliate
302
- temperature: float = 0.5
303
 
304
- @app.post("/generate-response/")
305
- async def generate_response(req: QueryRequest):
306
- user_input = req.message
307
- logger.info(f"Utente dice: {user_input}")
308
 
309
- if not is_ontology_related(user_input):
310
- # Prompt generico per domande non pertinenti all'ontologia
311
- generic_prompt = f"{user_input}"
312
- try:
313
- response = await call_hf_model(generic_prompt, req.temperature, req.max_tokens, stream=False)
314
- return {
315
- "type": "NATURAL",
316
- "response": response.strip()
317
- }
318
- except Exception as e:
319
- logger.error(f"Errore nella chiamata al modello Hugging Face per domanda generica: {e}")
320
- return {
321
- "type": "ERROR",
322
- "response": f"Errore nella generazione della risposta per domanda generica: {e}"
323
- }
324
 
325
- try:
326
- # Recupera linee rilevanti usando FAISS
327
- retrieved_docs = retrieve_relevant_lines(user_input, top_k=5, embedding_model_instance=embedding_model)
328
- except Exception as e:
329
- logger.error(f"Errore nel recupero delle linee rilevanti: {e}")
330
- return {"type": "ERROR", "response": f"Errore nel recupero delle linee: {e}"}
331
 
332
- sys_msg = create_system_message(retrieved_docs)
333
- prompt = sys_msg + f"\nUtente: {user_input}\nAssistente:"
 
334
 
335
- # Primo tentativo
336
- try:
337
- r1 = await call_hf_model(prompt, req.temperature, req.max_tokens, stream=False)
338
- logger.info(f"PRIMA RISPOSTA:\n{r1}")
339
- except Exception as e:
340
- logger.error(f"Errore nella chiamata al modello Hugging Face: {e}")
341
- return {"type": "ERROR", "response": f"Errore nella generazione della risposta: {e}"}
342
 
343
- # Se non parte con "PREFIX base:"
344
- if not r1.startswith("PREFIX base:"):
345
- sc = f"Non hai risposto con query SPARQL su una sola riga. Riprova. Domanda: {user_input}"
346
- fallback_prompt = sys_msg + f"\nAssistente: {r1}\nUtente: {sc}\nAssistente:"
347
- try:
348
- r2 = await call_hf_model(fallback_prompt, req.temperature, req.max_tokens, stream=False)
349
- logger.info(f"SECONDA RISPOSTA:\n{r2}")
350
- if r2.startswith("PREFIX base:"):
351
- sparql_query = r2
352
- else:
353
- return {"type": "NATURAL", "response": r2}
354
- except Exception as e:
355
- logger.error(f"Errore nella seconda chiamata al modello Hugging Face: {e}")
356
- return {"type": "ERROR", "response": f"Errore nella generazione della seconda risposta: {e}"}
357
- else:
358
- sparql_query = r1
359
 
360
- # Esegui la query con rdflib
361
- g = rdflib.Graph()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  try:
363
- g.parse(RDF_FILE, format="xml")
364
- logger.info(f"Parsing RDF di {RDF_FILE} riuscito per l'esecuzione della query.")
 
365
  except Exception as e:
366
- logger.error(f"Parsing RDF error: {e}")
367
- return {"type": "ERROR", "response": f"Parsing RDF error: {e}"}
 
 
 
 
 
 
 
 
368
 
 
369
  try:
370
- results = g.query(sparql_query)
371
- logger.info(f"Query SPARQL eseguita con successo. Risultati: {len(results)}")
372
- except Exception as e:
373
- fallback = f"La query SPARQL ha fallito. Riprova. Domanda: {user_input}"
374
- fallback_prompt = sys_msg + f"\nAssistente: {sparql_query}\nUtente: {fallback}\nAssistente:"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  try:
376
- r3 = await call_hf_model(fallback_prompt, req.temperature, req.max_tokens, stream=False)
377
- logger.info(f"TERZA RISPOSTA (fallback):\n{r3}")
378
- if r3.startswith("PREFIX base:"):
379
- sparql_query = r3
380
- try:
381
- results = g.query(sparql_query)
382
- logger.info(f"Seconda query SPARQL eseguita con successo. Risultati: {len(results)}")
383
- except Exception as e2:
384
- logger.error(f"Seconda Query fallita: {e2}")
385
- return {"type": "ERROR", "response": f"Query fallita di nuovo: {e2}"}
386
- else:
387
- return {"type": "NATURAL", "response": r3}
388
- except Exception as e:
389
- logger.error(f"Errore nella chiamata al modello Hugging Face durante il fallback: {e}")
390
- return {"type": "ERROR", "response": f"Errore durante il fallback della risposta: {e}"}
391
-
392
- if len(results) == 0:
393
- return {"type": "NATURAL", "sparql_query": sparql_query, "response": "Nessuna info."}
394
-
395
- # Confeziona risultati
396
- row_list = []
397
- for row in results:
398
- # Converti il risultato della query in un dizionario
399
- row_dict = dict(row)
400
- row_str = ", ".join([f"{k}: {v}" for k, v in row_dict.items()])
401
- row_list.append(row_str)
402
- results_str = "\n".join(row_list)
403
-
404
- # Spiegazione
405
- exp_prompt = create_explanation_prompt(results_str)
406
- try:
407
- explanation = await call_hf_model(exp_prompt, req.temperature, req.max_tokens, stream=False)
408
- except Exception as e:
409
- logger.error(f"Errore nella generazione della spiegazione: {e}")
410
- return {"type": "ERROR", "response": f"Errore nella generazione della spiegazione: {e}"}
411
 
412
- return {
413
- "type": "NATURAL",
414
- "sparql_query": sparql_query,
415
- "sparql_results": row_list,
416
- "explanation": explanation
417
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
- @app.post("/prova")
420
- async def prova(req: QueryRequest):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  return {
422
- "type": "NATURAL",
423
- "response": "Questa è una prova di richiesta"
424
  }
425
 
 
 
 
426
  @app.get("/")
427
  def home():
428
- return {"message": "Assistente Museale con supporto SPARQL."}
 
 
 
429
 
430
- # Avvia la preparazione al caricamento delle linee e indicizzazione
431
- try:
432
- create_data_directory()
433
- prepare_retrieval(embedding_model)
434
- load_entity_labels(RDF_FILE)
435
- logger.info("Applicazione avviata e pronta per ricevere richieste.")
436
- except Exception as e:
437
- logger.error(f"Errore durante la preparazione dell'applicazione: {e}")
438
- raise e
 
1
  import os
2
  import logging
3
+ import uvicorn
 
4
  from fastapi import FastAPI, HTTPException
5
+ from pydantic import BaseModel
6
  import rdflib
7
+ from rdflib.plugins.sparql.parser import parseQuery
 
 
 
 
 
8
  from huggingface_hub import InferenceClient
9
+ import re
10
 
11
+ # ---------------------------------------------------------------------------
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.
109
+ SE LA DOMANDA NON È ATTINENTE, RISPONDI 'NO_SPARQL'.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ REGOLE SINTATTICHE RIGOROSE:
112
+ 1) Usare: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
113
+ 2) Query in UNA SOLA RIGA (niente a capo), forma: PREFIX progettoMuseo: <...> SELECT ?x WHERE {{ ... }} LIMIT N
114
+ 3) Attento agli spazi:
115
+ - Dopo SELECT: es. SELECT ?autore
116
+ - Tra proprietà e variabile: es. progettoMuseo:autoreOpera ?autore .
117
+ - Non incollare il '?' a 'progettoMuseo:'.
118
+ - Ogni tripla termina con un punto.
119
+ 4) Se non puoi generare una query valida, rispondi solo 'NO_SPARQL'.
 
120
 
121
+ Esempi di Domande Specifiche e relative Query:
122
+ 1) Utente: Chi ha creato l'opera 'Afrodite di Milo'?
123
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?autore WHERE {{ progettoMuseo:AfroditeDiMilo progettoMuseo:autoreOpera ?autore . }} LIMIT 10
 
 
 
 
 
 
 
124
 
125
+ 2) Utente: Quali sono le tecniche utilizzate nelle opere?
126
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?tecnica WHERE {{ ?opera progettoMuseo:tecnicaOpera ?tecnica . }} LIMIT 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ 3) Utente: Quali sono le dimensioni delle opere?
129
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?dimensione WHERE {{ ?opera progettoMuseo:dimensioneOpera ?dimensione . }} LIMIT 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ 4) Utente: Quali opere sono esposte nella stanza Greca?
132
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:StanzaGrecia progettoMuseo:Espone ?opera . }} LIMIT 100
133
+
134
+ 5) Utente: Quali sono le proprietà e i tipi delle proprietà nell'ontologia?
135
+ Risposta: PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX owl: <http://www.w3.org/2002/07/owl#> PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT DISTINCT ?property ?type WHERE {{ ?property rdf:type ?type . FILTER(?type IN (owl:ObjectProperty, owl:DatatypeProperty)) }}
136
+
137
+ 6) Utente: Recupera tutti i biglietti e i tipi di biglietto.
138
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?biglietto ?tipoBiglietto WHERE {{ ?biglietto rdf:type progettoMuseo:Biglietto . ?biglietto progettoMuseo:tipoBiglietto ?tipoBiglietto . }} LIMIT 100
139
+
140
+ 7) Utente: Recupera tutti i visitatori e i tour a cui partecipano.
141
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?visitatore ?tour WHERE {{ ?visitatore progettoMuseo:Partecipazione_a_Evento ?tour . }} LIMIT 100
142
+
143
+ 8) Utente: Recupera tutte le stanze tematiche e le opere esposte.
144
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?stanza ?opera WHERE {{ ?stanza rdf:type progettoMuseo:Stanza_Tematica . ?stanza progettoMuseo:Espone ?opera . }} LIMIT 100
145
+
146
+ 9) Utente: Recupera tutte le opere con materiale 'Marmo'.
147
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ ?opera progettoMuseo:materialeOpera "Marmo"@it . }} LIMIT 100
148
+
149
+ 10) Utente: Recupera tutti i visitatori con data di nascita dopo il 2000.
150
+ Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?visitatore WHERE {{ ?visitatore rdf:type progettoMuseo:Visitatore_Individuale . ?visitatore progettoMuseo:dataDiNascitaVisitatore ?data . FILTER(?data > "2000-01-01T00:00:00"^^xsd:dateTime) . }} LIMIT 100
151
+
152
+ ECCO L'ONTOLOGIA (TURTLE) PER CONTESTO:
153
+ {ontology_turtle}
154
+ FINE ONTOLOGIA.
155
  """
156
+ logger.debug("[create_system_prompt_for_sparql] Prompt generato con ESEMPI e regole SPARQL.")
157
+ return prompt
158
 
 
 
 
 
 
159
 
160
+ def create_system_prompt_for_guide() -> str:
161
+ """
162
+ SECONDO PROMPT DI SISTEMA:
163
+ - Risponde in stile "guida museale" in modo breve (max ~50 parole).
164
+ - Se c'è una query e risultati, descrive brevemente.
165
+ - Se non c'è query o non ci sono risultati, prova comunque a dare una risposta.
166
+ """
167
+ prompt = (
168
+ "SEI UNA GUIDA MUSEALE VIRTUALE. "
169
+ "RISPONDI IN MODO BREVE (~50 PAROLE), SENZA SALUTI O INTRODUZIONI PROLISSE. "
170
+ "SE HAI RISULTATI SPARQL, USALI. "
171
+ "SE NON HAI RISULTATI O NON HAI UNA QUERY, RISPONDI COMUNQUE CERCANDO DI RIARRANGIARE LE TUE CONOSCENZE."
172
+ )
173
+ logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
174
+ return prompt
175
 
 
176
 
177
+ def correct_sparql_syntax_advanced(query: str) -> str:
178
+ """
179
+ Corregge in maniera più complessa gli errori sintattici comuni generati dal modello
180
+ nelle query SPARQL, tramite euristiche:
181
+ - Spazi dopo SELECT, WHERE
182
+ - Rimozione di '?autore' attaccato a 'progettoMuseo:autoreOpera?autore'
183
+ - Aggiunta di PREFIX se assente
184
+ - Rimozione newline (una riga)
185
+ - Aggiunta di '.' se manca a fine tripla
186
+ - Pulizia di spazi doppi
187
+ """
188
+ original_query = query
189
+ logger.debug(f"[correct_sparql_syntax_advanced] Query originaria:\n{original_query}")
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ # 1) Rimuoviamo newline e forziamo un'unica riga
192
+ query = query.replace('\n', ' ').replace('\r', ' ')
193
 
194
+ # 2) Se manca il PREFIX, lo aggiungiamo in testa (solo se notiamo che non c'è "PREFIX progettoMuseo:")
195
+ if 'PREFIX progettoMuseo:' not in query:
196
+ logger.debug("[correct_sparql_syntax_advanced] Aggiungo PREFIX progettoMuseo.")
197
+ query = ("PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> "
198
+ + query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
+ # 3) Spazio dopo SELECT se manca
201
+ query = re.sub(r'(SELECT)(\?|\*)', r'\1 \2', query, flags=re.IGNORECASE)
202
 
203
+ # 4) Spazio dopo WHERE se manca
204
+ query = re.sub(r'(WHERE)\{', r'\1 {', query, flags=re.IGNORECASE)
 
 
205
 
206
+ # 5) Correggiamo i punti interrogativi attaccati alle proprietà:
207
+ # "progettoMuseo:autoreOpera?autore" => "progettoMuseo:autoreOpera ?autore"
208
+ query = re.sub(r'(progettoMuseo:\w+)\?(\w+)', r'\1 ?\2', query)
 
209
 
210
+ # 6) Rimuoviamo spazi multipli
211
+ query = re.sub(r'\s+', ' ', query).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ # 7) Aggiungiamo '.' a fine tripla prima del '}' se manca
214
+ query = re.sub(r'(\?\w+)\s*\}', r'\1 . }', query)
 
 
 
 
215
 
216
+ # 8) Se manca la clausola WHERE, proviamo ad aggiungerla
217
+ if 'WHERE' not in query.upper():
218
+ query = re.sub(r'(SELECT\s+[^\{]+)\{', r'\1 WHERE {', query, flags=re.IGNORECASE)
219
 
220
+ # 9) Pulizia finale di spazi
221
+ query = re.sub(r'\s+', ' ', query).strip()
 
 
 
 
 
222
 
223
+ logger.debug(f"[correct_sparql_syntax_advanced] Query dopo correzioni:\n{query}")
224
+ return query
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+
227
+ def is_sparql_query_valid(query: str) -> bool:
228
+ """Verifica la sintassi SPARQL tramite rdflib."""
229
+ logger.debug(f"[is_sparql_query_valid] Validazione SPARQL: {query}")
230
+ try:
231
+ parseQuery(query)
232
+ logger.debug("[is_sparql_query_valid] Query SPARQL sintatticamente corretta.")
233
+ return True
234
+ except Exception as ex:
235
+ logger.warning(f"[is_sparql_query_valid] Query non valida: {ex}")
236
+ return False
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # ENDPOINT UNICO
240
+ # ---------------------------------------------------------------------------
241
+ @app.post("/assistant")
242
+ def assistant_endpoint(req: AssistantRequest):
243
+ """
244
+ Endpoint UNICO con due step interni:
245
+ 1) Genera la query SPARQL (prompt prolisso).
246
+ 2) Esegue la query (se valida) e fornisce una risposta breve stile "guida museale",
247
+ anche se i risultati sono vuoti o la query non esiste.
248
+ """
249
+ logger.info("Ricevuta chiamata POST su /assistant")
250
+ user_message = req.message
251
+ max_tokens = req.max_tokens
252
+ temperature = req.temperature
253
+ label = classify_message_inference_api(user_message)
254
+ logger.info(label)
255
+ logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
256
+ # STEP 1: Generazione SPARQL
257
  try:
258
+ logger.debug("Serializzazione dell'ontologia in formato Turtle per contesto nel prompt.")
259
+ ontology_turtle = ontology_graph.serialize(format="xml")
260
+ logger.debug("Ontologia serializzata con successo (XML).")
261
  except Exception as e:
262
+ logger.warning(f"Impossibile serializzare l'ontologia in Turtle: {e}")
263
+ ontology_turtle = ""
264
+ system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
265
+ # Inizializziamo client Hugging Face
266
+ try:
267
+ logger.debug(f"Inizializzazione InferenceClient con modello='{HF_MODEL}'.")
268
+ hf_client = InferenceClient(model=HF_MODEL, token=HF_API_KEY)
269
+ except Exception as ex:
270
+ logger.error(f"Errore inizializzazione HF client: {ex}")
271
+ raise HTTPException(status_code=500, detail="Impossibile inizializzare il modello Hugging Face.")
272
 
273
+ # Chiediamo al modello la query SPARQL (fase interna 1)
274
  try:
275
+ logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
276
+ gen_sparql_output = hf_client.chat.completions.create(
277
+ messages=[
278
+ {"role": "system", "content": system_prompt_sparql},
279
+ {"role": "user", "content": user_message}
280
+ ],
281
+ max_tokens=512,
282
+ temperature=0.3
283
+ )
284
+ possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
285
+ logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
286
+ except Exception as ex:
287
+ logger.error(f"Errore nella generazione della query SPARQL: {ex}")
288
+ # Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
289
+ possible_query = "NO_SPARQL"
290
+
291
+ # Verifica se la query è NO_SPARQL
292
+ if possible_query.upper().startswith("NO_SPARQL"):
293
+ generated_query = None
294
+ logger.debug("[assistant_endpoint] Modello indica 'NO_SPARQL', nessuna query generata.")
295
+ else:
296
+ # Correggiamo in modo avanzato
297
+ advanced_corrected = correct_sparql_syntax_advanced(possible_query)
298
+ # Dopo la correzione, verifichiamo se è valida
299
+ if is_sparql_query_valid(advanced_corrected):
300
+ generated_query = advanced_corrected
301
+ logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
302
+ else:
303
+ logger.debug("[assistant_endpoint] Query SPARQL non valida dopo correzione avanzata. La ignoriamo.")
304
+ generated_query = None
305
+
306
+ # STEP 2: Esecuzione query (se presente) e risposta guida
307
+ results = []
308
+ if generated_query:
309
+ logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
310
  try:
311
+ query_result = ontology_graph.query(generated_query)
312
+ results = list(query_result)
313
+ logger.info(f"[assistant_endpoint] Query eseguita con successo. Numero risultati = {len(results)}")
314
+ except Exception as ex:
315
+ logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
316
+ results = []
317
+
318
+ # Creiamo il prompt di sistema "guida museale"
319
+ system_prompt_guide = create_system_prompt_for_guide()
320
+ if generated_query and results:
321
+ # Abbiamo query + risultati
322
+ # Convertiamo i risultati in una stringa più leggibile
323
+ results_str = "\n".join(
324
+ f"{idx+1}) " + ", ".join(
325
+ f"{var}={row[var]}"
326
+ for var in row.labels
327
+ )
328
+ for idx, row in enumerate(results)
329
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ second_prompt = (
332
+ f"{system_prompt_guide}\n\n"
333
+ f"Domanda utente: {user_message}\n"
334
+ f"Query generata: {generated_query}\n"
335
+ f"Risultati:\n{results_str}\n"
336
+ "Rispondi in modo breve (max ~50 parole)."
337
+ )
338
+ logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
339
+ elif generated_query and not results:
340
+ # Query valida ma 0 risultati
341
+ second_prompt = (
342
+ f"{system_prompt_guide}\n\n"
343
+ f"Domanda utente: {user_message}\n"
344
+ f"Query generata: {generated_query}\n"
345
+ "Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
346
+ )
347
+ logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma nessun risultato.")
348
+ else:
349
+ # Nessuna query generata
350
+ second_prompt = (
351
+ f"{system_prompt_guide}\n\n"
352
+ f"Domanda utente: {user_message}\n"
353
+ "Nessuna query SPARQL generata. Rispondi come puoi, riarrangiando le tue conoscenze."
354
+ )
355
+ logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
356
 
357
+ # Ultima chiamata al modello per la risposta finale
358
+ try:
359
+ logger.debug("[assistant_endpoint] Chiamata HF per la risposta guida museale...")
360
+ final_output = hf_client.chat.completions.create(
361
+ messages=[
362
+ {"role": "system", "content": second_prompt},
363
+ {"role": "user", "content": "Fornisci la risposta finale."}
364
+ ],
365
+ max_tokens=512,
366
+ temperature=0.7
367
+ )
368
+ final_answer = final_output["choices"][0]["message"]["content"].strip()
369
+ logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
370
+ except Exception as ex:
371
+ logger.error(f"Errore nella generazione della risposta finale: {ex}")
372
+ raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
373
+
374
+ # Risposta JSON
375
+ logger.debug("[assistant_endpoint] Fine elaborazione. Restituzione risposta.")
376
  return {
377
+ "query": generated_query,
378
+ "response": final_answer
379
  }
380
 
381
+ # ---------------------------------------------------------------------------
382
+ # ENDPOINT DI TEST
383
+ # ---------------------------------------------------------------------------
384
  @app.get("/")
385
  def home():
386
+ logger.debug("Chiamata GET su '/' - home.")
387
+ return {
388
+ "message": "Endpoint con ESEMPI di query SPARQL + correzione avanzata + risposta guida museale."
389
+ }
390
 
391
+ # ---------------------------------------------------------------------------
392
+ # MAIN
393
+ # ---------------------------------------------------------------------------
394
+ if __name__ == "__main__":
395
+ logger.info("Avvio dell'applicazione FastAPI sulla porta 8000.")