GaetanoParente commited on
Commit
2e93420
·
1 Parent(s): a551bb6

irrobustimento del processo semantico

Browse files
api.py CHANGED
@@ -24,8 +24,8 @@ class DiscoveryRequest(BaseModel):
24
  # Carico i pesi dei modelli all'avvio del server (Warm-up)
25
  print("⏳ Inizializzazione modelli (SentenceTransformers e Llama3)...")
26
  splitter = ActivaSemanticSplitter(model_name="all-MiniLM-L6-v2")
27
- gold_path = os.path.join("data", "gold_standard", "examples.json")
28
- extractor = NeuroSymbolicExtractor(model_name="llama3", gold_standard_path=gold_path)
29
  persister = KnowledgeGraphPersister()
30
  resolver = EntityResolver(neo4j_driver=persister.driver, similarity_threshold=0.85)
31
  validator = SemanticValidator()
 
24
  # Carico i pesi dei modelli all'avvio del server (Warm-up)
25
  print("⏳ Inizializzazione modelli (SentenceTransformers e Llama3)...")
26
  splitter = ActivaSemanticSplitter(model_name="all-MiniLM-L6-v2")
27
+ schema_path = os.path.join("data", "schemas", "ARCO_schema.json")
28
+ extractor = NeuroSymbolicExtractor(model_name="llama3", schema_path=schema_path)
29
  persister = KnowledgeGraphPersister()
30
  resolver = EntityResolver(neo4j_driver=persister.driver, similarity_threshold=0.85)
31
  validator = SemanticValidator()
app.py CHANGED
@@ -54,8 +54,8 @@ def get_splitter():
54
 
55
  @st.cache_resource
56
  def get_extractor():
57
- gold_path = os.path.join("data", "gold_standard", "examples.json")
58
- return NeuroSymbolicExtractor(model_name="llama3", gold_standard_path=gold_path)
59
 
60
  @st.cache_resource(show_spinner="🧩 Inizializzazione Entity Resolver...")
61
  def get_resolver():
 
54
 
55
  @st.cache_resource
56
  def get_extractor():
57
+ schema_path = os.path.join("data", "schemas", "ARCO_schema.json")
58
+ return NeuroSymbolicExtractor(model_name="llama3", schema_path=schema_path)
59
 
60
  @st.cache_resource(show_spinner="🧩 Inizializzazione Entity Resolver...")
61
  def get_resolver():
data/ontologie_raw/ARCO/arco.owl ADDED
The diff for this file is too large to render. See raw diff
 
data/ontologie_raw/ARCO/context-description.owl ADDED
The diff for this file is too large to render. See raw diff
 
data/ontologie_raw/ARCO/core.owl ADDED
The diff for this file is too large to render. See raw diff
 
data/ontologie_raw/ARCO/location.owl ADDED
The diff for this file is too large to render. See raw diff
 
data/schemas/ARCO_schema.json ADDED
The diff for this file is too large to render. See raw diff
 
data/schemas/arco_schema.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "arco:CulturalProperty",
4
+ "type": "Class",
5
+ "description": "Qualsiasi bene culturale, materiale o immateriale. Include monumenti, reperti archeologici, statue, dipinti, edifici storici, strade antiche come la Via Appia."
6
+ },
7
+ {
8
+ "id": "cis:CulturalInstituteOrSite",
9
+ "type": "Class",
10
+ "description": "Un istituto o luogo della cultura. Include musei, archivi, biblioteche, parchi archeologici, complessi monumentali."
11
+ },
12
+ {
13
+ "id": "l0:Location",
14
+ "type": "Class",
15
+ "description": "Un'entità geografica o amministrativa. Include città, comuni, regioni, nazioni, fiumi, o aree topografiche."
16
+ },
17
+ {
18
+ "id": "core:Event",
19
+ "type": "Class",
20
+ "description": "Un evento storico, una battaglia, una mostra, una scoperta archeologica o una campagna di scavo."
21
+ },
22
+ {
23
+ "id": "a-loc:hasCurrentLocation",
24
+ "type": "Property",
25
+ "description": "Collega un bene culturale al luogo fisico o all'istituto (es. un museo) in cui è attualmente conservato o esposto."
26
+ },
27
+ {
28
+ "id": "core:hasPart",
29
+ "type": "Property",
30
+ "description": "Indica che un'entità contiene o è composta da un'altra entità. Utile per indicare che un museo contiene una collezione, o una città contiene un'area."
31
+ },
32
+ {
33
+ "id": "cis:hasSite",
34
+ "type": "Property",
35
+ "description": "Collega un istituto culturale (come un museo) alla sua sede fisica o al comune in cui si trova."
36
+ },
37
+ {
38
+ "id": "ti:atTime",
39
+ "type": "Property",
40
+ "description": "Collega un evento, una scoperta o un reperto alla sua epoca, data o periodo storico."
41
+ }
42
+ ]
src/extraction/extractor.py CHANGED
@@ -17,20 +17,20 @@ load_dotenv() # in locale carica il file .env , su HF non trovando il file utili
17
 
