GaetanoParente commited on
Commit
a968971
·
verified ·
1 Parent(s): b7da60c

Upload 16 files

Browse files
app/ui.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from neo4j import GraphDatabase
3
+ import pandas as pd
4
+ from pyvis.network import Network
5
+ import streamlit.components.v1 as components
6
+ import os
7
+ import csv
8
+ from datetime import datetime
9
+ from dotenv import load_dotenv
10
+
11
+ # --- CONFIGURAZIONE ---
12
+
13
+ # Carica variabili d'ambiente
14
+ load_dotenv()
15
+
16
+ st.set_page_config(page_title="Activa Semantic Discovery", layout="wide")
17
+ # Usa le variabili d'ambiente (con fallback per sicurezza locale)
18
+ URI = os.getenv("NEO4J_URI", "neo4j+s://748d6c94.databases.neo4j.io")
19
+ USER = os.getenv("NEO4J_USER", "neo4j")
20
+ PASSWORD = os.getenv("NEO4J_PASSWORD", "t1bT1DiXwDOGMYfX89qR20loSN8FXurB3Dfg8bPQcTI")
21
+ AUTH = (USER, PASSWORD)
22
+
23
+ # --- CONNESSIONE NEO4J ---
24
+ @st.cache_resource
25
+ def get_driver():
26
+ return GraphDatabase.driver(URI, auth=AUTH)
27
+
28
+ def run_query(query, params=None):
29
+ driver = get_driver()
30
+ with driver.session() as session:
31
+ result = session.run(query, params)
32
+ return [r.data() for r in result]
33
+
34
+ # --- LOGICA CORE: FEEDBACK LOOP (Nuova Funzionalità) ---
35
+ def reject_relationship(rel_id, subj, pred, obj, reason="Human Rejection"):
36
+ """
37
+ 1. Cancella dal DB (Azione Reale).
38
+ 2. Salva in CSV per Active Learning (Data Lineage del rifiuto).
39
+ """
40
+ # 1. Cancellazione Reale
41
+ query = "MATCH ()-[r]->() WHERE elementId(r) = $id DELETE r"
42
+ try:
43
+ run_query(query, {"id": rel_id})
44
+ except Exception as e:
45
+ st.error(f"Errore durante la cancellazione: {e}")
46
+ return False
47
+
48
+ # 2. Logging per Fine-Tuning
49
+ log_file = "rejected_triples.csv"
50
+ file_exists = os.path.isfile(log_file)
51
+
52
+ try:
53
+ with open(log_file, mode='a', newline='', encoding='utf-8') as f:
54
+ writer = csv.writer(f)
55
+ if not file_exists:
56
+ writer.writerow(["timestamp", "subject", "predicate", "object", "reason"])
57
+ writer.writerow([datetime.now(), subj, pred, obj, reason])
58
+ return True
59
+ except Exception as e:
60
+ st.warning(f"Relazione cancellata dal DB, ma errore nel log CSV: {e}")
61
+ return True
62
+
63
+ # --- UI: HEADER ---
64
+ st.title("🧠 Automated Semantic Discovery | Lab")
65
+ st.markdown("""
66
+ **Piattaforma Human-in-the-Loop** per la validazione delle ontologie generate.
67
+ Vedi Sezione 5.2.4 della Relazione Tecnica.
68
+ """)
69
+
70
+ # --- UI: KPI METRICS (Mantenuti dalla versione vecchia perché più completi) ---
71
+ col1, col2, col3 = st.columns(3)
72
+ try:
73
+ node_count = run_query("MATCH (n) RETURN count(n) as count")[0]['count']
74
+ rel_count = run_query("MATCH ()-[r]->() RETURN count(r) as count")[0]['count']
75
+ concept_count = run_query("MATCH (n:Resource) RETURN count(n) as count")[0]['count']
76
+
77
+ col1.metric("Nodi Totali", node_count)
78
+ col2.metric("Relazioni Attive", rel_count)
79
+ col3.metric("Concetti Semantici", concept_count)
80
+ except Exception as e:
81
+ st.error(f"Errore connessione Neo4j: {e}")
82
+ st.stop()
83
+
84
+ # --- UI: TAB DI NAVIGAZIONE ---
85
+ tab1, tab2 = st.tabs(["🔍 Validazione (Active Learning)", "🕸️ Visualizzazione Grafo"])
86
+
87
+ # --- TAB 1: CURATION TABLE (Aggiornato con Lineage e Delete Reale) ---
88
+ with tab1:
89
+ st.subheader("Curation & Feedback Loop")
90
+ st.info("Qui l'esperto valida le ipotesi dell'IA. Le cancellazioni addestrano il modello futuro.")
91
+
92
+ # Query aggiornata: Recupera anche 'r.source' (Lineage)
93
+ triples_data = run_query("""
94
+ MATCH (s)-[r]->(o)
95
+ RETURN elementId(r) as id, s.label as Soggetto, type(r) as Predicato, o.label as Oggetto, r.confidence as Confidenza, r.source as Fonte
96
+ ORDER BY r.confidence ASC LIMIT 50
97
+ """)
98
+
99
+ if triples_data:
100
+ df = pd.DataFrame(triples_data)
101
+
102
+ # Selezione Riga
103
+ selection = st.dataframe(
104
+ df.drop(columns=["id"]),
105
+ width='stretch',
106
+ hide_index=True,
107
+ selection_mode="single-row",
108
+ on_select="rerun"
109
+ )
110
+
111
+ # Azione di Reject
112
+ if selection.selection.rows:
113
+ idx = selection.selection.rows[0]
114
+ row = df.iloc[idx]
115
+
116
+ st.error(f"Stai per rifiutare: **{row['Soggetto']}** --[{row['Predicato']}]--> **{row['Oggetto']}**")
117
+
118
+ if st.button("🗑️ CONFERMA RIFIUTO (Training Feedback)", type="primary"):
119
+ success = reject_relationship(row['id'], row['Soggetto'], row['Predicato'], row['Oggetto'])
120
+ if success:
121
+ st.success("Relazione eliminata e loggata per il ri-addestramento!")
122
+ st.rerun()
123
+ else:
124
+ st.info("Nessuna relazione da validare o DB vuoto.")
125
+
126
+ # --- TAB 2: GRAPH VISUALIZATION (Mantenuto dalla versione vecchia per la Fisica) ---
127
+ with tab2:
128
+ st.subheader("Esplorazione Topologica")
129
+
130
+ # Manteniamo la checkbox della fisica (utile per grafi grandi)
131
+ physics = st.checkbox("Abilita Fisica (Gravità)", value=True)
132
+
133
+ net = Network(height="600px", width="100%", bgcolor="#222222", font_color="white", notebook=False)
134
+
135
+ # Carichiamo i dati (Max 100 relazioni)
136
+ graph_data = run_query("MATCH (s)-[r]->(o) RETURN s.label as src, type(r) as rel, o.label as dst LIMIT 100")
137
+
138
+ if graph_data:
139
+ for item in graph_data:
140
+ # Colori personalizzati come nel vecchio file
141
+ net.add_node(item['src'], label=item['src'], color="#4facfe")
142
+ net.add_node(item['dst'], label=item['dst'], color="#00f2fe")
143
+ net.add_edge(item['src'], item['dst'], title=item['rel'], label=item['rel'])
144
+
145
+ # Applichiamo la fisica se selezionata
146
+ net.toggle_physics(physics)
147
+
148
+ try:
149
+ path = "tmp_graph.html"
150
+ net.save_graph(path)
151
+ with open(path, 'r', encoding='utf-8') as f:
152
+ html_string = f.read()
153
+ components.html(html_string, height=600, scrolling=True)
154
+ except Exception as e:
155
+ st.error(f"Errore generazione grafo: {e}")
156
+ else:
157
+ st.write("Grafo vuoto.")
158
+
159
+ # Footer
160
+ st.markdown("---")
161
+ st.caption("Activa Digital | Next Gen Tech | Prototipo v0.2 (Feedback Loop Enabled)")
data/gold_standard/examples.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "text": "Il Menhir di Canne, situato lungo la strada provinciale, è un monolite calcareo che fungeva da segnacolo funerario o confine territoriale in epoca pre-romana.",
4
+ "triples": [
5
+ {"subject": "Menhir di Canne", "predicate": "rdf:type", "object": "xchh:HeritageObject", "confidence": 1.0},
6
+ {"subject": "Menhir di Canne", "predicate": "crm:P45_consists_of", "object": "Calcare", "confidence": 1.0},
7
+ {"subject": "Menhir di Canne", "predicate": "crm:P2_has_type", "object": "Segnacolo funerario", "confidence": 0.9},
8
+ {"subject": "Menhir di Canne", "predicate": "crm:P53_has_former_or_current_location", "object": "Strada Provinciale", "confidence": 1.0}
9
+ ]
10
+ },
11
+ {
12
+ "text": "La Battaglia di Canne del 216 a.C. vide la vittoria dell'esercito cartaginese guidato da Annibale contro le legioni romane.",
13
+ "triples": [
14
+ {"subject": "Battaglia di Canne", "predicate": "rdf:type", "object": "xchh:HistoricalEvent", "confidence": 1.0},
15
+ {"subject": "Battaglia di Canne", "predicate": "crm:P4_has_time-span", "object": "216 a.C.", "confidence": 1.0},
16
+ {"subject": "Battaglia di Canne", "predicate": "crm:P11_had_participant", "object": "Esercito Cartaginese", "confidence": 1.0},
17
+ {"subject": "Annibale", "predicate": "crm:P14_carried_out_by", "object": "Esercito Cartaginese", "confidence": 0.95}
18
+ ]
19
+ },
20
+ {
21
+ "text": "L'Antiquarium custodisce un prezioso corredo funerario proveniente dalla necropoli dauna, inclusi vasi a figure rosse.",
22
+ "triples": [
23
+ {"subject": "Antiquarium", "predicate": "rdf:type", "object": "xchh:Place", "confidence": 1.0},
24
+ {"subject": "Corredo funerario", "predicate": "crm:P55_has_current_location", "object": "Antiquarium", "confidence": 1.0},
25
+ {"subject": "Corredo funerario", "predicate": "crm:P108i_was_produced_by", "object": "Cultura Dauna", "confidence": 0.9},
26
+ {"subject": "Vasi a figure rosse", "predicate": "crm:P46_is_composed_of", "object": "Corredo funerario", "confidence": 1.0}
27
+ ]
28
+ },
29
+ {
30
+ "text": "Il visitatore, avvicinandosi al totem multimediale, attiva l'esperienza di Realtà Aumentata che mostra la ricostruzione della cittadella medievale.",
31
+ "triples": [
32
+ {"subject": "Visitatore", "predicate": "rdf:type", "object": "xcha:Agent", "confidence": 1.0},
33
+ {"subject": "Esperienza AR", "predicate": "rdf:type", "object": "xche:ExperienceSession", "confidence": 1.0},
34
+ {"subject": "Visitatore", "predicate": "xch:activates", "object": "Esperienza AR", "confidence": 1.0},
35
+ {"subject": "Esperienza AR", "predicate": "xch:visualizes", "object": "Cittadella Medievale", "confidence": 1.0}
36
+ ]
37
+ },
38
+ {
39
+ "text": "I resti della Domus Apula testimoniano l'organizzazione abitativa romana. Le mura sono realizzate in opera reticolata.",
40
+ "triples": [
41
+ {"subject": "Domus Apula", "predicate": "rdf:type", "object": "xchh:Site", "confidence": 1.0},
42
+ {"subject": "Domus Apula", "predicate": "crm:P2_has_type", "object": "Abitazione Romana", "confidence": 0.9},
43
+ {"subject": "Mura", "predicate": "crm:P46_forms_part_of", "object": "Domus Apula", "confidence": 1.0},
44
+ {"subject": "Mura", "predicate": "crm:P32_used_general_technique", "object": "Opera Reticolata", "confidence": 1.0}
45
+ ]
46
+ },
47
+ {
48
+ "text": "L'Agente Cognitivo ha inferito con una confidenza del 90% che il frammento ceramico appartiene al periodo tardo-antico.",
49
+ "triples": [
50
+ {"subject": "Agente Cognitivo", "predicate": "rdf:type", "object": "xcha:ArtificialAgent", "confidence": 1.0},
51
+ {"subject": "Frammento ceramico", "predicate": "xch:hasInferredPeriod", "object": "Periodo Tardo-Antico", "confidence": 0.9},
52
+ {"subject": "Inferenza", "predicate": "prov:wasGeneratedBy", "object": "Agente Cognitivo", "confidence": 1.0}
53
+ ]
54
+ },
55
+ {
56
+ "text": "Il progetto Canusium xCH mira a creare un'eterotopia digitale per la valorizzazione del patrimonio culturale della provincia BAT.",
57
+ "triples": [
58
+ {"subject": "Canusium xCH", "predicate": "rdf:type", "object": "xch:Project", "confidence": 1.0},
59
+ {"subject": "Canusium xCH", "predicate": "xch:targetsDomain", "object": "Patrimonio Culturale", "confidence": 1.0},
60
+ {"subject": "Provincia BAT", "predicate": "crm:P89_falls_within", "object": "Puglia", "confidence": 1.0}
61
+ ]
62
+ }
63
+ ]
data/processed/chunks_debug.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ --- CHUNK 0 ---
2
+ Il Menhir di Canne della Battaglia rappresenta uno dei punti di ancoraggio simbolici e spaziali più densi del Parco Archeologico. Isolato ma al centro di un paesaggio carico di memoria, il monolite diventa un nodo di connessione tra materia e contesto. L'obiettivo del progetto Canusium xCH non è la mera restituzione digitale dell'oggetto, ma la costruzione di una soglia esperienziale.
3
+
4
+ --- CHUNK 1 ---
5
+ L'esperienza comincia nell'approccio fisico al luogo. Avvicinandosi al Menhir, l'utente viene riconosciuto dal sistema tramite geo-anchoring e riceve sul proprio dispositivo un invito discreto ad attivare la modalità immersiva. La sovrapposizione digitale appare come una finestra trasparente che mantiene visibile il paesaggio, mentre introduce il modello 3D calibrato.
6
+
data/raw/menhir_test.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Il Menhir di Canne della Battaglia rappresenta uno dei punti di ancoraggio simbolici e spaziali più densi del Parco Archeologico.
2
+ Isolato ma al centro di un paesaggio carico di memoria, il monolite diventa un nodo di connessione tra materia e contesto.
3
+ L'obiettivo del progetto Canusium xCH non è la mera restituzione digitale dell'oggetto, ma la costruzione di una soglia esperienziale.
4
+ L'esperienza comincia nell'approccio fisico al luogo. Avvicinandosi al Menhir, l'utente viene riconosciuto dal sistema tramite geo-anchoring e riceve sul proprio dispositivo un invito discreto ad attivare la modalità immersiva.
5
+ La sovrapposizione digitale appare come una finestra trasparente che mantiene visibile il paesaggio, mentre introduce il modello 3D calibrato.
data/raw/venezia_arte.doc ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Il Palazzo Ducale, capolavoro dell'arte gotica, sorge in Piazza San Marco a Venezia.
2
+ Antica sede del Doge e delle magistrature veneziane, è il simbolo della potenza della Serenissima.
3
+ Fondato nel IX secolo, l'edificio ha subito numerose ristrutturazioni a causa di incendi devastanti.
4
+ La struttura attuale è il risultato dei lavori iniziati nel 1340.
5
+
6
+ All'interno del palazzo si possono ammirare opere di inestimabile valore.
7
+ La Sala del Maggior Consiglio ospita "Il Paradiso", una tela monumentale dipinta da Jacopo Tintoretto e dalla sua bottega tra il 1588 e il 1592.
8
+ Questa sala era il cuore politico della Città Lagunare, dove si riunivano i nobili per prendere decisioni di stato.
9
+ Un altro protagonista della decorazione interna è Paolo Veronese, che ha realizzato lo splendido soffitto della Sala del Collegio.
10
+
11
+ Collegato al Palazzo Ducale tramite il celebre Ponte dei Sospiri, si trova il palazzo delle Prigioni Nuove.
12
+ Il ponte, costruito nel 1600 in stile barocco, attraversa il Rio di Palazzo ed era attraversato dai condannati.
13
+ Venezia continua ad attrarre milioni di visitatori che rimangono incantati dalla sua storia millenaria e dalla sua architettura unica al mondo.
src/extraction/__pycache__/extractor.cpython-312.pyc ADDED
Binary file (9.75 kB). View file
 
