File size: 9,751 Bytes
ac52c4d
 
89f944e
ac52c4d
 
89f944e
8607561
ac52c4d
8607561
 
 
2ecaeab
8607561
2ecaeab
 
89f944e
 
2ecaeab
 
ac52c4d
 
8607561
 
 
ac52c4d
 
 
 
 
89f944e
 
8607561
 
 
 
 
 
 
89f944e
ac52c4d
8607561
 
ac52c4d
89f944e
ac52c4d
69d6e40
89f944e
ac52c4d
89f944e
ac52c4d
89f944e
8607561
89f944e
8607561
89f944e
 
 
 
 
 
 
 
8607561
89f944e
 
8607561
89f944e
 
8607561
 
89f944e
8607561
 
 
 
ac52c4d
8607561
4c13256
8607561
 
89f944e
 
 
8607561
 
 
 
 
 
 
 
 
 
 
3aa01bd
8607561
 
 
 
 
 
 
 
 
 
 
69d6e40
8607561
059bef5
8607561
 
 
 
 
 
 
 
 
 
89f944e
8607561
 
 
ac52c4d
8607561
ac52c4d
 
8607561
ac52c4d
 
 
8607561
ac52c4d
 
8607561
ac52c4d
8607561
ac52c4d
8607561
ac52c4d
 
8607561
 
 
ac52c4d
 
 
 
89f944e
 
ac52c4d
 
89f944e
8607561
 
3aa01bd
8607561
 
 
89f944e
8607561
ac52c4d
8607561
 
ac52c4d
8607561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3aa01bd
8607561
 
 
 
 
 
 
 
 
 
 
69d6e40
8607561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69d6e40
8607561
 
 
 
 
 
 
ac52c4d
 
 
8607561
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import os
import logging
from typing import Optional
from pydantic import BaseModel
from fastapi import FastAPI, HTTPException
import rdflib
from huggingface_hub import InferenceClient

