File size: 18,777 Bytes
ac52c4d cc035bf ac52c4d cc035bf 89f944e cc035bf cf9b229 cc035bf 8b2fc25 cc035bf 2ecaeab cc035bf 2ecaeab 51695fc 2ecaeab ac52c4d cc035bf cf9b229 cc035bf cf9b229 848d0c0 ac52c4d cc035bf 2ecaeb3 cc035bf 2ecaeb3 cc035bf 10352ef cc035bf 10352ef cc035bf 10352ef cf9b229 cc035bf ff8bfcc cc035bf 3421ea1 cc035bf 3421ea1 cc035bf ac52c4d cc035bf 3421ea1 cc035bf 3421ea1 cc035bf 69d6e40 cc035bf ff8bfcc cc035bf 51695fc cc035bf 51695fc cc035bf ff8bfcc 30e0c2a cc035bf cf9b229 9ead203 cc035bf ac52c4d cc035bf cb51311 cc035bf 30e0c2a cc035bf ac52c4d cc035bf 2ecaeb3 cc035bf e250196 cc035bf 30e0c2a cc035bf d9ab7eb cc035bf 8b2fc25 cc035bf 51695fc cc035bf 8607561 cc035bf 8607561 cc035bf 8607561 cc035bf e250196 cc035bf 8607561 cc035bf 3421ea1 cc035bf 69d6e40 cc035bf 9ead203 cc035bf 0feac5e cc035bf 0feac5e 9ead203 cc035bf ac52c4d 848d0c0 cc035bf 2ecaeb3 cc035bf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 |
import os
import logging
import uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import rdflib
from rdflib.plugins.sparql.parser import parseQuery
from huggingface_hub import InferenceClient
import re
# ---------------------------------------------------------------------------
# CONFIGURAZIONE LOGGING
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.DEBUG, # DEBUG per un log più dettagliato
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("app.log"), logging.StreamHandler()]
)
logger = logging.getLogger(__name__)
# Categorie di zero-shot classification
CANDIDATE_LABELS = ["domanda_museo", "small_talk", "fuori_contesto"]
HF_API_KEY = os.getenv("HF_API_KEY")
HF_MODEL = "meta-llama/Llama-3.3-70B-Instruct" # modello per query SPARQL e risposte
ZERO_SHOT_MODEL = "facebook/bart-large-mnli" # modello per zero-shot classification
if not HF_API_KEY:
logger.error("HF_API_KEY non impostata.")
raise EnvironmentError("HF_API_KEY non impostata.")
# ---------------------------------------------------------------------------
# INIZIALIZZIAMO IL CLIENT PER ZERO-SHOT
# ---------------------------------------------------------------------------
try:
logger.info("Inizializzazione del client per Zero-Shot Classification.")
client_cls = InferenceClient(
token=HF_API_KEY,
model=ZERO_SHOT_MODEL
)
logger.info("Client zero-shot creato con successo.")
except Exception as ex:
logger.error(f"Errore nell'inizializzazione del client zero-shot: {ex}")
raise ex
# ---------------------------------------------------------------------------
# FUNZIONE DI CLASSIFICAZIONE
# ---------------------------------------------------------------------------
def classify_message_inference_api(text: str) -> str:
"""
Usa client_cls.zero_shot_classification(...) per classificare
'domanda_museo', 'small_talk' o 'fuori_contesto'.
Restituisce la label top.
"""
try:
hypothesis_template = "Questa domanda è inerente all'arte o all'ontologia di un museo ({}), oppure no?"
# multi_label=False => elegge UNA sola label top
results = client_cls.zero_shot_classification(
text=text,
candidate_labels=CANDIDATE_LABELS,
multi_label=False,
hypothesis_template=hypothesis_template
)
# results è una lista di ZeroShotClassificationOutputElement
# es: [ZeroShotClassificationOutputElement(label='domanda_museo', score=0.85), ...]
top_label = results[0].label
top_score = results[0].score
logger.info(f"[ZeroShot] top_label={top_label}, score={top_score}")
return top_label
except Exception as e:
logger.error(f"Errore nella zero-shot classification: {e}")
return "fuori_contesto" # fallback in caso di errore
# Inizializziamo la nostra ontologia
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RDF_FILE = os.path.join(BASE_DIR, "Ontologia_corretto.rdf")
client_cls = InferenceClient(token=HF_API_KEY)
ontology_graph = rdflib.Graph()
try:
# L'ontologia è in formato RDF/XML
logger.info(f"Caricamento ontologia da file: {RDF_FILE}")
ontology_graph.parse(RDF_FILE, format="xml")
logger.info("Ontologia RDF caricata correttamente (formato XML).")
except Exception as e:
logger.error(f"Errore nel caricamento dell'ontologia: {e}")
raise e
# ---------------------------------------------------------------------------
# DEFINIZIONE DELL'APP FASTAPI
# ---------------------------------------------------------------------------
app = FastAPI()
# Modello di request
class AssistantRequest(BaseModel):
message: str
max_tokens: int = 512
temperature: float = 0.5
# ---------------------------------------------------------------------------
# FUNZIONI DI SUPPORTO (Prompts, validazione SPARQL, correzione)
# ---------------------------------------------------------------------------
def create_system_prompt_for_sparql(ontology_turtle: str) -> str:
"""
PRIMO PROMPT DI SISTEMA molto prolisso e stringente sulle regole SPARQL,
con i vari esempi (1-10) inclusi.
"""
prompt = f"""SEI UN GENERATORE DI QUERY SPARQL PER L'ONTOLOGIA DI UN MUSEO.
DEVI GENERARE SOLO UNA QUERY SPARQL (IN UNA SOLA RIGA) SE LA DOMANDA RIGUARDA INFORMAZIONI NELL'ONTOLOGIA.
SE LA DOMANDA NON È ATTINENTE, RISPONDI 'NO_SPARQL'.
REGOLE SINTATTICHE RIGOROSE:
1) Usare: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
2) Query in UNA SOLA RIGA (niente a capo), forma: PREFIX progettoMuseo: <...> SELECT ?x WHERE {{ ... }} LIMIT N
3) Attento agli spazi:
- Dopo SELECT: es. SELECT ?autore
- Tra proprietà e variabile: es. progettoMuseo:autoreOpera ?autore .
- Non incollare il '?' a 'progettoMuseo:'.
- Ogni tripla termina con un punto.
4) Se non puoi generare una query valida, rispondi solo 'NO_SPARQL'.
Esempi di Domande Specifiche e relative Query:
1) Utente: Chi ha creato l'opera 'Afrodite di Milo'?
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?autore WHERE {{ progettoMuseo:AfroditeDiMilo progettoMuseo:autoreOpera ?autore . }} LIMIT 10
2) Utente: Quali sono le tecniche utilizzate nelle opere?
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?tecnica WHERE {{ ?opera progettoMuseo:tecnicaOpera ?tecnica . }} LIMIT 100
3) Utente: Quali sono le dimensioni delle opere?
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera ?dimensione WHERE {{ ?opera progettoMuseo:dimensioneOpera ?dimensione . }} LIMIT 100
4) Utente: Quali opere sono esposte nella stanza Greca?
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ progettoMuseo:StanzaGrecia progettoMuseo:Espone ?opera . }} LIMIT 100
5) Utente: Quali sono le proprietà e i tipi delle proprietà nell'ontologia?
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)) }}
6) Utente: Recupera tutti i biglietti e i tipi di biglietto.
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
7) Utente: Recupera tutti i visitatori e i tour a cui partecipano.
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?visitatore ?tour WHERE {{ ?visitatore progettoMuseo:Partecipazione_a_Evento ?tour . }} LIMIT 100
8) Utente: Recupera tutte le stanze tematiche e le opere esposte.
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
9) Utente: Recupera tutte le opere con materiale 'Marmo'.
Risposta: PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?opera WHERE {{ ?opera progettoMuseo:materialeOpera "Marmo"@it . }} LIMIT 100
10) Utente: Recupera tutti i visitatori con data di nascita dopo il 2000.
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
ECCO L'ONTOLOGIA (TURTLE) PER CONTESTO:
{ontology_turtle}
FINE ONTOLOGIA.
"""
logger.debug("[create_system_prompt_for_sparql] Prompt generato con ESEMPI e regole SPARQL.")
return prompt
def create_system_prompt_for_guide() -> str:
"""
SECONDO PROMPT DI SISTEMA:
- Risponde in stile "guida museale" in modo breve (max ~50 parole).
- Se c'è una query e risultati, descrive brevemente.
- Se non c'è query o non ci sono risultati, prova comunque a dare una risposta.
"""
prompt = (
"SEI UNA GUIDA MUSEALE VIRTUALE. "
"RISPONDI IN MODO BREVE (~50 PAROLE), SENZA SALUTI O INTRODUZIONI PROLISSE. "
"SE HAI RISULTATI SPARQL, USALI. "
"SE NON HAI RISULTATI O NON HAI UNA QUERY, RISPONDI COMUNQUE CERCANDO DI RIARRANGIARE LE TUE CONOSCENZE."
)
logger.debug("[create_system_prompt_for_guide] Prompt per la risposta guida museale generato.")
return prompt
def correct_sparql_syntax_advanced(query: str) -> str:
"""
Corregge in maniera più complessa gli errori sintattici comuni generati dal modello
nelle query SPARQL, tramite euristiche:
- Spazi dopo SELECT, WHERE
- Rimozione di '?autore' attaccato a 'progettoMuseo:autoreOpera?autore'
- Aggiunta di PREFIX se assente
- Rimozione newline (una riga)
- Aggiunta di '.' se manca a fine tripla
- Pulizia di spazi doppi
"""
original_query = query
logger.debug(f"[correct_sparql_syntax_advanced] Query originaria:\n{original_query}")
# 1) Rimuoviamo newline e forziamo un'unica riga
query = query.replace('\n', ' ').replace('\r', ' ')
# 2) Se manca il PREFIX, lo aggiungiamo in testa (solo se notiamo che non c'è "PREFIX progettoMuseo:")
if 'PREFIX progettoMuseo:' not in query:
logger.debug("[correct_sparql_syntax_advanced] Aggiungo PREFIX progettoMuseo.")
query = ("PREFIX progettoMuseo: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> "
+ query)
# 3) Spazio dopo SELECT se manca
query = re.sub(r'(SELECT)(\?|\*)', r'\1 \2', query, flags=re.IGNORECASE)
# 4) Spazio dopo WHERE se manca
query = re.sub(r'(WHERE)\{', r'\1 {', query, flags=re.IGNORECASE)
# 5) Correggiamo i punti interrogativi attaccati alle proprietà:
# "progettoMuseo:autoreOpera?autore" => "progettoMuseo:autoreOpera ?autore"
query = re.sub(r'(progettoMuseo:\w+)\?(\w+)', r'\1 ?\2', query)
# 6) Rimuoviamo spazi multipli
query = re.sub(r'\s+', ' ', query).strip()
# 7) Aggiungiamo '.' a fine tripla prima del '}' se manca
query = re.sub(r'(\?\w+)\s*\}', r'\1 . }', query)
# 8) Se manca la clausola WHERE, proviamo ad aggiungerla
if 'WHERE' not in query.upper():
query = re.sub(r'(SELECT\s+[^\{]+)\{', r'\1 WHERE {', query, flags=re.IGNORECASE)
# 9) Pulizia finale di spazi
query = re.sub(r'\s+', ' ', query).strip()
logger.debug(f"[correct_sparql_syntax_advanced] Query dopo correzioni:\n{query}")
return query
def is_sparql_query_valid(query: str) -> bool:
"""Verifica la sintassi SPARQL tramite rdflib."""
logger.debug(f"[is_sparql_query_valid] Validazione SPARQL: {query}")
try:
parseQuery(query)
logger.debug("[is_sparql_query_valid] Query SPARQL sintatticamente corretta.")
return True
except Exception as ex:
logger.warning(f"[is_sparql_query_valid] Query non valida: {ex}")
return False
# ---------------------------------------------------------------------------
# ENDPOINT UNICO
# ---------------------------------------------------------------------------
@app.post("/assistant")
def assistant_endpoint(req: AssistantRequest):
"""
Endpoint UNICO con due step interni:
1) Genera la query SPARQL (prompt prolisso).
2) Esegue la query (se valida) e fornisce una risposta breve stile "guida museale",
anche se i risultati sono vuoti o la query non esiste.
"""
logger.info("Ricevuta chiamata POST su /assistant")
user_message = req.message
max_tokens = req.max_tokens
temperature = req.temperature
label = classify_message_inference_api(user_message)
logger.info(label)
logger.debug(f"Parametri utente: message='{user_message}', max_tokens={max_tokens}, temperature={temperature}")
# STEP 1: Generazione SPARQL
try:
logger.debug("Serializzazione dell'ontologia in formato Turtle per contesto nel prompt.")
ontology_turtle = ontology_graph.serialize(format="xml")
logger.debug("Ontologia serializzata con successo (XML).")
except Exception as e:
logger.warning(f"Impossibile serializzare l'ontologia in Turtle: {e}")
ontology_turtle = ""
system_prompt_sparql = create_system_prompt_for_sparql(ontology_turtle)
# Inizializziamo client Hugging Face
try:
logger.debug(f"Inizializzazione InferenceClient con modello='{HF_MODEL}'.")
hf_client = InferenceClient(model=HF_MODEL, token=HF_API_KEY)
except Exception as ex:
logger.error(f"Errore inizializzazione HF client: {ex}")
raise HTTPException(status_code=500, detail="Impossibile inizializzare il modello Hugging Face.")
# Chiediamo al modello la query SPARQL (fase interna 1)
try:
logger.debug("[assistant_endpoint] Chiamata HF per generare la query SPARQL...")
gen_sparql_output = hf_client.chat.completions.create(
messages=[
{"role": "system", "content": system_prompt_sparql},
{"role": "user", "content": user_message}
],
max_tokens=512,
temperature=0.3
)
possible_query = gen_sparql_output["choices"][0]["message"]["content"].strip()
logger.info(f"[assistant_endpoint] Query generata dal modello: {possible_query}")
except Exception as ex:
logger.error(f"Errore nella generazione della query SPARQL: {ex}")
# Se fallisce la generazione, consideriamo la query come "NO_SPARQL"
possible_query = "NO_SPARQL"
# Verifica se la query è NO_SPARQL
if possible_query.upper().startswith("NO_SPARQL"):
generated_query = None
logger.debug("[assistant_endpoint] Modello indica 'NO_SPARQL', nessuna query generata.")
else:
# Correggiamo in modo avanzato
advanced_corrected = correct_sparql_syntax_advanced(possible_query)
# Dopo la correzione, verifichiamo se è valida
if is_sparql_query_valid(advanced_corrected):
generated_query = advanced_corrected
logger.debug(f"[assistant_endpoint] Query SPARQL valida dopo correzione avanzata: {generated_query}")
else:
logger.debug("[assistant_endpoint] Query SPARQL non valida dopo correzione avanzata. La ignoriamo.")
generated_query = None
# STEP 2: Esecuzione query (se presente) e risposta guida
results = []
if generated_query:
logger.debug(f"[assistant_endpoint] Esecuzione della query SPARQL:\n{generated_query}")
try:
query_result = ontology_graph.query(generated_query)
results = list(query_result)
logger.info(f"[assistant_endpoint] Query eseguita con successo. Numero risultati = {len(results)}")
except Exception as ex:
logger.error(f"[assistant_endpoint] Errore nell'esecuzione della query: {ex}")
results = []
# Creiamo il prompt di sistema "guida museale"
system_prompt_guide = create_system_prompt_for_guide()
if generated_query and results:
# Abbiamo query + risultati
# Convertiamo i risultati in una stringa più leggibile
results_str = "\n".join(
f"{idx+1}) " + ", ".join(
f"{var}={row[var]}"
for var in row.labels
)
for idx, row in enumerate(results)
)
second_prompt = (
f"{system_prompt_guide}\n\n"
f"Domanda utente: {user_message}\n"
f"Query generata: {generated_query}\n"
f"Risultati:\n{results_str}\n"
"Rispondi in modo breve (max ~50 parole)."
)
logger.debug("[assistant_endpoint] Prompt di risposta con risultati SPARQL.")
elif generated_query and not results:
# Query valida ma 0 risultati
second_prompt = (
f"{system_prompt_guide}\n\n"
f"Domanda utente: {user_message}\n"
f"Query generata: {generated_query}\n"
"Nessun risultato dalla query. Prova comunque a rispondere con le tue conoscenze."
)
logger.debug("[assistant_endpoint] Prompt di risposta: query valida ma nessun risultato.")
else:
# Nessuna query generata
second_prompt = (
f"{system_prompt_guide}\n\n"
f"Domanda utente: {user_message}\n"
"Nessuna query SPARQL generata. Rispondi come puoi, riarrangiando le tue conoscenze."
)
logger.debug("[assistant_endpoint] Prompt di risposta: nessuna query generata.")
# Ultima chiamata al modello per la risposta finale
try:
logger.debug("[assistant_endpoint] Chiamata HF per la risposta guida museale...")
final_output = hf_client.chat.completions.create(
messages=[
{"role": "system", "content": second_prompt},
{"role": "user", "content": "Fornisci la risposta finale."}
],
max_tokens=512,
temperature=0.7
)
final_answer = final_output["choices"][0]["message"]["content"].strip()
logger.info(f"[assistant_endpoint] Risposta finale generata: {final_answer}")
except Exception as ex:
logger.error(f"Errore nella generazione della risposta finale: {ex}")
raise HTTPException(status_code=500, detail="Errore nella generazione della risposta in linguaggio naturale.")
# Risposta JSON
logger.debug("[assistant_endpoint] Fine elaborazione. Restituzione risposta.")
return {
"query": generated_query,
"response": final_answer
}
# ---------------------------------------------------------------------------
# ENDPOINT DI TEST
# ---------------------------------------------------------------------------
@app.get("/")
def home():
logger.debug("Chiamata GET su '/' - home.")
return {
"message": "Endpoint con ESEMPI di query SPARQL + correzione avanzata + risposta guida museale."
}
# ---------------------------------------------------------------------------
# MAIN
# ---------------------------------------------------------------------------
if __name__ == "__main__":
logger.info("Avvio dell'applicazione FastAPI sulla porta 8000.") |