18
  # --- DEFINIZIONE DELLO SCHEMA ---
19
  class GraphTriple(BaseModel):
20
- subject: str = Field(..., description="Entità sorgente (Canonical).")
21
- predicate: str = Field(..., description="Relazione (snake_case).")
22
  object: str = Field(..., description="Entità target.")
23
  confidence: float = Field(..., description="Confidenza (0.0 - 1.0).")
24
- source: Optional[str] = Field(None, description="ID del documento o chunk.")
25
 
26
  class KnowledgeGraphExtraction(BaseModel):
27
  reasoning: Optional[str] = Field(None, description="Breve ragionamento logico.")
28
- entities: List[str] = Field(default_factory=list, description="Lista di entità rilevanti estratte, incluse quelle senza relazioni.")
29
  triples: List[GraphTriple]
30
 
31
- # --- ESTRATTORE DINAMICO (Dynamic Few-Shot) ---
32
  class NeuroSymbolicExtractor:
33
- def __init__(self, model_name="llama3", temperature=0, gold_standard_path=None):
34
 
35
  hf_token = os.getenv("HF_TOKEN")
36
  groq_api_key=os.getenv("GROQ_API_KEY")
@@ -57,8 +57,7 @@ class NeuroSymbolicExtractor:
57
  try:
58
  self.llm = ChatGroq(
59
  temperature=0,
60
- model="llama-3.1-8b-instant",
61
- #model="llama-3.3-70b-versatile", #modello più performante, numero di token maggiori ma richiede un credito di utilizzo più elevato
62
  api_key=os.getenv("GROQ_API_KEY")
63
  )
64
  except Exception as e:
@@ -79,109 +78,88 @@ class NeuroSymbolicExtractor:
79
  print("🧠 Caricamento modello embedding per Dynamic Selection...")
80
  self.embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
81
 
82
- # Caricamento e Indicizzazione Gold Standard
83
- self.examples = []
84
- self.example_embeddings = None
85
 
86
- if gold_standard_path and os.path.exists(gold_standard_path):
87
- print(f"🌟 Indicizzazione vettoriale Gold Standard da: {gold_standard_path}")
88
- self._index_examples(gold_standard_path)
89
- else:
90
- # Crea una lista vuota per evitare crash se il path non esiste
91
- print("⚠️ Nessun Gold Standard trovato. Modalità Zero-Shot.")
92
 