src/extraction/extractor.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import numpy as np
4
+ from typing import List, Optional
5
+ from pydantic import BaseModel, Field, ValidationError
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
8
+ from langchain_ollama import ChatOllama
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
+ from sklearn.metrics.pairwise import cosine_similarity
11
+
12
+ # --- 1. DEFINIZIONE DELLO SCHEMA ---
13
+ class GraphTriple(BaseModel):
14
+ subject: str = Field(..., description="Entità sorgente (Canonical).")
15
+ predicate: str = Field(..., description="Relazione (snake_case).")
16
+ object: str = Field(..., description="Entità target.")
17
+ confidence: float = Field(..., description="Confidenza (0.0 - 1.0).")
18
+ source: Optional[str] = Field(None, description="ID del documento o chunk.")
19
+
20
+ class KnowledgeGraphExtraction(BaseModel):
21
+ reasoning: Optional[str] = Field(None, description="Breve ragionamento logico.")
22
+ triples: List[GraphTriple]
23
+
24
+ # --- 2. ESTRATTORE DINAMICO (Dynamic Few-Shot) ---
25
+ class NeuroSymbolicExtractor:
26
+ def __init__(self, model_name="llama3", temperature=0, gold_standard_path=None):
27
+ print(f"🦙 Inizializzazione Local LLM: {model_name}...")
28
+
29
+ # 1. LLM per l'inferenza
30
+ self.llm = ChatOllama(
31
+ model=model_name,
32
+ temperature=temperature,
33
+ format="json",
34
+ base_url="http://localhost:11434"
35
+ )
36
+
37
+ # 2. Modello Embedding per la selezione dinamica
38
+ print("🧠 Caricamento modello embedding per Dynamic Selection...")
39
+ # Nota: Usiamo lo stesso modello dello splitter per coerenza
40
+ self.embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
41
+
42
+ # 3. Caricamento e Indicizzazione Gold Standard
43
+ self.examples = []
44
+ self.example_embeddings = None
45
+
46
+ if gold_standard_path and os.path.exists(gold_standard_path):
47
+ print(f"🌟 Indicizzazione vettoriale Gold Standard da: {gold_standard_path}")
48
+ self._index_examples(gold_standard_path)
49
+ else:
50
+ print("⚠️ Nessun Gold Standard trovato. Modalità Zero-Shot.")
51
+
52
+ # Template Specializzato per Canusium xCH (CIDOC-CRM + Ontology Layers)
53
+ self.system_template_base = """Sei l'Agente Cognitivo (AC) del sistema Canusium xCH.
54
+ Il tuo compito è trasformare il testo non strutturato in un Digital Twin Graph (RDF).
55
+
56
+ SCHEMA JSON RICHIESTO:
57
+ {{
58
+ "reasoning": "Spiega brevemente perché hai scelto queste classi/relazioni...",
59
+ "triples": [
60
+ {{"subject": "Entità", "predicate": "prefix:Relazione", "object": "Entità", "confidence": 0.95}}
61
+ ]
62
+ }}
63
+
64
+ ONTOLOGIA DI RIFERIMENTO (Usa questi prefissi):
65
+ - xchh: (Heritage) -> Per oggetti fisici, siti, reperti (es. xchh:HeritageObject, xchh:Site).
66
+ - crm: (CIDOC-CRM) -> Per relazioni standard (es. crm:P55_has_current_location, crm:P4_has_time-span).
67
+ - xche: (Experience) -> Per sessioni AR/VR, visitatori, interazioni (es. xche:ExperienceSession).
68
+ - xcha: (Agents) -> Per agenti umani o artificiali.
69
+ - skos: -> Per concetti generici o gerarchie.
70
+
71
+ ESEMPI CONTESTUALI (Dynamic Few-Shot):
72
+ {selected_examples}
73
+
74
+ REGOLE DI CONFIDENZA (Trust Layer):
75
+ - 1.0 (Fatto Curato): Informazione esplicita e certa nel testo.
76
+ - 0.8 - 0.9 (Inferenza): Deduzione logica forte ma non esplicita.
77
+ - < 0.7 (Ipotesi): Associazione probabile ma incerta (da marcare per revisione umana).
78
+
79
+ Canonicalizza i nomi (es. "Il Parco" -> "Parco Archeologico di Canne").
80
+ """
81
+
82
+ def _index_examples(self, path: str):
83
+ """Carica il JSON e calcola i vettori per ogni esempio."""
84
+ try:
85
+ with open(path, 'r', encoding='utf-8') as f:
86
+ self.examples = json.load(f)
87
+
88
+ # Estraiamo solo il testo di input per calcolare l'embedding
89
+ texts = [ex['text'] for ex in self.examples]
90
+ self.example_embeddings = self.embedding_model.embed_documents(texts)
91
+ print(f"✅ Indicizzati {len(self.examples)} esempi di Gold Standard.")
92
+ except Exception as e:
93
+ print(f"❌ Errore indicizzazione Gold Standard: {e}")
94
+ self.examples = []
95
+
96
+ def _get_relevant_examples(self, query_text: str, k=2) -> str:
97
+ """
98
+ Trova i k esempi più simili semanticamente al chunk attuale.
99
+ """
100
+ if not self.examples or self.example_embeddings is None:
101
+ return "Nessun esempio disponibile."
102
+
103
+ # 1. Embed del chunk attuale
104
+ query_embedding = self.embedding_model.embed_query(query_text)
105
+
106
+ # 2. Calcolo similarità coseno
107
+ similarities = cosine_similarity([query_embedding], self.example_embeddings)[0]
108
+
109
+ # 3. Selezione dei top-k
110
+ top_k_indices = np.argsort(similarities)[-k:][::-1]
111
+
112
+ formatted_text = ""
113
+ for i, idx in enumerate(top_k_indices):
114
+ ex = self.examples[idx]
115
+ sim_score = similarities[idx]
116
+ formatted_text += f"\n--- ESEMPIO RILEVANTE #{i+1} (Sim: {sim_score:.2f}) ---\n"
117
+ formatted_text += f"INPUT: {ex['text']}\n"
118
+ formatted_text += f"OUTPUT: {json.dumps({'triples': ex['triples']}, ensure_ascii=False)}\n"
119
+
120
+ return formatted_text
121
+
122
+ def extract(self, text_chunk: str, source_id: str = "unknown", max_retries=3) -> KnowledgeGraphExtraction:
123
+ print(f"🧠 Processing {source_id} con Llama 3 (Dynamic Mode)...")
124
+
125
+ # --- FASE DINAMICA: Selezione Esempi ---
126
+ relevant_examples_str = self._get_relevant_examples(text_chunk, k=2)
127
+
128
+ # Costruzione Prompt Finale (usando .format per iniettare gli esempi scelti)
129
+ final_sys_text = self.system_template_base.format(selected_examples=relevant_examples_str)
130
+
131
+ # Creazione del SystemMessage 'raw' per evitare problemi di parsing delle graffe
132
+ sys_msg = SystemMessage(content=final_sys_text)
133
+
134
+ prompt = ChatPromptTemplate.from_messages([
135
+ sys_msg,
136
+ ("human", "{text}")
137
+ ])
138
+
139
+ chain = prompt | self.llm
140
+
141
+ for attempt in range(max_retries):
142
+ try:
143
+ response = chain.invoke({"text": text_chunk})
144
+ data = json.loads(response.content)
145
+
146
+ # Normalizzazione output
147
+ if isinstance(data, list):
148
+ validated_data = KnowledgeGraphExtraction(triples=data, reasoning="Direct list output")
149
+ else:
150
+ validated_data = KnowledgeGraphExtraction(**data)
151
+
152
+ for t in validated_data.triples:
153
+ t.source = source_id
154
+
155
+ return validated_data
156
+
157
+ except (json.JSONDecodeError, ValidationError) as e:
158
+ print(f"⚠️ Errore Validazione (Tentativo {attempt+1}/{max_retries}): {e}")
159
+
160
+ # SELF-CORRECTION LOOP (Mantenuto dalla tua versione robusta)
161
+ correction_prompt = ChatPromptTemplate.from_messages([
162
+ sys_msg,
163
+ HumanMessage(content=text_chunk),
164
+ AIMessage(content=response.content), # La risposta sbagliata
165
+ HumanMessage(content=f"Errore nel JSON precedente: {e}. Correggi e restituisci SOLO JSON valido.")
166
+ ])
167
+
168
+ chain = correction_prompt | self.llm
169
+
170
+ except Exception as e:
171
+ print(f"❌ Errore critico: {e}")
172
+ break
173
+
174
+ return KnowledgeGraphExtraction(triples=[])
175
+
176
+ # --- TEST ---
177
+ if __name__ == "__main__":
178
+ # Testiamo se seleziona l'esempio giusto
179
+ chunk_arte = "Il dipinto mostra una tecnica a olio sopraffina."
180
+ chunk_storia = "Il senato elesse il nuovo capo di stato nel 1200."
181
+
182
+ # Nota: Assicurati che il percorso del file JSON sia corretto
183
+ extractor = NeuroSymbolicExtractor(gold_standard_path="data/gold_standard/examples.json")
184
+
185
+ print("\n--- TEST SELEZIONE DINAMICA (ARTE) ---")
186
+ # Dovrebbe pescare l'esempio della Primavera o Restauro
187
+ print(extractor._get_relevant_examples(chunk_arte, k=1))
188
+
189
+ print("\n--- TEST SELEZIONE DINAMICA (STORIA/POLITICA) ---")
190
+ # Dovrebbe pescare l'esempio del Doge o Colosseo
191
+ print(extractor._get_relevant_examples(chunk_storia, k=1))
src/graph/__pycache__/entity_resolver.cpython-312.pyc ADDED
Binary file (4.37 kB). View file
 
