GaetanoParente commited on
Commit
fe271ee
·
1 Parent(s): 9fb3deb

termine refactoring v1

Browse files
README.md CHANGED
@@ -15,47 +15,44 @@ short_description: Neurosymbolic prototype for automatic semantic discovery
15
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688)
16
  ![Streamlit](https://img.shields.io/badge/UI-Streamlit-FF4B4B)
17
  ![Neo4j](https://img.shields.io/badge/graphdb-Neo4j-green)
18
- ![Status](https://img.shields.io/badge/status-advanced%20prototype-orange)
 
19
 
20
- Questo repository contiene un **prototipo avanzato per la scoperta semantica automatica (Automated Semantic Discovery)**. Il sistema agisce come un microservizio finalizzato alla generazione di **ontologie leggere** e **vocabolari semantici** a partire da testo non strutturato, ponendosi come strumento abilitante per l'estrazione dati su larga scala in scenari aziendali e di BPO.
21
 
22
- Il progetto è progettato con una doppia interfaccia:
23
- 1. **API REST (Headless):** Ideale per l'integrazione asincrona e l'orchestrazione da parte di backend esterni ad alte prestazioni.
24
- 2. **Web UI (Streamlit):** Un'interfaccia interattiva ottimizzata per il deploy su Hugging Face Spaces, perfetta per demo, test curati e visualizzazione topologica.
25
 
26
- Il progetto implementa una **pipeline neuro-simbolica state-of-the-art** che fonde:
27
- - La flessibilità semantica dei **Large Language Models (LLM)** e dei **modelli vettoriali** (*Neuro*).
28
- - Il rigore deterministico della validazione **SHACL**, della risoluzione tramite **Vector Database** e dell'**Entity Linking** (*Symbolic*).
29
 
30
- ## Obiettivi del prototipo
31
 
32
- - Dimostrare la fattibilità di una **pipeline automatizzata e in-memory di Semantic Knowledge Discovery**.
33
- - Ridurre il *knowledge acquisition bottleneck* ancorando le entità isolate a vocabolari globali (es. Wikidata).
34
- - Validare un approccio a microservizi (stateless per l'inferenza, stateful per la risoluzione) integrabile nativamente in ecosistemi aziendali eterogenei.
35
- - Fornire un solido strato di persistenza pronto per alimentare applicazioni di **GraphRAG**.
36
 
37
- ## Workflow Architetturale
38
 
39
- La pipeline elabora i dati esclusivamente in memoria ed è orchestrata in **moduli indipendenti e sequenziali**:
40
 
41
- ### 1. Ingestion & Semantic Chunking (`semantic_splitter.py`)
42
- - Segmentazione del testo basata su **similarità semantica vettoriale** (`sentence-transformers`), garantendo la coerenza tematica dei frammenti elaborati senza scritture su disco.
43
 
44
- ### 2. Neuro-Symbolic Extraction (`extractor.py`)
45
- - Architettura **Schema-RAG**: iniezione dinamica nel prompt dell'LLM delle definizioni ontologiche (es. ArCo) più pertinenti al frammento di testo, recuperate tramite vector search.
46
- - Implementazione di meccanismi di **Graceful Degradation** e fallback semantici per azzerare le allucinazioni ontologiche su entità orfane.
47
- - Forzatura dell'output in strutture dati tipizzate tramite validazione **Pydantic**.
48
 
49
- ### 3. Stateful Entity Resolution & Linking (`entity_resolver.py`)
50
- - Deduplica locale in RAM tramite clustering spaziale (**DBSCAN** su embedding cosine-similarity).
51
- - Risoluzione globale interrogando i **Vector Index nativi di Neo4j**.
52
- - **Entity Linking** asincrono tramite chiamate REST all'API di **Wikidata** per l'ancoraggio semantico (`owl:sameAs`).
 
53
 
54
- ### 4. Semantic Validation (`validator.py`)
55
- - Validazione topologica e qualitativa dei dati estratti applicando vincoli ontologici deterministici (**SHACL**) tramite `pyshacl`, garantendo la coerenza del grafo prima della persistenza.
56
 
57
- ### 5. Knowledge Graph Persistence (`graph_loader.py`)
58
- - Salvataggio massivo e transazionale (`UNWIND` Cypher) su database a grafo **Neo4j**, includendo gli embedding vettoriali per le ricerche future.
59
 
60
  ## Struttura del repository
61
 
@@ -65,8 +62,10 @@ prototipo/
65
  ├── assets/
66
  │ └── style.css
67
 
68
- ├── data/
69
- ── arco_schema.json # Dizionario ontologico indicizzato per lo Schema-RAG
 
 
70
 
71
  ├── src/
72
  │ ├── ingestion/
@@ -74,17 +73,16 @@ prototipo/
74
  │ ├── extraction/
75
  │ │ └── extractor.py
76
  │ ├── validation/
77
- │ │ ── validator.py
78
- │ │ └── shapes/
79
- │ │ └── schema_constraints.ttl # Regole SHACL
80
  │ └── graph/
81
  │ ├── graph_loader.py
82
  │ └── entity_resolver.py
83
 
84
- ├── app.py # Entrypoint Web UI (Streamlit / Hugging Face)
85
  ├── api.py # Entrypoint API REST (FastAPI)
86
- ├── Dockerfile # Configurazione container per HF Spaces
87
- ├── .env.example # Template per le variabili d'ambiente locali
 
88
  ├── requirements.txt
89
  └── README.md
90
  ```
@@ -92,19 +90,11 @@ prototipo/
92
  ## Tech Stack & Requisiti
93
 
94
  - **Linguaggio**: Python 3.13
95
- - **Database**: Neo4j (Consigliato AuraDB cloud per istanze distribuite)
96
  - **Interfacce**: FastAPI, Uvicorn, Streamlit
97
-
98
- ### Core Libraries
99
-
100
- - **Neuro / LLM**
101
- `transformers`, `langchain`, `langchain-huggingface`, `langchain-groq`, `sentence-transformers`
102
-
103
- - **Symbolic / Graph**
104
- `neo4j`, `rdflib`, `pyshacl`, `scikit-learn`
105
-
106
- - **UI & Viz:**
107
- `streamlit`, `pyvis`, `pandas`
108
 
109
  > Le dipendenze complete sono elencate in `requirements.txt`.
110
 
@@ -116,15 +106,15 @@ Per testare il sistema in locale, creare un file `.env` a partire dal template:
116
  NEO4J_URI=neo4j+s://<tuo-cluster>.databases.neo4j.io
117
  NEO4J_USER=neo4j
118
  NEO4J_PASSWORD=la_tua_password
119
- HF_TOKEN=tuo_token_huggingface_opzionale
120
- GROQ_API_KEY=tua_api_key_groq_opzionale
 
121
  ```
122
- (Nota: Su Hugging Face Spaces, queste variabili vanno configurate nei "Secrets" delle impostazioni).
123
 
124
  ## Installazione ed Esecuzione
125
 
126
  ```bash
127
- # 1. Clona il repository e posizionati nella cartella
128
  git clone [https://github.com/](https://github.com/)<username>/<repository>.git
129
  cd prototipo
130
 
@@ -135,10 +125,13 @@ source venv/bin/activate # Linux / macOS
135
 
136
  # 3. Installa le dipendenze
137
  pip install -r requirements.txt
 
 
 
138
  ```
139
- ## Modalità 1: Interfaccia Visuale (Demo / HITL)
140
 
141
- Avvia la dashboard per testare visivamente l'estrazione e ispezionare il grafo interattivo:
142
 
143
  ```bash
144
  streamlit run app.py
@@ -146,9 +139,9 @@ streamlit run app.py
146
 
147
  L'interfaccia sarà disponibile su `http://localhost:8501`.
148
 
149
- ## Modalità 2: Servizio API (Integrazione Backend)
150
 
151
- Avvia il motore in modalità headless per metterlo in ascolto di payload JSON:
152
 
153
  ```bash
154
  python api.py
@@ -170,13 +163,6 @@ Il sistema produce una risposta JSON strutturata contenente:
170
  - **Rate Limiting Wikidata**: Le chiamate di Entity Linking dipendono dai tempi di risposta dell'API pubblica di Wikidata; per ingestion intensive è consigliato l'uso di cache locali stratificate.
171
  - **Dipendenza da Vocabolari**: L'accuratezza dell'estrazione semantica tramite Schema-RAG fluttua in base alla ricchezza descrittiva del dizionario JSON ontologico fornito in ingresso.
172
 
173
- ## Possibili estensioni future
174
-
175
- - Disaccoppiamento architetturale: implementazione di un orchestratore ad alte prestazioni (es. in Golang) per gestire code di messaggistica asincrone e chiamare l'API Python solo per l'inferenza pura.
176
- - Sviluppo di uno strato GraphRAG.
177
- - Creazione di una dashboard operativa SPA (es. in Angular) connessa direttamente a Neo4j per la validazione Human-in-the-Loop su larga scala nei processi di BPO.
178
- - Dockerizzazione multi-container per deploy enterprise in ambienti Kubernetes.
179
-
180
  ## Riferimenti
181
 
182
  **Automated Semantic Discovery – Generazione Neuro-Simbolica di Ontologie Leggere e Vocabolari Semantici**
 
15
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688)
16
  ![Streamlit](https://img.shields.io/badge/UI-Streamlit-FF4B4B)
17
  ![Neo4j](https://img.shields.io/badge/graphdb-Neo4j-green)
18
+ ![LLM](https://img.shields.io/badge/LLM-Groq%20%7C%20Llama%203-black)
19
+ ![Status](https://img.shields.io/badge/status-Phase%201%20Completed-success)
20
 
21
+ Questo repository contiene un **prototipo avanzato per la scoperta semantica automatica (Automated Semantic Discovery)**. Il sistema agisce come un microservizio finalizzato alla generazione di **ontologie authoritative** e **Knowledge Graphs** a partire da testo non strutturato.
22
 
23
+ Nasce come strumento abilitante per scenari aziendali e di BPO, dove l'estrazione massiva di dati deve coniugarsi con il rigore dei vocabolari semantici formali (es. ArCo, OntoPiA, CIDOC-CRM).
 
 
24
 
25
+ Il progetto espone una doppia interfaccia:
26
+ 1. **API REST (FastAPI):** Ideale per l'integrazione asincrona e l'orchestrazione da parte di backend esterni ad alte prestazioni (es. Go/Java).
27
+ 2. **Web UI (Streamlit):** Un'interfaccia interattiva, perfetta per demo, test curati e visualizzazione topologica del grafo.
28
 
29
+ ## Il Paradigma Neuro-Simbolico
30
 
31
+ Il progetto supera i limiti dei tradizionali sistemi RAG o delle semplici estrazioni LLM implementando una pipeline ibrida:
32
+ - **Neuro (AI Generativa & Vettoriale):** Sfrutta la comprensione del testo dei Large Language Models (tramite Groq/Llama 3) e il clustering semantico basato su embedding spaziali (`sentence-transformers`).
33
+ - **Symbolic (Logica Deterministica):** Applica regole algoritmiche rigide per la validazione ontologica (**SHACL** via `pyshacl`), il Type-Driven Domain Traversal (**TDDT**) e l'Entity Linking formale.
 
34
 
35
+ ## Workflow Architetturale (Fase 1)
36
 
37
+ La pipeline elabora i dati in memoria ed è orchestrata in moduli sequenziali indipendenti:
38
 
39
+ ### 1. Semantic Chunking (`semantic_splitter.py`)
40
+ Segmentazione dinamica del testo basata su **cosine similarity** vettoriale. L'algoritmo calcola i percentili di distanza per individuare i reali "punti di rottura" argomentativi, garantendo chunk semanticamente coesi.
41
 
42
+ ### 2. Type-Driven Domain Traversal - TDDT (`extractor.py`)
43
+ Estrazione relazionale a "imbuto" in due passaggi (Pass 1: Macro-Categorizzazione e Specializzazione; Pass 2: Estrazione Relazionale). Il modello linguistico è vincolato tramite *Structured Outputs* (Pydantic JSON Schema) a utilizzare esclusivamente gli URI presenti nel **Domain Index**, azzerando le allucinazioni sui tipi.
 
 
44
 
45
+ ### 3. Hybrid Entity Resolution (`entity_resolver.py`)
46
+ - Deduplica locale in RAM tramite clustering spaziale (**DBSCAN**).
47
+ - Normalizzazione del "Label Bloat" tramite algoritmi di **Majority Voting** sui tipi ontologici.
48
+ - Risoluzione globale sui **Vector Index nativi di Neo4j**.
49
+ - **Entity Linking** asincrono tramite chiamate REST all'API di **Wikidata** per l'ancoraggio a concetti universali (`owl:sameAs`).
50
 
51
+ ### 4. SHACL Blocking & Validation (`validator.py` & `build_schema.py`)
52
+ Costruzione automatica di vincoli SHACL a partire dal dizionario ontologico. Durante l'estrazione, un reasoner OWL RL convalida la conformità (Domain/Range) delle triple. Le triple invalide vengono deviate su una DLQ (Dead Letter Queue) in MongoDB per non corrompere il grafo principale.
53
 
54
+ ### 5. Graph Persistence (`graph_loader.py`)
55
+ Salvataggio massivo transazionale (`UNWIND` Cypher) su database a grafo **Neo4j**, includendo tracciabilità della fonte (`evidence`, `reasoning`) per garantire la massima *Explainability*.
56
 
57
  ## Struttura del repository
58
 
 
62
  ├── assets/
63
  │ └── style.css
64
 
65
+ ├── ontology/
66
+ ── domain_index.json # Indice gerarchico delle ontologie (JSON)
67
+ │ └── shapes/
68
+ │ └── auto_constraints.ttl # Regole SHACL autogenerate
69
 
70
  ├── src/
71
  │ ├── ingestion/
 
73
  │ ├── extraction/
74
  │ │ └── extractor.py
75
  │ ├── validation/
76
+ │ │ ── validator.py
 
 
77
  │ └── graph/
78
  │ ├── graph_loader.py
79
  │ └── entity_resolver.py
80
 
81
+ ├── app.py # Entrypoint Web UI (Streamlit)
82
  ├── api.py # Entrypoint API REST (FastAPI)
83
+ ├── build_schema.py # Script per la generazione di index e shapes SHACL
84
+ ├── Dockerfile # Configurazione container
85
+ ├── .env.example
86
  ├── requirements.txt
87
  └── README.md
88
  ```
 
90
  ## Tech Stack & Requisiti
91
 
92
  - **Linguaggio**: Python 3.13
93
+ - **Database**: Neo4j (Graph), MongoDB (DLQ)
94
  - **Interfacce**: FastAPI, Uvicorn, Streamlit
95
+ - **Core Libraries**:
96
+ - **Neuro** : `langchain`, `langchain-huggingface`, `langchain-groq`, `scikit-learn`
97
+ - **Symbolic** : `neo4j`, `rdflib`, `pyshacl`, `pydantic`
 
 
 
 
 
 
 
 
98
 
99
  > Le dipendenze complete sono elencate in `requirements.txt`.
100
 
 
106
  NEO4J_URI=neo4j+s://<tuo-cluster>.databases.neo4j.io
107
  NEO4J_USER=neo4j
108
  NEO4J_PASSWORD=la_tua_password
109
+ MONGO_URI=mongodb://localhost:27017/
110
+ GROQ_API_KEY=tua_api_key_groq
111
+ ONTOLOGY_PATH=./ontology
112
  ```
 
113
 
114
  ## Installazione ed Esecuzione
115
 
116
  ```bash
117
+ # 1. Clona il repository
118
  git clone [https://github.com/](https://github.com/)<username>/<repository>.git
119
  cd prototipo
120
 
 
125
 
126
  # 3. Installa le dipendenze
127
  pip install -r requirements.txt
128
+
129
+ # 4. Genera gli indici ontologici (una tantum)
130
+ python build_schema.py
131
  ```
132
+ ## Modalità 1: Web UI (Streamlit)
133
 
134
+ Avvia la dashboard interattiva per visualizzare il grafo e testare l'estrazione:
135
 
136
  ```bash
137
  streamlit run app.py
 
139
 
140
  L'interfaccia sarà disponibile su `http://localhost:8501`.
141
 
142
+ ## Modalità 2: API REST Headless
143
 
144
+ Avvia il motore in ascolto per l'orchestrazione backend:
145
 
146
  ```bash
147
  python api.py
 
163
  - **Rate Limiting Wikidata**: Le chiamate di Entity Linking dipendono dai tempi di risposta dell'API pubblica di Wikidata; per ingestion intensive è consigliato l'uso di cache locali stratificate.
164
  - **Dipendenza da Vocabolari**: L'accuratezza dell'estrazione semantica tramite Schema-RAG fluttua in base alla ricchezza descrittiva del dizionario JSON ontologico fornito in ingresso.
165
 
 
 
 
 
 
 
 
166
  ## Riferimenti
167
 
168
  **Automated Semantic Discovery – Generazione Neuro-Simbolica di Ontologie Leggere e Vocabolari Semantici**
app.py CHANGED
@@ -70,6 +70,24 @@ def get_validator():
70
  shapes_file="ontology/shapes/auto_constraints.ttl"
71
  )
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  # Pre-load dei modelli in memoria
74
  _ = get_splitter()
75
  _ = get_extractor()
@@ -353,40 +371,112 @@ with tab_vis:
353
  if driver:
354
  col_ctrl, col_info = st.columns([1, 4])
355
  with col_ctrl:
356
- physics = st.checkbox("Abilita Fisica (Gravità)", value=True)
357
  generate_graph = st.button("🔄 Genera / Aggiorna Grafo", type="primary")
358
 
359
  if generate_graph:
360
  with st.spinner("Estrazione dati e generazione del grafo interattivo..."):
361
  cypher_vis = """
362
- MATCH (s)-[r]->(o)
363
- RETURN COALESCE(s["label"], s["name"], head(labels(s))) as src,
364
- type(r) as rel,
365
- COALESCE(o["label"], o["name"], head(labels(o))) as dst
366
- LIMIT 300
 
 
 
367
  """
368
  graph_data = run_query(driver, cypher_vis)
369
 
370
  if graph_data:
371
- net = Network(height="600px", width="100%", bgcolor="#222222", font_color="white", notebook=False)
 
372
  for item in graph_data:
373
- src, dst, rel = str(item['src']), str(item['dst']), str(item['rel'])
374
- net.add_node(src, label=src, color="#4facfe", title=src)
375
- net.add_node(dst, label=dst, color="#00f2fe", title=dst)
376
- net.add_edge(src, dst, title=rel, label=rel)
 
 
 
 
 
 
 
 
 
377
 
378
- net.toggle_physics(physics)
 
 
 
 
 
 
 
379
 
380
  with tempfile.NamedTemporaryFile(delete=False, suffix='.html') as tmp:
381
  net.save_graph(tmp.name)
382
  with open(tmp.name, 'r', encoding='utf-8') as f:
383
- st.session_state.graph_html = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  else:
385
  st.warning("Il grafo è attualmente vuoto.")
386
  st.session_state.graph_html = None
387
 
388
  if st.session_state.graph_html:
389
- components.html(st.session_state.graph_html, height=600, scrolling=True)
390
  else:
391
  st.info("👆 Clicca su 'Genera / Aggiorna Grafo' per visualizzare i dati attuali di Neo4j.")
392
 
 
70
  shapes_file="ontology/shapes/auto_constraints.ttl"
71
  )
72
 
73
+ COLOR_PALETTE = {
74
+ "arco_CulturalProperty": "#FF5733", # Arancio
75
+ "core_Agent": "#33FF57", # Verde
76
+ "l0_Location": "#3357FF", # Blu
77
+ "l0_Object": "#F333FF", # Viola
78
+ "core_EventOrSituation": "#FFD433",# Giallo
79
+ "clv_City": "#33FFF3", # Turchese
80
+ "DEFAULT": "#97C2FC" # Blu standard
81
+ }
82
+
83
+ def get_node_color(labels):
84
+ specific_labels = [l for l in labels if l != 'Resource']
85
+ if not specific_labels:
86
+ return COLOR_PALETTE["DEFAULT"]
87
+
88
+ label = specific_labels[0]
89
+ return COLOR_PALETTE.get(label, COLOR_PALETTE["DEFAULT"])
90
+
91
  # Pre-load dei modelli in memoria
92
  _ = get_splitter()
93
  _ = get_extractor()
 
371
  if driver:
372
  col_ctrl, col_info = st.columns([1, 4])
373
  with col_ctrl:
 
374
  generate_graph = st.button("🔄 Genera / Aggiorna Grafo", type="primary")
375
 
376
  if generate_graph:
377
  with st.spinner("Estrazione dati e generazione del grafo interattivo..."):
378
  cypher_vis = """
379
+ MATCH (s:Resource)
380
+ OPTIONAL MATCH (s)-[r]->(o:Resource)
381
+ RETURN
382
+ s.label AS src,
383
+ labels(s) AS src_labels,
384
+ type(r) AS rel,
385
+ o.label AS dst,
386
+ labels(o) AS dst_labels
387
  """
388
  graph_data = run_query(driver, cypher_vis)
389
 
390
  if graph_data:
391
+ net = Network(height="800px", width="100%", bgcolor="#222222", font_color="white", notebook=False)
392
+
393
  for item in graph_data:
394
+ if item['src']:
395
+ src_label_text = str(item['src'])
396
+ src_color = get_node_color(item['src_labels'])
397
+ net.add_node(src_label_text, label=src_label_text, color=src_color, title=f"Labels: {item['src_labels']}")
398
+
399
+ if item['dst'] and item['rel']:
400
+ dst_label_text = str(item['dst'])
401
+ rel_type = str(item['rel'])
402
+ dst_color = get_node_color(item['dst_labels'])
403
+
404
+ net.add_node(dst_label_text, label=dst_label_text, color=dst_color, title=f"Labels: {item['dst_labels']}")
405
+
406
+ net.add_edge(src_label_text, dst_label_text, title=rel_type)
407
 
408
+ net.force_atlas_2based(
409
+ gravity=-50,
410
+ central_gravity=0.01,
411
+ spring_length=100,
412
+ spring_strength=0.08,
413
+ damping=0.4
414
+ )
415
+ net.toggle_physics(True)
416
 
417
  with tempfile.NamedTemporaryFile(delete=False, suffix='.html') as tmp:
418
  net.save_graph(tmp.name)
419
  with open(tmp.name, 'r', encoding='utf-8') as f:
420
+ raw_html = f.read()
421
+
422
+ fullscreen_addon = """
423
+ <style>
424
+ /* Quando l'iframe entra in fullscreen, forziamo il div di Pyvis a coprire l'intero schermo */
425
+ :fullscreen #mynetwork { height: 100vh !important; width: 100vw !important; }
426
+ :-webkit-full-screen #mynetwork { height: 100vh !important; width: 100vw !important; }
427
+ :-moz-full-screen #mynetwork { height: 100vh !important; width: 100vw !important; }
428
+
429
+ #fs-btn {
430
+ position: absolute; top: 15px; right: 15px; z-index: 9999;
431
+ width: 40px; height: 40px;
432
+ background-color: rgba(34, 34, 34, 0.7);
433
+ color: #4facfe; border: 1px solid #4facfe; border-radius: 8px;
434
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
435
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: all 0.2s ease-in-out;
436
+ }
437
+ #fs-btn:hover { background-color: #4facfe; color: white; }
438
+ </style>
439
+
440
+ <button id="fs-btn" onclick="toggleFullScreen()" title="Schermo Intero">
441
+ <svg id="fs-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
442
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
443
+ </svg>
444
+ </button>
445
+
446
+ <script>
447
+ const iconExpand = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>';
448
+ const iconCompress = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>';
449
+
450
+ function toggleFullScreen() {
451
+ if (!document.fullscreenElement) {
452
+ document.documentElement.requestFullscreen().catch(err => console.log(err));
453
+ } else {
454
+ if (document.exitFullscreen) { document.exitFullscreen(); }
455
+ }
456
+ }
457
+
458
+ // Ascoltiamo l'evento fullscreen per cambiare l'icona (Espandi/Riduci) anche se l'utente preme "ESC"
459
+ document.addEventListener('fullscreenchange', (event) => {
460
+ const icon = document.getElementById('fs-icon');
461
+ if (document.fullscreenElement) {
462
+ icon.innerHTML = iconCompress;
463
+ document.getElementById('fs-btn').title = "Riduci Schermo";
464
+ } else {
465
+ icon.innerHTML = iconExpand;
466
+ document.getElementById('fs-btn').title = "Schermo Intero";
467
+ }
468
+ });
469
+ </script>
470
+ </body>
471
+ """
472
+
473
+ st.session_state.graph_html = raw_html.replace("</body>", fullscreen_addon)
474
  else:
475
  st.warning("Il grafo è attualmente vuoto.")
476
  st.session_state.graph_html = None
477
 
478
  if st.session_state.graph_html:
479
+ components.html(st.session_state.graph_html, height=800, scrolling=True)
480
  else:
481
  st.info("👆 Clicca su 'Genera / Aggiorna Grafo' per visualizzare i dati attuali di Neo4j.")
482
 
src/extraction/extractor.py CHANGED
@@ -145,12 +145,17 @@ class NeuroSymbolicExtractor:
145
  # ==========================================
146
  roots_text = "\n".join([f"- {uri} — \"{data['label']}: {data['description']}\"" for uri, data in self.root_classes.items()])
147
 
148
- sys_l1 = f"""Identifica le entità principali nel testo e assegna a ciascuna la macro-categoria più appropriata.
149
- Puoi assegnare fino a 2 candidati per entità se sei incerto, ordinandoli per confidenza.
150
 
151
  MACRO-CATEGORIE DISPONIBILI:
152
- {roots_text}"""
153
-
 
 
 
 
 
 
154
  res_l1: MacroClassificationResult = self._execute_with_retry(
155
  self.chain_pass1_l1,
156
  [SystemMessage(content=sys_l1), HumanMessage(content=text_chunk)]
@@ -204,7 +209,7 @@ SOTTO-TIPI DISPONIBILI:
204
  # PASS 2: Estrazione Relazionale
205
  # ==========================================
206
  # Mappa dei tipi finali
207
- typed_entities_map = {e.name: e.type for e in res_l2.entities}
208
 
209
  # Recupero deterministico delle proprietà
210
  valid_properties = []
@@ -228,10 +233,12 @@ PROPRIETÀ CONSENTITE (con vincoli domain → range):
228
  {props_text}
229
  - skos:related: Qualsiasi → Qualsiasi (Usa SOLO se nessuna proprietà sopra descrive accuratamente il legame)
230
 
231
- REGOLE CRITICHE:
232
  1. Usa SOLO le proprietà elencate sopra.
233
- 2. Rispetta rigorosamente i vincoli ontologici: il tipo del 'subject' DEVE essere compatibile con il domain, e il tipo dell''object' con il range.
234
- 3. Compila sempre i campi 'evidence' citando esattamente il testo, e 'reasoning' spiegando la scelta logica.
 
 
235
  """
236
 
237
  final_res: KnowledgeGraphExtraction = self._execute_with_retry(
 
145
  # ==========================================
146
  roots_text = "\n".join([f"- {uri} — \"{data['label']}: {data['description']}\"" for uri, data in self.root_classes.items()])
147
 
148
+ sys_l1 = f"""Sei un estrattore esperto di entità semantiche per il dominio dei Beni Culturali. Il tuo unico compito è individuare le entità rilevanti nel testo e classificarle.
 
149
 
150
  MACRO-CATEGORIE DISPONIBILI:
151
+ {roots_text}
152
+
153
+ REGOLE DI ESTRAZIONE (TASSATIVE E OBBLIGATORIE):
154
+ 1. DIVIETO DI ALLUCINAZIONE URI: Usa ESCLUSIVAMENTE gli URI esatti elencati sopra. È severamente vietato usare etichette inventate come "Person", "Location" o "Group". Se devi categorizzare una persona, usa l'URI corrispondente agli Agenti (es. core:Agent o l0:Agent).
155
+ 2. RUMORE EDITORIALE: IGNORA e non estrarre MAI riferimenti alla struttura del libro o alle immagini. È vietato estrarre entità che contengono o sono composte da: "Capitolo", "Sezione", "Tavola", "Fig.", "Figura", "Pagina", "Pag.".
156
+ 3. Estrai SOLO veri monumenti storici, luoghi geografici reali, personaggi storici, popoli e concetti architettonici.
157
+ 4. Puoi assegnare fino a 2 candidati per entità, ordinandoli per confidenza logica.
158
+ """
159
  res_l1: MacroClassificationResult = self._execute_with_retry(
160
  self.chain_pass1_l1,
161
  [SystemMessage(content=sys_l1), HumanMessage(content=text_chunk)]
 
209
  # PASS 2: Estrazione Relazionale
210
  # ==========================================
211
  # Mappa dei tipi finali
212
+ typed_entities_map = {e.name: e.type.strip() for e in res_l2.entities}
213
 
214
  # Recupero deterministico delle proprietà
215
  valid_properties = []
 
233
  {props_text}
234
  - skos:related: Qualsiasi → Qualsiasi (Usa SOLO se nessuna proprietà sopra descrive accuratamente il legame)
235
 
236
+ REGOLE CRITICHE E OBBLIGATORIE:
237
  1. Usa SOLO le proprietà elencate sopra.
238
+ 2. Usa ESCLUSIVAMENTE le entità presenti nella lista "ENTITÀ IDENTIFICATE". È severamente vietato inventare o aggiungere entità non presenti in questo elenco.
239
+ 3. I campi 'subject_type' e 'object_type' sono OBBLIGATORI. Devi sempre compilarli copiando esattamente il tipo indicato tra parentesi nella lista delle entità.
240
+ 4. Rispetta rigorosamente i vincoli ontologici: il tipo del 'subject' DEVE essere compatibile con il domain, e il tipo dell''object' con il range.
241
+ 5. Compila sempre i campi 'evidence' citando esattamente il testo, e 'reasoning' spiegando la scelta logica.
242
  """
243
 
244
  final_res: KnowledgeGraphExtraction = self._execute_with_retry(
src/graph/entity_resolver.py CHANGED
@@ -1,5 +1,6 @@
1
  import numpy as np
2
  import requests
 
3
  from sklearn.cluster import DBSCAN
4
  from langchain_huggingface import HuggingFaceEmbeddings
5
 
@@ -48,21 +49,30 @@ class EntityResolver:
48
  "search": entity_name,
49
  "language": "it",
50
  "format": "json",
51
- "limit": 1 # Ci serve solo il top-match per fare riconciliazione a tappeto, niente paginazione.
52
  }
 
 
 
 
 
53
  try:
54
- # Metto un timeout super restrittivo (3s). Se Wikidata è congestionato,
55
- # preferisco fallire silenziosamente il linking piuttosto che bloccare tutta l'ingestion della pipeline.
56
- response = requests.get(url, params=params, timeout=3.0)
57
  if response.status_code == 200:
58
  data = response.json()
59
  if data.get("search"):
60
  best_match = data["search"][0]
61
  return f"wd:{best_match['id']}"
62
  else:
63
- print(f" [DEBUG] Wikidata non ha trovato corrispondenze per: '{entity_name}'")
 
 
 
 
64
  except Exception as e:
65
  print(f" ⚠️ Errore lookup Wikidata per '{entity_name}' (ignorato): {e}")
 
66
  return None
67
 
68
  def resolve_entities(self, extracted_entities, triples):
@@ -142,4 +152,44 @@ class EntityResolver:
142
 
143
  resolved_entities = list(set([entity_replacement_map.get(e, e) for e in extracted_entities]))
144
 
145
- return resolved_entities, resolved_triples, entities_to_save
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import numpy as np
2
  import requests
3
+ from collections import Counter
4
  from sklearn.cluster import DBSCAN
5
  from langchain_huggingface import HuggingFaceEmbeddings
6
 
 
49
  "search": entity_name,
50
  "language": "it",
51
  "format": "json",
52
+ "limit": 1
53
  }
54
+
55
+ headers = {
56
+ "User-Agent": "ActivaSemanticDiscoveryBot/1.0 (https://activadigital.it; contact@activadigital.it) python-requests"
57
+ }
58
+
59
  try:
60
+ response = requests.get(url, params=params, headers=headers, timeout=3.0)
61
+
 
62
  if response.status_code == 200:
63
  data = response.json()
64
  if data.get("search"):
65
  best_match = data["search"][0]
66
  return f"wd:{best_match['id']}"
67
  else:
68
+ print(f" [DEBUG] Wikidata vuoto per: '{entity_name}'")
69
+ pass
70
+ else:
71
+ print(f" ⚠️ Wikidata ha rifiutato la richiesta. Status: {response.status_code}")
72
+
73
  except Exception as e:
74
  print(f" ⚠️ Errore lookup Wikidata per '{entity_name}' (ignorato): {e}")
75
+
76
  return None
77
 
78
  def resolve_entities(self, extracted_entities, triples):
 
152
 
153
  resolved_entities = list(set([entity_replacement_map.get(e, e) for e in extracted_entities]))
154
 
155
+ resolved_triples = self._normalize_types(resolved_triples)
156
+
157
+ return resolved_entities, resolved_triples, entities_to_save
158
+
159
+
160
+ def _normalize_types(self, resolved_triples):
161
+ print("⚖️ Normalizzazione Ontologica: Avvio Majority Voting per i tipi...")
162
+
163
+ # 1. Raccogliamo i voti: contiamo quante volte ogni tipo viene assegnato a un'entità
164
+ type_votes = {}
165
+
166
+ for t in resolved_triples:
167
+ # Conteggio per il subject
168
+ if t.subject not in type_votes:
169
+ type_votes[t.subject] = Counter()
170
+ type_votes[t.subject][t.subject_type] += 1
171
+
172
+ # Conteggio per l'object
173
+ if t.object not in type_votes:
174
+ type_votes[t.object] = Counter()
175
+ type_votes[t.object][t.object_type] += 1
176
+
177
+ # 2. Eleggiamo il vincitore: creiamo una mappa definitiva { "Nome Entità": "Tipo Dominante" }
178
+ canonical_types = {}
179
+ for entity, counter in type_votes.items():
180
+ # most_common(1) restituisce una lista di tuple es. [('cis:CreativeWork', 4)]
181
+ winning_type = counter.most_common(1)[0][0]
182
+ canonical_types[entity] = winning_type
183
+
184
+ # Opzionale: log se c'è stata una correzione
185
+ if len(counter) > 1:
186
+ print(f" -> Normalizzata '{entity}' a {winning_type} (Scartati: {list(counter.keys())})")
187
+
188
+ # 3. Riscriviamo le triple con il tipo vincitore
189
+ final_triples = []
190
+ for t in resolved_triples:
191
+ t.subject_type = canonical_types[t.subject]
192
+ t.object_type = canonical_types[t.object]
193
+ final_triples.append(t)
194
+
195
+ return final_triples
src/graph/graph_loader.py CHANGED
@@ -77,7 +77,7 @@ class KnowledgeGraphPersister:
77
  # Convenzione Neo4j: le relationships sono sempre in UPPERCASE
78
  return clean.upper() if clean else "RELATED_TO"
79
 
80
- def save_triples(self, triples):
81
  if not self.driver or not triples:
82
  return
83
 
 
77
  # Convenzione Neo4j: le relationships sono sempre in UPPERCASE
78
  return clean.upper() if clean else "RELATED_TO"
79
 
80
+ def save_triples(self, triples):
81
  if not self.driver or not triples:
82
  return
83