93
- # Template Specializzato (Prompt Engineering)
94
- self.system_template_base = """Sei un Agente Cognitivo (AC).
95
- Il tuo compito è trasformare il testo non strutturato in un Digital Twin Graph (RDF) conforme allo standard italiano ArCo.
 
 
 
 
 
 
 
 
 
 
96
 
97
- SCHEMA JSON RICHIESTO:
 
 
 
 
 
98
  {{
99
- "reasoning": "Spiega brevemente perché hai scelto queste classi/relazioni...",
100
- "entities": ["Nome Entità 1", "Nome Entità 2 Isolata"],
101
  "triples": [
102
- {{"subject": "Entità", "predicate": "prefix:Relazione", "object": "Entità", "confidence": 0.95}}
 
103
  ]
104
  }}
105
-
106
- ONTOLOGIA DI RIFERIMENTO ArCo (Usa rigorosamente questi prefissi):
107
- - arco: (Beni Culturali) -> Tipologia del bene (es. arco:HistoricOrArtisticProperty, arco:ArchaeologicalProperty).
108
- - cis: (Luoghi della Cultura) -> Musei, siti, parchi (es. cis:CulturalInstituteOrSite, cis:hasSite).
109
- - a-loc: (Localizzazione) -> Relazioni spaziali e contenimento (es. a-loc:hasCulturalPropertyAddress, a-loc:isLocatedIn).
110
- - ti: (Tempo) -> Datazioni ed epoche (es. ti:hasTimeInterval, ti:atTime).
111
- - ro: (Ruoli e Agenti) -> Autori, committenti, scopritori (es. ro:hasRole, ro:isRoleOf).
112
- - core: (Core) -> Relazioni di base e tipologie (es. core:hasType, core:hasConcept).
113
-
114
- ESEMPI CONTESTUALI (Dynamic Few-Shot):
115
- {selected_examples}
116
-
117
- REGOLE DI CONFIDENZA (Trust Layer):
118
- - 1.0 (Fatto Curato): Informazione esplicita e certa nel testo.
119
- - 0.8 - 0.9 (Inferenza): Deduzione logica forte ma non esplicita.
120
- - < 0.7 (Ipotesi): Associazione probabile ma incerta (da marcare per revisione umana).
121
-
122
- VINCOLI SULLE ENTITÀ (CRITICO):
123
- - L'array "entities" deve contenere ESCLUSIVAMENTE parole o frasi realmente estratte dal testo sorgente.
124
- - È SEVERAMENTE VIETATO inserire i prefissi ontologici (es. arco:, core:, cis:, ro:) o i nomi delle
125
- classi all'interno dell'array "entities". I prefissi vanno utilizzati ESCLUSIVAMENTE come valore del campo "predicate" all'interno delle triple.
126
-
127
- Canonicalizza i nomi (es. "Il Parco" -> "Parco Archeologico di Canne della Battaglia").
128
- Rispondi ESCLUSIVAMENTE con un JSON valido.
129
  """
130
 
131
- def _index_examples(self, path: str):
132
- """Carica il JSON e calcola i vettori per ogni esempio."""
133
  try:
134
  with open(path, 'r', encoding='utf-8') as f:
135
- self.examples = json.load(f)
136
-
137
- # Estraggo solo il testo di input per calcolare l'embedding
138
- texts = [ex['text'] for ex in self.examples]
139
- self.example_embeddings = self.embedding_model.embed_documents(texts)
140
- print(f"✅ Indicizzati {len(self.examples)} esempi di Gold Standard.")
141
  except Exception as e:
142
- print(f"❌ Errore indicizzazione Gold Standard: {e}")
143
- self.examples = []
144
 
145
- def _get_relevant_examples(self, query_text: str, k=2) -> str:
146
- """
147
- Trova i k esempi più simili semanticamente al chunk attuale.
148
- """
149
- if not self.examples or self.example_embeddings is None:
150
- return "Nessun esempio disponibile."
151
 
152
- # Embed del chunk attuale
153
  query_embedding = self.embedding_model.embed_query(query_text)
 
154
 
155
- # Calcolo similarità coseno
156
- similarities = cosine_similarity([query_embedding], self.example_embeddings)[0]
157
 
158
- # Selezione dei top-k
159
- top_k_indices = np.argsort(similarities)[-k:][::-1]
160
 
161
- formatted_text = ""
162
- for i, idx in enumerate(top_k_indices):
163
- ex = self.examples[idx]
164
- sim_score = similarities[idx]
165
- formatted_text += f"\n--- ESEMPIO RILEVANTE #{i+1} (Sim: {sim_score:.2f}) ---\n"
166
- formatted_text += f"INPUT: {ex['text']}\n"
167
-
168
- output_dict = {
169
- "reasoning": ex.get("reasoning", "N/A"),
170
- "entities": ex.get("entities", []),
171
- "triples": ex.get("triples", [])
172
- }
173
- formatted_text += f"OUTPUT: {json.dumps(output_dict, ensure_ascii=False)}\n"
174
-
175
- return formatted_text
176
 
177
  def extract(self, text_chunk: str, source_id: str = "unknown", max_retries=3) -> KnowledgeGraphExtraction:
178
- print(f"🧠 Processing {source_id} (Dynamic Mode)...")
179
 
180
- # Selezione Esempi
181
- relevant_examples_str = self._get_relevant_examples(text_chunk, k=2)
182
 
183
- # Costruzione Prompt Finale
184
- final_sys_text = self.system_template_base.format(selected_examples=relevant_examples_str)
 
 
 
185
 