src/graph/__pycache__/graph_loader.cpython-312.pyc ADDED
Binary file (5.38 kB). View file
 
src/graph/entity_resolver.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.cluster import DBSCAN
3
+ from langchain_huggingface import HuggingFaceEmbeddings
4
+ from collections import Counter
5
+
6
+ class EntityResolver:
7
+ def __init__(self, model_name="all-MiniLM-L6-v2", similarity_threshold=0.85):
8
+ """
9
+ Inizializza il modello per il calcolo delle similarità.
10
+ similarity_threshold: quanto devono essere vicini i vettori (0-1).
11
+ Convertito in 'eps' per DBSCAN.
12
+ """
13
+ print("🧩 Inizializzazione Entity Resolver (DBSCAN)...")
14
+ self.embedding_model = HuggingFaceEmbeddings(model_name=model_name)
15
+ # DBSCAN usa la distanza, non la similarità. Distanza = 1 - Similarità.
16
+ # Se threshold è 0.85 (alta similarità), eps deve essere 0.15 (bassa distanza).
17
+ self.eps = 1 - similarity_threshold
18
+
19
+ def resolve_entities(self, triples):
20
+ """
21
+ Prende una lista di triple (GraphTriple) e normalizza i nomi delle entità.
22
+ """
23
+ if not triples:
24
+ return []
25
+
26
+ # 1. Estrazione di tutte le entità uniche (Soggetti e Oggetti)
27
+ all_entities = set()
28
+ for t in triples:
29
+ all_entities.add(t.subject)
30
+ all_entities.add(t.object)
31
+
32
+ unique_entities = list(all_entities)
33
+ print(f" Analisi di {len(unique_entities)} entità uniche per deduplica...")
34
+
35
+ if len(unique_entities) < 2:
36
+ return triples
37
+
38
+ # 2. Calcolo Embeddings
39
+ embeddings = self.embedding_model.embed_documents(unique_entities)
40
+ X = np.array(embeddings)
41
+
42
+ # 3. Clustering DBSCAN
43
+ # metrica='cosine' è fondamentale per vettori semantici
44
+ clustering = DBSCAN(eps=self.eps, min_samples=1, metric='cosine').fit(X)
45
+ labels = clustering.labels_
46
+
47
+ # 4. Creazione Mappa {Variante -> Canonico}
48
+ # Raggruppiamo le entità per Cluster ID
49
+ cluster_map = {}
50
+ for entity, label in zip(unique_entities, labels):
51
+ if label not in cluster_map:
52
+ cluster_map[label] = []
53
+ cluster_map[label].append(entity)
54
+
55
+ # Per ogni cluster, eleggiamo il "Canonico" (es. la stringa più lunga)
56
+ entity_replacement_map = {}
57
+ for label, variants in cluster_map.items():
58
+ if len(variants) > 1:
59
+ # Euristiche di canonicalizzazione:
60
+ # 1. Preferisci quella che inizia con maiuscola
61
+ # 2. Preferisci la più lunga (spesso più descrittiva: "San Marco" vs "Basilica di San Marco")
62
+ canonical = sorted(variants, key=len, reverse=True)[0]
63
+ print(f" ✨ Deduplica: {variants} -> '{canonical}'")
64
+ for v in variants:
65
+ entity_replacement_map[v] = canonical
66
+ else:
67
+ entity_replacement_map[variants[0]] = variants[0]
68
+
69
+ # 5. Riscrittura Triple
70
+ resolved_triples = []
71
+ for t in triples:
72
+ # Sostituiamo soggetto e oggetto con le versioni canoniche
73
+ t.subject = entity_replacement_map.get(t.subject, t.subject)
74
+ t.object = entity_replacement_map.get(t.object, t.object)
75
+ resolved_triples.append(t)
76
+
77
+ return resolved_triples
78
+
79
+ # --- TEST ---
80
+ if __name__ == "__main__":
81
+ from pydantic import BaseModel
82
+ class MockTriple(BaseModel):
83
+ subject: str
84
+ predicate: str
85
+ object: str
86
+
87
+ # Esempio con sinonimi
88
+ raw_triples = [
89
+ MockTriple(subject="Venezia", predicate="ha_monumento", object="Basilica di San Marco"),
90
+ MockTriple(subject="La Serenissima", predicate="situata_in", object="Laguna"), # Venezia = Serenissima
91
+ MockTriple(subject="S. Marco", predicate="stile", object="Bizantino") # S. Marco = Basilica di San Marco
92
+ ]
93
+
94
+ resolver = EntityResolver(similarity_threshold=0.6) # Soglia bassa per il test
95
+ clean_triples = resolver.resolve_entities(raw_triples)
96
+
97
+ print("\n--- RISULTATO ---")
98
+ for t in clean_triples:
99
+ print(f"{t.subject} --[{t.predicate}]--> {t.object}")
src/graph/graph_loader.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from neo4j import GraphDatabase
3
+ from dotenv import load_dotenv
4
+
5
+ # Carica variabili d'ambiente
6
+ load_dotenv()
7
+
8
+ class KnowledgeGraphPersister:
9
+ def __init__(self):
10
+ """
11
+ Inizializza il driver Neo4j usando le variabili d'ambiente per sicurezza.
12
+ """
13
+ uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
14
+ user = os.getenv("NEO4J_USER", "neo4j")
15
+ password = os.getenv("NEO4J_PASSWORD", "activa_semantic_lab")
16
+
17
+ try:
18
+ self.driver = GraphDatabase.driver(uri, auth=(user, password))
19
+ self.driver.verify_connectivity()
20
+ print(f"✅ Connesso a Neo4j ({uri}) successfully.")
21
+ except Exception as e:
22
+ print(f"❌ Errore critico connessione Neo4j: {e}")
23
+ self.driver = None
24
+
25
+ def close(self):
26
+ if self.driver:
27
+ self.driver.close()
28
+
29
+ def sanitize_name(self, name):
30
+ """
31
+ Normalizza i nomi per creare URI coerenti (Canonicalization base).
32
+ """
33
+ if not name: return "Unknown"
34
+ # Rimuove caratteri speciali e spazi extra, mantiene coerenza maiuscole/minuscole
35
+ return name.strip().replace(" ", "_").replace("'", "").replace('"', "")
36
+
37
+ def save_triples(self, triples):
38
+ """
39
+ Salva le triple in BATCH (ottimizzazione performance).
40
+ Usa UNWIND per processare liste di dati in un'unica transazione.
41
+ """
42
+ if not self.driver:
43
+ print("⚠️ Driver non connesso. Impossibile salvare.")
44
+ return
45
+
46
+ if not triples:
47
+ return
48
+
49
+ print(f"💾 Salvataggio BATCH di {len(triples)} triple su Neo4j...")
50
+
51
+ # 1. Prepariamo i dati come lista di dizionari (Payload leggero)
52
+ batch_data = []
53
+ for t in triples:
54
+ batch_data.append({
55
+ "subj_uri": self.sanitize_name(t.subject),
56
+ "subj_label": t.subject,
57
+ "pred": t.predicate, # Nota: Il predicato dinamico richiede attenzione in Cypher
58
+ "obj_uri": self.sanitize_name(t.object),
59
+ "obj_label": t.object,
60
+ "conf": t.confidence,
61
+ "src": t.source
62
+ })
63
+
64
+ # 2. Query Batch Ottimizzata
65
+ # Nota: In Cypher non si può parametrizzare il TIPO di relazione (es. :RELAZIONE).
66
+ # Per performance pura con relazioni dinamiche, usiamo APOC o un approccio ibrido.
67
+ # Qui usiamo un approccio sicuro iterando nel driver ma con transazione unica,
68
+ # oppure raggruppiamo per tipo di relazione.
69
+
70
+ # Approccio Migliore per MVP: Transazione singola
71
+ with self.driver.session() as session:
72
+ try:
73
+ session.execute_write(self._batch_write_tx, batch_data)
74
+ print("✅ Batch completato.")
75
+ except Exception as e:
76
+ print(f"⚠️ Errore durante il salvataggio batch: {e}")
77
+
78
+ @staticmethod
79
+ def _batch_write_tx(tx, batch_data):
80
+ """Funzione transazionale interna."""
81
+ for item in batch_data:
82
+ # Usiamo MERGE per evitare duplicati
83
+ # Usiamo apoc.create.relationship se disponibile per predicati dinamici,
84
+ # altrimenti usiamo string formatting controllata (safe perché interna).
85
+
86
+ # Sanitizzazione predicato per evitare injection (solo caratteri sicuri)
87
+ safe_pred = "".join(x for x in item['pred'] if x.isalnum() or x in "_:")
88
+ if not safe_pred: safe_pred = "RELATED_TO"
89
+
90
+ query = (
91
+ f"MERGE (s:Resource {{uri: $subj_uri}}) "
92
+ f"ON CREATE SET s.label = $subj_label "
93
+ f"MERGE (o:Resource {{uri: $obj_uri}}) "
94
+ f"ON CREATE SET o.label = $obj_label "
95
+ f"MERGE (s)-[r:`{safe_pred}`]->(o) "
96
+ f"SET r.confidence = $conf, r.source = $src"
97
+ )
98
+
99
+ tx.run(query, item)
100
+
101
+ # --- TEST ISOLATO ---
102
+ if __name__ == "__main__":
103
+ # Creiamo un mock per testare senza dipendenze esterne
104
+ from collections import namedtuple
105
+ MockTriple = namedtuple("MockTriple", ["subject", "predicate", "object", "confidence", "source"])
106
+
107
+ triples = [
108
+ MockTriple("Batch Node 1", "TEST_BATCH", "Batch Node 2", 0.99, "test_doc_1"),
109
+ MockTriple("Batch Node 2", "IS_RELATED_TO", "Batch Node 3", 0.85, "test_doc_1")
110
+ ]
111
+
112
+ # Assicurati di avere le variabili d'ambiente o fallback attivi
113
+ persister = KnowledgeGraphPersister()
114
+ persister.save_triples(triples)
115
+ persister.close()
src/ingestion/__pycache__/semantic_splitter.cpython-312.pyc ADDED
Binary file (8.83 kB). View file
 
