Update app.py
Browse files
app.py
CHANGED
|
@@ -7,7 +7,8 @@ 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 |
# ---------------------------------------------------------------------------
|
|
@@ -18,11 +19,27 @@ logging.basicConfig(
|
|
| 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.")
|
|
@@ -43,7 +60,6 @@ TRANSLATOR_MODEL_PREFIX = "Helsinki-NLP/opus-mt"
|
|
| 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 |
"""
|
|
@@ -104,13 +120,29 @@ class AssistantRequest(BaseModel):
|
|
| 104 |
# ---------------------------------------------------------------------------
|
| 105 |
# FUNZIONI DI SUPPORTO (Prompts, validazione SPARQL, correzioni, ecc.)
|
| 106 |
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
|
| 109 |
"""
|
| 110 |
Genera il testo di prompt che istruisce il modello su come costruire
|
| 111 |
SOLO UNA query SPARQL, in un'unica riga, o in alternativa 'NO_SPARQL'
|
| 112 |
se la domanda non è pertinente all'ontologia. Il prompt include regole
|
| 113 |
-
di formattazione
|
|
|
|
| 114 |
|
| 115 |
Parametri:
|
| 116 |
- ontology_turtle: una stringa con l'ontologia in formato Turtle (o simile).
|
|
@@ -121,7 +153,6 @@ def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
|
|
| 121 |
prompt = f"""SEI UN GENERATORE DI QUERY SPARQL PER L'ONTOLOGIA DI UN MUSEO.
|
| 122 |
DEVI GENERARE SOLO UNA QUERY SPARQL (IN UNA SOLA RIGA) SE LA DOMANDA RIGUARDA INFORMAZIONI NELL'ONTOLOGIA.
|
| 123 |
SE LA DOMANDA NON È ATTINENTE, RISPONDI 'NO_SPARQL'.
|
| 124 |
-
|
| 125 |
REGOLE SINTATTICHE RIGOROSE:
|
| 126 |
1) Usare: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
|
| 127 |
2) Query in UNA SOLA RIGA (niente a capo), forma: PREFIX progettoMuseo: <...> SELECT ?x WHERE {{ ... }} LIMIT N
|
|
@@ -135,43 +166,51 @@ REGOLE SINTATTICHE RIGOROSE:
|
|
| 135 |
Esempi di Domande Specifiche e relative Query:
|
| 136 |
1) Utente: Chi ha creato l'opera 'Afrodite di Milo'?
|
| 137 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?autore WHERE {{ progettoMuseo:AfroditeDiMilo progettoMuseo:autoreOpera ?autore . }} LIMIT 10
|
| 138 |
-
|
| 139 |
2) Utente: Quali sono le tecniche utilizzate nelle opere?
|
| 140 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?tecnica WHERE {{ ?opera progettoMuseo:tecnicaOpera ?tecnica . }} LIMIT 100
|
| 141 |
-
|
| 142 |
3) Utente: Quali sono le dimensioni delle opere?
|
| 143 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?dimensione WHERE {{ ?opera progettoMuseo:dimensioneOpera ?dimensione . }} LIMIT 100
|
| 144 |
-
|
| 145 |
4) Utente: Quali opere sono esposte nella stanza Greca?
|
| 146 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:StanzaGrecia progettoMuseo:Espone ?opera . }} LIMIT 100
|
| 147 |
-
|
| 148 |
5) Utente: Quali sono le proprietà e i tipi delle proprietà nell'ontologia?
|
| 149 |
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)) }}
|
| 150 |
-
|
| 151 |
6) Utente: Recupera tutti i biglietti e i tipi di biglietto.
|
| 152 |
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
|
| 153 |
-
|
| 154 |
7) Utente: Recupera tutti i visitatori e i tour a cui partecipano.
|
| 155 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?visitatore ?tour WHERE {{ ?visitatore progettoMuseo:Partecipazione_a_Evento ?tour . }} LIMIT 100
|
| 156 |
-
|
| 157 |
8) Utente: Recupera tutte le stanze tematiche e le opere esposte.
|
| 158 |
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
|
| 159 |
-
|
| 160 |
9) Utente: Recupera tutte le opere con materiale 'Marmo'.
|
| 161 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ ?opera progettoMuseo:materialeOpera "Marmo"@it . }} LIMIT 100
|
| 162 |
-
|
| 163 |
10) Utente: Recupera tutti i visitatori con data di nascita dopo il 2000.
|
| 164 |
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
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
ECCO L'ONTOLOGIA (TURTLE) PER CONTESTO:
|
| 167 |
{ontology_turtle}
|
| 168 |
FINE ONTOLOGIA.
|
| 169 |
"""
|
| 170 |
-
logger.debug("[create_system_prompt_for_sparql] Prompt generato con
|
| 171 |
return prompt
|
| 172 |
|
| 173 |
|
| 174 |
-
|
|
|
|
| 175 |
"""
|
| 176 |
Classifica la lingua della domanda e della risposta, quindi traduce la risposta
|
| 177 |
se la lingua è diversa da quella della domanda. L'idea è di restituire una
|
|
@@ -184,7 +223,6 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
|
|
| 184 |
Restituisce:
|
| 185 |
- La risposta tradotta nella lingua della domanda o la risposta originale
|
| 186 |
se entrambe le lingue coincidono.
|
| 187 |
-
|
| 188 |
NB: Qui l'oggetto 'lang_detect_client' (per rilevamento lingua) è già
|
| 189 |
stato inizializzato all'avvio dell'app. Mentre il 'translator_client'
|
| 190 |
viene creato 'al volo' poiché la direzione di traduzione dipende
|
|
@@ -194,23 +232,27 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
|
|
| 194 |
try:
|
| 195 |
question_lang_result = lang_detect_client.text_classification(text=question_text)
|
| 196 |
question_lang = question_lang_result[0]['label']
|
|
|
|
| 197 |
logger.info(f"[LangDetect] Lingua della domanda: {question_lang}")
|
| 198 |
except Exception as e:
|
| 199 |
logger.error(f"Errore nel rilevamento della lingua della domanda: {e}")
|
| 200 |
question_lang = "en" # Fallback se non riusciamo a rilevare la lingua
|
| 201 |
-
|
| 202 |
# Rileva la lingua della risposta
|
| 203 |
try:
|
| 204 |
answer_lang_result = lang_detect_client.text_classification(text=model_answer_text)
|
| 205 |
answer_lang = answer_lang_result[0]['label']
|
|
|
|
| 206 |
logger.info(f"[LangDetect] Lingua della risposta: {answer_lang}")
|
| 207 |
except Exception as e:
|
| 208 |
logger.error(f"Errore nel rilevamento della lingua della risposta: {e}")
|
| 209 |
answer_lang = "it" # Fallback se non riusciamo a rilevare la lingua
|
|
|
|
| 210 |
|
| 211 |
# Se domanda e risposta sono nella stessa lingua, non traduciamo
|
| 212 |
if question_lang == answer_lang:
|
| 213 |
logger.info("[Translate] Nessuna traduzione necessaria: stessa lingua.")
|
|
|
|
| 214 |
return model_answer_text
|
| 215 |
|
| 216 |
# Altrimenti, costruiamo "al volo" il modello di traduzione appropriato
|
|
@@ -220,15 +262,17 @@ def classify_and_translate(question_text: str, model_answer_text: str) -> str:
|
|
| 220 |
token=HF_API_KEY,
|
| 221 |
model=translator_model
|
| 222 |
)
|
| 223 |
-
|
| 224 |
# Traduzione della risposta
|
| 225 |
try:
|
| 226 |
translation_result = translator_client.translation(text=model_answer_text)
|
| 227 |
translated_answer = translation_result["translation_text"]
|
| 228 |
-
|
| 229 |
except Exception as e:
|
| 230 |
logger.error(f"Errore nella traduzione {answer_lang} -> {question_lang}: {e}")
|
|
|
|
| 231 |
# Se fallisce, restituiamo la risposta originale come fallback
|
|
|
|
| 232 |
translated_answer = model_answer_text
|
| 233 |
|
| 234 |
return translated_answer
|
|
@@ -243,9 +287,12 @@ def create_system_prompt_for_guide() -> str:
|
|
| 243 |
"""
|
| 244 |
prompt = (
|
| 245 |
"SEI UNA GUIDA MUSEALE VIRTUALE. "
|
| 246 |
-
"RISPONDI IN MODO
|
| 247 |
-
"SE HAI RISULTATI SPARQL, USALI. "
|
| 248 |
-
"
|
|
|
|
|
|
|
|
|
|
| 249 |
)
|
| 250 |
logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
|
| 251 |
return prompt
|
|
@@ -266,7 +313,6 @@ def correct_sparql_syntax_advanced(query: str) -> str:
|
|
| 266 |
|
| 267 |
Parametri:
|
| 268 |
- query: stringa con la query SPARQL potenzialmente mal formattata.
|
| 269 |
-
|
| 270 |
Ritorna:
|
| 271 |
- La query SPARQL corretta se possibile, in singola riga.
|
| 272 |
"""
|
|
@@ -329,6 +375,7 @@ def is_sparql_query_valid(query: str) -> bool:
|
|
| 329 |
# ---------------------------------------------------------------------------
|
| 330 |
@app.post("/assistant")
|
| 331 |
def assistant_endpoint(req: AssistantRequest):
|
|
|
|
| 332 |
"""
|
| 333 |
Endpoint che gestisce l'intera pipeline:
|
| 334 |
1) Genera una query SPARQL dal messaggio dell'utente (prompt dedicato).
|
|
@@ -350,14 +397,37 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 350 |
}
|
| 351 |
"""
|
| 352 |
logger.info("Ricevuta chiamata POST su /assistant")
|
| 353 |
-
|
| 354 |
# Estraggo i campi dal body della richiesta
|
| 355 |
user_message = req.message
|
| 356 |
max_tokens = req.max_tokens
|
| 357 |
temperature = req.temperature
|
| 358 |
-
|
| 359 |
logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
# -----------------------------------------------------------------------
|
| 362 |
# STEP 1: Generazione della query SPARQL
|
| 363 |
# -----------------------------------------------------------------------
|
|
@@ -365,16 +435,19 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 365 |
# Serializziamo l'ontologia in XML per fornirla al prompt (anche se si chiama 'turtle' va bene così).
|
| 366 |
ontology_turtle = ontology_graph.serialize(format="xml")
|
| 367 |
logger.debug("Ontologia serializzata con successo (XML).")
|
|
|
|
| 368 |
except Exception as e:
|
| 369 |
logger.warning(f"Impossibile serializzare l'ontologia in formato XML: {e}")
|
|
|
|
| 370 |
ontology_turtle = ""
|
| 371 |
|
| 372 |
# Creiamo il prompt di sistema per la generazione SPARQL
|
| 373 |
system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
|
| 374 |
-
|
| 375 |
# Chiamata al modello per generare la query SPARQL
|
| 376 |
try:
|
| 377 |
logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
|
|
|
|
| 378 |
gen_sparql_output = hf_generation_client.chat.completions.create(
|
| 379 |
messages=[
|
| 380 |
{"role": "system", "content": system_prompt_sparql},
|
|
@@ -385,8 +458,10 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 385 |
)
|
| 386 |
possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
|
| 387 |
logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
|
|
|
|
| 388 |
except Exception as ex:
|
| 389 |
logger.error(f"Errore nella generazione della query SPARQL: {ex}")
|
|
|
|
| 390 |
# Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
|
| 391 |
possible_query = "NO_SPARQL"
|
| 392 |
|
|
@@ -397,12 +472,15 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 397 |
else:
|
| 398 |
# Applichiamo la correzione avanzata
|
| 399 |
advanced_corrected = correct_sparql_syntax_advanced(possible_query)
|
|
|
|
| 400 |
# Verifichiamo la validità della query
|
| 401 |
if is_sparql_query_valid(advanced_corrected):
|
| 402 |
generated_query = advanced_corrected
|
| 403 |
logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
|
|
|
|
| 404 |
else:
|
| 405 |
logger.debug("[assistant_endpoint] Query SPARQL non valida. Verrà ignorata.")
|
|
|
|
| 406 |
generated_query = None
|
| 407 |
|
| 408 |
# -----------------------------------------------------------------------
|
|
@@ -411,19 +489,23 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 411 |
results = []
|
| 412 |
if generated_query:
|
| 413 |
logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
|
|
|
|
| 414 |
try:
|
| 415 |
query_result = ontology_graph.query(generated_query)
|
| 416 |
results = list(query_result)
|
| 417 |
logger.info(f"[assistant_endpoint] Query eseguita con successo. Numero risultati = {len(results)}")
|
|
|
|
| 418 |
except Exception as ex:
|
| 419 |
logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
|
|
|
|
| 420 |
results = []
|
| 421 |
-
|
|
|
|
| 422 |
# -----------------------------------------------------------------------
|
| 423 |
# STEP 3: Generazione della risposta finale stile "guida museale"
|
| 424 |
# -----------------------------------------------------------------------
|
| 425 |
system_prompt_guide = create_system_prompt_for_guide()
|
| 426 |
-
|
| 427 |
if generated_query and results:
|
| 428 |
# Caso: query generata + risultati SPARQL
|
| 429 |
# Convertiamo i risultati in una stringa più leggibile
|
|
@@ -439,7 +521,7 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 439 |
"Rispondi in modo breve (max ~50 parole)."
|
| 440 |
)
|
| 441 |
logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
|
| 442 |
-
|
| 443 |
elif generated_query and not results:
|
| 444 |
# Caso: query valida ma 0 risultati
|
| 445 |
second_prompt = (
|
|
@@ -449,7 +531,7 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 449 |
"Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
|
| 450 |
)
|
| 451 |
logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma senza risultati.")
|
| 452 |
-
|
| 453 |
else:
|
| 454 |
# Caso: nessuna query generata
|
| 455 |
second_prompt = (
|
|
@@ -458,22 +540,25 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 458 |
"Nessuna query SPARQL generata. Rispondi come puoi, riarrangiando le tue conoscenze."
|
| 459 |
)
|
| 460 |
logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
|
| 461 |
-
|
| 462 |
# Chiamata finale al modello per la risposta "guida museale"
|
| 463 |
try:
|
| 464 |
logger.debug("[assistant_endpoint] Chiamata HF per generare la risposta finale...")
|
|
|
|
| 465 |
final_output = hf_generation_client.chat.completions.create(
|
| 466 |
messages=[
|
| 467 |
{"role": "system", "content": second_prompt},
|
| 468 |
{"role": "user", "content": "Fornisci la risposta finale."}
|
| 469 |
],
|
| 470 |
-
max_tokens=
|
| 471 |
-
temperature=
|
| 472 |
)
|
| 473 |
final_answer = final_output["choices"][0]["message"]["content"].strip()
|
| 474 |
logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
|
|
|
|
| 475 |
except Exception as ex:
|
| 476 |
logger.error(f"Errore nella generazione della risposta finale: {ex}")
|
|
|
|
| 477 |
raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
|
| 478 |
|
| 479 |
# -----------------------------------------------------------------------
|
|
@@ -481,15 +566,16 @@ def assistant_endpoint(req: AssistantRequest):
|
|
| 481 |
# -----------------------------------------------------------------------
|
| 482 |
final_ans = classify_and_translate(user_message, final_answer)
|
| 483 |
final_ans = final_ans.replace('\\"', "").replace('\"', "")
|
|
|
|
| 484 |
# -----------------------------------------------------------------------
|
| 485 |
# Restituzione in formato JSON
|
| 486 |
# -----------------------------------------------------------------------
|
| 487 |
logger.debug("[assistant_endpoint] Fine elaborazione, restituzione risposta JSON.")
|
| 488 |
return {
|
| 489 |
"query": generated_query,
|
| 490 |
-
"response": final_ans
|
|
|
|
| 491 |
}
|
| 492 |
-
|
| 493 |
# ---------------------------------------------------------------------------
|
| 494 |
# ENDPOINT DI TEST / HOME
|
| 495 |
# ---------------------------------------------------------------------------
|
|
@@ -502,7 +588,78 @@ def home():
|
|
| 502 |
return {
|
| 503 |
"message": "Endpoint attivo. Esempio di backend per generare query SPARQL e risposte guida museale."
|
| 504 |
}
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
# ---------------------------------------------------------------------------
|
| 507 |
# MAIN
|
| 508 |
# ---------------------------------------------------------------------------
|
|
@@ -511,4 +668,5 @@ if __name__ == "__main__":
|
|
| 511 |
Avvio dell'applicazione FastAPI sulla porta 8000,
|
| 512 |
utile se eseguito come script principale.
|
| 513 |
"""
|
| 514 |
-
logger.info("Avvio dell'applicazione FastAPI.")
|
|
|
|
|
|
| 7 |
from rdflib.plugins.sparql.parser import parseQuery
|
| 8 |
from huggingface_hub import InferenceClient
|
| 9 |
import re
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import DistilBertForSequenceClassification, DistilBertTokenizer
|
| 12 |
# ---------------------------------------------------------------------------
|
| 13 |
# CONFIGURAZIONE LOGGING
|
| 14 |
# ---------------------------------------------------------------------------
|
|
|
|
| 19 |
)
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
+
|
| 23 |
+
# Determina il device (GPU se disponibile, altrimenti CPU)
|
| 24 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 25 |
+
logger.info(f"Device per il classificatore: {device}")
|
| 26 |
+
|
| 27 |
+
# Carica il modello e il tokenizer del classificatore fine-tuned
|
| 28 |
+
try:
|
| 29 |
+
logger.info("Caricamento del modello di classificazione fine-tuned da 'finetuned-bert-model'.")
|
| 30 |
+
classifier_model = DistilBertForSequenceClassification.from_pretrained("finetuned-bert-model")
|
| 31 |
+
classifier_tokenizer = DistilBertTokenizer.from_pretrained("finetuned-bert-model")
|
| 32 |
+
classifier_model.to(device)
|
| 33 |
+
logger.info("Modello di classificazione caricato correttamente.")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
logger.error(f"Errore nel caricamento del modello di classificazione: {e}")
|
| 36 |
+
classifier_model = None
|
| 37 |
+
explanation_dict = {}
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
# COSTANTI / CHIAVI / MODELLI
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
# Nota: HF_API_KEY deve essere impostata a una chiave valida di Hugging Face.
|
| 42 |
+
HF_API_KEY = os.getenv("HF_API_KEY")
|
| 43 |
if not HF_API_KEY:
|
| 44 |
# Se la chiave API non è impostata, solleva un errore
|
| 45 |
logger.error("HF_API_KEY non impostata.")
|
|
|
|
| 60 |
"""
|
| 61 |
Qui inizializziamo i client necessari. In questo modo, evitiamo di istanziare
|
| 62 |
continuamente nuovi oggetti InferenceClient a ogni chiamata delle funzioni.
|
|
|
|
| 63 |
- hf_generation_client: per generare query SPARQL e risposte stile "guida museale"
|
| 64 |
- lang_detect_client: per rilevare la lingua della domanda e della risposta
|
| 65 |
"""
|
|
|
|
| 120 |
# ---------------------------------------------------------------------------
|
| 121 |
# FUNZIONI DI SUPPORTO (Prompts, validazione SPARQL, correzioni, ecc.)
|
| 122 |
# ---------------------------------------------------------------------------
|
| 123 |
+
def create_system_prompt_for_classification(ontology_turtle:str) -> str:
|
| 124 |
+
prompt = f"""
|
| 125 |
+
SEI UN CLASSIFICATORE DI DOMANDE NEL CONTESTO DI UN MUSEO.
|
| 126 |
+
DEVI ANALIZZARE LA DOMANDA DELL'UTENTE E, BASANDOTI SUL CONTESTO DELL'ONTOLOGIA (CHE TI VIENE FORNITA QUI SOTTO), RISPONDERE SOLAMENTE CON "SI" SE LA DOMANDA È PERTINENTE AL MUSEO, O CON "NO" SE LA DOMANDA NON È PERTINENTE.
|
| 127 |
+
RICORDA:
|
| 128 |
+
- "PERTINENTE" SIGNIFICA CHE LA DOMANDA RIGUARDA OPERE D'ARTE, ESPOSTE, BIGLIETTI, VISITATORI, CURATORI, RESTAURI, STANZE E TUTTI GLI ASPETTI RELATIVI ALL'AMBIENTE MUSEALE.
|
| 129 |
+
- "NON PERTINENTE" SIGNIFICA CHE LA DOMANDA RIGUARDA ARGOMENTI ESTERNI AL CONTESTO MUSEALE (PER ESEMPIO, TECNOLOGIA, SPORT, POLITICA, CUCINA, ECC.).
|
| 130 |
+
NON FORNIRE ULTERIORI SPIEGAZIONI O COMMENTI: LA TUA RISPOSTA DEVE ESSERE ESCLUSIVAMENTE "SI" O "NO".
|
| 131 |
+
|
| 132 |
+
ONTOLOGIA (TURTLE/XML/ALTRO FORMATO):
|
| 133 |
+
{ontology_turtle}
|
| 134 |
|
| 135 |
+
FINE ONTOLOGIA.
|
| 136 |
+
|
| 137 |
+
"""
|
| 138 |
+
return prompt
|
| 139 |
def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
|
| 140 |
"""
|
| 141 |
Genera il testo di prompt che istruisce il modello su come costruire
|
| 142 |
SOLO UNA query SPARQL, in un'unica riga, o in alternativa 'NO_SPARQL'
|
| 143 |
se la domanda non è pertinente all'ontologia. Il prompt include regole
|
| 144 |
+
di formattazione, esempi di domanda-risposta SPARQL e regole rigorose
|
| 145 |
+
per la gestione della posizione dell'opera.
|
| 146 |
|
| 147 |
Parametri:
|
| 148 |
- ontology_turtle: una stringa con l'ontologia in formato Turtle (o simile).
|
|
|
|
| 153 |
prompt = f"""SEI UN GENERATORE DI QUERY SPARQL PER L'ONTOLOGIA DI UN MUSEO.
|
| 154 |
DEVI GENERARE SOLO UNA QUERY SPARQL (IN UNA SOLA RIGA) SE LA DOMANDA RIGUARDA INFORMAZIONI NELL'ONTOLOGIA.
|
| 155 |
SE LA DOMANDA NON È ATTINENTE, RISPONDI 'NO_SPARQL'.
|
|
|
|
| 156 |
REGOLE SINTATTICHE RIGOROSE:
|
| 157 |
1) Usare: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
|
| 158 |
2) Query in UNA SOLA RIGA (niente a capo), forma: PREFIX progettoMuseo: <...> SELECT ?x WHERE {{ ... }} LIMIT N
|
|
|
|
| 166 |
Esempi di Domande Specifiche e relative Query:
|
| 167 |
1) Utente: Chi ha creato l'opera 'Afrodite di Milo'?
|
| 168 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?autore WHERE {{ progettoMuseo:AfroditeDiMilo progettoMuseo:autoreOpera ?autore . }} LIMIT 10
|
|
|
|
| 169 |
2) Utente: Quali sono le tecniche utilizzate nelle opere?
|
| 170 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?tecnica WHERE {{ ?opera progettoMuseo:tecnicaOpera ?tecnica . }} LIMIT 100
|
|
|
|
| 171 |
3) Utente: Quali sono le dimensioni delle opere?
|
| 172 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?dimensione WHERE {{ ?opera progettoMuseo:dimensioneOpera ?dimensione . }} LIMIT 100
|
|
|
|
| 173 |
4) Utente: Quali opere sono esposte nella stanza Greca?
|
| 174 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:StanzaGrecia progettoMuseo:Espone ?opera . }} LIMIT 100
|
|
|
|
| 175 |
5) Utente: Quali sono le proprietà e i tipi delle proprietà nell'ontologia?
|
| 176 |
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)) }}
|
|
|
|
| 177 |
6) Utente: Recupera tutti i biglietti e i tipi di biglietto.
|
| 178 |
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
|
|
|
|
| 179 |
7) Utente: Recupera tutti i visitatori e i tour a cui partecipano.
|
| 180 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?visitatore ?tour WHERE {{ ?visitatore progettoMuseo:Partecipazione_a_Evento ?tour . }} LIMIT 100
|
|
|
|
| 181 |
8) Utente: Recupera tutte le stanze tematiche e le opere esposte.
|
| 182 |
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
|
|
|
|
| 183 |
9) Utente: Recupera tutte le opere con materiale 'Marmo'.
|
| 184 |
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ ?opera progettoMuseo:materialeOpera "Marmo"@it . }} LIMIT 100
|
|
|
|
| 185 |
10) Utente: Recupera tutti i visitatori con data di nascita dopo il 2000.
|
| 186 |
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
|
| 187 |
|
| 188 |
+
NUOVE REGOLE RIGUARDANTI LA POSIZIONE DELL'OPERA:
|
| 189 |
+
- SE la domanda include richieste come "fammi vedere l'opera X", "mi fai vedere l'opera X", "portami all'opera X", "voglio vedere l'opera X" o simili, la query SPARQL DEVE restituire la posizione dell'opera includendo la proprietà progettoMuseo:posizioneOpera.
|
| 190 |
+
- SE la domanda include espressioni come "dove si trova l'opera X", "ubicazione dell'opera X" o simili, la query SPARQL NON deve restituire la proprietà progettoMuseo:posizioneOpera, limitandosi al massimo a riportare la relazione esistente.
|
| 191 |
+
|
| 192 |
+
Esempi Aggiuntivi:
|
| 193 |
+
11) Utente: Fammi vedere l'opera 'Discobolo'.
|
| 194 |
+
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?posizione WHERE {{ progettoMuseo:Discobolo progettoMuseo:posizioneOpera ?posizione . }} LIMIT 10
|
| 195 |
+
12) Utente: Mi fai vedere l'opera 'Gioconda'?
|
| 196 |
+
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?posizione WHERE {{ progettoMuseo:Gioconda progettoMuseo:posizioneOpera ?posizione . }} LIMIT 10
|
| 197 |
+
13) Utente: Portami all'opera 'Autoritratto'.
|
| 198 |
+
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?posizione WHERE {{ progettoMuseo:Autoritratto progettoMuseo:posizioneOpera ?posizione . }} LIMIT 10
|
| 199 |
+
14) Utente: Dove si trova l'opera 'La Notte Stellata'?
|
| 200 |
+
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:LaNotteStellata ?rel ?info . FILTER(?rel != progettoMuseo:posizioneOpera) }} LIMIT 10
|
| 201 |
+
15) Utente: Qual è l'ubicazione dell'opera 'Ramo Di Mandorlo Fiorito'?
|
| 202 |
+
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:RamoDiMandorloFiorito ?rel ?info . FILTER(?rel != progettoMuseo:posizioneOpera) }} LIMIT 10
|
| 203 |
+
|
| 204 |
ECCO L'ONTOLOGIA (TURTLE) PER CONTESTO:
|
| 205 |
{ontology_turtle}
|
| 206 |
FINE ONTOLOGIA.
|
| 207 |
"""
|
| 208 |
+
logger.debug("[create_system_prompt_for_sparql] Prompt generato con esempi originali e nuove regole rigorose sulla posizione.")
|
| 209 |
return prompt
|
| 210 |
|
| 211 |
|
| 212 |
+
|
| 213 |
+
def classify_and_translate(question_text: str, model_answer_text: str):
|
| 214 |
"""
|
| 215 |
Classifica la lingua della domanda e della risposta, quindi traduce la risposta
|
| 216 |
se la lingua è diversa da quella della domanda. L'idea è di restituire una
|
|
|
|
| 223 |
Restituisce:
|
| 224 |
- La risposta tradotta nella lingua della domanda o la risposta originale
|
| 225 |
se entrambe le lingue coincidono.
|
|
|
|
| 226 |
NB: Qui l'oggetto 'lang_detect_client' (per rilevamento lingua) è già
|
| 227 |
stato inizializzato all'avvio dell'app. Mentre il 'translator_client'
|
| 228 |
viene creato 'al volo' poiché la direzione di traduzione dipende
|
|
|
|
| 232 |
try:
|
| 233 |
question_lang_result = lang_detect_client.text_classification(text=question_text)
|
| 234 |
question_lang = question_lang_result[0]['label']
|
| 235 |
+
explanation_dict['language_detection'] = f"Domanda in: {question_lang}."
|
| 236 |
logger.info(f"[LangDetect] Lingua della domanda: {question_lang}")
|
| 237 |
except Exception as e:
|
| 238 |
logger.error(f"Errore nel rilevamento della lingua della domanda: {e}")
|
| 239 |
question_lang = "en" # Fallback se non riusciamo a rilevare la lingua
|
| 240 |
+
explanation_dict['language_detection'] = "Lingua domanda non rilevata, impostata a 'en'."
|
| 241 |
# Rileva la lingua della risposta
|
| 242 |
try:
|
| 243 |
answer_lang_result = lang_detect_client.text_classification(text=model_answer_text)
|
| 244 |
answer_lang = answer_lang_result[0]['label']
|
| 245 |
+
explanation_dict['language_detection'] += f" Risposta in: {answer_lang}."
|
| 246 |
logger.info(f"[LangDetect] Lingua della risposta: {answer_lang}")
|
| 247 |
except Exception as e:
|
| 248 |
logger.error(f"Errore nel rilevamento della lingua della risposta: {e}")
|
| 249 |
answer_lang = "it" # Fallback se non riusciamo a rilevare la lingua
|
| 250 |
+
explanation_dict['language_detection'] += " Lingua risposta non rilevata, impostata a 'it'."
|
| 251 |
|
| 252 |
# Se domanda e risposta sono nella stessa lingua, non traduciamo
|
| 253 |
if question_lang == answer_lang:
|
| 254 |
logger.info("[Translate] Nessuna traduzione necessaria: stessa lingua.")
|
| 255 |
+
explanation_dict['translation'] = "Nessuna traduzione necessaria."
|
| 256 |
return model_answer_text
|
| 257 |
|
| 258 |
# Altrimenti, costruiamo "al volo" il modello di traduzione appropriato
|
|
|
|
| 262 |
token=HF_API_KEY,
|
| 263 |
model=translator_model
|
| 264 |
)
|
| 265 |
+
explanation_dict['translation'] = f"Usato modello: {translator_model}."
|
| 266 |
# Traduzione della risposta
|
| 267 |
try:
|
| 268 |
translation_result = translator_client.translation(text=model_answer_text)
|
| 269 |
translated_answer = translation_result["translation_text"]
|
| 270 |
+
explanation_dict['translation'] += " Traduzione riuscita."
|
| 271 |
except Exception as e:
|
| 272 |
logger.error(f"Errore nella traduzione {answer_lang} -> {question_lang}: {e}")
|
| 273 |
+
explanation_dict['translation'] += " Traduzione fallita, risposta originale usata."
|
| 274 |
# Se fallisce, restituiamo la risposta originale come fallback
|
| 275 |
+
explanation_dict['translation'] = "Errore inizializzazione traduttore."
|
| 276 |
translated_answer = model_answer_text
|
| 277 |
|
| 278 |
return translated_answer
|
|
|
|
| 287 |
"""
|
| 288 |
prompt = (
|
| 289 |
"SEI UNA GUIDA MUSEALE VIRTUALE. "
|
| 290 |
+
"RISPONDI IN MODO DISCORSIVO E NATURALE (circa 100 parole), SENZA SALUTI O INTRODUZIONI PROLISSE. "
|
| 291 |
+
"SE HAI RISULTATI SPARQL, USALI; SE NON CI SONO, RISPONDI BASANDOTI SULLE TUE CONOSCENZE. "
|
| 292 |
+
"QUALORA LA DOMANDA CONTENGA ESPRESSIONI COME 'fammi vedere', 'portami', 'mi fai vedere', O SIMILI, "
|
| 293 |
+
"TRADUCI LA RICHIESTA IN UN INVITO ALL'AZIONE, AD ESEMPIO 'Adesso ti accompagno all'opera', "
|
| 294 |
+
"SENZA RIPETERE DETTAGLI SPAZIALI O TECNICI (ES. COORDINATE, DISTANZE, POSITIONI FISICHE). "
|
| 295 |
+
"PER ALTRE DOMANDE, RISPONDI IN MODO DESCRITTIVO MA SENZA INCLUDERE INFORMAZIONI TECNICHE SULLA POSIZIONE."
|
| 296 |
)
|
| 297 |
logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
|
| 298 |
return prompt
|
|
|
|
| 313 |
|
| 314 |
Parametri:
|
| 315 |
- query: stringa con la query SPARQL potenzialmente mal formattata.
|
|
|
|
| 316 |
Ritorna:
|
| 317 |
- La query SPARQL corretta se possibile, in singola riga.
|
| 318 |
"""
|
|
|
|
| 375 |
# ---------------------------------------------------------------------------
|
| 376 |
@app.post("/assistant")
|
| 377 |
def assistant_endpoint(req: AssistantRequest):
|
| 378 |
+
explanation_dict = {}
|
| 379 |
"""
|
| 380 |
Endpoint che gestisce l'intera pipeline:
|
| 381 |
1) Genera una query SPARQL dal messaggio dell'utente (prompt dedicato).
|
|
|
|
| 397 |
}
|
| 398 |
"""
|
| 399 |
logger.info("Ricevuta chiamata POST su /assistant")
|
|
|
|
| 400 |
# Estraggo i campi dal body della richiesta
|
| 401 |
user_message = req.message
|
| 402 |
max_tokens = req.max_tokens
|
| 403 |
temperature = req.temperature
|
|
|
|
| 404 |
logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
|
| 405 |
+
# -------------------------------
|
| 406 |
+
# CLASSIFICAZIONE DEL TESTO RICEVUTO
|
| 407 |
+
# -------------------------------
|
| 408 |
+
if classifier_model is not None:
|
| 409 |
+
try:
|
| 410 |
+
# Prepara l'input per il modello di classificazione
|
| 411 |
+
inputs = classifier_tokenizer(user_message, return_tensors="pt", truncation=True, padding=True)
|
| 412 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 413 |
+
|
| 414 |
+
# Disattiva il calcolo del gradiente per velocizzare l'inferenza
|
| 415 |
+
with torch.no_grad():
|
| 416 |
+
outputs = classifier_model(**inputs)
|
| 417 |
+
logits = outputs.logits
|
| 418 |
+
pred = torch.argmax(logits, dim=1).item()
|
| 419 |
+
|
| 420 |
+
# Mappa l'etichetta numerica a una stringa (modifica secondo la tua logica)
|
| 421 |
+
label_mapping = {0: "NON PERTINENTE", 1: "PERTINENTE"}
|
| 422 |
+
classification_result = label_mapping.get(pred, f"Etichetta {pred}")
|
| 423 |
+
logger.info(f"[Classificazione] La domanda classificata come: {classification_result}")
|
| 424 |
+
explanation_dict['classification'] = f"Risultato classificazione: {classification_result}"
|
| 425 |
+
except Exception as e:
|
| 426 |
+
logger.error(f"Errore durante la classificazione della domanda: {e}")
|
| 427 |
+
explanation_dict['classification'] = f"Errore classificazione: {e}"
|
| 428 |
+
else:
|
| 429 |
+
logger.warning("Modello di classificazione non disponibile.")
|
| 430 |
+
explanation_dict['classification'] = "Modello di classificazione non disponibile."
|
| 431 |
# -----------------------------------------------------------------------
|
| 432 |
# STEP 1: Generazione della query SPARQL
|
| 433 |
# -----------------------------------------------------------------------
|
|
|
|
| 435 |
# Serializziamo l'ontologia in XML per fornirla al prompt (anche se si chiama 'turtle' va bene così).
|
| 436 |
ontology_turtle = ontology_graph.serialize(format="xml")
|
| 437 |
logger.debug("Ontologia serializzata con successo (XML).")
|
| 438 |
+
explanation_dict['ontology'] = "Ontologia serializzata."
|
| 439 |
except Exception as e:
|
| 440 |
logger.warning(f"Impossibile serializzare l'ontologia in formato XML: {e}")
|
| 441 |
+
explanation_dict['ontology'] = f"Errore serializzazione: {e}."
|
| 442 |
ontology_turtle = ""
|
| 443 |
|
| 444 |
# Creiamo il prompt di sistema per la generazione SPARQL
|
| 445 |
system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
|
| 446 |
+
explanation_dict['sparql_prompt'] = "Prompt SPARQL creato."
|
| 447 |
# Chiamata al modello per generare la query SPARQL
|
| 448 |
try:
|
| 449 |
logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
|
| 450 |
+
explanation_dict['sparql_generation'] = "Generazione query SPARQL iniziata."
|
| 451 |
gen_sparql_output = hf_generation_client.chat.completions.create(
|
| 452 |
messages=[
|
| 453 |
{"role": "system", "content": system_prompt_sparql},
|
|
|
|
| 458 |
)
|
| 459 |
possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
|
| 460 |
logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
|
| 461 |
+
explanation_dict['sparql_generation'] += f" Query: {possible_query}."
|
| 462 |
except Exception as ex:
|
| 463 |
logger.error(f"Errore nella generazione della query SPARQL: {ex}")
|
| 464 |
+
explanation_dict['sparql_generation'] = f"Errore generazione SPARQL: {ex}."
|
| 465 |
# Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
|
| 466 |
possible_query = "NO_SPARQL"
|
| 467 |
|
|
|
|
| 472 |
else:
|
| 473 |
# Applichiamo la correzione avanzata
|
| 474 |
advanced_corrected = correct_sparql_syntax_advanced(possible_query)
|
| 475 |
+
explanation_dict['sparql_correction'] = "Sintassi SPARQL corretta."
|
| 476 |
# Verifichiamo la validità della query
|
| 477 |
if is_sparql_query_valid(advanced_corrected):
|
| 478 |
generated_query = advanced_corrected
|
| 479 |
logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
|
| 480 |
+
explanation_dict['sparql_validation'] = "Query SPARQL valida."
|
| 481 |
else:
|
| 482 |
logger.debug("[assistant_endpoint] Query SPARQL non valida. Verrà ignorata.")
|
| 483 |
+
explanation_dict['sparql_validation'] = "Query SPARQL non valida."
|
| 484 |
generated_query = None
|
| 485 |
|
| 486 |
# -----------------------------------------------------------------------
|
|
|
|
| 489 |
results = []
|
| 490 |
if generated_query:
|
| 491 |
logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
|
| 492 |
+
explanation_dict['sparql_execution'] = "Esecuzione query SPARQL."
|
| 493 |
try:
|
| 494 |
query_result = ontology_graph.query(generated_query)
|
| 495 |
results = list(query_result)
|
| 496 |
logger.info(f"[assistant_endpoint] Query eseguita con successo. Numero risultati = {len(results)}")
|
| 497 |
+
explanation_dict['sparql_execution'] += f" Risultati: {len(results)}."
|
| 498 |
except Exception as ex:
|
| 499 |
logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
|
| 500 |
+
explanation_dict['sparql_execution'] = f"Errore esecuzione: {ex}."
|
| 501 |
results = []
|
| 502 |
+
else:
|
| 503 |
+
explanation_dict['sparql_execution'] = "Nessuna query eseguita."
|
| 504 |
# -----------------------------------------------------------------------
|
| 505 |
# STEP 3: Generazione della risposta finale stile "guida museale"
|
| 506 |
# -----------------------------------------------------------------------
|
| 507 |
system_prompt_guide = create_system_prompt_for_guide()
|
| 508 |
+
explanation_dict['guide_prompt'] = "Prompt guida museale creato."
|
| 509 |
if generated_query and results:
|
| 510 |
# Caso: query generata + risultati SPARQL
|
| 511 |
# Convertiamo i risultati in una stringa più leggibile
|
|
|
|
| 521 |
"Rispondi in modo breve (max ~50 parole)."
|
| 522 |
)
|
| 523 |
logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
|
| 524 |
+
explanation_dict['guide_prompt'] += " Inclusi risultati SPARQL."
|
| 525 |
elif generated_query and not results:
|
| 526 |
# Caso: query valida ma 0 risultati
|
| 527 |
second_prompt = (
|
|
|
|
| 531 |
"Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
|
| 532 |
)
|
| 533 |
logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma senza risultati.")
|
| 534 |
+
explanation_dict['guide_prompt'] += " Query valida senza risultati."
|
| 535 |
else:
|
| 536 |
# Caso: nessuna query generata
|
| 537 |
second_prompt = (
|
|
|
|
| 540 |
"Nessuna query SPARQL generata. Rispondi come puoi, riarrangiando le tue conoscenze."
|
| 541 |
)
|
| 542 |
logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
|
| 543 |
+
explanation_dict['guide_prompt'] += " Nessuna query generata."
|
| 544 |
# Chiamata finale al modello per la risposta "guida museale"
|
| 545 |
try:
|
| 546 |
logger.debug("[assistant_endpoint] Chiamata HF per generare la risposta finale...")
|
| 547 |
+
explanation_dict['response_generation'] = "Generazione risposta finale iniziata."
|
| 548 |
final_output = hf_generation_client.chat.completions.create(
|
| 549 |
messages=[
|
| 550 |
{"role": "system", "content": second_prompt},
|
| 551 |
{"role": "user", "content": "Fornisci la risposta finale."}
|
| 552 |
],
|
| 553 |
+
max_tokens=1024,
|
| 554 |
+
temperature=0.8
|
| 555 |
)
|
| 556 |
final_answer = final_output["choices"][0]["message"]["content"].strip()
|
| 557 |
logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
|
| 558 |
+
explanation_dict['response_generation'] += " Risposta generata."
|
| 559 |
except Exception as ex:
|
| 560 |
logger.error(f"Errore nella generazione della risposta finale: {ex}")
|
| 561 |
+
explanation_dict['response_generation'] = f"Errore generazione risposta finale: {ex}."
|
| 562 |
raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
|
| 563 |
|
| 564 |
# -----------------------------------------------------------------------
|
|
|
|
| 566 |
# -----------------------------------------------------------------------
|
| 567 |
final_ans = classify_and_translate(user_message, final_answer)
|
| 568 |
final_ans = final_ans.replace('\\"', "").replace('\"', "")
|
| 569 |
+
explanation_dict['translation_completion'] = "Traduzione completata."
|
| 570 |
# -----------------------------------------------------------------------
|
| 571 |
# Restituzione in formato JSON
|
| 572 |
# -----------------------------------------------------------------------
|
| 573 |
logger.debug("[assistant_endpoint] Fine elaborazione, restituzione risposta JSON.")
|
| 574 |
return {
|
| 575 |
"query": generated_query,
|
| 576 |
+
"response": final_ans,
|
| 577 |
+
"explanation": explanation_dict
|
| 578 |
}
|
|
|
|
| 579 |
# ---------------------------------------------------------------------------
|
| 580 |
# ENDPOINT DI TEST / HOME
|
| 581 |
# ---------------------------------------------------------------------------
|
|
|
|
| 588 |
return {
|
| 589 |
"message": "Endpoint attivo. Esempio di backend per generare query SPARQL e risposte guida museale."
|
| 590 |
}
|
| 591 |
+
# ---------------------------------------------------------------------------
|
| 592 |
+
# ENDPOINT QUERY STANZE /QUERY_STANZE
|
| 593 |
+
# ---------------------------------------------------------------------------
|
| 594 |
+
@app.get("/query_stanze")
|
| 595 |
+
def query_stanze_endpoint():
|
| 596 |
+
"""
|
| 597 |
+
Endpoint per restituire le stanze con le opere esposte e relativi punti.
|
| 598 |
+
La query restituisce, per ogni triple, la stanza, l'opera e il valore della proprietà
|
| 599 |
+
progettoMuseo:posizioneOpera (facoltativo). Successivamente, in Python, si raggruppa
|
| 600 |
+
per stanza in un dizionario. Per ogni stanza, viene creata una lista di dizionari,
|
| 601 |
+
dove ogni dizionario rappresenta un'opera e contiene:
|
| 602 |
+
- "nome": il localName dell'opera (la parte dopo il simbolo "#")
|
| 603 |
+
- "punto": il valore della proprietà progettoMuseo:posizioneOpera (se presente, altrimenti una stringa vuota)
|
| 604 |
+
|
| 605 |
+
Query usata (in una riga):
|
| 606 |
+
PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
|
| 607 |
+
SELECT ?stanza ?opera ?p
|
| 608 |
+
WHERE {
|
| 609 |
+
?opera progettoMuseo:èEsposto ?stanza.
|
| 610 |
+
OPTIONAL { ?opera progettoMuseo:posizioneOpera ?p. }
|
| 611 |
+
}
|
| 612 |
+
"""
|
| 613 |
+
# Definiamo la query in una singola riga
|
| 614 |
+
query_str = (
|
| 615 |
+
"PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> "
|
| 616 |
+
"SELECT ?stanza ?opera ?p WHERE { ?opera progettoMuseo:èEsposto ?stanza. "
|
| 617 |
+
"OPTIONAL { ?opera progettoMuseo:posizioneOpera ?p. } }"
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
# Applichiamo eventuali correzioni sintattiche
|
| 621 |
+
corrected_query = correct_sparql_syntax_advanced(query_str)
|
| 622 |
+
logger.debug(f"[query_stanze_endpoint] Query corretta:\n{corrected_query}")
|
| 623 |
+
|
| 624 |
+
# Verifichiamo la validità della query utilizzando il parser di rdflib
|
| 625 |
+
if not is_sparql_query_valid(corrected_query):
|
| 626 |
+
logger.error("[query_stanze_endpoint] Query SPARQL non valida.")
|
| 627 |
+
raise HTTPException(status_code=400, detail="Query SPARQL non valida.")
|
| 628 |
+
|
| 629 |
+
try:
|
| 630 |
+
query_result = ontology_graph.query(corrected_query)
|
| 631 |
+
|
| 632 |
+
# Costruiamo un dizionario: per ogni stanza (localName), una lista di dizionari
|
| 633 |
+
# con chiavi "nome" (localName dell'opera) e "punto" (valore di progettoMuseo:posizioneOpera)
|
| 634 |
+
dict_stanze = {}
|
| 635 |
+
for row in query_result:
|
| 636 |
+
# Estraiamo il localName della stanza
|
| 637 |
+
stanza_uri = str(row["stanza"])
|
| 638 |
+
stanza_local = stanza_uri.split("#")[-1].strip() if "#" in stanza_uri else stanza_uri
|
| 639 |
+
|
| 640 |
+
# Estraiamo il localName dell'opera
|
| 641 |
+
opera_uri = str(row["opera"])
|
| 642 |
+
opera_local = opera_uri.split("#")[-1].strip() if "#" in opera_uri else opera_uri
|
| 643 |
+
|
| 644 |
+
# Estraiamo il punto (se presente)
|
| 645 |
+
punto = str(row["p"]) if row["p"] is not None else ""
|
| 646 |
+
|
| 647 |
+
# Costruiamo il dizionario per l'opera
|
| 648 |
+
opera_dict = {"nome": opera_local, "punto": punto}
|
| 649 |
+
|
| 650 |
+
# Raggruppiamo per stanza
|
| 651 |
+
if stanza_local not in dict_stanze:
|
| 652 |
+
dict_stanze[stanza_local] = []
|
| 653 |
+
dict_stanze[stanza_local].append(opera_dict)
|
| 654 |
+
|
| 655 |
+
logger.info(f"[query_stanze_endpoint] Trovate {len(dict_stanze)} stanze.")
|
| 656 |
+
except Exception as ex:
|
| 657 |
+
logger.error(f"[query_stanze_endpoint] Errore nell'esecuzione della query: {ex}")
|
| 658 |
+
raise HTTPException(status_code=500, detail="Errore nell'esecuzione della query SPARQL.")
|
| 659 |
+
|
| 660 |
+
return {
|
| 661 |
+
"stanze": dict_stanze
|
| 662 |
+
}
|
| 663 |
# ---------------------------------------------------------------------------
|
| 664 |
# MAIN
|
| 665 |
# ---------------------------------------------------------------------------
|
|
|
|
| 668 |
Avvio dell'applicazione FastAPI sulla porta 8000,
|
| 669 |
utile se eseguito come script principale.
|
| 670 |
"""
|
| 671 |
+
logger.info("Avvio dell'applicazione FastAPI.")
|
| 672 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|