186
  sys_msg = SystemMessage(content=final_sys_text)
187
 
@@ -208,7 +186,6 @@ class NeuroSymbolicExtractor:
208
  if not content:
209
  raise ValueError("Il modello ha restituito una stringa vuota o un formato non parsabile.")
210
 
211
-
212
  data = json.loads(content)
213
 
214
  # Normalizzazione output
@@ -219,7 +196,7 @@ class NeuroSymbolicExtractor:
219
  triples = [GraphTriple(**t) for t in data.get("triples", [])]
220
  validated_data = KnowledgeGraphExtraction(
221
  reasoning=data.get("reasoning", "N/A"),
222
- entities=data.get("entities", []), #
223
  triples=triples
224
  )
225
 
 
17
 
18
  # --- DEFINIZIONE DELLO SCHEMA ---
19
  class GraphTriple(BaseModel):
20
+ subject: str = Field(..., description="Entità sorgente.")
21
+ predicate: str = Field(..., description="Relazione (es. arco:hasCurrentLocation).")
22
  object: str = Field(..., description="Entità target.")
23
  confidence: float = Field(..., description="Confidenza (0.0 - 1.0).")
24
+ source: Optional[str] = Field(None)
25
 
26
  class KnowledgeGraphExtraction(BaseModel):
27
  reasoning: Optional[str] = Field(None, description="Breve ragionamento logico.")
28
+ entities: List[str] = Field(default_factory=list, description="TUTTE le entità estratte, incluse quelle isolate/orfane.")
29
  triples: List[GraphTriple]
30
 
31
+ # --- ESTRATTORE DINAMICO (Schema-RAG) ---
32
  class NeuroSymbolicExtractor:
33
+ def __init__(self, model_name="llama3", temperature=0, schema_path=None):
34
 
35
  hf_token = os.getenv("HF_TOKEN")
36
  groq_api_key=os.getenv("GROQ_API_KEY")
 
57
  try:
58
  self.llm = ChatGroq(
59
  temperature=0,
60
+ model="llama-3.3-70b-versatile",
 
61
  api_key=os.getenv("GROQ_API_KEY")
62
  )
63
  except Exception as e:
 
78
  print("🧠 Caricamento modello embedding per Dynamic Selection...")
79
  self.embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
80
 
81
+ # Caricamento vocabolario ontologico
82
+ self.ontology_elements = []
83
+ self.ontology_embeddings = None
84
 
85
+ if schema_path and os.path.exists(schema_path):
86
+ print(f"🌟 Indicizzazione vettoriale Ontologia da: {schema_path}")
87
+ self._index_ontology(schema_path)
 
 
 
88
 
89
+ # Template Specializzato con regole di Graceful Degradation
90
+ self.system_template_base = """Sei un Agente Cognitivo per l'estrazione dati (Information Extraction).
91
+ Il tuo compito è analizzare il testo e generare un JSON contenente entità e relazioni.
92
+
93
+ REGOLE FONDAMENTALI:
94
+ 1. Estrai TUTTI i concetti rilevanti e inseriscili nell'array "entities" (anche se non sai come collegarli).
95
+ 2. Per creare le "triples", puoi usare ESCLUSIVAMENTE le seguenti Classi (per rdf:type) e Proprietà che sono pertinenti a questo testo:
96
+
97
+ CLASSI CONSENTITE (usa come oggetto quando predicate = rdf:type):
98
+ {retrieved_classes}
99
+
100
+ PROPRIETÀ CONSENTITE (usa come predicate):
101
+ {retrieved_properties}
102
 
103
+ REGOLE DI GRACEFUL DEGRADATION E ANTI-ALLUCINAZIONE (CRITICO):
104
+ - Relazioni (Fallback): Se due entità sono correlate ma nessuna delle proprietà fornite è adatta al contesto esatto, non inventare predicati. Usa il predicato 'skos:related'.
105
+ - Classificazione (rdf:type): Se non trovi una Classe specifica esatta tra quelle fornite per tipizzare un'entità, NON FORZARE la classificazione in classi errate. Usa i tipi di salvataggio universali: 'core:Agent' per le persone/popoli, 'core:Concept' per concetti astratti/materiali, 'l0:Location' per i luoghi geografici.
106
+ - Entità Orfane: Se sei in forte dubbio su come collegare o classificare un'entità testuale, limitati a inserirla nell'array "entities" come orfana senza creare alcuna tripla. Non inquinare il grafo con dati inesatti.
107
+
108
+ Rispondi SOLO ed ESCLUSIVAMENTE con un JSON valido strutturato così:
109
  {{
110
+ "reasoning": "Breve logica delle estrazioni fatte...",
111
+ "entities": ["Entità 1", "Entità orfana"],
112
  "triples": [
113
+ {{"subject": "Entità 1", "predicate": "rdf:type", "object": "Classe Consentita", "confidence": 0.9}},
114
+ {{"subject": "Entità 1", "predicate": "Proprietà Consentita", "object": "Entità 2", "confidence": 0.8}}
115
  ]
116
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  """
118
 
