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