AshenClock commited on
Commit
8607561
·
verified ·
1 Parent(s): 89f944e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -86
app.py CHANGED
@@ -3,12 +3,14 @@ import logging
3
  from typing import Optional
4
  from pydantic import BaseModel
5
  from fastapi import FastAPI, HTTPException
6
- from huggingface_hub import InferenceClient
7
  import rdflib
 
8
 
9
- # Configurazione logging
 
 
10
  logging.basicConfig(
11
- level=logging.DEBUG,
12
  format="%(asctime)s - %(levelname)s - %(message)s",
13
  handlers=[
14
  logging.FileHandler("app.log"),
@@ -17,29 +19,27 @@ logging.basicConfig(
17
  )
18
  logger = logging.getLogger(__name__)
19
 
20
- # =========================================
21
- # PARAMETRI BASE
22
- # =========================================
23
  API_KEY = os.getenv("HF_API_KEY")
24
  if not API_KEY:
25
  logger.error("HF_API_KEY non impostata nell'ambiente.")
26
  raise EnvironmentError("HF_API_KEY non impostata nell'ambiente.")
27
 
28
- # Nome del file RDF
29
- RDF_FILE = "Ontologia.rdf"
30
-
31
- # Inizializza il client
32
  client = InferenceClient(api_key=API_KEY)
33
 
34
- # =========================================
35
- # Carica e Preprocessa l'Ontologia
36
- # =========================================
 
 
 
 
37
  def load_ontology_as_text(rdf_file: str, max_triples: int = 300) -> str:
38
  """
39
- Legge l'ontologia dal file RDF e costruisce
40
- una stringa "knowledge_text" con un numero limitato di triple,
41
- per non sforare i limiti di contesto.
42
- max_triples limita quante triple includere.
43
  """
44
  if not os.path.exists(rdf_file):
45
  return "Nessun file RDF trovato."
@@ -49,92 +49,99 @@ def load_ontology_as_text(rdf_file: str, max_triples: int = 300) -> str:
49
  g.parse(rdf_file, format="xml")
50
  except Exception as e:
51
  logger.error(f"Errore parsing RDF: {e}")
52
- return "Errore parsing RDF."
53
 
54
- # Convertiamo un certo numero di triple in stringa
55
- knowledge_lines = []
56
  count = 0
57
  for s, p, o in g:
58
- # Troncamento se necessario (evita di incollare 10.000 triple)
59
  if count >= max_triples:
60
  break
61
- # Abbreviamo s, p, o se sono lunghi
62
  s_str = str(s)[:200]
63
  p_str = str(p)[:200]
64
  o_str = str(o)[:200]
65
  line = f"- {s_str} | {p_str} | {o_str}"
66
- knowledge_lines.append(line)
67
  count += 1
68
 
69
- knowledge_text = "\n".join(knowledge_lines)
70
  return knowledge_text
71
 
72
- # Carichiamo e preprocessiamo la knowledge
73
- knowledge_text = load_ontology_as_text(RDF_FILE, max_triples=600) # Aumenta/diminuisci se serve
74
 
75
- # =========================================
76
- # Creazione Prompt di Sistema
77
- # =========================================
78
- def create_system_message(knowledge_text: str) -> str:
79
  """
80
- Crea un prompt di sistema con l'ontologia (o una versione ridotta) inline,
81
- obbligando il modello a cercare le info per la query SPARQL da qui.
82
  """
83
- # Nota: qui rafforziamo che deve SEMPRE consultare questi triple.
84
- # E deve generare query SPARQL SOLO su questi triple, etc.
85
- system_message = f"""
86
- Sei un assistente per un museo. Qui hai l'estratto dell'ontologia RDF in formato triple:
87
- (EntitaSoggetto, EntitaProprieta, EntitaOggetto).
88
-
89
- Devi usare ESCLUSIVAMENTE queste informazioni come knowledge base.
90
-
91
- Ecco la knowledge in forma di triple (max 600):
92
  {knowledge_text}
93
 
94
  REGOLE TASSATIVE:
95
- 1. Se la domanda dell'utente richiede info su entità/opere ecc., DEVI generare una query SPARQL che si basa su questi triple.
96
- 2. Se la domanda è chat semplice (saluto, ecc.), puoi rispondere brevemente, ma se serve una knowledge, la cerchi nei triple.
97
- 3. Se i triple non contengono l'info, di' che non è disponibile.
98
- 4. Il prefisso da usare:
99
- PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
 
 
 
 
 
 
100
 
101
- 5. Se hai generato la query e bisogna interpretare i risultati (es. autoreOpera?), puoi fornire la risposta.
102
- Ma l'info deve derivare dai triple sopra.
 
 
 
 
 
 
 
 
 
103
 
104
- Attenzione al fatto che le stringhe (es. <nomeOpera> ) possono avere @it o differenze minuscole/maiuscole.
105
- Puoi usare FILTER( STR(?x) = "valore" ) se serve.
106
 
107
- Buona fortuna!
 
 
 
 
 
 
 
 
 
108
  """
109
- return system_message
 
 
110
 
111
- # =========================================
112
- # Funzione che chiama il modello
113
- # =========================================
114
- async def call_model(messages, temperature=0.7, max_tokens=2048):
115
- logger.info("Chiamata al modello iniziata.")
116
  try:
117
  response = client.chat.completions.create(
118
- model="Qwen/Qwen2.5-72B-Instruct",
119
  messages=messages,
120
  temperature=temperature,
121
  max_tokens=max_tokens,
122
- top_p=0.9, # puoi settare come vuoi
123
- stream=False
124
  )
125
  raw_text = response["choices"][0]["message"]["content"]
126
- logger.debug(f"Risposta del modello: {raw_text}")
127
- # Togli newline
128
  return raw_text.replace("\n", " ").strip()
 
129
  except Exception as e:
130
- logger.error(f"Errore durante la chiamata al modello: {e}")
131
  raise HTTPException(status_code=500, detail=str(e))
132
 
133
- # =========================================
134
- # FASTAPI
135
- # =========================================
136
- from fastapi import FastAPI
137
-
138
  app = FastAPI()
139
 
140
  class QueryRequest(BaseModel):
@@ -144,31 +151,118 @@ class QueryRequest(BaseModel):
144
 
145
  @app.post("/generate-response/")
146
  async def generate_response(req: QueryRequest):
147
- user_msg = req.message
148
- logger.info(f"Ricevuta richiesta: {user_msg}")
149
 
150
- # 1) Creiamo un system_msg che contiene TUTTI i triple (preprocessati)
151
- system_msg = create_system_message(knowledge_text)
 
152
 
 
153
  messages = [
154
- {"role": "system", "content": system_msg},
155
- {"role": "user", "content": user_msg}
156
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- # 2) Chiamiamo il modello
159
- response_text = await call_model(
160
- messages,
161
- temperature=req.temperature,
162
- max_tokens=req.max_tokens
163
- )
 
 
 
 
 
164
 
165
- logger.info(f"Risposta generata dal modello: {response_text}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # (Opzionale) Se vuoi, qui potresti controllare se la risposta inizia con "PREFIX base:"
168
- # e se sì, eseguire su un Graph locale la query, interpretarla, ecc.
169
- # Ma in questo esempio, ci fermiamo e inviamo la raw response:
170
- return {"type": "NATURAL", "response": response_text}
 
 
 
171
 
172
  @app.get("/")
173
  async def root():
174
- return {"message": "Server con ontologia in prompt di sistema!"}
 
3
  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
+ # ==============================
10
+ # CONFIGURAZIONE LOGGING
11
+ # ==============================
12
  logging.basicConfig(
13
+ level=logging.DEBUG, # Se vuoi log dettagliato
14
  format="%(asctime)s - %(levelname)s - %(message)s",
15
  handlers=[
16
  logging.FileHandler("app.log"),
 
19
  )
20
  logger = logging.getLogger(__name__)
21
 
22
+ # ==============================
23
+ # PARAMETRI
24
+ # ==============================
25
  API_KEY = os.getenv("HF_API_KEY")
26
  if not API_KEY:
27
  logger.error("HF_API_KEY non impostata nell'ambiente.")
28
  raise EnvironmentError("HF_API_KEY non impostata nell'ambiente.")
29
 
 
 
 
 
30
  client = InferenceClient(api_key=API_KEY)
31
 
32
+ RDF_FILE = "Ontologia.rdf"
33
+ MAX_TRIPLES = 600 # Numero di triple da includere nel prompt
34
+ HF_MODEL = "Qwen/Qwen2.5-72B-Instruct" # Nome del modello Hugging Face
35
+
36
+ # ==============================
37
+ # STEP 0: Caricamento e Preprocessing dell'Ontologia
38
+ # ==============================
39
  def load_ontology_as_text(rdf_file: str, max_triples: int = 300) -> str:
40
  """
41
+ Legge un numero limitato di triple dall'ontologia
42
+ e le converte in una stringa 'knowledge_text'.
 
 
43
  """
44
  if not os.path.exists(rdf_file):
45
  return "Nessun file RDF trovato."
 
49
  g.parse(rdf_file, format="xml")
50
  except Exception as e:
51
  logger.error(f"Errore parsing RDF: {e}")
52
+ return "Errore nel parsing RDF."
53
 
54
+ lines = []
 
55
  count = 0
56
  for s, p, o in g:
 
57
  if count >= max_triples:
58
  break
 
59
  s_str = str(s)[:200]
60
  p_str = str(p)[:200]
61
  o_str = str(o)[:200]
62
  line = f"- {s_str} | {p_str} | {o_str}"
63
+ lines.append(line)
64
  count += 1
65
 
66
+ knowledge_text = "\n".join(lines)
67
  return knowledge_text
68
 
69
+ knowledge_text = load_ontology_as_text(RDF_FILE, max_triples=MAX_TRIPLES)
70
+ logger.debug(f"[ONTOLOGY TEXT] {knowledge_text[:1000]} ...")
71
 
72
+ # ==============================
73
+ # FUNZIONI UTILI
74
+ # ==============================
75
+ def create_initial_system_message(knowledge_text: str) -> str:
76
  """
77
+ System prompt 'stringente' che ordina di generare SEMPRE una query SPARQL.
 
78
  """
79
+ system_prompt = f"""
80
+ Sei un assistente di un museo. Hai accesso a queste triple (max {MAX_TRIPLES}):
 
 
 
 
 
 
 
81
  {knowledge_text}
82
 
83
  REGOLE TASSATIVE:
84
+ 1) Per qualunque domanda riguardi il contenuto di queste triple, DEVI generare una query SPARQL
85
+ con il prefisso:
86
+ PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
87
+ 2) Se la query produce 0 risultati o fallisce, devi provare una seconda volta
88
+ cambiando la sintassi (ad es. usando FILTER(STR(?nome)) o @it).
89
+ 3) Se la domanda è chat generica, rispondi brevemente, ma se appare una parte di knowledge
90
+ potresti tentare comunque una query.
91
+ 4) Se trovi almeno un risultato, fornisci la query SPARQL come tua risposta.
92
+ 5) NON inventare triple che non esistono in questo testo.
93
+ 6) Usa esclusivamente i dati contenuti qui. Se non trovi nulla, di' "Non ci sono info".
94
+ 7) Se la query è generata ma produce 0 results, prova un secondo tentativo.
95
 
96
+ Buona fortuna!
97
+ """
98
+ return system_prompt
99
+
100
+ def create_explanation_prompt(results_str: str) -> str:
101
+ """
102
+ Prompt da passare al modello per spiegare i risultati.
103
+ """
104
+ # Chiedi una spiegazione dettagliata ma non prolissa.
105
+ explanation_prompt = f"""
106
+ Abbiamo ottenuto questi risultati SPARQL:
107
 
108
+ {results_str}
 
109
 
110
+ Per favore spiega in modo dettagliato (ma non eccessivamente lungo) i risultati
111
+ come farebbe una guida museale. Cita, se serve, periodi storici o materiali
112
+ trovati, e fa' un breve richiamo all'ontologia. Non inventare nulla oltre.
113
+ """
114
+ return explanation_prompt
115
+
116
+ async def call_model(messages, temperature=0.7, max_tokens=2048) -> str:
117
+ """
118
+ Chiama HuggingFace Inference endpoint con i messaggi forniti.
119
+ Logga diversi step per debugging.
120
  """
121
+ logger.debug("[call_model] MESSAGGI INVIATI AL MODELLO:")
122
+ for msg in messages:
123
+ logger.debug(f"ROLE={msg['role']} CONTENT={msg['content'][:500]}")
124
 
125
+ logger.info("[call_model] Chiamata al modello Hugging Face...")
 
 
 
 
126
  try:
127
  response = client.chat.completions.create(
128
+ model=HF_MODEL,
129
  messages=messages,
130
  temperature=temperature,
131
  max_tokens=max_tokens,
132
+ top_p=0.9
 
133
  )
134
  raw_text = response["choices"][0]["message"]["content"]
135
+ logger.debug("[call_model] Risposta raw del modello HF:\n" + raw_text)
 
136
  return raw_text.replace("\n", " ").strip()
137
+
138
  except Exception as e:
139
+ logger.error(f"[call_model] Errore durante la chiamata: {e}")
140
  raise HTTPException(status_code=500, detail=str(e))
141
 
142
+ # ==============================
143
+ # FASTAPI
144
+ # ==============================
 
 
145
  app = FastAPI()
146
 
147
  class QueryRequest(BaseModel):
 
151
 
152
  @app.post("/generate-response/")
153
  async def generate_response(req: QueryRequest):
154
+ user_input = req.message
155
+ logger.info(f"[REQUEST] Ricevuta richiesta: {user_input}")
156
 
157
+ # 1) Creiamo system message con l'ontologia
158
+ system_prompt = create_initial_system_message(knowledge_text)
159
+ logger.debug("[SYSTEM PROMPT] " + system_prompt[:1000] + "...")
160
 
161
+ # 2) Prima chiamata => generare query SPARQL
162
  messages = [
163
+ {"role": "system", "content": system_prompt},
164
+ {"role": "user", "content": user_input}
165
  ]
166
+ response_text = await call_model(messages, req.temperature, req.max_tokens)
167
+ logger.info(f"[PRIMA RISPOSTA MODELLO] {response_text}")
168
+
169
+ # 3) Se non abbiamo "PREFIX base:" => second attempt
170
+ if not response_text.startswith("PREFIX base:"):
171
+ second_try_prompt = f"""
172
+ Non hai fornito una query SPARQL. Ricorda di generarla SEMPRE.
173
+ Riprova con un approccio differente per questa domanda:
174
+ {user_input}
175
+ """
176
+ second_messages = [
177
+ {"role": "system", "content": system_prompt},
178
+ {"role": "assistant", "content": response_text},
179
+ {"role": "user", "content": second_try_prompt}
180
+ ]
181
+ response_text_2 = await call_model(second_messages, req.temperature, req.max_tokens)
182
+ logger.info(f"[SECONDA RISPOSTA MODELLO] {response_text_2}")
183
+
184
+ if response_text_2.startswith("PREFIX base:"):
185
+ response_text = response_text_2
186
+ else:
187
+ # Neanche al secondo tentativo => restituiamo come "chat"
188
+ logger.info("[FALLBACK] Nessuna query generata anche al secondo tentativo.")
189
+ return {
190
+ "type": "NATURAL",
191
+ "response": response_text_2
192
+ }
193
 
194
+ # 4) Ora dovremmo avere una query SPARQL in 'response_text'
195
+ sparql_query = response_text
196
+ logger.info(f"[FINAL QUERY] {sparql_query}")
197
+
198
+ # 5) Eseguiamo la query con rdflib
199
+ g = rdflib.Graph()
200
+ try:
201
+ g.parse(RDF_FILE, format="xml")
202
+ except Exception as e:
203
+ logger.error(f"[ERROR] Parsing RDF: {e}")
204
+ return {"type": "ERROR", "response": "Errore nel parsing dell'ontologia."}
205
 
206
+ # 6) Validazione + esecuzione
207
+ try:
208
+ results = g.query(sparql_query)
209
+ except Exception as e:
210
+ logger.warning(f"[QUERY FAIL] {e}")
211
+ # Tenta un 2° fallback in caso la query non sia valida
212
+ second_try_prompt2 = f"""
213
+ La query SPARQL che hai fornito non è valida o non produce risultati.
214
+ Riprova con una sintassi differente.
215
+ Domanda utente: {user_input}
216
+ """
217
+ second_messages2 = [
218
+ {"role": "system", "content": system_prompt},
219
+ {"role": "assistant", "content": sparql_query},
220
+ {"role": "user", "content": second_try_prompt2}
221
+ ]
222
+ second_response2 = await call_model(second_messages2, req.temperature, req.max_tokens)
223
+ logger.info(f"[TERZO TENTATIVO MODELLO] {second_response2}")
224
+ if second_response2.startswith("PREFIX base:"):
225
+ # eseguiamo di nuovo
226
+ sparql_query = second_response2
227
+ try:
228
+ results = g.query(sparql_query)
229
+ except Exception as e2:
230
+ logger.error(f"[QUERY FAIL 2] {e2}")
231
+ return {"type": "ERROR", "response": "Query fallita di nuovo."}
232
+ else:
233
+ return {"type": "NATURAL", "response": second_response2}
234
+
235
+ # 7) Se 0 results => fine
236
+ if len(results) == 0:
237
+ logger.info("[SPARQL RESULT] 0 risultati.")
238
+ return {"type": "NATURAL", "response": "Nessun risultato trovato nella nostra ontologia."}
239
+
240
+ # 8) Ok => costruiamo una stringa con i risultati e facciamo un NUOVO prompt di interpretazione
241
+ row_list = []
242
+ for row in results:
243
+ row_str = ", ".join([f"{k}:{v}" for k, v in row.asdict().items()])
244
+ row_list.append(row_str)
245
+ results_str = "\n".join(row_list)
246
+
247
+ logger.info(f"[SPARQL OK] Trovati {len(results)} risultati.\n{results_str}")
248
+
249
+ # 9) Creiamo prompt di interpretazione
250
+ explain_prompt = create_explanation_prompt(results_str)
251
+ explain_messages = [
252
+ {"role": "system", "content": explain_prompt},
253
+ {"role": "user", "content": ""}
254
+ ]
255
+ explanation_text = await call_model(explain_messages, req.temperature, req.max_tokens)
256
+ logger.info(f"[EXPLANATION RESPONSE] {explanation_text}")
257
 
258
+ # 10) Restituiamo i risultati e la spiegazione finale
259
+ return {
260
+ "type": "NATURAL",
261
+ "sparql_query": sparql_query,
262
+ "sparql_results": row_list, # Lista di stringhe
263
+ "explanation": explanation_text
264
+ }
265
 
266
  @app.get("/")
267
  async def root():
268
+ return {"message": "Server con ontologia in system prompt, doppio tentativo SPARQL e interpretazione."}