119
+ def _index_ontology(self, path: str):
 
120
  try:
121
  with open(path, 'r', encoding='utf-8') as f:
122
+ self.ontology_elements = json.load(f)
123
+ # Vettorizziamo le descrizioni semantiche delle classi/proprietà
124
+ texts = [el['description'] for el in self.ontology_elements]
125
+ self.ontology_embeddings = self.embedding_model.embed_documents(texts)
126
+ print(f"✅ Indicizzati {len(self.ontology_elements)} elementi dell'ontologia.")
 
127
  except Exception as e:
128
+ print(f"❌ Errore indicizzazione Ontologia: {e}")
 
129
 
130
+ def _retrieve_schema(self, query_text: str, top_k_classes=3, top_k_props=4):
131
+ if not self.ontology_elements or self.ontology_embeddings is None:
132
+ return "Nessuna classe specifica.", "skos:related"
 
 
 
133
 
 
134
  query_embedding = self.embedding_model.embed_query(query_text)
135
+ similarities = cosine_similarity([query_embedding], self.ontology_embeddings)[0]
136
 
137
+ # Ordiniamo gli indici per similarità
138
+ sorted_indices = np.argsort(similarities)[::-1]
139
 
140
+ classes = []
141
+ properties = []
142
 
143
+ for idx in sorted_indices:
144
+ element = self.ontology_elements[idx]
145
+ if element["type"] == "Class" and len(classes) < top_k_classes:
146
+ classes.append(f"- {element['id']}: {element['description']}")
147
+ elif element["type"] == "Property" and len(properties) < top_k_props:
148
+ properties.append(f"- {element['id']}: {element['description']}")
149
+
150
+ return "\n".join(classes), "\n".join(properties)
 
 
 
 
 
 
 
151
 
152
  def extract(self, text_chunk: str, source_id: str = "unknown", max_retries=3) -> KnowledgeGraphExtraction:
153
+ print(f"🧠 Processing {source_id} (Schema-RAG Mode)...")
154
 
155
+ # 1. Recupero dinamico dello schema basato sul testo
156
+ retrieved_classes, retrieved_properties = self._retrieve_schema(text_chunk)
157
 
158
+ # 2. Iniezione nel prompt
159
+ final_sys_text = self.system_template_base.format(
160
+ retrieved_classes=retrieved_classes,
161
+ retrieved_properties=retrieved_properties
162
+ )
163
 
164
  sys_msg = SystemMessage(content=final_sys_text)
165
 
 
186
  if not content:
187
  raise ValueError("Il modello ha restituito una stringa vuota o un formato non parsabile.")
188
 
 
189
  data = json.loads(content)
190
 
191
  # Normalizzazione output
 
196
  triples = [GraphTriple(**t) for t in data.get("triples", [])]
197
  validated_data = KnowledgeGraphExtraction(
198
  reasoning=data.get("reasoning", "N/A"),
199
+ entities=data.get("entities", []),
200
  triples=triples
201
  )
202
 