# ==============================
# CONFIGURAZIONE LOGGING
# ==============================
logging.basicConfig(
    level=logging.DEBUG,  # Se vuoi log dettagliato
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler("app.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# ==============================
# PARAMETRI
# ==============================
API_KEY = os.getenv("HF_API_KEY")
if not API_KEY:
    logger.error("HF_API_KEY non impostata nell'ambiente.")
    raise EnvironmentError("HF_API_KEY non impostata nell'ambiente.")

client = InferenceClient(api_key=API_KEY)

RDF_FILE = "Ontologia.rdf"
MAX_TRIPLES = 600  # Numero di triple da includere nel prompt
HF_MODEL = "Qwen/Qwen2.5-72B-Instruct"  # Nome del modello Hugging Face

# ==============================
# STEP 0: Caricamento e Preprocessing dell'Ontologia
# ==============================
def load_ontology_as_text(rdf_file: str, max_triples: int = 300) -> str:
    """
    Legge un numero limitato di triple dall'ontologia
    e le converte in una stringa 'knowledge_text'.
    """
    if not os.path.exists(rdf_file):
        return "Nessun file RDF trovato."

    g = rdflib.Graph()
    try:
        g.parse(rdf_file, format="xml")
    except Exception as e:
        logger.error(f"Errore parsing RDF: {e}")
        return "Errore nel parsing RDF."

    lines = []
    count = 0
    for s, p, o in g:
        if count >= max_triples:
            break
        s_str = str(s)[:200]
        p_str = str(p)[:200]
        o_str = str(o)[:200]
        line = f"- {s_str} | {p_str} | {o_str}"
        lines.append(line)
        count += 1

    knowledge_text = "\n".join(lines)
    return knowledge_text

knowledge_text = load_ontology_as_text(RDF_FILE, max_triples=MAX_TRIPLES)
logger.debug(f"[ONTOLOGY TEXT] {knowledge_text[:1000]} ...")

# ==============================
#  FUNZIONI UTILI
# ==============================
def create_initial_system_message(knowledge_text: str) -> str:
    """
    System prompt 'stringente' che ordina di generare SEMPRE una query SPARQL.
    """
    system_prompt = f"""
Sei un assistente di un museo. Hai accesso a queste triple (max {MAX_TRIPLES}):
{knowledge_text}

REGOLE TASSATIVE:
1) Per qualunque domanda riguardi il contenuto di queste triple, DEVI generare una query SPARQL
   con il prefisso:
     PREFIX base: <http://www.semanticweb.org/lucreziamosca/ontologies/progettoMuseo#>
2) Se la query produce 0 risultati o fallisce, devi provare una seconda volta
   cambiando la sintassi (ad es. usando FILTER(STR(?nome)) o @it).
3) Se la domanda è chat generica, rispondi brevemente, ma se appare una parte di knowledge
   potresti tentare comunque una query.
4) Se trovi almeno un risultato, fornisci la query SPARQL come tua risposta.
5) NON inventare triple che non esistono in questo testo.
6) Usa esclusivamente i dati contenuti qui. Se non trovi nulla, di' "Non ci sono info".
7) Se la query è generata ma produce 0 results, prova un secondo tentativo.

Buona fortuna! 
    """
    return system_prompt

def create_explanation_prompt(results_str: str) -> str:
    """
    Prompt da passare al modello per spiegare i risultati.
    """
    # Chiedi una spiegazione dettagliata ma non prolissa.
    explanation_prompt = f"""
Abbiamo ottenuto questi risultati SPARQL:

{results_str}

Per favore spiega in modo dettagliato (ma non eccessivamente lungo) i risultati
come farebbe una guida museale. Cita, se serve, periodi storici o materiali
trovati, e fa' un breve richiamo all'ontologia. Non inventare nulla oltre.
    """
    return explanation_prompt

async def call_model(messages, temperature=0.7, max_tokens=2048) -> str:
    """
    Chiama HuggingFace Inference endpoint con i messaggi forniti.
    Logga diversi step per debugging.
    """
    logger.debug("[call_model] MESSAGGI INVIATI AL MODELLO:")
    for msg in messages:
        logger.debug(f"ROLE={msg['role']} CONTENT={msg['content'][:500]}")

    logger.info("[call_model] Chiamata al modello Hugging Face...")
    try:
        response = client.chat.completions.create(
            model=HF_MODEL,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            top_p=0.9
        )
        raw_text = response["choices"][0]["message"]["content"]
        logger.debug("[call_model] Risposta raw del modello HF:\n" + raw_text)
        return raw_text.replace("\n", " ").strip()

    except Exception as e:
        logger.error(f"[call_model] Errore durante la chiamata: {e}")
        raise HTTPException(status_code=500, detail=str(e))

# ==============================
#  FASTAPI
# ==============================
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"[REQUEST] Ricevuta richiesta: {user_input}")

    # 1) Creiamo system message con l'ontologia
    system_prompt = create_initial_system_message(knowledge_text)
    logger.debug("[SYSTEM PROMPT] " + system_prompt[:1000] + "...")

    # 2) Prima chiamata => generare query SPARQL
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_input}
    ]
    response_text = await call_model(messages, req.temperature, req.max_tokens)
    logger.info(f"[PRIMA RISPOSTA MODELLO] {response_text}")

    # 3) Se non abbiamo "PREFIX base:" => second attempt
    if not response_text.startswith("PREFIX base:"):
        second_try_prompt = f"""
Non hai fornito una query SPARQL. Ricorda di generarla SEMPRE.
Riprova con un approccio differente per questa domanda:
{user_input}
        """
        second_messages = [
            {"role": "system", "content": system_prompt},
            {"role": "assistant", "content": response_text},
            {"role": "user", "content": second_try_prompt}
        ]
        response_text_2 = await call_model(second_messages, req.temperature, req.max_tokens)
        logger.info(f"[SECONDA RISPOSTA MODELLO] {response_text_2}")

        if response_text_2.startswith("PREFIX base:"):
            response_text = response_text_2
        else:
            # Neanche al secondo tentativo => restituiamo come "chat"
            logger.info("[FALLBACK] Nessuna query generata anche al secondo tentativo.")
            return {
                "type": "NATURAL",
                "response": response_text_2
            }

    # 4) Ora dovremmo avere una query SPARQL in 'response_text'
    sparql_query = response_text
    logger.info(f"[FINAL QUERY] {sparql_query}")

    # 5) Eseguiamo la query con rdflib
    g = rdflib.Graph()
    try:
        g.parse(RDF_FILE, format="xml")
    except Exception as e:
        logger.error(f"[ERROR] Parsing RDF: {e}")
        return {"type": "ERROR", "response": "Errore nel parsing dell'ontologia."}

    # 6) Validazione + esecuzione
    try:
        results = g.query(sparql_query)
    except Exception as e:
        logger.warning(f"[QUERY FAIL] {e}")
        # Tenta un 2° fallback in caso la query non sia valida
        second_try_prompt2 = f"""
La query SPARQL che hai fornito non è valida o non produce risultati.
Riprova con una sintassi differente.
Domanda utente: {user_input}
        """
        second_messages2 = [
            {"role": "system", "content": system_prompt},
            {"role": "assistant", "content": sparql_query},
            {"role": "user", "content": second_try_prompt2}
        ]
        second_response2 = await call_model(second_messages2, req.temperature, req.max_tokens)
        logger.info(f"[TERZO TENTATIVO MODELLO] {second_response2}")
        if second_response2.startswith("PREFIX base:"):
            # eseguiamo di nuovo
            sparql_query = second_response2
            try:
                results = g.query(sparql_query)
            except Exception as e2:
                logger.error(f"[QUERY FAIL 2] {e2}")
                return {"type": "ERROR", "response": "Query fallita di nuovo."}
        else:
            return {"type": "NATURAL", "response": second_response2}

    # 7) Se 0 results => fine
    if len(results) == 0:
        logger.info("[SPARQL RESULT] 0 risultati.")
        return {"type": "NATURAL", "response": "Nessun risultato trovato nella nostra ontologia."}

    # 8) Ok => costruiamo una stringa con i risultati e facciamo un NUOVO prompt di interpretazione
    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)

    logger.info(f"[SPARQL OK] Trovati {len(results)} risultati.\n{results_str}")

    # 9) Creiamo prompt di interpretazione
    explain_prompt = create_explanation_prompt(results_str)
    explain_messages = [
        {"role": "system", "content": explain_prompt},
        {"role": "user", "content": ""}
    ]
    explanation_text = await call_model(explain_messages, req.temperature, req.max_tokens)
    logger.info(f"[EXPLANATION RESPONSE] {explanation_text}")

    # 10) Restituiamo i risultati e la spiegazione finale
    return {
        "type": "NATURAL",
        "sparql_query": sparql_query,
        "sparql_results": row_list,  # Lista di stringhe
        "explanation": explanation_text
    }

@app.get("/")
async def root():
    return {"message": "Server con ontologia in system prompt, doppio tentativo SPARQL e interpretazione."}