|
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 nell'ambiente.") |
|
raise EnvironmentError("HF_API_KEY non impostata nell'ambiente.") |
|
|
|
client = InferenceClient(api_key=API_KEY) |
|
|
|
RDF_FILE = "Ontologia.rdf" |
|
MAX_TRIPLES = 600 |
|
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 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]} ...") |
|
|
|
|
|
|
|
|
|
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: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> |
|
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. |
|
""" |
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
system_prompt = create_initial_system_message(knowledge_text) |
|
logger.debug("[SYSTEM PROMPT] " + system_prompt[:1000] + "...") |
|
|
|
|
|
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}") |
|
|
|
|
|
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: |
|
|
|
logger.info("[FALLBACK] Nessuna query generata anche al secondo tentativo.") |
|
return { |
|
"type": "NATURAL", |
|
"response": response_text_2 |
|
} |
|
|
|
|
|
sparql_query = response_text |
|
logger.info(f"[FINAL QUERY] {sparql_query}") |
|
|
|
|
|
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."} |
|
|
|
|
|
try: |
|
results = g.query(sparql_query) |
|
except Exception as e: |
|
logger.warning(f"[QUERY FAIL] {e}") |
|
|
|
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:"): |
|
|
|
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} |
|
|
|
|
|
if len(results) == 0: |
|
logger.info("[SPARQL RESULT] 0 risultati.") |
|
return {"type": "NATURAL", "response": "Nessun risultato trovato nella nostra ontologia."} |
|
|
|
|
|
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}") |
|
|
|
|
|
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}") |
|
|
|
|
|
return { |
|
"type": "NATURAL", |
|
"sparql_query": sparql_query, |
|
"sparql_results": row_list, |
|
"explanation": explanation_text |
|
} |
|
|
|
@app.get("/") |
|
async def root(): |
|
return {"message": "Server con ontologia in system prompt, doppio tentativo SPARQL e interpretazione."} |
|
|