src/utils/build_schema.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from rdflib import Graph
5
+
6
+ def build_schema_from_ontology(owl_folder_path: str, output_json_path: str):
7
+ print(f"⏳ Inizializzazione Graph e caricamento file .owl da {owl_folder_path}...")
8
+ g = Graph()
9
+
10
+ # 1. Caricamento di tutti i moduli dell'ontologia
11
+ owl_files = list(Path(owl_folder_path).glob('**/*.owl'))
12
+ if not owl_files:
13
+ print("❌ Nessun file .owl trovato nella directory specificata.")
14
+ return
15
+
16
+ for file_path in owl_files:
17
+ try:
18
+ # I file .owl standard sono scritti in RDF/XML
19
+ g.parse(file_path, format="xml")
20
+ print(f" -> Caricato (XML): {file_path.name}")
21
+ except Exception as e_xml:
22
+ try:
23
+ g.parse(file_path, format="turtle")
24
+ print(f" -> Caricato (Turtle): {file_path.name}")
25
+ except Exception as e_ttl:
26
+ print(f" ⚠️ Impossibile parsare {file_path.name}. XML err: {e_xml} | TTL err: {e_ttl}")
27
+
28
+
29
+ print("✅ Ontologia caricata in memoria. Esecuzione query SPARQL...")
30
+
31
+ # 2. Query SPARQL per estrarre Classi e ObjectProperties con le loro descrizioni in italiano
32
+ sparql_query = """
33
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
34
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
35
+
36
+ SELECT DISTINCT ?entity ?type ?label ?comment
37
+ WHERE {
38
+ {
39
+ ?entity a owl:Class .
40
+ BIND("Class" AS ?type)
41
+ } UNION {
42
+ ?entity a owl:ObjectProperty .
43
+ BIND("Property" AS ?type)
44
+ }
45
+
46
+ # Recuperiamo le label in italiano (o senza lingua)
47
+ OPTIONAL {
48
+ ?entity rdfs:label ?label .
49
+ FILTER(LANGMATCHES(LANG(?label), "it") || LANG(?label) = "")
50
+ }
51
+
52
+ # Recuperiamo i commenti/definizioni in italiano (o senza lingua)
53
+ OPTIONAL {
54
+ ?entity rdfs:comment ?comment .
55
+ FILTER(LANGMATCHES(LANG(?comment), "it") || LANG(?comment) = "")
56
+ }
57
+
58
+ # Filtriamo per evitare i blank nodes (nodi senza URI)
59
+ FILTER(isIRI(?entity))
60
+ }
61
+ """
62
+
63
+ results = g.query(sparql_query)
64
+
65
+ schema_elements = {}
66
+
67
+ # 3. Elaborazione e formattazione dei risultati
68
+ for row in results:
69
+ entity_uri = row.entity
70
+ entity_type = str(row.type)
71
+ label = str(row.label) if row.label else ""
72
+ comment = str(row.comment) if row.comment else ""
73
+
74
+ # Trasformiamo l'URI lungo in un prefisso leggibile (es. arco:CulturalProperty)
75
+ try:
76
+ prefix, namespace, name = g.compute_qname(entity_uri)
77
+ qname = f"{prefix}:{name}"
78
+ except Exception:
79
+ # Fallback se non riesce a calcolare il prefisso
80
+ qname = str(entity_uri).split('/')[-1].split('#')[-1]
81
+
82
+ # Costruiamo la descrizione aggregata per l'LLM
83
+ description_parts = []
84
+ if label: description_parts.append(label)
85
+ if comment: description_parts.append(comment)
86
+
87
+ final_description = " - ".join(description_parts)
88
+
89
+ # Se una classe non ha né label né commento, la scartiamo per non confondere l'LLM
90
+ if not final_description.strip():
91
+ continue
92
+
93
+ # Usiamo un dizionario per evitare duplicati (spesso le ontologie definiscono la stessa classe in più file)
94
+ if qname not in schema_elements:
95
+ schema_elements[qname] = {
96
+ "id": qname,
97
+ "type": entity_type,
98
+ "description": final_description.strip()
99
+ }
100
+
101
+ # 4. Salvataggio in JSON
102
+ output_list = list(schema_elements.values())
103
+
104
+ with open(output_json_path, 'w', encoding='utf-8') as f:
105
+ json.dump(output_list, f, ensure_ascii=False, indent=2)
106
+
107
+ print(f"🎉 Finito! Generato dizionario con {len(output_list)} elementi.")
108
+ print(f"💾 Salvato in: {output_json_path}")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ # Esempio di utilizzo:
113
+ # Assicurati di scaricare i file .ttl di ArCo e metterli in una cartella, ad es. 'data/arco_raw/'
114
+ NOME_ONTOLOGIA = "ARCO"
115
+ INPUT_FOLDER = f"data/ontologie_raw/{NOME_ONTOLOGIA}"
116
+ OUTPUT_FILE = f"data/schemas/{NOME_ONTOLOGIA}_schema.json"
117
+
118
+ # Crea la directory di output se non esiste
119
+ os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
120
+
121
+ build_schema_from_ontology(INPUT_FOLDER, OUTPUT_FILE)
src/validation/shapes/schema_constraints.ttl CHANGED
@@ -1,37 +1,36 @@
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
  ] .
 
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 arco: <https://w3id.org/arco/ontology/arco/> .
5
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
 
