import os import logging from typing import Optional from pydantic import BaseModel from fastapi import FastAPI, HTTPException import rdflib from huggingface_hub import InferenceClient # ============================== # CONFIGURAZIONE LOGGING # ============================== logging.basicConfig( level=logging.DEBUG, # Se vuoi log dettagliato format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("app.log"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # ============================== # PARAMETRI # ============================== API_KEY = os.getenv("HF_API_KEY") if not API_KEY: logger.error("HF_API_KEY non impostata nell'ambiente.") raise EnvironmentError("HF_API_KEY non impostata nell'ambiente.") client = InferenceClient(api_key=API_KEY) RDF_FILE = "Ontologia.rdf" MAX_TRIPLES = 600 # Numero di triple da includere nel prompt HF_MODEL = "Qwen/Qwen2.5-72B-Instruct" # Nome del modello Hugging Face # ============================== # STEP 0: Caricamento e Preprocessing dell'Ontologia # ============================== def load_ontology_as_text(rdf_file: str, max_triples: int = 300) -> str: """ Legge un numero limitato di triple dall'ontologia e le converte in una stringa 'knowledge_text'. """ if not os.path.exists(rdf_file): return "Nessun file RDF trovato." g = rdflib.Graph() try: g.parse(rdf_file, format="xml") except Exception as e: logger.error(f"Errore parsing RDF: {e}") return "Errore nel parsing RDF." lines = [] count = 0 for s, p, o in g: if count >= max_triples: break s_str = str(s)[:200] p_str = str(p)[:200] o_str = str(o)[:200] line = f"- {s_str} | {p_str} | {o_str}" lines.append(line) count += 1 knowledge_text = "\n".join(lines) return knowledge_text knowledge_text = load_ontology_as_text(RDF_FILE, max_triples=MAX_TRIPLES) logger.debug(f"[ONTOLOGY TEXT] {knowledge_text[:1000]} ...") # ============================== # FUNZIONI UTILI # ============================== def create_initial_system_message(knowledge_text: str) -> str: """ System prompt 'stringente' che ordina di generare SEMPRE una query SPARQL. """ system_prompt = f""" Sei un assistente di un museo. Hai accesso a queste triple (max {MAX_TRIPLES}): {knowledge_text} REGOLE TASSATIVE: 1) Per qualunque domanda riguardi il contenuto di queste triple, DEVI generare una query SPARQL con il prefisso: PREFIX base: 2) Se la query produce 0 risultati o fallisce, devi provare una seconda volta cambiando la sintassi (ad es. usando FILTER(STR(?nome)) o @it). 3) Se la domanda è chat generica, rispondi brevemente, ma se appare una parte di knowledge potresti tentare comunque una query. 4) Se trovi almeno un risultato, fornisci la query SPARQL come tua risposta. 5) NON inventare triple che non esistono in questo testo. 6) Usa esclusivamente i dati contenuti qui. Se non trovi nulla, di' "Non ci sono info". 7) Se la query è generata ma produce 0 results, prova un secondo tentativo. Buona fortuna! """ return system_prompt def create_explanation_prompt(results_str: str) -> str: """ Prompt da passare al modello per spiegare i risultati. """ # Chiedi una spiegazione dettagliata ma non prolissa. explanation_prompt = f""" Abbiamo ottenuto questi risultati SPARQL: {results_str} Per favore spiega in modo dettagliato (ma non eccessivamente lungo) i risultati come farebbe una guida museale. Cita, se serve, periodi storici o materiali trovati, e fa' un breve richiamo all'ontologia. Non inventare nulla oltre. """ return explanation_prompt async def call_model(messages, temperature=0.7, max_tokens=2048) -> str: """ Chiama HuggingFace Inference endpoint con i messaggi forniti. Logga diversi step per debugging. """ logger.debug("[call_model] MESSAGGI INVIATI AL MODELLO:") for msg in messages: logger.debug(f"ROLE={msg['role']} CONTENT={msg['content'][:500]}") logger.info("[call_model] Chiamata al modello Hugging Face...") try: response = client.chat.completions.create( model=HF_MODEL, messages=messages, temperature=temperature, max_tokens=max_tokens, top_p=0.9 ) raw_text = response["choices"][0]["message"]["content"] logger.debug("[call_model] Risposta raw del modello HF:\n" + raw_text) return raw_text.replace("\n", " ").strip() except Exception as e: logger.error(f"[call_model] Errore durante la chiamata: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================== # FASTAPI # ============================== app = FastAPI() class QueryRequest(BaseModel): message: str max_tokens: int = 1024 temperature: float = 0.5 @app.post("/generate-response/") async def generate_response(req: QueryRequest): user_input = req.message logger.info(f"[REQUEST] Ricevuta richiesta: {user_input}") # 1) Creiamo system message con l'ontologia system_prompt = create_initial_system_message(knowledge_text) logger.debug("[SYSTEM PROMPT] " + system_prompt[:1000] + "...") # 2) Prima chiamata => generare query SPARQL messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ] response_text = await call_model(messages, req.temperature, req.max_tokens) logger.info(f"[PRIMA RISPOSTA MODELLO] {response_text}") # 3) Se non abbiamo "PREFIX base:" => second attempt if not response_text.startswith("PREFIX base:"): second_try_prompt = f""" Non hai fornito una query SPARQL. Ricorda di generarla SEMPRE. Riprova con un approccio differente per questa domanda: {user_input} """ second_messages = [ {"role": "system", "content": system_prompt}, {"role": "assistant", "content": response_text}, {"role": "user", "content": second_try_prompt} ] response_text_2 = await call_model(second_messages, req.temperature, req.max_tokens) logger.info(f"[SECONDA RISPOSTA MODELLO] {response_text_2}") if response_text_2.startswith("PREFIX base:"): response_text = response_text_2 else: # Neanche al secondo tentativo => restituiamo come "chat" logger.info("[FALLBACK] Nessuna query generata anche al secondo tentativo.") return { "type": "NATURAL", "response": response_text_2 } # 4) Ora dovremmo avere una query SPARQL in 'response_text' sparql_query = response_text logger.info(f"[FINAL QUERY] {sparql_query}") # 5) Eseguiamo la query con rdflib g = rdflib.Graph() try: g.parse(RDF_FILE, format="xml") except Exception as e: logger.error(f"[ERROR] Parsing RDF: {e}") return {"type": "ERROR", "response": "Errore nel parsing dell'ontologia."} # 6) Validazione + esecuzione try: results = g.query(sparql_query) except Exception as e: logger.warning(f"[QUERY FAIL] {e}") # Tenta un 2° fallback in caso la query non sia valida second_try_prompt2 = f""" La query SPARQL che hai fornito non è valida o non produce risultati. Riprova con una sintassi differente. Domanda utente: {user_input} """ second_messages2 = [ {"role": "system", "content": system_prompt}, {"role": "assistant", "content": sparql_query}, {"role": "user", "content": second_try_prompt2} ] second_response2 = await call_model(second_messages2, req.temperature, req.max_tokens) logger.info(f"[TERZO TENTATIVO MODELLO] {second_response2}") if second_response2.startswith("PREFIX base:"): # eseguiamo di nuovo sparql_query = second_response2 try: results = g.query(sparql_query) except Exception as e2: logger.error(f"[QUERY FAIL 2] {e2}") return {"type": "ERROR", "response": "Query fallita di nuovo."} else: return {"type": "NATURAL", "response": second_response2} # 7) Se 0 results => fine if len(results) == 0: logger.info("[SPARQL RESULT] 0 risultati.") return {"type": "NATURAL", "response": "Nessun risultato trovato nella nostra ontologia."} # 8) Ok => costruiamo una stringa con i risultati e facciamo un NUOVO prompt di interpretazione row_list = [] for row in results: row_str = ", ".join([f"{k}:{v}" for k, v in row.asdict().items()]) row_list.append(row_str) results_str = "\n".join(row_list) logger.info(f"[SPARQL OK] Trovati {len(results)} risultati.\n{results_str}") # 9) Creiamo prompt di interpretazione explain_prompt = create_explanation_prompt(results_str) explain_messages = [ {"role": "system", "content": explain_prompt}, {"role": "user", "content": ""} ] explanation_text = await call_model(explain_messages, req.temperature, req.max_tokens) logger.info(f"[EXPLANATION RESPONSE] {explanation_text}") # 10) Restituiamo i risultati e la spiegazione finale return { "type": "NATURAL", "sparql_query": sparql_query, "sparql_results": row_list, # Lista di stringhe "explanation": explanation_text } @app.get("/") async def root(): return {"message": "Server con ontologia in system prompt, doppio tentativo SPARQL e interpretazione."}