src/ingestion/semantic_splitter.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
+ from dotenv import load_dotenv
7
+ from langchain_huggingface import HuggingFaceEmbeddings
8
+
9
+ load_dotenv()
10
+
11
+ class ActivaSemanticSplitter:
12
+ def __init__(self, model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", batch_size=32):
13
+ self.batch_size = batch_size
14
+ provider = os.getenv("EMBEDDING_PROVIDER", "huggingface").lower()
15
+
16
+ print(f"🔄 Inizializzazione Embedding Engine (Provider: {provider})...")
17
+
18
+ try:
19
+ if provider == "openai":
20
+ from langchain_openai import OpenAIEmbeddings
21
+ api_key = os.getenv("OPENAI_API_KEY")
22
+ if not api_key:
23
+ raise ValueError("OPENAI_API_KEY mancante nel file .env")
24
+ self.embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
25
+ else:
26
+ self.embedding_model = HuggingFaceEmbeddings(model_name=model_name)
27
+
28
+ print("✅ Modello caricato correttamente.")
29
+
30
+ except Exception as e:
31
+ print(f"❌ Errore caricamento modello: {e}")
32
+ raise e
33
+
34
+ def _split_sentences(self, text):
35
+ """
36
+ Divide il testo in frasi gestendo le abbreviazioni custom (es. sec., S.).
37
+ """
38
+ text = text.strip()
39
+ try:
40
+ import nltk
41
+ # Assicuriamoci che i dati ci siano
42
+ try:
43
+ nltk.data.find('tokenizers/punkt')
44
+ nltk.data.find('tokenizers/punkt_tab')
45
+ except LookupError:
46
+ print("⬇️ Download risorse NLTK...")
47
+ nltk.download('punkt', quiet=True)
48
+ nltk.download('punkt_tab', quiet=True)
49
+
50
+ # FIX: Carichiamo il tokenizer italiano specifico
51
+ # Invece di usare sent_tokenize() che è una black box, carichiamo l'oggetto.
52
+ try:
53
+ tokenizer = nltk.data.load('tokenizers/punkt/italian.pickle')
54
+ except:
55
+ # Fallback se il pickle path non viene risolto automaticamente
56
+ from nltk.tokenize.punkt import PunktSentenceTokenizer
57
+ tokenizer = PunktSentenceTokenizer()
58
+
59
+ # --- LISTA ECCEZIONI ABBREVIAZIONI ---
60
+ # Diciamo al tokenizer che queste parole seguite da punto NON chiudono la frase
61
+ custom_abbrevs = ['sec', 's', 'prof', 'dott', 'avv', 'pag', 'fig', 'nr', 'art']
62
+ for abbr in custom_abbrevs:
63
+ tokenizer._params.abbrev_types.add(abbr)
64
+
65
+ sentences = tokenizer.tokenize(text)
66
+
67
+ except ImportError:
68
+ print("⚠️ NLTK non installato. Fallback su Regex semplice.")
69
+ sentences = re.split(r'(?<=[.?!])\s+', text)
70
+ except Exception as e:
71
+ print(f"⚠️ Errore NLTK ({e}). Fallback su Regex.")
72
+ sentences = re.split(r'(?<=[.?!])\s+', text)
73
+
74
+ return [s.strip() for s in sentences if len(s.strip()) > 5]
75
+
76
+ def combine_sentences(self, sentences, buffer_size=1):
77
+ combined = []
78
+ for i in range(len(sentences)):
79
+ start = max(0, i - buffer_size)
80
+ end = min(len(sentences), i + 1 + buffer_size)
81
+ combined_context = " ".join(sentences[start:end])
82
+ combined.append(combined_context)
83
+ return combined
84
+
85
+ def calculate_cosine_distances(self, sentences):
86
+ embeddings = []
87
+ total = len(sentences)
88
+
89
+ for i in range(0, total, self.batch_size):
90
+ batch = sentences[i : i + self.batch_size]
91
+ batch_embeddings = self.embedding_model.embed_documents(batch)
92
+ embeddings.extend(batch_embeddings)
93
+
94
+ distances = []
95
+ for i in range(len(embeddings) - 1):
96
+ similarity = cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0]
97
+ distances.append(similarity)
98
+
99
+ return distances, embeddings
100
+
101
+ def create_chunks(self, text, percentile_threshold=95):
102
+ single_sentences = self._split_sentences(text)
103
+ if not single_sentences:
104
+ return [], [], 0
105
+
106
+ combined_sentences = self.combine_sentences(single_sentences)
107
+ distances, _ = self.calculate_cosine_distances(combined_sentences)
108
+
109
+ if not distances:
110
+ return [text], [], 0
111
+
112
+ threshold = np.percentile(distances, 100 - percentile_threshold)
113
+ indices_above_thresh = [i for i, x in enumerate(distances) if x < threshold]
114
+
115
+ chunks = []
116
+ start_index = 0
117
+ breakpoints = indices_above_thresh + [len(single_sentences)]
118
+
119
+ for i in breakpoints:
120
+ end_index = i + 1
121
+ chunk_text = " ".join(single_sentences[start_index:end_index])
122
+ if len(chunk_text) > 20:
123
+ chunks.append(chunk_text)
124
+ start_index = end_index
125
+
126
+ return chunks, distances, threshold
127
+
128
+ def plot_similarity(self, distances, threshold, filename="chunking_analysis.png"):
129
+ try:
130
+ plt.figure(figsize=(10, 6))
131
+ plt.plot(distances, label="Cosine Similarity")
132
+ plt.axhline(y=threshold, color='r', linestyle='--', label=f"Threshold")
133
+ plt.title("Analisi della Coerenza Vettoriale")
134
+ plt.xlabel("Frase")
135
+ plt.ylabel("Similarità")
136
+ plt.legend()
137
+ plt.savefig(filename)
138
+ print(f"📊 Grafico salvato: {filename}")
139
+ plt.close()
140
+ except Exception:
141
+ pass
142
+
143
+ # --- TEST ---
144
+ if __name__ == "__main__":
145
+ sample_text = """
146
+ La Basilica di S. Marco a Venezia è un'opera d'arte unica.
147
+ Risale al sec. XI e rappresenta lo stile bizantino.
148
+ L'interno è ricco di mosaici.
149
+
150
+ Tuttavia, cambiando argomento, la cucina veneziana offre piatti come le sarde in saor.
151
+ È un piatto a base di cipolle e aceto.
152
+ """
153
+
154
+ splitter = ActivaSemanticSplitter()
155
+ # Soglia molto bassa (10) per FORZARE lo split solo sul cambio drastico di argomento
156
+ chunks, dists, thresh = splitter.create_chunks(sample_text, percentile_threshold=50)
157
+
158
+ print(f"\n--- TEST FIX ABBREVIAZIONI ---")
159
+ print(f"Input: {len(sample_text)} chars")
160
+
161
+ # Debug delle frasi grezze riconosciute
162
+ sentences = splitter._split_sentences(sample_text)
163
+ print(f"Frasi riconosciute ({len(sentences)}):")
164
+ for s in sentences:
165
+ print(f" - {s}")
166
+
167
+ print(f"\n--- CHUNK GENERATI ---")
168
+ for i, c in enumerate(chunks):
169
+ print(f"🔹 Chunk {i+1}: {c}")
src/validation/__pycache__/validator.cpython-312.pyc ADDED
Binary file (4.73 kB). View file
 