6
 
7
+ # 1. REGOLA BASE: Ogni entità (soggetto o oggetto) deve avere un nome testuale (Label)
8
+ ex:NodeLabelShape
9
  a sh:NodeShape ;
10
+ sh:targetSubjectsOf skos:prefLabel ;
 
 
11
  sh:property [
12
  sh:path skos:prefLabel ;
13
  sh:minCount 1 ;
14
  sh:nodeKind sh:Literal ;
15
+ sh:message "Errore Topologico: Ogni entità nel grafo deve possedere un nome leggibile."
16
+ ] .
17
 
18
+ # 2. REGOLA RELAZIONALE: Le proprietà non devono puntare a testi (Literal), ma ad altri nodi (IRI)
19
+ ex:ObjectPropertyShape
20
+ a sh:NodeShape ;
21
+ sh:targetSubjectsOf skos:prefLabel ; # Si applica a tutti i nodi
22
  sh:property [
23
  sh:path skos:related ;
24
+ sh:nodeKind sh:IRI ;
25
+ sh:message "Errore Semantico (skos:related): Le relazioni generiche devono collegare due nodi distinti, non un nodo a un testo."
26
+ ] .
27
+
28
+ # 3. REGOLA ONTOLOGICA: Se un nodo ha un rdf:type, deve essere un IRI (es. arco:CulturalProperty)
29
+ ex:TypeShape
30
+ a sh:NodeShape ;
31
+ sh:targetSubjectsOf rdf:type ;
 
 
 
32
  sh:property [
33
+ sh:path rdf:type ;
34
+ sh:nodeKind sh:IRI ;
35
+ sh:message "Errore Ontologico: La classe assegnata tramite rdf:type deve essere un URI valido dell'ontologia, non una stringa."
36
  ] .
src/validation/validator.py CHANGED
@@ -5,11 +5,17 @@ from pyshacl import validate
5
 
6
  class SemanticValidator:
7
  def __init__(self):
8
- # Definisco 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")
@@ -18,44 +24,51 @@ class SemanticValidator:
18
  print("⚠️ File SHACL non trovato. Validazione disabilitata.")
19
  self.shacl_graph = None
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  def _json_to_rdf(self, entities, triples):
22
- """Converte le triple e le entità isolate in un grafo RDFLib in memoria."""
23
  g = Graph()
 
 
 
24
  g.bind("skos", SKOS)
25
- g.bind("ex", self.EX)
26
 
27
- # Aggiungo le entità isolate come Nodi
28
  if entities:
29
  for ent in entities:
30
- # Gestisce sia se 'ent' è una stringa semplice, sia se è un dict (es. da entity_resolver)
31
  label = ent["label"] if isinstance(ent, dict) else str(ent)
32
- ent_uri = URIRef(self.EX[label.replace(" ", "_")])
33
-
34
- g.add((ent_uri, RDF.type, SKOS.Concept))
35
  g.add((ent_uri, SKOS.prefLabel, Literal(label, lang="it")))
36
 
37
- # Aggiungo le Triple
38
  if triples:
39
  for t in triples:
40
- subj_uri = URIRef(self.EX[t.subject.replace(" ", "_")])
41
- obj_uri = URIRef(self.EX[t.object.replace(" ", "_")])
42
 
43
- # Aggiungo il tipo Concept per soggetto e oggetto
44
- g.add((subj_uri, RDF.type, SKOS.Concept))
45
  g.add((subj_uri, SKOS.prefLabel, Literal(t.subject, lang="it")))
46
-
47
- g.add((obj_uri, RDF.type, SKOS.Concept))
48
- g.add((obj_uri, SKOS.prefLabel, Literal(t.object, lang="it")))
49
 
50
- # Mappo il predicato
51
- if t.predicate == "skos:related" or t.predicate == "related":
52
- pred = SKOS.related
53
- elif t.predicate == "skos:broader" or t.predicate == "broader":
54
- pred = SKOS.broader
55
  else:
