File size: 8,377 Bytes
ac52c4d 89f944e ac52c4d 89f944e 51695fc 8607561 d9ab7eb ac52c4d 2ecaeab 848d0c0 2ecaeab 51695fc 2ecaeab ac52c4d 848d0c0 ac52c4d 89f944e 8607561 848d0c0 8607561 e250196 d9ab7eb bfa70d6 e250196 ac52c4d bfa70d6 ac52c4d 89f944e 51695fc 69d6e40 89f944e ac52c4d 89f944e ac52c4d 51695fc 848d0c0 89f944e e250196 51695fc e250196 51695fc e250196 51695fc e250196 51695fc e250196 51695fc e250196 848d0c0 d9ab7eb ac52c4d d9ab7eb 4c13256 51695fc bfa70d6 51695fc 848d0c0 e250196 d9ab7eb bfa70d6 d9ab7eb 51695fc e250196 8607561 bfa70d6 51695fc 8607561 bfa70d6 e250196 848d0c0 e250196 ac52c4d e250196 8607561 ac52c4d 8607561 ac52c4d 51695fc bfa70d6 ac52c4d e250196 ac52c4d 51695fc ac52c4d 51695fc ac52c4d 51695fc e250196 d9ab7eb 51695fc 848d0c0 ac52c4d bfa70d6 51695fc bfa70d6 51695fc bfa70d6 8607561 bfa70d6 8607561 bfa70d6 848d0c0 bfa70d6 8607561 bfa70d6 51695fc 8607561 51695fc 8607561 bfa70d6 e250196 8607561 51695fc 8607561 bfa70d6 51695fc 848d0c0 51695fc 8607561 bfa70d6 8607561 51695fc 8607561 bfa70d6 8607561 bfa70d6 8607561 848d0c0 51695fc 8607561 bfa70d6 51695fc 8607561 e250196 51695fc e250196 51695fc e250196 51695fc 848d0c0 8607561 e250196 69d6e40 8607561 848d0c0 51695fc 8607561 ac52c4d 848d0c0 bfa70d6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
import os
import logging
from typing import Optional
from pydantic import BaseModel
from fastapi import FastAPI, HTTPException
import rdflib
from rdflib import RDF, RDFS, OWL
from huggingface_hub import InferenceClient
from sentence_transformers import SentenceTransformer
import faiss
import json
import numpy as np
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"
HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"
MAX_CLASSES = 30
MAX_PROPERTIES = 30
# Carica i documenti e l'indice FAISS
with open("data/documents.json", "r", encoding="utf-8") as f:
documents = json.load(f)
index = faiss.read_index("data/faiss.index")
model = SentenceTransformer('all-MiniLM-L6-v2')
def retrieve_relevant_documents(query: str, top_k: int = 5):
query_embedding = model.encode([query], convert_to_numpy=True)
distances, indices = index.search(query_embedding, top_k)
relevant_docs = [documents[idx] for idx in indices[0]]
return relevant_docs
def extract_classes_and_properties(rdf_file:str) -> str:
"""
Carica l'ontologia e crea un 'sunto' di Classi e Proprietà
(senza NamedIndividuals) per ridurre i token.
"""
if not os.path.exists(rdf_file):
return "NO_RDF_FILE"
g = rdflib.Graph()
try:
g.parse(rdf_file, format="xml")
except Exception as e:
logger.error(f"Parsing RDF error: {e}")
return "PARSING_ERROR"
# Troviamo le classi
classes_found = set()
for s in g.subjects(RDF.type, OWL.Class):
classes_found.add(s)
for s in g.subjects(RDF.type, RDFS.Class):
classes_found.add(s)
classes_list = sorted(str(c) for c in classes_found)
classes_list = classes_list[:MAX_CLASSES]
# Troviamo le proprietà
props_found = set()
for p in g.subjects(RDF.type, OWL.ObjectProperty):
props_found.add(p)
for p in g.subjects(RDF.type, OWL.DatatypeProperty):
props_found.add(p)
for p in g.subjects(RDF.type, RDF.Property):
props_found.add(p)
props_list = sorted(str(x) for x in props_found)
props_list = props_list[:MAX_PROPERTIES]
txt_classes = "\n".join([f"- CLASSE: {c}" for c in classes_list])
txt_props = "\n".join([f"- PROPRIETA': {p}" for p in props_list])
summary = f"""\
# CLASSI (max {MAX_CLASSES})
{txt_classes}
# PROPRIETA' (max {MAX_PROPERTIES})
{txt_props}
"""
return summary
knowledge_text = extract_classes_and_properties(RDF_FILE)
def create_system_message(ont_text:str, retrieved_docs:str)->str:
"""
Prompt di sistema robusto, con regole su query in una riga e
informazioni recuperate tramite RAG.
"""
return f"""
Sei un assistente museale. Ecco un estratto di CLASSI e PROPRIETA' dell'ontologia (senza NamedIndividuals):
--- ONTOLOGIA ---
{ont_text}
--- FINE ---
Ecco alcune informazioni rilevanti recuperate dalla base di conoscenza:
{retrieved_docs}
Suggerimento: se l'utente chiede il 'materiale' di un'opera, potresti usare qualcosa come
'base:materialeOpera' o un'altra proprietà simile (se esiste). Non è tassativo: usa
la proprietà che ritieni più affine se ci sono riferimenti in ontologia.
REGOLE STRINGENTI:
1) Se l'utente chiede info su questa ontologia, genera SEMPRE una query SPARQL in UNA SOLA RIGA,
con prefix:
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
2) Se la query produce 0 risultati o fallisce, ritenta con un secondo tentativo.
3) Se la domanda è generica (tipo 'Ciao, come stai?'), rispondi breve.
4) Se trovi risultati, risposta finale = la query SPARQL (una sola riga).
5) Se non trovi nulla, di' 'Nessuna info.'
6) Non multiline. Esempio: PREFIX base: <...> SELECT ?x WHERE { ... }.
FINE REGOLE
"""
def create_explanation_prompt(results_str:str)->str:
return f"""
Ho ottenuto questi risultati SPARQL:
{results_str}
Ora fornisci una breve spiegazione museale (massimo ~10 righe), senza inventare oltre i risultati.
"""
async def call_hf_model(messages, temperature=0.5, max_tokens=1024)->str:
logger.debug("Chiamo HF con i seguenti messaggi:")
for m in messages:
logger.debug(f"ROLE={m['role']} => {m['content'][:300]}")
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"]
# Forziamo la query su linea singola se multiline
single_line = " ".join(raw.splitlines())
logger.debug(f"Risposta HF single-line: {single_line}")
return single_line.strip()
except Exception as e:
logger.error(f"HuggingFace error: {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 dice: {user_input}")
# Recupera documenti rilevanti usando RAG
relevant_docs = retrieve_relevant_documents(user_input, top_k=3)
retrieved_text = "\n".join([doc['text'] for doc in relevant_docs])
sys_msg=create_system_message(knowledge_text, retrieved_text)
msgs=[
{"role":"system","content":sys_msg},
{"role":"user","content":user_input}
]
# Primo tentativo
r1=await call_hf_model(msgs, req.temperature, req.max_tokens)
logger.info(f"PRIMA RISPOSTA:\n{r1}")
# Se non parte con "PREFIX base:"
if not r1.startswith("PREFIX base:"):
sc=f"Non hai risposto con query SPARQL su una sola riga. Riprova. Domanda: {user_input}"
msgs2=[
{"role":"system","content":sys_msg},
{"role":"assistant","content":r1},
{"role":"user","content":sc}
]
r2=await call_hf_model(msgs2,req.temperature,req.max_tokens)
logger.info(f"SECONDA RISPOSTA:\n{r2}")
if r2.startswith("PREFIX base:"):
sparql_query=r2
else:
return {"type":"NATURAL","response": r2}
else:
sparql_query=r1
# Esegui la query con rdflib
g=rdflib.Graph()
try:
g.parse(RDF_FILE,format="xml")
except Exception as e:
logger.error(f"Parsing RDF error: {e}")
return {"type":"ERROR","response":f"Parsing RDF error: {e}"}
try:
results=g.query(sparql_query)
except Exception as e:
fallback=f"La query SPARQL ha fallito. Riprova. Domanda: {user_input}"
msgs3=[
{"role":"system","content":sys_msg},
{"role":"assistant","content":sparql_query},
{"role":"user","content":fallback}
]
r3=await call_hf_model(msgs3,req.temperature,req.max_tokens)
if r3.startswith("PREFIX base:"):
sparql_query=r3
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":r3}
if len(results)==0:
return {"type":"NATURAL","sparql_query":sparql_query,"response":"Nessun risultato."}
# Confeziona risultati
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)
# Spiegazione
exp_prompt=create_explanation_prompt(results_str)
msgs4=[
{"role":"system","content":exp_prompt},
{"role":"user","content":""}
]
explanation=await call_hf_model(msgs4,req.temperature,req.max_tokens)
return {
"type":"NATURAL",
"sparql_query":sparql_query,
"sparql_results":row_list,
"explanation":explanation
}
@app.get("/")
def home():
return {"message":"Prompt lascia libertà su come chiamare la proprietà del materiale, ma suggerisce un possibile 'materialeOpera'."}
|