AshenClock commited on
Commit
51695fc
·
verified ·
1 Parent(s): 848d0c0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -153
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
- # Carica e "preprocessa" l'ontologia
32
- def load_ontology_as_text(rdf_file: str, max_triples: int=300) -> str:
 
 
 
 
 
 
33
  """
34
- Legge un numero limitato di triple dal file RDF e le concatena in
35
- una stringa. Limita i literal in lunghezza per non gonfiare troppo il prompt.
 
 
 
 
 
36
  """
37
  if not os.path.exists(rdf_file):
38
- logger.warning("File RDF non trovato.")
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"Errore parsing RDF: {e}")
46
  return "PARSING_ERROR"
47
 
48
- lines = []
49
- count = 0
50
- for s,p,o in g:
51
- if count >= max_triples:
52
- break
53
- # Taglia i literal in modo da non superare un tot di caratteri
54
- s_str = str(s)[:100].replace("\n"," ")
55
- p_str = str(p)[:100].replace("\n"," ")
56
- o_str = str(o)[:100].replace("\n"," ")
57
- lines.append(f"{s_str}|{p_str}|{o_str}")
58
- count+=1
59
-
60
- # Concateniamo le triple su righe separate (più facile da leggere).
61
- # Se preferisci su un'unica riga, puoi usare " ".join(lines).
62
- ontology_text = "\n".join(lines)
63
- logger.debug(f"Caricate {count} triple.")
64
- return ontology_text
65
-
66
- knowledge_text = load_ontology_as_text(RDF_FILE, MAX_TRIPLES)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  def create_system_message(ont_text: str) -> str:
69
  """
70
- Prompt di sistema robusto e stringente.
71
- - Forza query SPARQL in UNA sola riga
72
- - Richiede secondi tentativi
73
- - Domande generiche -> risposte brevi, ma se c'è un modo di fare query, farlo
74
  """
75
- system_msg = f"""
76
- Sei un assistente museale. Hai a disposizione un estratto di triple RDF (massimo {MAX_TRIPLES}):
 
 
 
77
 
78
- --- TRIPLE ---
79
  {ont_text}
80
- --- FINE TRIPLE ---
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
- def create_explanation_prompt(results_str: str) -> str:
104
- """
105
- Prompt per spiegazione finale, robusto:
106
- - Spiega i risultati come guida museale
107
- - Non inventare
108
- - 10-15 righe max
109
- """
110
- msg = f"""
111
- Ho ottenuto questi risultati SPARQL:
 
 
 
 
 
 
 
 
112
  {results_str}
113
 
114
- Ora fornisci una spiegazione come guida museale, in modo comprensibile e dettagliato (max ~10-15 righe).
115
- Cita eventuali materiali, periodi storici, autore, facendo riferimento al contesto RDF (ma senza contraddizioni).
116
- Non inventare nulla oltre ciò che appare nei risultati.
117
- """
118
- return msg
119
 
120
- async def call_hf_model(messages, temperature=0.7, max_tokens=1024) -> str:
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
- logger.debug(f"ROLE={m['role']} -> {m['content'][:500]}")
128
-
129
  try:
130
- resp = client.chat.completions.create(
131
  model=HF_MODEL,
132
  messages=messages,
133
  temperature=temperature,
134
  max_tokens=max_tokens,
135
  top_p=0.9
136
  )
137
- raw = resp["choices"][0]["message"]["content"]
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 = FastAPI()
145
 
146
  class QueryRequest(BaseModel):
147
- message: str
148
- max_tokens: int = 1024
149
- temperature: float = 0.5
150
 
151
  @app.post("/generate-response/")
152
- async def generate_response(req: QueryRequest):
153
- user_input = req.message
154
- logger.info(f"Utente chiede: {user_input}")
155
-
156
- system_msg = create_system_message(knowledge_text)
157
- messages = [
158
- {"role":"system","content":system_msg},
159
  {"role":"user","content":user_input}
160
  ]
161
- # Prima risposta
162
- first_response = await call_hf_model(messages, req.temperature, req.max_tokens)
163
- logger.info(f"Prima risposta generata:\n{first_response}")
164
-
165
- # Se non inizia con 'PREFIX base:' => second attempt
166
- if not first_response.startswith("PREFIX base:"):
167
- second_prompt = f"Non hai risposto con query SPARQL in una sola riga. Riprova. Domanda: {user_input}"
168
- second_msgs = [
169
- {"role":"system","content":system_msg},
170
- {"role":"assistant","content":first_response},
171
- {"role":"user","content":second_prompt}
172
  ]
173
- second_response = await call_hf_model(second_msgs, req.temperature, req.max_tokens)
174
- logger.info(f"Seconda risposta:\n{second_response}")
175
- if second_response.startswith("PREFIX base:"):
176
- sparql_query = second_response
177
  else:
178
- return {"type":"NATURAL","response": second_response}
179
  else:
180
- sparql_query = first_response
181
 
182
- # Eseguiamo la query con rdflib
183
- g = rdflib.Graph()
184
  try:
185
- g.parse(RDF_FILE, format="xml")
186
  except Exception as e:
187
- logger.error("Errore parse RDF: ", e)
188
- return {"type":"ERROR","response":"Parsing RDF fallito."}
189
-
190
- # Se la query è malformata, second attempt
191
  try:
192
- results = g.query(sparql_query)
193
  except Exception as e:
194
- logger.warning(f"La query SPARQL ha fallito: {e}")
195
- fallback_prompt = f"La query SPARQL è fallita. Riprova con altra sintassi. Domanda: {user_input}"
196
- fallback_msgs = [
197
- {"role":"system","content":system_msg},
198
  {"role":"assistant","content":sparql_query},
199
- {"role":"user","content":fallback_prompt}
200
  ]
201
- fallback_resp = await call_hf_model(fallback_msgs, req.temperature, req.max_tokens)
202
- if fallback_resp.startswith("PREFIX base:"):
203
- sparql_query = fallback_resp
204
  try:
205
- results = g.query(sparql_query)
206
  except Exception as e2:
207
- return {"type":"ERROR","response":f"Query fallita di nuovo: {e2}"}
208
  else:
209
- return {"type":"NATURAL","response": fallback_resp}
210
 
211
  if len(results)==0:
212
- logger.info("0 risultati SPARQL.")
213
- return {"type":"NATURAL","sparql_query": sparql_query,"response":"Nessun risultato."}
214
 
215
- # Prepara i risultati per la spiegazione
216
- row_list = []
217
  for row in results:
218
- row_list.append(", ".join([f"{k}:{v}" for k,v in row.asdict().items()]))
219
- results_str = "\n".join(row_list)
220
- logger.info(f"Risultati trovati ({len(results)}):\n{results_str}")
221
-
222
- # Prompt di interpretazione
223
- explain_sys = create_explanation_prompt(results_str)
224
- explain_msgs = [
225
- {"role":"system","content":explain_sys},
226
  {"role":"user","content":""}
227
  ]
228
- explanation = await call_hf_model(explain_msgs, req.temperature, req.max_tokens)
229
- logger.info(f"Spiegazione:\n{explanation}")
230
 
231
- # Ritorniamo query, risultati e spiegazione
232
  return {
233
  "type":"NATURAL",
234
- "sparql_query": sparql_query,
235
- "sparql_results": row_list,
236
- "explanation": explanation
237
  }
238
 
239
  @app.get("/")
240
  def home():
241
- return {"message":"OK robusto con regole rigide e doppio tentativo."}
 
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."}