56
- pred = self.EX[t.predicate]
57
-
58
- g.add((subj_uri, pred, obj_uri))
 
 
 
59
 
60
  return g
61
 
@@ -67,7 +80,7 @@ class SemanticValidator:
67
  if not self.shacl_graph:
68
  return True, "No Constraints", None
69
 
70
- # Passo entrambe le liste al convertitore
71
  data_graph = self._json_to_rdf(entities, triples)
72
 
73
  print("🔍 Esecuzione Validazione SHACL...")
 
5
 
6
  class SemanticValidator:
7
  def __init__(self):
 
 
8
  self.shapes_file = os.path.join(os.path.dirname(__file__), "shapes/schema_constraints.ttl")
9
 
10
+ # Dizionario dei Namespace ufficiali di ArCo e fallback
11
+ self.namespaces = {
12
+ "arco": Namespace("https://w3id.org/arco/ontology/arco/"),
13
+ "core": Namespace("https://w3id.org/arco/ontology/core/"),
14
+ "a-loc": Namespace("https://w3id.org/arco/ontology/location/"),
15
+ "cis": Namespace("http://dati.beniculturali.it/cis/"),
16
+ "ex": Namespace("http://activa.ai/ontology/") # Fallback per le entità
17
+ }
18
+
19
  if os.path.exists(self.shapes_file):
20
  self.shacl_graph = Graph()
21
  self.shacl_graph.parse(self.shapes_file, format="turtle")
 
24
  print("⚠️ File SHACL non trovato. Validazione disabilitata.")
25
  self.shacl_graph = None
26
 
27
+ def _get_uri(self, text_val):
28
+ """Metodo di supporto per tradurre un testo 'prefisso:nome' in un URIRef reale."""
29
+ if ":" in text_val and not text_val.startswith("http"):
30
+ prefix, name = text_val.split(":", 1)
31
+ if prefix in self.namespaces:
32
+ return self.namespaces[prefix][name]
33
+
34
+ # Se è un'entità senza prefisso (es. "Menhir di Canne"), uso il namespace custom
35
+ clean_name = text_val.replace(" ", "_").replace("'", "").replace('"', "")
36
+ return self.namespaces["ex"][clean_name]
37
+
38
  def _json_to_rdf(self, entities, triples):
39
+ """Converte dinamicamente rispettando l'ontologia ArCo."""
40
  g = Graph()
41
+ # Registriamo i prefissi nel grafo per leggibilità
42
+ for prefix, ns in self.namespaces.items():
43
+ g.bind(prefix, ns)
44
  g.bind("skos", SKOS)
 
45
 
46
+ # 1. Popolamento Entità Isolate (Orfani)
47
  if entities:
48
  for ent in entities:
 
49
  label = ent["label"] if isinstance(ent, dict) else str(ent)
50
+ ent_uri = self._get_uri(label)
 
 
51
  g.add((ent_uri, SKOS.prefLabel, Literal(label, lang="it")))
52
 
53
+ # 2. Popolamento delle Triple
54
  if triples:
55
  for t in triples:
56
+ subj_uri = self._get_uri(t.subject)
 
57
 
58
+ # Assicuriamoci che ogni nodo abbia un nome leggibile
 
59
  g.add((subj_uri, SKOS.prefLabel, Literal(t.subject, lang="it")))
 
 
 
60
 
61
+ if t.predicate in ["rdf:type", "a", "type"]:
62
+ # Se l'LLM sta classificando il nodo (es. oggetto = arco:CulturalProperty)
63
+ obj_uri = self._get_uri(t.object)
64
+ g.add((subj_uri, RDF.type, obj_uri))
 
65
  else:
66
+ # Se è una relazione standard (es. a-loc:hasCurrentLocation)
67
+ pred_uri = self._get_uri(t.predicate)
68
+ obj_uri = self._get_uri(t.object)
69
+
70
+ g.add((subj_uri, pred_uri, obj_uri))
71
+ g.add((obj_uri, SKOS.prefLabel, Literal(t.object, lang="it")))
72
 
73
  return g
74
 
 
80
  if not self.shacl_graph:
81
  return True, "No Constraints", None
82
 
83
+ # Passiamo entrambe le liste al convertitore
84
  data_graph = self._json_to_rdf(entities, triples)
85
 
86
  print("🔍 Esecuzione Validazione SHACL...")