|
import os |
|
import logging |
|
from rdflib import Graph |
|
from pydantic import BaseModel |
|
from fastapi import FastAPI, HTTPException |
|
from huggingface_hub import InferenceClient |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
API_KEY = os.getenv("HF_API_KEY") |
|
client = InferenceClient(api_key=API_KEY) |
|
|
|
|
|
RDF_FILE = "Ontologia.rdf" |
|
|
|
|
|
|
|
|
|
def load_rdf_summary(): |
|
""" |
|
Carica un riassunto dell'ontologia dal file RDF (se necessario). |
|
Qui puoi usare parse e scansionare classi e proprietà reali. |
|
""" |
|
if not os.path.exists(RDF_FILE): |
|
return "Nessun file RDF trovato." |
|
try: |
|
g = Graph() |
|
g.parse(RDF_FILE, format="xml") |
|
|
|
|
|
classes = set() |
|
properties = set() |
|
for s, p, o in g.triples((None, None, None)): |
|
if "Class" in str(o): |
|
classes.add(s) |
|
if "Property" in str(o): |
|
properties.add(s) |
|
|
|
class_summary = "\n".join([f"- Classe: {cls}" for cls in classes]) |
|
prop_summary = "\n".join([f"- Proprietà: {prop}" for prop in properties]) |
|
return f"Classi:\n{class_summary}\n\nProprietà:\n{prop_summary}" |
|
except Exception as e: |
|
logger.error(f"Errore durante il parsing del file RDF: {e}") |
|
return "Errore nel caricamento del file RDF." |
|
|
|
rdf_context = load_rdf_summary() |
|
logger.info("RDF Summary: %s", rdf_context) |
|
|
|
|
|
|
|
|
|
def validate_sparql_query(query: str, rdf_file_path: str) -> bool: |
|
""" |
|
Esegue il parsing e l'esecuzione di test della query su RDF, |
|
per verificare che sia sintatticamente e semanticamente corretta. |
|
""" |
|
g = Graph() |
|
try: |
|
g.parse(rdf_file_path, format="xml") |
|
g.query(query) |
|
return True |
|
except Exception as e: |
|
logger.error(f"Errore durante la validazione della query SPARQL: {e}") |
|
return False |
|
|
|
|
|
|
|
|
|
def create_system_message(rdf_context: str) -> str: |
|
""" |
|
Prompt di sistema estremo: |
|
- impone l'uso di un SOLO prefisso |
|
- vieta righe multiple |
|
- vieta di inventare prefissi |
|
- obbliga a iniziare con `PREFIX base: ... SELECT` o `ASK` |
|
""" |
|
return f""" |
|
Sei un assistente esperto nella generazione di query SPARQL basate su un'ontologia RDF. |
|
Ecco un riassunto dell'ontologia su cui devi lavorare: |
|
{rdf_context} |
|
|
|
DI SEGUITO LE REGOLE TASSATIVE: |
|
1. DEVI usare ESCLUSIVAMENTE questo prefisso di base (e NON modificarlo in nessun modo): |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/> |
|
2. La query deve stare in UNA SOLA RIGA, senza andare a capo. |
|
3. La query deve INIZIARE con: |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/> SELECT |
|
oppure |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/> ASK |
|
4. Se devi indicare una classe, usa: ?qualcosa a base:NomeClasse . |
|
5. Se devi indicare una proprietà, usa: ?s base:NomeProprieta ?o . |
|
6. NON generare alcun altro prefisso. |
|
7. NON utilizzare URI lunghe senza < > e NON inventare prefissi o risorse inesistenti. |
|
8. Se non puoi rispondere con una query SPARQL valida secondo questi criteri, scrivi: |
|
"Non posso generare una query SPARQL per questa richiesta." |
|
|
|
Esempio di query corretta (fittizia) in una sola riga: |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/> SELECT ?stanza WHERE {{ ?stanza a base:Stanza . }} LIMIT 10 |
|
|
|
RISPONDI ESCLUSIVAMENTE CON LA QUERY O IL MESSAGGIO DI IMPOSSIBILITA'. |
|
""" |
|
|
|
|
|
|
|
|
|
def create_correction_message(rdf_context: str, errore: str) -> str: |
|
""" |
|
Questo prompt serve per la seconda iterazione se la query non è valida. |
|
Invita a correggere la query e a rispettare le regole. |
|
""" |
|
return f""" |
|
La query che hai fornito è risultata NON valida per il seguente motivo: |
|
{errore} |
|
|
|
RICORDA LE REGOLE TASSATIVE, in particolare l'uso ESATTO del prefisso: |
|
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/> |
|
Riscrivi la query in UNA SOLA RIGA, rispettando la sintassi SPARQL e usando solo classi e proprietà presenti nell'ontologia. |
|
Se non riesci, dì: "Non posso generare una query SPARQL per questa richiesta." |
|
""" |
|
|
|
|
|
|
|
|
|
async def call_model(messages, temperature=0.7, max_tokens=2048): |
|
try: |
|
response = client.chat.completions.create( |
|
model="Qwen/Qwen2.5-72B-Instruct", |
|
messages=messages, |
|
temperature=temperature, |
|
max_tokens=max_tokens, |
|
top_p=0.7, |
|
stream=False |
|
) |
|
raw_text = response["choices"][0]["message"]["content"] |
|
|
|
return raw_text.replace("\n", " ").strip() |
|
except Exception as e: |
|
logger.error(f"Errore nel modello: {e}") |
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
app = FastAPI() |
|
|
|
class QueryRequest(BaseModel): |
|
message: str |
|
max_tokens: int = 2048 |
|
temperature: float = 0.7 |
|
|
|
@app.post("/generate-query/") |
|
async def generate_query(request: QueryRequest): |
|
|
|
system_msg = create_system_message(rdf_context) |
|
user_msg = request.message |
|
|
|
messages_first = [ |
|
{"role": "system", "content": system_msg}, |
|
{"role": "user", "content": user_msg} |
|
] |
|
response1 = await call_model(messages_first, request.temperature, request.max_tokens) |
|
logger.info(f"[Prima iterazione] Risposta generata dal modello: {response1}") |
|
|
|
|
|
mandated_prefix = "PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/2024/11/untitled-ontology-39/>" |
|
if not (response1.startswith(mandated_prefix + " SELECT") or response1.startswith(mandated_prefix + " ASK")): |
|
return { |
|
"query": None, |
|
"explanation": "Il modello non ha usato il prefisso obbligatorio o non ha usato SELECT/ASK." |
|
} |
|
|
|
|
|
if validate_sparql_query(response1, RDF_FILE): |
|
return {"query": response1, "explanation": "Query valida alla prima iterazione."} |
|
else: |
|
|
|
correction_msg = create_correction_message(rdf_context, "Query non valida alla prima iterazione.") |
|
|
|
messages_second = [ |
|
{"role": "system", "content": system_msg}, |
|
{"role": "assistant", "content": response1}, |
|
{"role": "system", "content": correction_msg} |
|
] |
|
response2 = await call_model(messages_second, request.temperature, request.max_tokens) |
|
logger.info(f"[Seconda iterazione] Risposta generata dal modello: {response2}") |
|
|
|
|
|
if not (response2.startswith(mandated_prefix + " SELECT") and not response2.startswith(mandated_prefix + " ASK")): |
|
|
|
|
|
if not (response2.startswith(mandated_prefix + " SELECT") or response2.startswith(mandated_prefix + " ASK")): |
|
return { |
|
"query": None, |
|
"explanation": "Anche la seconda iterazione non ha usato il prefisso e SELECT/ASK corretti." |
|
} |
|
|
|
|
|
if validate_sparql_query(response2, RDF_FILE): |
|
return {"query": response2, "explanation": "Query valida alla seconda iterazione (corretta)."} |
|
else: |
|
return { |
|
"query": None, |
|
"explanation": "Anche la seconda iterazione ha prodotto una query non valida. Interrompo." |
|
} |
|
|
|
@app.get("/") |
|
async def root(): |
|
return {"message": "Server attivo e pronto a generare query SPARQL!"} |