Update app.py
Browse files
app.py
CHANGED
@@ -4,19 +4,16 @@ from typing import Optional
|
|
4 |
from pydantic import BaseModel
|
5 |
from fastapi import FastAPI, HTTPException
|
6 |
import rdflib
|
|
|
7 |
from huggingface_hub import InferenceClient
|
8 |
|
9 |
logging.basicConfig(
|
10 |
level=logging.DEBUG,
|
11 |
format="%(asctime)s - %(levelname)s - %(message)s",
|
12 |
-
handlers=[
|
13 |
-
logging.FileHandler("app.log"),
|
14 |
-
logging.StreamHandler()
|
15 |
-
]
|
16 |
)
|
17 |
logger = logging.getLogger(__name__)
|
18 |
|
19 |
-
# Imposta la tua HF_API_KEY
|
20 |
API_KEY = os.getenv("HF_API_KEY")
|
21 |
if not API_KEY:
|
22 |
logger.error("HF_API_KEY non impostata.")
|
@@ -25,217 +22,246 @@ if not API_KEY:
|
|
25 |
client = InferenceClient(api_key=API_KEY)
|
26 |
|
27 |
RDF_FILE = "Ontologia.rdf"
|
28 |
-
MAX_TRIPLES = 300 # Se la tua ontologia è enorme, abbassa questo
|
29 |
HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"
|
30 |
|
31 |
-
#
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
"""
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
36 |
"""
|
37 |
if not os.path.exists(rdf_file):
|
38 |
-
|
39 |
-
return "NO_RDF"
|
40 |
|
41 |
g = rdflib.Graph()
|
42 |
try:
|
43 |
g.parse(rdf_file, format="xml")
|
44 |
except Exception as e:
|
45 |
-
logger.error(f"
|
46 |
return "PARSING_ERROR"
|
47 |
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
#
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
|
68 |
def create_system_message(ont_text: str) -> str:
|
69 |
"""
|
70 |
-
Prompt di sistema robusto
|
71 |
-
-
|
72 |
-
-
|
73 |
-
-
|
74 |
"""
|
75 |
-
|
76 |
-
Sei un assistente museale.
|
|
|
|
|
|
|
77 |
|
78 |
-
---
|
79 |
{ont_text}
|
80 |
-
--- FINE
|
81 |
-
|
82 |
-
REGOLE STRINGENTI:
|
83 |
-
1) Se l'utente chiede informazioni correlate alle triple, DEVI generare una query SPARQL in UNA SOLA RIGA,
|
84 |
-
con il prefisso:
|
85 |
-
PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
|
86 |
-
Esempio: PREFIX base: <...> SELECT ?x WHERE {{ ... }} (tutto su una sola riga).
|
87 |
-
2) Se la query produce 0 risultati o fallisce, devi ritentare con un secondo tentativo,
|
88 |
-
magari usando FILTER(STR(...)) o @it, o cambiando minuscole/maiuscole.
|
89 |
-
3) Se la domanda è una chat generica, rispondi breve (saluto, ecc.). Ma se c'è qualcosa
|
90 |
-
di correlato, prova comunque la query.
|
91 |
-
4) Se generi la query e trovi risultati, la risposta finale deve essere la query SPARQL
|
92 |
-
(una sola riga). Non inventare triple inesistenti.
|
93 |
-
5) Se non trovi nulla, rispondi 'Non ci sono informazioni in queste triple.'
|
94 |
-
6) Non ignorare: se l'utente fa domanda su 'David', 'Amore e Psiche', etc., devi
|
95 |
-
estrarre dai triple tutti i dettagli possibili con SPARQL.
|
96 |
-
7) Se la query produce 0, prova un secondo tentativo con sintassi differente.
|
97 |
-
8) Non fare risposte su più righe per la query: una sola riga.
|
98 |
-
|
99 |
-
FINE REGOLE.
|
100 |
-
"""
|
101 |
-
return system_msg
|
102 |
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
{results_str}
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
"""
|
118 |
-
return msg
|
119 |
|
120 |
-
async def call_hf_model(messages, temperature=0.7, max_tokens=1024)
|
121 |
-
""
|
122 |
-
Funzione di chiamata al modello su HuggingFace,
|
123 |
-
con log e robustezza. max_tokens=1024 come richiesto.
|
124 |
-
"""
|
125 |
-
logger.debug("Chiamo HF con i seguenti messaggi:")
|
126 |
for m in messages:
|
127 |
-
|
128 |
-
|
129 |
try:
|
130 |
-
resp
|
131 |
model=HF_MODEL,
|
132 |
messages=messages,
|
133 |
temperature=temperature,
|
134 |
max_tokens=max_tokens,
|
135 |
top_p=0.9
|
136 |
)
|
137 |
-
raw
|
138 |
-
logger.debug(f"Risposta HF: {raw}")
|
139 |
return raw.replace("\n"," ").strip()
|
140 |
except Exception as e:
|
141 |
-
logger.error(f"Errore HF: {e}")
|
142 |
raise HTTPException(status_code=500, detail=str(e))
|
143 |
|
144 |
-
app
|
145 |
|
146 |
class QueryRequest(BaseModel):
|
147 |
-
message:
|
148 |
-
max_tokens:
|
149 |
-
temperature:
|
150 |
|
151 |
@app.post("/generate-response/")
|
152 |
-
async def generate_response(req:
|
153 |
-
user_input
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
{"role":"system","content":
|
159 |
{"role":"user","content":user_input}
|
160 |
]
|
161 |
-
# Prima risposta
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
# Se non inizia con
|
166 |
-
if not
|
167 |
-
|
168 |
-
|
169 |
-
{"role":"system","content":
|
170 |
-
{"role":"assistant","content":
|
171 |
-
{"role":"user","content":
|
172 |
]
|
173 |
-
|
174 |
-
|
175 |
-
if
|
176 |
-
sparql_query
|
177 |
else:
|
178 |
-
return {"type":"NATURAL","response":
|
179 |
else:
|
180 |
-
sparql_query
|
181 |
|
182 |
-
#
|
183 |
-
g
|
184 |
try:
|
185 |
-
g.parse(RDF_FILE,
|
186 |
except Exception as e:
|
187 |
-
|
188 |
-
return {"type":"ERROR","response":"Parsing RDF fallito."}
|
189 |
-
|
190 |
-
# Se la query è malformata, second attempt
|
191 |
try:
|
192 |
-
results
|
193 |
except Exception as e:
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
{"role":"system","content":
|
198 |
{"role":"assistant","content":sparql_query},
|
199 |
-
{"role":"user","content":
|
200 |
]
|
201 |
-
|
202 |
-
if
|
203 |
-
sparql_query
|
204 |
try:
|
205 |
-
results
|
206 |
except Exception as e2:
|
207 |
-
return {"type":"ERROR","response":f"Query fallita
|
208 |
else:
|
209 |
-
return {"type":"NATURAL","response":
|
210 |
|
211 |
if len(results)==0:
|
212 |
-
|
213 |
-
return {"type":"NATURAL","sparql_query": sparql_query,"response":"Nessun risultato."}
|
214 |
|
215 |
-
#
|
216 |
-
row_list
|
217 |
for row in results:
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
#
|
223 |
-
|
224 |
-
|
225 |
-
{"role":"system","content":
|
226 |
{"role":"user","content":""}
|
227 |
]
|
228 |
-
explanation
|
229 |
-
logger.info(f"Spiegazione:\n{explanation}")
|
230 |
|
231 |
-
# Ritorniamo query, risultati e spiegazione
|
232 |
return {
|
233 |
"type":"NATURAL",
|
234 |
-
"sparql_query":
|
235 |
-
"sparql_results":
|
236 |
-
"explanation":
|
237 |
}
|
238 |
|
239 |
@app.get("/")
|
240 |
def home():
|
241 |
-
return {"
|
|
|
4 |
from pydantic import BaseModel
|
5 |
from fastapi import FastAPI, HTTPException
|
6 |
import rdflib
|
7 |
+
from rdflib import RDF, RDFS, OWL
|
8 |
from huggingface_hub import InferenceClient
|
9 |
|
10 |
logging.basicConfig(
|
11 |
level=logging.DEBUG,
|
12 |
format="%(asctime)s - %(levelname)s - %(message)s",
|
13 |
+
handlers=[logging.FileHandler("app.log"), logging.StreamHandler()]
|
|
|
|
|
|
|
14 |
)
|
15 |
logger = logging.getLogger(__name__)
|
16 |
|
|
|
17 |
API_KEY = os.getenv("HF_API_KEY")
|
18 |
if not API_KEY:
|
19 |
logger.error("HF_API_KEY non impostata.")
|
|
|
22 |
client = InferenceClient(api_key=API_KEY)
|
23 |
|
24 |
RDF_FILE = "Ontologia.rdf"
|
|
|
25 |
HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"
|
26 |
|
27 |
+
# Limiti per non sforare la dimensione del prompt
|
28 |
+
MAX_CLASSES = 30
|
29 |
+
MAX_PROPERTIES = 30
|
30 |
+
MAX_INDIVIDUALS = 50
|
31 |
+
MAX_TRIPLES_PER_IND = 20
|
32 |
+
MAX_LITERAL_CHARS = 100
|
33 |
+
|
34 |
+
def extract_ontology_summaries(rdf_file: str) -> str:
|
35 |
"""
|
36 |
+
1) Carica l'ontologia con rdflib.
|
37 |
+
2) Estrae:
|
38 |
+
- un elenco (massimo MAX_CLASSES) di classi
|
39 |
+
- un elenco (massimo MAX_PROPERTIES) di proprietà
|
40 |
+
- un estratto di triple relative alle istanze (NamedIndividual)
|
41 |
+
(massimo MAX_INDIVIDUALS individui, e MAX_TRIPLES_PER_IND triple per individuo).
|
42 |
+
3) Ritorna una stringa 'knowledge_text' che unisce questi contenuti.
|
43 |
"""
|
44 |
if not os.path.exists(rdf_file):
|
45 |
+
return "NO_RDF_FILE"
|
|
|
46 |
|
47 |
g = rdflib.Graph()
|
48 |
try:
|
49 |
g.parse(rdf_file, format="xml")
|
50 |
except Exception as e:
|
51 |
+
logger.error(f"Parsing RDF error: {e}")
|
52 |
return "PARSING_ERROR"
|
53 |
|
54 |
+
# ====== Troviamo le Classi ======
|
55 |
+
# Con un pattern: (s, RDF.type, OWL.Class) o RDFS.Class
|
56 |
+
# Alcune ontologie usano direct typing, altre no.
|
57 |
+
classes_found = set()
|
58 |
+
for s in g.subjects(RDF.type, OWL.Class):
|
59 |
+
classes_found.add(s)
|
60 |
+
# Alcune volte ci sono (s, RDF.type, RDFS.Class)
|
61 |
+
for s in g.subjects(RDF.type, RDFS.Class):
|
62 |
+
classes_found.add(s)
|
63 |
+
|
64 |
+
classes_list = sorted(str(c) for c in classes_found)
|
65 |
+
classes_list = classes_list[:MAX_CLASSES]
|
66 |
+
|
67 |
+
# ====== Troviamo le Proprietà ======
|
68 |
+
# Cerchiamo soggetti con RDF.type in {OWL.ObjectProperty, OWL.DatatypeProperty, RDF.Property}
|
69 |
+
props_found = set()
|
70 |
+
for p in g.subjects(RDF.type, OWL.ObjectProperty):
|
71 |
+
props_found.add(p)
|
72 |
+
for p in g.subjects(RDF.type, OWL.DatatypeProperty):
|
73 |
+
props_found.add(p)
|
74 |
+
for p in g.subjects(RDF.type, RDF.Property):
|
75 |
+
props_found.add(p)
|
76 |
+
props_list = sorted(str(x) for x in props_found)
|
77 |
+
props_list = props_list[:MAX_PROPERTIES]
|
78 |
+
|
79 |
+
# ====== Troviamo NamedIndividuals e relative triple ======
|
80 |
+
named_inds = set()
|
81 |
+
for s in g.subjects(RDF.type, OWL.NamedIndividual):
|
82 |
+
named_inds.add(s)
|
83 |
+
logger.debug(f"Found {len(named_inds)} individuals.")
|
84 |
+
inds_list = sorted(named_inds)[:MAX_INDIVIDUALS]
|
85 |
+
|
86 |
+
# Costruisci un testo con le triple di ogni individuo
|
87 |
+
lines_inds = []
|
88 |
+
for ind in inds_list:
|
89 |
+
triple_count = 0
|
90 |
+
for p,o in g.predicate_objects(ind):
|
91 |
+
if triple_count >= MAX_TRIPLES_PER_IND:
|
92 |
+
break
|
93 |
+
s_str = str(ind)[:80]
|
94 |
+
p_str = str(p)[:80]
|
95 |
+
o_str = str(o)[:MAX_LITERAL_CHARS].replace("\n"," ")
|
96 |
+
lines_inds.append(f"{s_str}|{p_str}|{o_str}")
|
97 |
+
triple_count += 1
|
98 |
+
|
99 |
+
# Ora componiamo la stringa finale
|
100 |
+
txt_classes = "\n".join([f"- CLASSE: {c}" for c in classes_list])
|
101 |
+
txt_props = "\n".join([f"- PROPRIETA': {p}" for p in props_list])
|
102 |
+
txt_inds = "\n".join(lines_inds)
|
103 |
+
|
104 |
+
# Il knowledge_text unisce tre sezioni
|
105 |
+
knowledge_text = f"""\
|
106 |
+
# CLASSES (max {MAX_CLASSES})
|
107 |
+
{txt_classes}
|
108 |
+
|
109 |
+
# PROPERTIES (max {MAX_PROPERTIES})
|
110 |
+
{txt_props}
|
111 |
+
|
112 |
+
# INDIVIDUALS
|
113 |
+
{txt_inds}
|
114 |
+
"""
|
115 |
+
return knowledge_text
|
116 |
+
|
117 |
+
knowledge_text = extract_ontology_summaries(RDF_FILE)
|
118 |
|
119 |
def create_system_message(ont_text: str) -> str:
|
120 |
"""
|
121 |
+
Prompt di sistema robusto con regole stringenti:
|
122 |
+
- Query SPARQL su una sola riga
|
123 |
+
- Se 0 results => secondo tentativo
|
124 |
+
- Chat generica => risposte brevi
|
125 |
"""
|
126 |
+
return f"""
|
127 |
+
Sei un assistente museale. Qui sotto c'è un riassunto dell'ontologia:
|
128 |
+
(1) elenco di classi,
|
129 |
+
(2) elenco di proprietà,
|
130 |
+
(3) triple relative agli individui (NamedIndividual).
|
131 |
|
132 |
+
--- ONTOLOGIA ---
|
133 |
{ont_text}
|
134 |
+
--- FINE ONTOLOGIA ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
+
REGOLE FONDAMENTALI:
|
137 |
+
1) Se la domanda dell'utente riguarda info contenute in questa ontologia, produci SEMPRE una query SPARQL in UNA SOLA RIGA,
|
138 |
+
con: PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
|
139 |
+
Esempio: PREFIX base: <...> SELECT ?x WHERE {{ ... }}
|
140 |
+
2) Se la query produce 0 risultati o fallisce, fai un secondo tentativo (magari con FILTER).
|
141 |
+
3) Se la domanda è di chat generica (es. ciao come stai?), rispondi breve.
|
142 |
+
4) Se trovi risultati, la tua risposta finale dev'essere la query SPARQL su una riga.
|
143 |
+
5) Non inventare triple inesistenti. Se non c'è info, di' "Nessuna info."
|
144 |
+
6) Non fare risposte multiline per la query. DEVI metterla su una singola riga.
|
145 |
+
7) Usa i dati qui sopra. Se la domanda menziona 'David' o 'Amore e Psiche', controlla le triple Individuals.
|
146 |
+
|
147 |
+
FINE REGOLE
|
148 |
+
"""
|
149 |
+
|
150 |
+
def create_explanation_prompt(results_str:str)->str:
|
151 |
+
return f"""
|
152 |
+
Risultati SPARQL:
|
153 |
{results_str}
|
154 |
|
155 |
+
Fornisci una spiegazione come guida museale, in ~10 righe, riferendo a materiali, autori, periodi,
|
156 |
+
senza inventare nulla che non sia presente.
|
157 |
+
"""
|
|
|
|
|
158 |
|
159 |
+
async def call_hf_model(messages, temperature=0.7, max_tokens=1024)->str:
|
160 |
+
logging.debug("Chiamo modello HF con i seguenti msg:")
|
|
|
|
|
|
|
|
|
161 |
for m in messages:
|
162 |
+
logging.debug(f"ROLE={m['role']} => {m['content'][:300]}")
|
|
|
163 |
try:
|
164 |
+
resp=client.chat.completions.create(
|
165 |
model=HF_MODEL,
|
166 |
messages=messages,
|
167 |
temperature=temperature,
|
168 |
max_tokens=max_tokens,
|
169 |
top_p=0.9
|
170 |
)
|
171 |
+
raw=resp["choices"][0]["message"]["content"]
|
|
|
172 |
return raw.replace("\n"," ").strip()
|
173 |
except Exception as e:
|
|
|
174 |
raise HTTPException(status_code=500, detail=str(e))
|
175 |
|
176 |
+
app=FastAPI()
|
177 |
|
178 |
class QueryRequest(BaseModel):
|
179 |
+
message:str
|
180 |
+
max_tokens:int=1024
|
181 |
+
temperature:float=0.5
|
182 |
|
183 |
@app.post("/generate-response/")
|
184 |
+
async def generate_response(req:QueryRequest):
|
185 |
+
user_input=req.message
|
186 |
+
logging.info(f"Utente dice: {user_input}")
|
187 |
+
# 1) Prompt di sistema
|
188 |
+
sys_msg=create_system_message(knowledge_text)
|
189 |
+
msgs=[
|
190 |
+
{"role":"system","content":sys_msg},
|
191 |
{"role":"user","content":user_input}
|
192 |
]
|
193 |
+
# 2) Prima risposta
|
194 |
+
r1=await call_hf_model(msgs, req.temperature, req.max_tokens)
|
195 |
+
logging.info(f"PRIMA RISPOSTA:\n{r1}")
|
196 |
+
|
197 |
+
# Se non inizia con "PREFIX base:"
|
198 |
+
if not r1.startswith("PREFIX base:"):
|
199 |
+
second_q=f"Non hai risposto con query SPARQL su una sola riga. Ritenta. Domanda: {user_input}"
|
200 |
+
msgs2=[
|
201 |
+
{"role":"system","content":sys_msg},
|
202 |
+
{"role":"assistant","content":r1},
|
203 |
+
{"role":"user","content":second_q}
|
204 |
]
|
205 |
+
r2=await call_hf_model(msgs2,req.temperature,req.max_tokens)
|
206 |
+
logging.info(f"SECONDA RISPOSTA:\n{r2}")
|
207 |
+
if r2.startswith("PREFIX base:"):
|
208 |
+
sparql_query=r2
|
209 |
else:
|
210 |
+
return {"type":"NATURAL","response": r2}
|
211 |
else:
|
212 |
+
sparql_query=r1
|
213 |
|
214 |
+
# 3) Esegui la query su rdflib
|
215 |
+
g=rdflib.Graph()
|
216 |
try:
|
217 |
+
g.parse(RDF_FILE,format="xml")
|
218 |
except Exception as e:
|
219 |
+
return {"type":"ERROR","response":f"Parsing RDF error: {e}"}
|
|
|
|
|
|
|
220 |
try:
|
221 |
+
results=g.query(sparql_query)
|
222 |
except Exception as e:
|
223 |
+
# fallback
|
224 |
+
fallback=f"Query fallita. Riprova con altra sintassi. Domanda: {user_input}"
|
225 |
+
msgs3=[
|
226 |
+
{"role":"system","content":sys_msg},
|
227 |
{"role":"assistant","content":sparql_query},
|
228 |
+
{"role":"user","content":fallback}
|
229 |
]
|
230 |
+
r3=await call_hf_model(msgs3,req.temperature,req.max_tokens)
|
231 |
+
if r3.startswith("PREFIX base:"):
|
232 |
+
sparql_query=r3
|
233 |
try:
|
234 |
+
results=g.query(sparql_query)
|
235 |
except Exception as e2:
|
236 |
+
return {"type":"ERROR","response":f"Query fallita ancora: {e2}"}
|
237 |
else:
|
238 |
+
return {"type":"NATURAL","response":r3}
|
239 |
|
240 |
if len(results)==0:
|
241 |
+
return {"type":"NATURAL","sparql_query":sparql_query,"response":"Nessun risultato."}
|
|
|
242 |
|
243 |
+
# 4) Costruisci i result row
|
244 |
+
row_list=[]
|
245 |
for row in results:
|
246 |
+
row_txt=", ".join([f"{k}:{v}" for k,v in row.asdict().items()])
|
247 |
+
row_list.append(row_txt)
|
248 |
+
results_str="\n".join(row_list)
|
249 |
+
|
250 |
+
# 5) Spiegazione
|
251 |
+
exp_prompt=create_explanation_prompt(results_str)
|
252 |
+
exp_msgs=[
|
253 |
+
{"role":"system","content":exp_prompt},
|
254 |
{"role":"user","content":""}
|
255 |
]
|
256 |
+
explanation=await call_hf_model(exp_msgs,req.temperature,req.max_tokens)
|
|
|
257 |
|
|
|
258 |
return {
|
259 |
"type":"NATURAL",
|
260 |
+
"sparql_query":sparql_query,
|
261 |
+
"sparql_results":row_list,
|
262 |
+
"explanation":explanation
|
263 |
}
|
264 |
|
265 |
@app.get("/")
|
266 |
def home():
|
267 |
+
return {"msg":"Ok con sunto di classi, proprietà e triple di NamedIndividuals."}
|