AshenClock commited on
Commit
3421ea1
·
verified ·
1 Parent(s): 8b2fc25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +289 -61
app.py CHANGED
@@ -15,6 +15,7 @@ from dotenv import load_dotenv
15
  # Carica le variabili d'ambiente
16
  load_dotenv()
17
 
 
18
  logging.basicConfig(
19
  level=logging.DEBUG,
20
  format="%(asctime)s - %(levelname)s - %(message)s",
@@ -22,44 +23,188 @@ logging.basicConfig(
22
  )
23
  logger = logging.getLogger(__name__)
24
 
 
25
  API_KEY = os.getenv("HF_API_KEY")
26
  if not API_KEY:
27
  logger.error("HF_API_KEY non impostata.")
28
  raise EnvironmentError("HF_API_KEY non impostata.")
29
 
 
30
  client = InferenceClient(token=API_KEY)
31
 
32
- RDF_FILE = "Ontologia.rdf"
 
 
33
  HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"
34
 
35
- MAX_CLASSES = 30
36
  MAX_PROPERTIES = 30
37
 
38
- # Carica i documenti e l'indice FAISS
39
- with open("data/documents.json", "r", encoding="utf-8") as f:
40
- documents = json.load(f)
41
- index = faiss.read_index("data/faiss.index")
42
- model = SentenceTransformer('all-MiniLM-L6-v2')
43
 
44
- def retrieve_relevant_documents(query: str, top_k: int = 5):
45
- query_embedding = model.encode([query], convert_to_numpy=True)
46
- distances, indices = index.search(query_embedding, top_k)
47
- relevant_docs = [documents[idx] for idx in indices[0]]
48
- return relevant_docs
49
 
50
- def extract_classes_and_properties(rdf_file:str) -> str:
51
  """
52
- Carica l'ontologia e crea un 'sunto' di Classi e Proprietà
53
- (senza NamedIndividuals) per ridurre i token.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  """
55
- if not os.path.exists(rdf_file):
56
- return "NO_RDF_FILE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
 
 
 
58
  g = rdflib.Graph()
59
  try:
60
  g.parse(rdf_file, format="xml")
 
61
  except Exception as e:
62
- logger.error(f"Parsing RDF error: {e}")
63
  return "PARSING_ERROR"
64
 
65
  # Troviamo le classi
@@ -82,57 +227,109 @@ def extract_classes_and_properties(rdf_file:str) -> str:
82
  props_list = sorted(str(x) for x in props_found)
83
  props_list = props_list[:MAX_PROPERTIES]
84
 
 
 
 
 
 
 
 
85
  txt_classes = "\n".join([f"- CLASSE: {c}" for c in classes_list])
86
- txt_props = "\n".join([f"- PROPRIETA': {p}" for p in props_list])
 
87
 
88
  summary = f"""\
89
  # CLASSI (max {MAX_CLASSES})
90
  {txt_classes}
91
- # PROPRIETA' (max {MAX_PROPERTIES})
92
  {txt_props}
 
 
93
  """
 
94
  return summary
95
 
96
- knowledge_text = extract_classes_and_properties(RDF_FILE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- def create_system_message(ont_text:str, retrieved_docs:str)->str:
99
  """
100
  Prompt di sistema robusto, con regole su query in una riga e
101
  informazioni recuperate tramite RAG.
102
  """
103
  return f"""
104
- Sei un assistente museale. Ecco un estratto di CLASSI e PROPRIETA' dell'ontologia (senza NamedIndividuals):
105
  --- ONTOLOGIA ---
106
  {ont_text}
107
  --- FINE ---
108
  Ecco alcune informazioni rilevanti recuperate dalla base di conoscenza:
109
  {retrieved_docs}
110
- Suggerimento: se l'utente chiede il 'materiale' di un'opera, potresti usare qualcosa come
111
- 'base:materialeOpera' o un'altra proprietà simile (se esiste). Non è tassativo: usa
112
- la proprietà che ritieni più affine se ci sono riferimenti in ontologia.
113
  REGOLE STRINGENTI:
114
- 1) Se l'utente chiede info su questa ontologia, genera SEMPRE una query SPARQL in UNA SOLA RIGA,
115
- con prefix:
116
- PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
117
- 2) Se la query produce 0 risultati o fallisce, ritenta con un secondo tentativo.
118
- 3) Se la domanda è generica (tipo 'Ciao, come stai?'), rispondi breve.
119
- 4) Se trovi risultati, risposta finale = la query SPARQL (una sola riga).
120
- 5) Se non trovi nulla, di' 'Nessuna info.'
121
- 6) Non multiline. Esempio: PREFIX base: <...> SELECT ?x WHERE { ... }.
 
 
 
 
 
122
  FINE REGOLE
123
  """
124
 
125
- def create_explanation_prompt(results_str:str)->str:
 
126
  return f"""
127
  Ho ottenuto questi risultati SPARQL:
128
  {results_str}
129
  Ora fornisci una breve spiegazione museale (massimo ~10 righe), senza inventare oltre i risultati.
130
  """
131
-
132
- async def call_hf_model(messages, temperature=0.5, max_tokens=1024)->str:
 
133
  logger.debug("Chiamo HF con i seguenti messaggi:")
134
  for m in messages:
135
- logger.debug(f"ROLE={m['role']} => {m['content'][:300]}")
 
136
  try:
137
  resp = client.chat.completions.create(
138
  model=HF_MODEL,
@@ -150,6 +347,12 @@ async def call_hf_model(messages, temperature=0.5, max_tokens=1024)->str:
150
  logger.error(f"HuggingFace error: {e}")
151
  raise HTTPException(status_code=500, detail=str(e))
152
 
 
 
 
 
 
 
153
  app = FastAPI()
154
 
155
  class QueryRequest(BaseModel):
@@ -162,19 +365,26 @@ async def generate_response(req: QueryRequest):
162
  user_input = req.message
163
  logger.info(f"Utente dice: {user_input}")
164
 
165
- # Recupera documenti rilevanti usando RAG
166
- relevant_docs = retrieve_relevant_documents(user_input, top_k=3)
167
- retrieved_text = "\n".join([doc['text'] for doc in relevant_docs])
 
 
 
168
 
169
- sys_msg = create_system_message(knowledge_text, retrieved_text)
170
  msgs = [
171
  {"role": "system", "content": sys_msg},
172
  {"role": "user", "content": user_input}
173
  ]
174
 
175
  # Primo tentativo
176
- r1 = await call_hf_model(msgs, req.temperature, req.max_tokens)
177
- logger.info(f"PRIMA RISPOSTA:\n{r1}")
 
 
 
 
178
 
179
  # Se non parte con "PREFIX base:"
180
  if not r1.startswith("PREFIX base:"):
@@ -184,12 +394,16 @@ async def generate_response(req: QueryRequest):
184
  {"role": "assistant", "content": r1},
185
  {"role": "user", "content": sc}
186
  ]
187
- r2 = await call_hf_model(msgs2, req.temperature, req.max_tokens)
188
- logger.info(f"SECONDA RISPOSTA:\n{r2}")
189
- if r2.startswith("PREFIX base:"):
190
- sparql_query = r2
191
- else:
192
- return {"type": "NATURAL", "response": r2}
 
 
 
 
193
  else:
194
  sparql_query = r1
195
 
@@ -197,12 +411,14 @@ async def generate_response(req: QueryRequest):
197
  g = rdflib.Graph()
198
  try:
199
  g.parse(RDF_FILE, format="xml")
 
200
  except Exception as e:
201
  logger.error(f"Parsing RDF error: {e}")
202
  return {"type": "ERROR", "response": f"Parsing RDF error: {e}"}
203
 
204
  try:
205
  results = g.query(sparql_query)
 
206
  except Exception as e:
207
  fallback = f"La query SPARQL ha fallito. Riprova. Domanda: {user_input}"
208
  msgs3 = [
@@ -210,15 +426,22 @@ async def generate_response(req: QueryRequest):
210
  {"role": "assistant", "content": sparql_query},
211
  {"role": "user", "content": fallback}
212
  ]
213
- r3 = await call_hf_model(msgs3, req.temperature, req.max_tokens)
214
- if r3.startswith("PREFIX base:"):
215
- sparql_query = r3
216
- try:
217
- results = g.query(sparql_query)
218
- except Exception as e2:
219
- return {"type": "ERROR", "response": f"Query fallita di nuovo: {e2}"}
220
- else:
221
- return {"type": "NATURAL", "response": r3}
 
 
 
 
 
 
 
222
 
223
  if len(results) == 0:
224
  return {"type": "NATURAL", "sparql_query": sparql_query, "response": "Nessun risultato."}
@@ -226,7 +449,8 @@ async def generate_response(req: QueryRequest):
226
  # Confeziona risultati
227
  row_list = []
228
  for row in results:
229
- row_str = ", ".join([f"{k}:{v}" for k, v in row.asdict().items()])
 
230
  row_list.append(row_str)
231
  results_str = "\n".join(row_list)
232
 
@@ -236,7 +460,11 @@ async def generate_response(req: QueryRequest):
236
  {"role": "system", "content": exp_prompt},
237
  {"role": "user", "content": ""}
238
  ]
239
- explanation = await call_hf_model(msgs4, req.temperature, req.max_tokens)
 
 
 
 
240
 
241
  return {
242
  "type": "NATURAL",
 
15
  # Carica le variabili d'ambiente
16
  load_dotenv()
17
 
18
+ # Configura il logging
19
  logging.basicConfig(
20
  level=logging.DEBUG,
21
  format="%(asctime)s - %(levelname)s - %(message)s",
 
23
  )
24
  logger = logging.getLogger(__name__)
25
 
26
+ # Recupera la chiave API
27
  API_KEY = os.getenv("HF_API_KEY")
28
  if not API_KEY:
29
  logger.error("HF_API_KEY non impostata.")
30
  raise EnvironmentError("HF_API_KEY non impostata.")
31
 
32
+ # Inizializza InferenceClient
33
  client = InferenceClient(token=API_KEY)
34
 
35
+ # Definisci i percorsi dei file
36
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
37
+ RDF_FILE = os.path.join(BASE_DIR, "Ontologia.rdf")
38
  HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"
39
 
40
+ MAX_CLASSES = 30
41
  MAX_PROPERTIES = 30
42
 
43
+ # Percorsi dei file generati
44
+ DOCUMENTS_FILE = os.path.join(BASE_DIR, "data", "documents.json")
45
+ FAISS_INDEX_FILE = os.path.join(BASE_DIR, "data", "faiss.index")
 
 
46
 
47
+ def create_data_directory():
48
+ """Crea la directory 'data/' se non esiste."""
49
+ os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True)
50
+ logger.info("Directory 'data/' creata o già esistente.")
 
51
 
52
+ def extract_ontology(rdf_file: str, output_file: str):
53
  """
54
+ Estrae classi, proprietà ed entità dall'ontologia RDF e le salva in un file JSON come un unico documento.
55
+ """
56
+ logger.info(f"Inizio estrazione dell'ontologia da {rdf_file}.")
57
+ g = rdflib.Graph()
58
+ try:
59
+ g.parse(rdf_file, format="xml")
60
+ logger.info(f"Parsing RDF di {rdf_file} riuscito.")
61
+ except Exception as e:
62
+ logger.error(f"Errore nel parsing RDF: {e}")
63
+ raise e
64
+
65
+ # Estrai Classi
66
+ classes = []
67
+ for cls in g.subjects(RDF.type, OWL.Class):
68
+ label = g.value(cls, RDFS.label, default=str(cls))
69
+ description = g.value(cls, RDFS.comment, default="No description.")
70
+ classes.append({"class": str(cls), "label": str(label), "description": str(description)})
71
+
72
+ for cls in g.subjects(RDF.type, RDFS.Class):
73
+ label = g.value(cls, RDFS.label, default=str(cls))
74
+ description = g.value(cls, RDFS.comment, default="No description.")
75
+ classes.append({"class": str(cls), "label": str(label), "description": str(description)})
76
+
77
+ # Estrai Proprietà
78
+ properties = []
79
+ for prop in g.subjects(RDF.type, OWL.ObjectProperty):
80
+ label = g.value(prop, RDFS.label, default=str(prop))
81
+ description = g.value(prop, RDFS.comment, default="No description.")
82
+ properties.append({"property": str(prop), "label": str(label), "description": str(description)})
83
+
84
+ for prop in g.subjects(RDF.type, OWL.DatatypeProperty):
85
+ label = g.value(prop, RDFS.label, default=str(prop))
86
+ description = g.value(prop, RDFS.comment, default="No description.")
87
+ properties.append({"property": str(prop), "label": str(label), "description": str(description)})
88
+
89
+ for prop in g.subjects(RDF.type, RDF.Property):
90
+ label = g.value(prop, RDFS.label, default=str(prop))
91
+ description = g.value(prop, RDFS.comment, default="No description.")
92
+ properties.append({"property": str(prop), "label": str(label), "description": str(description)})
93
+
94
+ # Estrai Entità (NamedIndividuals)
95
+ entities = []
96
+ for entity in g.subjects(RDF.type, OWL.NamedIndividual):
97
+ label = g.value(entity, RDFS.label, default=str(entity))
98
+ description = g.value(entity, RDFS.comment, default="No description.")
99
+ # Estrai le proprietà dell'entità
100
+ entity_properties = {}
101
+ for predicate, obj in g.predicate_objects(entity):
102
+ if predicate != RDFS.label and predicate != RDFS.comment:
103
+ entity_properties[str(predicate)] = str(obj)
104
+ entities.append({
105
+ "entity": str(entity),
106
+ "label": str(label),
107
+ "description": str(description),
108
+ "properties": entity_properties
109
+ })
110
+
111
+ # Crea un unico documento
112
+ ontology_summary = {
113
+ "title": "Ontologia Museo",
114
+ "classes": classes[:MAX_CLASSES],
115
+ "properties": properties[:MAX_PROPERTIES],
116
+ "entities": entities, # Aggiungi le entità
117
+ "full_ontology": g.serialize(format="xml").decode("utf-8") # Opzionale: include l'intero RDF/XML
118
+ }
119
+
120
+ # Salva il documento in JSON
121
+ try:
122
+ with open(output_file, "w", encoding="utf-8") as f:
123
+ json.dump(ontology_summary, f, ensure_ascii=False, indent=2)
124
+ logger.info(f"Ontologia estratta e salvata in {output_file}")
125
+ except Exception as e:
126
+ logger.error(f"Errore nel salvataggio di {output_file}: {e}")
127
+ raise e
128
+
129
+ def create_faiss_index(documents_file: str, index_file: str, embedding_model: str = 'all-MiniLM-L6-v2'):
130
+ """
131
+ Crea un indice FAISS a partire dal documento estratto.
132
  """
133
+ logger.info(f"Inizio creazione dell'indice FAISS da {documents_file}.")
134
+ try:
135
+ # Carica il documento
136
+ with open(documents_file, "r", encoding="utf-8") as f:
137
+ document = json.load(f)
138
+ logger.info(f"Documento caricato da {documents_file}.")
139
+
140
+ # Genera embedding
141
+ model = SentenceTransformer(embedding_model)
142
+ # Concatenazione delle classi, proprietà e entità per l'embedding
143
+ texts = [f"Classe: {cls['label']}. Descrizione: {cls['description']}" for cls in document['classes']]
144
+ texts += [f"Proprietà: {prop['label']}. Descrizione: {prop['description']}" for prop in document['properties']]
145
+ texts += [f"Entità: {entity['label']}. Descrizione: {entity['description']}. Proprietà: {entity['properties']}" for entity in document.get('entities', [])]
146
+ embeddings = model.encode(texts, convert_to_numpy=True)
147
+ logger.info("Embedding generati con SentenceTransformer.")
148
+
149
+ # Crea l'indice FAISS
150
+ dimension = embeddings.shape[1]
151
+ index = faiss.IndexFlatL2(dimension)
152
+ index.add(embeddings)
153
+ logger.info(f"Indice FAISS creato con dimensione: {dimension}.")
154
+
155
+ # Salva l'indice
156
+ faiss.write_index(index, index_file)
157
+ logger.info(f"Indice FAISS salvato in {index_file}.")
158
+ except Exception as e:
159
+ logger.error(f"Errore nella creazione dell'indice FAISS: {e}")
160
+ raise e
161
+
162
+ def prepare_retrieval():
163
+ """Prepara i file necessari per l'approccio RAG."""
164
+ logger.info("Inizio preparazione per il retrieval.")
165
+ create_data_directory()
166
+
167
+ # Verifica se Ontologia.rdf esiste
168
+ if not os.path.exists(RDF_FILE):
169
+ logger.error(f"File RDF non trovato: {RDF_FILE}")
170
+ raise FileNotFoundError(f"File RDF non trovato: {RDF_FILE}")
171
+ else:
172
+ logger.info(f"File RDF trovato: {RDF_FILE}")
173
+
174
+ # Verifica se documents.json esiste, altrimenti generalo
175
+ if not os.path.exists(DOCUMENTS_FILE):
176
+ logger.info(f"File {DOCUMENTS_FILE} non trovato. Estrazione dell'ontologia.")
177
+ try:
178
+ extract_ontology(RDF_FILE, DOCUMENTS_FILE)
179
+ except Exception as e:
180
+ logger.error(f"Errore nell'estrazione dell'ontologia: {e}")
181
+ raise e
182
+ else:
183
+ logger.info(f"File {DOCUMENTS_FILE} trovato.")
184
+
185
+ # Verifica se faiss.index esiste, altrimenti crealo
186
+ if not os.path.exists(FAISS_INDEX_FILE):
187
+ logger.info(f"File {FAISS_INDEX_FILE} non trovato. Creazione dell'indice FAISS.")
188
+ try:
189
+ create_faiss_index(DOCUMENTS_FILE, FAISS_INDEX_FILE)
190
+ except Exception as e:
191
+ logger.error(f"Errore nella creazione dell'indice FAISS: {e}")
192
+ raise e
193
+ else:
194
+ logger.info(f"File {FAISS_INDEX_FILE} trovato.")
195
 
196
+ def extract_classes_and_properties(rdf_file: str) -> str:
197
+ """
198
+ Carica l'ontologia e crea un 'sunto' di Classi, Proprietà ed Entità
199
+ (senza NamedIndividuals) per ridurre i token.
200
+ """
201
+ logger.info(f"Inizio estrazione di classi, proprietà ed entità da {rdf_file}.")
202
  g = rdflib.Graph()
203
  try:
204
  g.parse(rdf_file, format="xml")
205
+ logger.info(f"Parsing RDF di {rdf_file} riuscito.")
206
  except Exception as e:
207
+ logger.error(f"Errore nel parsing RDF: {e}")
208
  return "PARSING_ERROR"
209
 
210
  # Troviamo le classi
 
227
  props_list = sorted(str(x) for x in props_found)
228
  props_list = props_list[:MAX_PROPERTIES]
229
 
230
+ # Troviamo le entità
231
+ entities_found = set()
232
+ for e in g.subjects(RDF.type, OWL.NamedIndividual):
233
+ entities_found.add(e)
234
+ entities_list = sorted(str(e) for e in entities_found)
235
+ entities_list = entities_list[:MAX_CLASSES] # Puoi impostare un limite adeguato
236
+
237
  txt_classes = "\n".join([f"- CLASSE: {c}" for c in classes_list])
238
+ txt_props = "\n".join([f"- PROPRIETÀ: {p}" for p in props_list])
239
+ txt_entities = "\n".join([f"- ENTITÀ: {e}" for e in entities_list])
240
 
241
  summary = f"""\
242
  # CLASSI (max {MAX_CLASSES})
243
  {txt_classes}
244
+ # PROPRIETÀ (max {MAX_PROPERTIES})
245
  {txt_props}
246
+ # ENTITÀ (max {MAX_CLASSES})
247
+ {txt_entities}
248
  """
249
+ logger.info("Estrazione di classi, proprietà ed entità completata.")
250
  return summary
251
 
252
+ def retrieve_relevant_documents(query: str, top_k: int = 5):
253
+ """Recupera i documenti rilevanti usando FAISS."""
254
+ logger.info(f"Recupero dei documenti rilevanti per la query: {query}")
255
+ try:
256
+ # Carica il documento
257
+ with open(DOCUMENTS_FILE, "r", encoding="utf-8") as f:
258
+ document = json.load(f)
259
+ logger.info(f"Documento caricato da {DOCUMENTS_FILE}.")
260
+
261
+ # Carica l'indice FAISS
262
+ index = faiss.read_index(FAISS_INDEX_FILE)
263
+ logger.info(f"Indice FAISS caricato da {FAISS_INDEX_FILE}.")
264
+
265
+ # Genera embedding della query
266
+ model = SentenceTransformer('all-MiniLM-L6-v2')
267
+ query_embedding = model.encode([query], convert_to_numpy=True)
268
+ logger.info("Embedding della query generati.")
269
+
270
+ # Ricerca nell'indice
271
+ distances, indices = index.search(query_embedding, top_k)
272
+ logger.info(f"Ricerca FAISS completata. Risultati ottenuti: {len(indices[0])}")
273
+
274
+ # Concatenazione delle descrizioni per la ricerca
275
+ texts = [f"Classe: {cls['label']}. Descrizione: {cls['description']}" for cls in document['classes']]
276
+ texts += [f"Proprietà: {prop['label']}. Descrizione: {prop['description']}" for prop in document['properties']]
277
+ texts += [f"Entità: {entity['label']}. Descrizione: {entity['description']}. Proprietà: {entity['properties']}" for entity in document.get('entities', [])]
278
+
279
+ # Recupera i testi rilevanti
280
+ relevant_texts = [texts[idx] for idx in indices[0] if idx < len(texts)]
281
+ retrieved_docs = "\n".join(relevant_texts)
282
+ logger.info(f"Documenti rilevanti recuperati: {len(relevant_texts)}")
283
+ return retrieved_docs
284
+ except Exception as e:
285
+ logger.error(f"Errore nel recupero dei documenti rilevanti: {e}")
286
+ raise e
287
 
288
+ def create_system_message(ont_text: str, retrieved_docs: str) -> str:
289
  """
290
  Prompt di sistema robusto, con regole su query in una riga e
291
  informazioni recuperate tramite RAG.
292
  """
293
  return f"""
294
+ Sei un assistente museale esperto in ontologie RDF. Utilizza le informazioni fornite per generare query SPARQL precise e pertinenti. Ecco un estratto di CLASSI, PROPRIETÀ ed ENTITÀ dell'ontologia (senza NamedIndividuals):
295
  --- ONTOLOGIA ---
296
  {ont_text}
297
  --- FINE ---
298
  Ecco alcune informazioni rilevanti recuperate dalla base di conoscenza:
299
  {retrieved_docs}
300
+ 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.
301
+
 
302
  REGOLE STRINGENTI:
303
+ 1) Se l'utente chiede informazioni su questa ontologia, genera SEMPRE una query SPARQL in UNA SOLA RIGA, con prefix:
304
+ PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
305
+ 2) La query SPARQL deve essere precisa e cercare esattamente le entità specificate dall'utente. Ad esempio, se l'utente chiede "Chi ha creato l'opera 'Amore e Psiche'?", la query dovrebbe cercare l'opera esattamente con quel nome.
306
+ 3) Se la query produce 0 risultati o fallisce, ritenta con un secondo tentativo.
307
+ 4) Se la domanda è generica (tipo 'Ciao, come stai?'), rispondi breve.
308
+ 5) Se trovi risultati, la risposta finale deve essere la query SPARQL (una sola riga).
309
+ 6) Se non trovi nulla, rispondi con 'Nessuna info.'
310
+ 7) Non multiline. Esempio: PREFIX base: <...> SELECT ?x WHERE { ... }.
311
+
312
+ Esempio:
313
+ Utente: Chi ha creato l'opera 'Amore e Psiche'?
314
+ Risposta: PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#> SELECT ?creatore WHERE { ?opera base:hasName "Amore e Psiche" . ?opera base:creatoDa ?creatore . }
315
+
316
  FINE REGOLE
317
  """
318
 
319
+ def create_explanation_prompt(results_str: str) -> str:
320
+ """Prompt per generare una spiegazione museale dei risultati SPARQL."""
321
  return f"""
322
  Ho ottenuto questi risultati SPARQL:
323
  {results_str}
324
  Ora fornisci una breve spiegazione museale (massimo ~10 righe), senza inventare oltre i risultati.
325
  """
326
+
327
+ async def call_hf_model(messages, temperature=0.5, max_tokens=1024) -> str:
328
+ """Chiama il modello Hugging Face e gestisce la risposta."""
329
  logger.debug("Chiamo HF con i seguenti messaggi:")
330
  for m in messages:
331
+ content_preview = (m['content'][:300] + '...') if len(m['content']) > 300 else m['content']
332
+ logger.debug(f"ROLE={m['role']} => {content_preview}")
333
  try:
334
  resp = client.chat.completions.create(
335
  model=HF_MODEL,
 
347
  logger.error(f"HuggingFace error: {e}")
348
  raise HTTPException(status_code=500, detail=str(e))
349
 
350
+ # Prepara i file necessari per RAG
351
+ prepare_retrieval()
352
+
353
+ # Carica il 'sunto' di classi, proprietà ed entità
354
+ knowledge_text = extract_classes_and_properties(RDF_FILE)
355
+
356
  app = FastAPI()
357
 
358
  class QueryRequest(BaseModel):
 
365
  user_input = req.message
366
  logger.info(f"Utente dice: {user_input}")
367
 
368
+ try:
369
+ # Recupera documenti rilevanti usando RAG
370
+ retrieved_docs = retrieve_relevant_documents(user_input, top_k=3)
371
+ except Exception as e:
372
+ logger.error(f"Errore nel recupero dei documenti rilevanti: {e}")
373
+ return {"type": "ERROR", "response": f"Errore nel recupero dei documenti: {e}"}
374
 
375
+ sys_msg = create_system_message(knowledge_text, retrieved_docs)
376
  msgs = [
377
  {"role": "system", "content": sys_msg},
378
  {"role": "user", "content": user_input}
379
  ]
380
 
381
  # Primo tentativo
382
+ try:
383
+ r1 = await call_hf_model(msgs, req.temperature, req.max_tokens)
384
+ logger.info(f"PRIMA RISPOSTA:\n{r1}")
385
+ except Exception as e:
386
+ logger.error(f"Errore nella chiamata al modello Hugging Face: {e}")
387
+ return {"type": "ERROR", "response": f"Errore nella generazione della risposta: {e}"}
388
 
389
  # Se non parte con "PREFIX base:"
390
  if not r1.startswith("PREFIX base:"):
 
394
  {"role": "assistant", "content": r1},
395
  {"role": "user", "content": sc}
396
  ]
397
+ try:
398
+ r2 = await call_hf_model(msgs2, req.temperature, req.max_tokens)
399
+ logger.info(f"SECONDA RISPOSTA:\n{r2}")
400
+ if r2.startswith("PREFIX base:"):
401
+ sparql_query = r2
402
+ else:
403
+ return {"type": "NATURAL", "response": r2}
404
+ except Exception as e:
405
+ logger.error(f"Errore nella seconda chiamata al modello Hugging Face: {e}")
406
+ return {"type": "ERROR", "response": f"Errore nella generazione della seconda risposta: {e}"}
407
  else:
408
  sparql_query = r1
409
 
 
411
  g = rdflib.Graph()
412
  try:
413
  g.parse(RDF_FILE, format="xml")
414
+ logger.info(f"Parsing RDF di {RDF_FILE} riuscito per l'esecuzione della query.")
415
  except Exception as e:
416
  logger.error(f"Parsing RDF error: {e}")
417
  return {"type": "ERROR", "response": f"Parsing RDF error: {e}"}
418
 
419
  try:
420
  results = g.query(sparql_query)
421
+ logger.info(f"Query SPARQL eseguita con successo. Risultati: {len(results)}")
422
  except Exception as e:
423
  fallback = f"La query SPARQL ha fallito. Riprova. Domanda: {user_input}"
424
  msgs3 = [
 
426
  {"role": "assistant", "content": sparql_query},
427
  {"role": "user", "content": fallback}
428
  ]
429
+ try:
430
+ r3 = await call_hf_model(msgs3, req.temperature, req.max_tokens)
431
+ logger.info(f"TERZA RISPOSTA (fallback):\n{r3}")
432
+ if r3.startswith("PREFIX base:"):
433
+ sparql_query = r3
434
+ try:
435
+ results = g.query(sparql_query)
436
+ logger.info(f"Seconda query SPARQL eseguita con successo. Risultati: {len(results)}")
437
+ except Exception as e2:
438
+ logger.error(f"Seconda Query fallita: {e2}")
439
+ return {"type": "ERROR", "response": f"Query fallita di nuovo: {e2}"}
440
+ else:
441
+ return {"type": "NATURAL", "response": r3}
442
+ except Exception as e:
443
+ logger.error(f"Errore nella chiamata al modello Hugging Face durante il fallback: {e}")
444
+ return {"type": "ERROR", "response": f"Errore durante il fallback della risposta: {e}"}
445
 
446
  if len(results) == 0:
447
  return {"type": "NATURAL", "sparql_query": sparql_query, "response": "Nessun risultato."}
 
449
  # Confeziona risultati
450
  row_list = []
451
  for row in results:
452
+ row_dict = row.asdict()
453
+ row_str = ", ".join([f"{k}:{v}" for k, v in row_dict.items()])
454
  row_list.append(row_str)
455
  results_str = "\n".join(row_list)
456
 
 
460
  {"role": "system", "content": exp_prompt},
461
  {"role": "user", "content": ""}
462
  ]
463
+ try:
464
+ explanation = await call_hf_model(msgs4, req.temperature, req.max_tokens)
465
+ except Exception as e:
466
+ logger.error(f"Errore nella generazione della spiegazione: {e}")
467
+ return {"type": "ERROR", "response": f"Errore nella generazione della spiegazione: {e}"}
468
 
469
  return {
470
  "type": "NATURAL",