src/validation/shapes/schema_constraints.ttl ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
2
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
3
+ @prefix ex: <http://activa.ai/ontology/> .
4
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
6
+
7
+ # REGOLA GENERALE PER TUTTI I CONCETTI
8
+ ex:ConceptShape
9
+ a sh:NodeShape ;
10
+ sh:targetClass skos:Concept ;
11
+
12
+ # 1. Obbligo di Label (Accetta qualsiasi Literal con lingua)
13
+ sh:property [
14
+ sh:path skos:prefLabel ;
15
+ sh:minCount 1 ;
16
+ sh:nodeKind sh:Literal ;
17
+ sh:message "Ogni concetto deve avere una label."
18
+ ] ;
19
+
20
+ # 2. Relazione: Related
21
+ sh:property [
22
+ sh:path skos:related ;
23
+ sh:class skos:Concept ;
24
+ sh:message "La relazione 'related' deve puntare a un nodo di tipo Concept."
25
+ ] ;
26
+
27
+ # 3. Relazione: Situato In
28
+ sh:property [
29
+ sh:path ex:situato_in ;
30
+ sh:class skos:Concept
31
+ ] ;
32
+
33
+ # 4. Relazione: Broader
34
+ sh:property [
35
+ sh:path skos:broader ;
36
+ sh:class skos:Concept
37
+ ] .
src/validation/validator.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from rdflib import Graph, Literal, RDF, URIRef, Namespace
3
+ from rdflib.namespace import SKOS, XSD
4
+ from pyshacl import validate
5
+
6
+ class SemanticValidator:
7
+ def __init__(self):
8
+ # Definiamo i namespace
9
+ self.EX = Namespace("http://activa.ai/ontology/")
10
+ self.shapes_file = os.path.join(os.path.dirname(__file__), "shapes/schema_constraints.ttl")
11
+
12
+ # Carica le shapes se il file esiste, altrimenti usa grafo vuoto
13
+ if os.path.exists(self.shapes_file):
14
+ self.shacl_graph = Graph()
15
+ self.shacl_graph.parse(self.shapes_file, format="turtle")
16
+ print("🛡️ SHACL Constraints caricati.")
17
+ else:
18
+ print("⚠️ File SHACL non trovato. Validazione disabilitata.")
19
+ self.shacl_graph = None
20
+
21
+ def _json_to_rdf(self, triples):
22
+ """Converte le triple JSON (Pydantic) in un grafo RDFLib in memoria."""
23
+ g = Graph()
24
+ g.bind("skos", SKOS)
25
+ g.bind("ex", self.EX)
26
+
27
+ for t in triples:
28
+ # Creiamo URI sanitizzati
29
+ subj_uri = URIRef(self.EX[t.subject.replace(" ", "_")])
30
+ obj_uri = URIRef(self.EX[t.object.replace(" ", "_")])
31
+
32
+ # Aggiungiamo il tipo Concept
33
+ g.add((subj_uri, RDF.type, SKOS.Concept))
34
+ g.add((subj_uri, SKOS.prefLabel, Literal(t.subject, lang="it")))
35
+
36
+ g.add((obj_uri, RDF.type, SKOS.Concept))
37
+ g.add((obj_uri, SKOS.prefLabel, Literal(t.object, lang="it")))
38
+
39
+ # Mappiamo il predicato (se è standard o custom)
40
+ if t.predicate == "skos:related" or t.predicate == "related":
41
+ pred = SKOS.related
42
+ elif t.predicate == "skos:broader" or t.predicate == "broader":
43
+ pred = SKOS.broader
44
+ else:
45
+ # Fallback su namespace custom per predicati non standard (es. situato_in)
46
+ pred = self.EX[t.predicate]
47
+
48
+ g.add((subj_uri, pred, obj_uri))
49
+
50
+ return g
51
+
52
+ def validate_batch(self, triples):
53
+ """
54
+ Esegue la validazione SHACL sulle triple.
55
+ Ritorna (is_valid, report_text, rdf_graph)
56
+ """
57
+ if not self.shacl_graph:
58
+ return True, "No Constraints", None
59
+
60
+ data_graph = self._json_to_rdf(triples)
61
+
62
+ print("🔍 Esecuzione Validazione SHACL...")
63
+ conforms, report_graph, report_text = validate(
64
+ data_graph,
65
+ shacl_graph=self.shacl_graph,
66
+ inference='rdfs',
67
+ serialize_report_graph=True
68
+ )
69
+
70
+ return conforms, report_text, data_graph
71
+
72
+ # --- TEST DEL MODULO ---
73
+ if __name__ == "__main__":
74
+ # Simuliamo triple dall'LLM
75
+ from collections import namedtuple
76
+ Triple = namedtuple("Triple", ["subject", "predicate", "object", "confidence"])
77
+
78
+ # Caso Test: Una tripla valida e una (potenzialmente) invalida
79
+ mock_triples = [
80
+ Triple("Basilica San Marco", "situato_in", "Venezia", 0.9),
81
+ Triple("Venezia", "skos:related", "Laguna", 0.95)
82
+ ]
83
+
84
+ validator = SemanticValidator()
85
+ is_valid, report, _ = validator.validate_batch(mock_triples)
86
+
87
+ if is_valid:
88
+ print("✅ Dati Conformi allo Schema SHACL.")
89
+ else:
90
+ print("❌ Violazione dei vincoli rilevata!")
91
+ print(report)