|
import os |
|
import logging |
|
from typing import Optional |
|
from pydantic import BaseModel |
|
from fastapi import FastAPI, HTTPException |
|
import rdflib |
|
from huggingface_hub import InferenceClient |
|
|
|
logging.basicConfig( |
|
level=logging.DEBUG, |
|
format="%(asctime)s - %(levelname)s - %(message)s", |
|
handlers=[ |
|
logging.FileHandler("app.log"), |
|
logging.StreamHandler() |
|
] |
|
) |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
API_KEY = os.getenv("HF_API_KEY") |
|
if not API_KEY: |
|
logger.error("HF_API_KEY non impostata.") |
|
raise EnvironmentError("HF_API_KEY non impostata.") |
|
|
|
client = InferenceClient(api_key=API_KEY) |
|
|
|
RDF_FILE = "Ontologia.rdf" |
|
MAX_TRIPLES = 300 |
|
HF_MODEL = "Qwen/Qwen2.5-72B-Instruct" |
|
|
|
|
|
def load_ontology_as_text(rdf_file: str, max_triples: int=300) -> str: |
|
""" |
|
Legge un numero limitato di triple dal file RDF e le concatena in |
|
una stringa. Limita i literal in lunghezza per non gonfiare troppo il prompt. |
|
""" |
|
if not os.path.exists(rdf_file): |
|
logger.warning("File RDF non trovato.") |
|
return "NO_RDF" |
|
|
|
g = rdflib.Graph() |
|
try: |
|
g.parse(rdf_file, format="xml") |
|
except Exception as e: |
|
logger.error(f"Errore parsing RDF: {e}") |
|
return "PARSING_ERROR" |
|
|
|
lines = [] |
|
count = 0 |
|
for s,p,o in g: |
|
if count >= max_triples: |
|
break |
|
|
|
s_str = str(s)[:100].replace("\n"," ") |
|
p_str = str(p)[:100].replace("\n"," ") |
|
o_str = str(o)[:100].replace("\n"," ") |
|
lines.append(f"{s_str}|{p_str}|{o_str}") |
|
count+=1 |
|
|
|
|
|
|
|
ontology_text = "\n".join(lines) |
|
logger.debug(f"Caricate {count} triple.") |
|
return ontology_text |
|
|
|
knowledge_text = load_ontology_as_text(RDF_FILE, MAX_TRIPLES) |
|
|
|
def create_system_message(ont_text: str) -> str: |
|
""" |
|
Prompt di sistema robusto e stringente. |
|
- Forza query SPARQL in UNA sola riga |
|
- Richiede secondi tentativi |
|
- Domande generiche -> risposte brevi, ma se c'è un modo di fare query, farlo |
|
""" |
|
system_msg = f""" |
|
Sei un assistente museale. Hai a disposizione un estratto di triple RDF (massimo {MAX_TRIPLES}): |
|
|
|
--- TRIPLE --- |
|
{ont_text} |
|
--- FINE TRIPLE --- |
|
|
|
REGOLE STRINGENTI: |
|
1) Se l'utente chiede informazioni correlate alle triple, DEVI generare una query SPARQL in UNA SOLA RIGA, |
|
con il prefisso: |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> |
|
Esempio: PREFIX base: <...> SELECT ?x WHERE {{ ... }} (tutto su una sola riga). |
|
2) Se la query produce 0 risultati o fallisce, devi ritentare con un secondo tentativo, |
|
magari usando FILTER(STR(...)) o @it, o cambiando minuscole/maiuscole. |
|
3) Se la domanda è una chat generica, rispondi breve (saluto, ecc.). Ma se c'è qualcosa |
|
di correlato, prova comunque la query. |
|
4) Se generi la query e trovi risultati, la risposta finale deve essere la query SPARQL |
|
(una sola riga). Non inventare triple inesistenti. |
|
5) Se non trovi nulla, rispondi 'Non ci sono informazioni in queste triple.' |
|
6) Non ignorare: se l'utente fa domanda su 'David', 'Amore e Psiche', etc., devi |
|
estrarre dai triple tutti i dettagli possibili con SPARQL. |
|
7) Se la query produce 0, prova un secondo tentativo con sintassi differente. |
|
8) Non fare risposte su più righe per la query: una sola riga. |
|
|
|
FINE REGOLE. |
|
""" |
|
return system_msg |
|
|
|
def create_explanation_prompt(results_str: str) -> str: |
|
""" |
|
Prompt per spiegazione finale, robusto: |
|
- Spiega i risultati come guida museale |
|
- Non inventare |
|
- 10-15 righe max |
|
""" |
|
msg = f""" |
|
Ho ottenuto questi risultati SPARQL: |
|
{results_str} |
|
|
|
Ora fornisci una spiegazione come guida museale, in modo comprensibile e dettagliato (max ~10-15 righe). |
|
Cita eventuali materiali, periodi storici, autore, facendo riferimento al contesto RDF (ma senza contraddizioni). |
|
Non inventare nulla oltre ciò che appare nei risultati. |
|
""" |
|
return msg |
|
|
|
async def call_hf_model(messages, temperature=0.7, max_tokens=1024) -> str: |
|
""" |
|
Funzione di chiamata al modello su HuggingFace, |
|
con log e robustezza. max_tokens=1024 come richiesto. |
|
""" |
|
logger.debug("Chiamo HF con i seguenti messaggi:") |
|
for m in messages: |
|
logger.debug(f"ROLE={m['role']} -> {m['content'][:500]}") |
|
|
|
try: |
|
resp = client.chat.completions.create( |
|
model=HF_MODEL, |
|
messages=messages, |
|
temperature=temperature, |
|
max_tokens=max_tokens, |
|
top_p=0.9 |
|
) |
|
raw = resp["choices"][0]["message"]["content"] |
|
logger.debug(f"Risposta HF: {raw}") |
|
return raw.replace("\n"," ").strip() |
|
except Exception as e: |
|
logger.error(f"Errore HF: {e}") |
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
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"Utente chiede: {user_input}") |
|
|
|
system_msg = create_system_message(knowledge_text) |
|
messages = [ |
|
{"role":"system","content":system_msg}, |
|
{"role":"user","content":user_input} |
|
] |
|
|
|
first_response = await call_hf_model(messages, req.temperature, req.max_tokens) |
|
logger.info(f"Prima risposta generata:\n{first_response}") |
|
|
|
|
|
if not first_response.startswith("PREFIX base:"): |
|
second_prompt = f"Non hai risposto con query SPARQL in una sola riga. Riprova. Domanda: {user_input}" |
|
second_msgs = [ |
|
{"role":"system","content":system_msg}, |
|
{"role":"assistant","content":first_response}, |
|
{"role":"user","content":second_prompt} |
|
] |
|
second_response = await call_hf_model(second_msgs, req.temperature, req.max_tokens) |
|
logger.info(f"Seconda risposta:\n{second_response}") |
|
if second_response.startswith("PREFIX base:"): |
|
sparql_query = second_response |
|
else: |
|
return {"type":"NATURAL","response": second_response} |
|
else: |
|
sparql_query = first_response |
|
|
|
|
|
g = rdflib.Graph() |
|
try: |
|
g.parse(RDF_FILE, format="xml") |
|
except Exception as e: |
|
logger.error("Errore parse RDF: ", e) |
|
return {"type":"ERROR","response":"Parsing RDF fallito."} |
|
|
|
|
|
try: |
|
results = g.query(sparql_query) |
|
except Exception as e: |
|
logger.warning(f"La query SPARQL ha fallito: {e}") |
|
fallback_prompt = f"La query SPARQL è fallita. Riprova con altra sintassi. Domanda: {user_input}" |
|
fallback_msgs = [ |
|
{"role":"system","content":system_msg}, |
|
{"role":"assistant","content":sparql_query}, |
|
{"role":"user","content":fallback_prompt} |
|
] |
|
fallback_resp = await call_hf_model(fallback_msgs, req.temperature, req.max_tokens) |
|
if fallback_resp.startswith("PREFIX base:"): |
|
sparql_query = fallback_resp |
|
try: |
|
results = g.query(sparql_query) |
|
except Exception as e2: |
|
return {"type":"ERROR","response":f"Query fallita di nuovo: {e2}"} |
|
else: |
|
return {"type":"NATURAL","response": fallback_resp} |
|
|
|
if len(results)==0: |
|
logger.info("0 risultati SPARQL.") |
|
return {"type":"NATURAL","sparql_query": sparql_query,"response":"Nessun risultato."} |
|
|
|
|
|
row_list = [] |
|
for row in results: |
|
row_list.append(", ".join([f"{k}:{v}" for k,v in row.asdict().items()])) |
|
results_str = "\n".join(row_list) |
|
logger.info(f"Risultati trovati ({len(results)}):\n{results_str}") |
|
|
|
|
|
explain_sys = create_explanation_prompt(results_str) |
|
explain_msgs = [ |
|
{"role":"system","content":explain_sys}, |
|
{"role":"user","content":""} |
|
] |
|
explanation = await call_hf_model(explain_msgs, req.temperature, req.max_tokens) |
|
logger.info(f"Spiegazione:\n{explanation}") |
|
|
|
|
|
return { |
|
"type":"NATURAL", |
|
"sparql_query": sparql_query, |
|
"sparql_results": row_list, |
|
"explanation": explanation |
|
} |
|
|
|
@app.get("/") |
|
def home(): |
|
return {"message":"OK robusto con regole rigide e doppio tentativo."} |
|
|