GaetanoParente commited on
Commit
cc3f780
·
1 Parent(s): fc23ce5

integrata riconciliazione semantica ed estrazione singole entità

Browse files
.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================================
2
+ # CONFIGURAZIONE AMBIENTE LOCALE
3
+ # ==========================================
4
+ # Copia questo file rinominandolo in ".env" e inserisci i tuoi valori.
5
+ # ATTENZIONE: Non committare MAI il file ".env" nel repository!
6
+
7
+ # --- Credenziali Backend LLM ---
8
+ # Token per le Inference API di Hugging Face (necessario per Llama 3 / Mistral)
9
+ HF_TOKEN=hf_qui_il_tuo_token_huggingface
10
+
11
+ # (Opzionale) API Key per Groq, usato come fallback ultra-veloce nell'extractor
12
+ GROQ_API_KEY=gsk_qui_la_tua_api_key_groq
13
+
14
+ # --- Connessione Knowledge Graph (Neo4j AuraDB o Locale) ---
15
+ # URI di connessione al database (es. neo4j+s://xxxxx.databases.neo4j.io per AuraDB)
16
+ NEO4J_URI=bolt://localhost:7687
17
+
18
+ # Utente del database (di default 'neo4j')
19
+ NEO4J_USER=neo4j
20
+
21
+ # Password del database
22
+ NEO4J_PASSWORD=la_tua_password_super_segreta
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__
2
+ .env
Dockerfile CHANGED
@@ -1,32 +1,24 @@
1
- # Usa un'immagine base di Python
2
- FROM python:3.10-slim
3
 
4
- # Imposta la workdir
5
  WORKDIR /app
6
 
7
- # Installa le dipendenze di sistema necessarie (es. per pyvis o compilatori)
8
  RUN apt-get update && apt-get install -y \
9
  build-essential \
10
  curl \
11
  git \
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
- # Copia i requirements e installali
15
  COPY requirements.txt .
16
- RUN pip3 install -r requirements.txt
17
 
18
  RUN python -m nltk.downloader punkt punkt_tab
19
-
20
  RUN python -m spacy download it_core_news_sm
21
 
22
- # Copia tutto il codice dell'applicazione
23
  COPY . .
24
 
25
- # Espone la porta usata da Hugging Face Spaces (7860)
26
  EXPOSE 7860
27
 
28
- # Healthcheck per monitorare lo stato dello space (corretta porta 7860)
29
  HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health
30
 
31
- # Comando di avvio specifico per Streamlit su Docker
32
  ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
 
1
+ FROM python:3.13-slim
 
2
 
 
3
  WORKDIR /app
4
 
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
7
  curl \
8
  git \
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
11
  COPY requirements.txt .
12
+ RUN pip3 install --no-cache-dir -r requirements.txt
13
 
14
  RUN python -m nltk.downloader punkt punkt_tab
 
15
  RUN python -m spacy download it_core_news_sm
16
 
 
17
  COPY . .
18
 
 
19
  EXPOSE 7860
20
 
21
+ # Healthcheck per monitorare lo stato dello space
22
  HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health
23
 
 
24
  ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -6,62 +6,55 @@ colorFrom: blue
6
  colorTo: red
7
  pinned: false
8
  emoji: 🧠
9
- short_description: prototipo di sistema per la scoperta semantica automatica
10
  ---
11
 
12
  # Automated Semantic Discovery – Prototype
13
 
14
- ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
 
 
15
  ![Neo4j](https://img.shields.io/badge/graphdb-Neo4j-green)
16
- ![Status](https://img.shields.io/badge/status-research%20prototype-orange)
17
 
18
- Questo repository contiene un **prototipo di sistema per la scoperta semantica automatica (Automated Semantic Discovery)**, finalizzato alla generazione di **ontologie leggere** e **vocabolari semantici** a partire da **corpora documentali non strutturati**.
19
 
20
- Il progetto nasce come **proof-of-concept di ricerca** e implementa una **pipeline neuro-simbolica** che integra:
 
 
21
 
22
- - la potenza rappresentazionale dei **modelli vettoriali** (*Neuro*);
23
- - regole di **estrazione ed inferenza NLP** (*Symbolic*).
 
24
 
25
  ## Obiettivi del prototipo
26
 
27
- Il prototipo ha i seguenti obiettivi principali:
28
-
29
- - dimostrare la fattibilità di una **pipeline automatizzata di Semantic Knowledge Discovery**;
30
- - ridurre il **knowledge acquisition bottleneck** nella costruzione di grafi di conoscenza;
31
- - validare un **approccio modulare e scalabile** alla scoperta semantica;
32
- - fornire una **base sperimentale per architetture GraphRAG**.
33
-
34
- > Il sistema **non è un prodotto industriale**, ma un **laboratorio sperimentale orientato alla ricerca applicata**.
35
 
36
  ## Workflow Architetturale
37
 
38
- <p align="center">
39
- <img src="docs/workflow.png" alt="Workflow Architetturale della Pipeline Neuro-Simbolica" width="90%">
40
- </p>
41
-
42
- ## Moduli della Pipeline
43
-
44
- La pipeline è organizzata in **moduli indipendenti e sequenziali**.
45
-
46
- ### 1. Ingestion & Pre-processing
47
 
48
- - Caricamento dei documenti testuali.
49
- - Normalizzazione e pulizia del testo.
50
 
51
- ### 2. Semantic Chunking (Componente *Neuro*)
 
 
52
 
53
- - Segmentazione del testo basata su **similarità semantica vettoriale**, non solo sintattica.
54
- - Utilizzo di **modelli di embedding** per garantire la coerenza tematica dei frammenti.
 
 
55
 
56
- ### 3. Information Extraction (Componente *Simbolica*)
 
57
 
58
- - Estrazione di **entità (NER)** e **relazioni** tramite analisi delle dipendenze sintattiche.
59
- - Produzione di **strutture intermedie** sotto forma di **triple concettuali (Soggetto–Predicato–Oggetto)**.
60
-
61
- ### 4. Knowledge Graph Construction
62
-
63
- - Mapping delle triple estratte nel **modello a grafo**.
64
- - Persistenza su **database a grafo (Neo4j)**.
65
 
66
  ## Struttura del repository
67
 
@@ -69,134 +62,116 @@ La pipeline è organizzata in **moduli indipendenti e sequenziali**.
69
  prototipo/
70
 
71
  ├── data/
72
- ── examples/ # Documenti da utilizzare nella demo del prototipo
73
- │ ├── raw/ # Documenti di input grezzi
74
- │ ├── processed/ # Output intermedi (chunk, debug JSON)
75
- │ └── gold_standard/ # Esempi e dati di riferimento
76
 
77
  ├── src/
78
  │ ├── ingestion/
79
  │ │ └── semantic_splitter.py
80
  │ ├── extraction/
81
  │ │ └── extractor.py
 
 
 
 
82
  │ └── graph/
83
- ── graph_builder.py
 
84
 
85
- ├── neo4j/ # Script o Docker Compose per il DB
86
- ├── .env.example # Template per le variabili d'ambiente
87
- ├── requirements.txt
 
 
88
  └── README.md
89
  ```
90
 
91
  ## Tech Stack & Requisiti
92
 
93
- - **Linguaggio**: Python 3.10+
94
- - **Database**: Neo4j (Community / Enterprise)
 
95
 
96
  ### Core Libraries
97
 
98
- - **Neuro / Vectors**
99
- `sentence-transformers`, `scikit-learn`
100
 
101
- - **NLP / Symbolic**
102
- `spacy`, `nltk`
103
 
104
- - **Data & Graph**
105
- `pandas`, `neo4j-driver`
106
 
107
  > Le dipendenze complete sono elencate in `requirements.txt`.
108
 
109
- ## Configurazione
110
 
111
- Creare un file `.env` nella root del progetto:
112
 
113
  ```env
114
- NEO4J_URI=bolt://localhost:7687
115
  NEO4J_USER=neo4j
116
- NEO4J_PASSWORD=la_tua_password_locale
 
 
117
  ```
 
118
 
119
- **Nota**: assicurarsi che il file `.env` sia incluso nel `.gitignore`.
120
-
121
- ## Installazione
122
 
123
  ```bash
124
- git clone https://github.com/<username>/<repository>.git
 
125
  cd prototipo
126
 
 
127
  python -m venv venv
128
  source venv/bin/activate # Linux / macOS
129
- # venv\\Scripts\activate # Windows
130
 
 
131
  pip install -r requirements.txt
132
  ```
 
133
 
134
- ## Utilizzo del prototipo
135
-
136
- ### 1. Inserimento dei documenti
137
-
138
- Copiare i documenti in `data/raw/`.
139
-
140
- ### 2. Segmentazione semantica
141
 
142
  ```bash
143
- python src/ingestion/semantic_splitter.py
144
  ```
145
 
146
- ### 3. Estrazione di entità e relazioni
147
 
148
- ```bash
149
- python src/extraction/extractor.py
150
- ```
151
 
152
- ### 4. Costruzione del Knowledge Graph
153
 
154
  ```bash
155
- python src/graph/graph_builder.py
156
  ```
157
 
158
- ## Output
159
-
160
- Il sistema produce:
161
-
162
- - file JSON intermedi per il tracciamento e il debug della pipeline;
163
- - dati strutturati utilizzabili per validazione manuale o semi-automatica;
164
- - un Knowledge Graph persistente su Neo4j, interrogabile tramite Cypher.
165
-
166
- ## Risultati e Validazione Visiva
167
-
168
- Questa sezione mostra alcuni output significativi del prototipo,
169
- utilizzati per la validazione qualitativa della pipeline di scoperta semantica.
170
-
171
- ### Validazione delle estrazioni
172
-
173
- <p align="center">
174
- <img src="docs/validation.png" alt="Validazione delle entità estratte" width="90%">
175
- </p>
176
-
177
- Lo screenshot mostra esempi di entità e relazioni estratte a partire dai chunk semantici,
178
- utilizzati per verificare la correttezza e la coerenza delle triple generate.
179
 
180
- ### Visualizzazione del Knowledge Graph
181
 
182
- <p align="center">
183
- <img src="docs/graph.png" alt="Grafo risultante su Neo4j" width="90%">
184
- </p>
185
 
186
- Il grafo risultante è persistito su Neo4j ed esplorabile tramite Neo4j Browser,
187
- consentendo l’analisi interattiva delle entità e delle relazioni scoperte.
 
 
188
 
189
  ## Limiti noti
190
 
191
- - **Scalabilità**: prototipo non ottimizzato per ingestione massiva.
192
- - **Reasoning**: regole simboliche basate su euristiche, dominio-dipendenti.
193
- - **LLM**: uso intenzionalmente limitato per privilegiare determinismo e spiegabilità.
194
 
195
  ## Possibili estensioni future
196
 
197
- - Integrazione LLM / GraphRAG
198
- - Supporto RDF / OWL / SHACL
199
- - Dockerizzazione
 
200
 
201
  ## Riferimenti
202
 
 
6
  colorTo: red
7
  pinned: false
8
  emoji: 🧠
9
+ short_description: Prototipo API neuro-simbolico per la scoperta semantica automatica e Knowledge Graph
10
  ---
11
 
12
  # Automated Semantic Discovery – Prototype
13
 
14
+ ![Python](https://img.shields.io/badge/python-3.13-blue)
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.
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
+ - Estrazione dinamica (Dynamic Few-Shot) di entità e relazioni tramite **LLM (Llama 3 / Groq / HF)**.
46
+ - Forzatura dell'output in strutture dati tipizzate tramite validazione **Pydantic**, con recupero di concetti isolati.
47
 
48
+ ### 3. Stateful Entity Resolution & Linking (`entity_resolver.py`)
49
+ - Deduplica locale in RAM tramite clustering spaziale (**DBSCAN** su embedding cosine-similarity).
50
+ - Risoluzione globale interrogando i **Vector Index nativi di Neo4j**.
51
+ - **Entity Linking** asincrono tramite chiamate REST all'API di **Wikidata** per l'ancoraggio semantico (`owl:sameAs`).
52
 
53
+ ### 4. Semantic Validation (`validator.py`)
54
+ - Validazione topologica e qualitativa dei dati estratti applicando vincoli ontologici deterministici (**SHACL**) tramite `pyshacl`.
55
 
56
+ ### 5. Knowledge Graph Persistence (`graph_loader.py`)
57
+ - Salvataggio massivo e transazionale (`UNWIND` Cypher) su database a grafo **Neo4j**, includendo gli embedding vettoriali per le ricerche future.
 
 
 
 
 
58
 
59
  ## Struttura del repository
60
 
 
62
  prototipo/
63
 
64
  ├── data/
65
+ ── gold_standard/ # Esempi (JSON) per il prompt dinamico dell'LLM
 
 
 
66
 
67
  ├── src/
68
  │ ├── ingestion/
69
  │ │ └── semantic_splitter.py
70
  │ ├── extraction/
71
  │ │ └── extractor.py
72
+ │ ├── validation/
73
+ │ │ ├── validator.py
74
+ │ │ └── shapes/
75
+ │ │ └── schema_constraints.ttl # Regole SHACL
76
  │ └── graph/
77
+ ── graph_loader.py
78
+ │ └── entity_resolver.py
79
 
80
+ ├── app.py # Entrypoint Web UI (Streamlit / Hugging Face)
81
+ ├── api.py # Entrypoint API REST (FastAPI)
82
+ ├── Dockerfile # Configurazione container per HF Spaces
83
+ ├── .env.example # Template per le variabili d'ambiente locali
84
+ ├── requirements.txt
85
  └── README.md
86
  ```
87
 
88
  ## Tech Stack & Requisiti
89
 
90
+ - **Linguaggio**: Python 3.13
91
+ - **Database**: Neo4j (Consigliato AuraDB cloud per istanze distribuite)
92
+ - **Interfacce**: FastAPI, Uvicorn, Streamlit
93
 
94
  ### Core Libraries
95
 
96
+ - **Neuro / LLM**
97
+ `transformers`, `langchain`, `langchain-huggingface`, `langchain-groq`, `sentence-transformers`
98
 
99
+ - **Symbolic / Graph**
100
+ `neo4j`, `rdflib`, `pyshacl`, `scikit-learn`
101
 
102
+ - **UI & Viz:**
103
+ `streamlit`, `pyvis`, `pandas`
104
 
105
  > Le dipendenze complete sono elencate in `requirements.txt`.
106
 
107
+ ## Configurazione Locale
108
 
109
+ Per testare il sistema in locale, creare un file `.env` a partire dal template:
110
 
111
  ```env
112
+ NEO4J_URI=neo4j+s://<tuo-cluster>.databases.neo4j.io
113
  NEO4J_USER=neo4j
114
+ NEO4J_PASSWORD=la_tua_password
115
+ HF_TOKEN=tuo_token_huggingface_opzionale
116
+ GROQ_API_KEY=tua_api_key_groq_opzionale
117
  ```
118
+ (Nota: Su Hugging Face Spaces, queste variabili vanno configurate nei "Secrets" delle impostazioni).
119
 
120
+ ## Installazione ed Esecuzione
 
 
121
 
122
  ```bash
123
+ # 1. Clona il repository e posizionati nella cartella
124
+ git clone [https://github.com/](https://github.com/)<username>/<repository>.git
125
  cd prototipo
126
 
127
+ # 2. Crea l'ambiente virtuale e attivalo
128
  python -m venv venv
129
  source venv/bin/activate # Linux / macOS
130
+ # venv\Scripts\activate # Windows
131
 
132
+ # 3. Installa le dipendenze
133
  pip install -r requirements.txt
134
  ```
135
+ ## Modalità 1: Interfaccia Visuale (Demo / HITL)
136
 
137
+ Avvia la dashboard per testare visivamente l'estrazione e ispezionare il grafo interattivo:
 
 
 
 
 
 
138
 
139
  ```bash
140
+ streamlit run app.py
141
  ```
142
 
143
+ L'interfaccia sarà disponibile su `http://localhost:8501`.
144
 
145
+ ## Modalità 2: Servizio API (Integrazione Backend)
 
 
146
 
147
+ Avvia il motore in modalità headless per metterlo in ascolto di payload JSON:
148
 
149
  ```bash
150
+ python api.py
151
  ```
152
 
153
+ L'endpoint sarà disponibile su `http://0.0.0.0:5000/api/discover`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ ## Output dell'API
156
 
157
+ Il sistema produce una risposta JSON strutturata contenente:
 
 
158
 
159
+ - Statistiche di esecuzione (tempo, chunk elaborati).
160
+ - Esito della validazione SHACL.
161
+ - La lista completa delle triple riconciliate e validate.
162
+ - Il feedback di avvenuto inserimento massivo su Neo4j.
163
 
164
  ## Limiti noti
165
 
166
+ - **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.
167
+ - **Dipendenza da LLM**: L'accuratezza dell'estrazione (confidence) fluttua in base al modello configurato e necessita di continui affinamenti del file `examples.json` (Gold Standard).
 
168
 
169
  ## Possibili estensioni future
170
 
171
+ - 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.
172
+ - Sviluppo di uno strato GraphRAG.
173
+ - 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.
174
+ - Dockerizzazione multi-container per deploy enterprise in ambienti Kubernetes.
175
 
176
  ## Riferimenti
177
 
api.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ import uvicorn
4
+ import os
5
+ import time
6
+ import hashlib
7
+
8
+ from src.ingestion.semantic_splitter import ActivaSemanticSplitter
9
+ from src.extraction.extractor import NeuroSymbolicExtractor
10
+ from src.validation.validator import SemanticValidator
11
+ from src.graph.graph_loader import KnowledgeGraphPersister
12
+ from src.graph.entity_resolver import EntityResolver
13
+
14
+ app = FastAPI(
15
+ title="Automated Semantic Discovery API",
16
+ description="Endpoint per l'ingestion testuale e l'estrazione neuro-simbolica",
17
+ version="1.0"
18
+ )
19
+
20
+ # Struttura del JSON in ingresso
21
+ class DiscoveryRequest(BaseModel):
22
+ documentText: str
23
+
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()
32
+ print("✅ Modelli caricati e pronti a ricevere richieste!")
33
+
34
+ # Endpoint principale
35
+ @app.post("/api/discover")
36
+ def run_discovery(payload: DiscoveryRequest):
37
+ start_time = time.time()
38
+ raw_text = payload.documentText
39
+
40
+ if not raw_text or not raw_text.strip():
41
+ raise HTTPException(status_code=400, detail="Il testo fornito è vuoto.")
42
+
43
+ # --- FASE 1: INGESTION ---
44
+ chunks, _, _ = splitter.create_chunks(raw_text, percentile_threshold=90)
45
+
46
+ # --- FASE 2: EXTRACTION ---
47
+ all_triples = []
48
+ all_entities = []
49
+ for i, chunk in enumerate(chunks):
50
+ chunk_id = f"api_req_chunk_{i+1}"
51
+ extraction_result = extractor.extract(chunk, source_id=chunk_id)
52
+
53
+ if extraction_result:
54
+ if extraction_result.triples:
55
+ all_triples.extend(extraction_result.triples)
56
+ if hasattr(extraction_result, 'entities') and extraction_result.entities:
57
+ all_entities.extend(extraction_result.entities)
58
+
59
+ if not all_triples:
60
+ return {
61
+ "status": "success",
62
+ "message": "Nessuna entità trovata.",
63
+ "graph_data": [] # Restituisco un array vuoto invece di fallire
64
+ }
65
+
66
+ # --- FASE 2.1: SYMBOLIC RESOLUTION ---
67
+ entities_to_save = []
68
+ try:
69
+ all_entities, all_triples, entities_to_save = resolver.resolve_entities(all_entities, all_triples)
70
+ except Exception as e:
71
+ print(f"⚠️ Errore nel resolver (skip): {e}")
72
+
73
+ # --- FASE 2.2: VALIDATION ---
74
+ is_valid, report, _ = validator.validate_batch(entities_to_save, all_triples)
75
+ if not is_valid:
76
+ print("\n❌ [SHACL VALIDATION FAILED] Rilevate entità o relazioni non conformi all'ontologia:")
77
+ # Il report di pyshacl contiene già l'elenco esatto dei nodi e delle regole violate
78
+ print(report)
79
+ print("-" * 60)
80
+ else:
81
+ print("\n✅ [SHACL VALIDATION SUCCESS] Tutte le triple ed entità rispettano i vincoli.")
82
+
83
+ # --- FASE 3: PERSISTENCE (Neo4j) ---
84
+ try:
85
+ persister.save_entities_and_triples(entities_to_save, all_triples)
86
+ persister.close()
87
+ except Exception as e:
88
+ print(f"⚠️ Errore salvataggio Neo4j: {e}")
89
+
90
+ graph_data = []
91
+ for t in all_triples:
92
+ subj = getattr(t, 'subject', t[0] if isinstance(t, tuple) else str(t))
93
+ pred = getattr(t, 'predicate', t[1] if isinstance(t, tuple) else '')
94
+ obj = getattr(t, 'object', t[2] if isinstance(t, tuple) else '')
95
+
96
+ if isinstance(t, tuple) and len(t) > 3:
97
+ conf = t[3]
98
+ else:
99
+ conf = getattr(t, 'confidence', 1.0)
100
+
101
+ subj_str = str(subj)
102
+ pred_str = str(pred)
103
+ obj_str = str(obj)
104
+
105
+ # Genero un ID univoco ma stabile per il nodo di partenza basato sul suo nome.
106
+ node_id = hashlib.md5(subj_str.encode('utf-8')).hexdigest()
107
+
108
+ graph_data.append({
109
+ "start_node_id": node_id,
110
+ "start_node_label": subj_str,
111
+ "relationship_type": pred_str,
112
+ "end_node_label": obj_str,
113
+ "confidence": float(conf)
114
+ })
115
+
116
+ return {
117
+ "status": "success",
118
+ "message": "Estrazione semantica completata",
119
+ "execution_time_seconds": round(time.time() - start_time, 2),
120
+ "chunks_processed": len(chunks),
121
+ "triples_extracted": len(graph_data),
122
+ "shacl_valid": is_valid,
123
+ "graph_data": graph_data
124
+ }
125
+
126
+ if __name__ == "__main__":
127
+ uvicorn.run(app, host="0.0.0.0", port=5000)
app.py CHANGED
@@ -1,10 +1,7 @@
1
  import streamlit as st
2
  import os
3
- import shutil
4
- import csv
5
- import json
6
  import pandas as pd
7
- from datetime import datetime
8
  from neo4j import GraphDatabase
9
  from pyvis.network import Network
10
  import streamlit.components.v1 as components
@@ -13,6 +10,7 @@ from dotenv import load_dotenv
13
  # --- IMPORT MODULI SPECIFICI ---
14
  from src.ingestion.semantic_splitter import ActivaSemanticSplitter
15
  from src.extraction.extractor import NeuroSymbolicExtractor, GraphTriple
 
16
  from src.graph.graph_loader import KnowledgeGraphPersister
17
  from src.graph.entity_resolver import EntityResolver
18
 
@@ -25,64 +23,60 @@ st.set_page_config(
25
  page_icon="🧠"
26
  )
27
 
28
- # --- CSS CUSTOM PER UX MIGLIORATA ---
29
- # Rende le card più leggibili e stilizza i messaggi di stato
30
- st.markdown("""
31
- <style>
32
- .step-card {
33
- padding: 20px;
34
- border-radius: 10px;
35
- border: 1px solid #e0e0e0;
36
- margin-bottom: 20px;
37
- background-color: #262730;
38
- }
39
- .step-header {
40
- font-size: 1.2rem;
41
- font-weight: bold;
42
- margin-bottom: 10px;
43
- color: #4facfe;
44
- }
45
- .success-box {
46
- padding: 10px;
47
- background-color: rgba(76, 175, 80, 0.1);
48
- border-left: 5px solid #4CAF50;
49
- border-radius: 5px;
50
- }
51
- </style>
52
- """, unsafe_allow_html=True)
53
-
54
- # --- SESSION STATE MANAGEMENT ---
55
  if 'pipeline_stage' not in st.session_state:
56
- st.session_state.pipeline_stage = 0 # 0: Init, 1: Chunked, 2: Extracted, 3: Loaded
57
- if 'current_file' not in st.session_state:
58
- st.session_state.current_file = None
 
 
 
 
 
 
59
 
60
  def reset_pipeline():
61
  st.session_state.pipeline_stage = 0
62
- st.session_state.current_file = None
63
- # Pulisce i file processati per evitare incongruenze
64
- if os.path.exists("data/processed"):
65
- shutil.rmtree("data/processed")
66
- os.makedirs("data/processed", exist_ok=True)
67
 
68
  # --- CACHING RISORSE ---
69
  @st.cache_resource
70
  def get_splitter():
71
- return ActivaSemanticSplitter()
72
 
73
  @st.cache_resource
74
  def get_extractor():
75
- return NeuroSymbolicExtractor()
 
76
 
77
- @st.cache_resource
78
  def get_resolver():
79
- return EntityResolver(similarity_threshold=0.85)
 
 
 
 
 
 
 
 
 
80
 
81
  # --- FUNZIONI NEO4J ---
82
  def get_driver(uri, user, password):
83
  if not uri or not password: return None
84
  try:
85
- return GraphDatabase.driver(uri, auth=(user, password))
 
 
86
  except: return None
87
 
88
  def run_query(driver, query, params=None):
@@ -94,38 +88,26 @@ def run_query(driver, query, params=None):
94
  # --- UI: SIDEBAR ---
95
  st.sidebar.title("⚙️ Configurazione")
96
 
97
- # Recuperiamo le variabili d'ambiente (Server Side)
98
- # NOTA: Queste variabili contengono i veri segreti ma NON vengono passate al frontend
99
  env_uri = os.getenv("NEO4J_URI", "")
100
  env_user = os.getenv("NEO4J_USER", "neo4j")
101
  env_password = os.getenv("NEO4J_PASSWORD", "")
102
  env_hf_token = os.getenv("HF_TOKEN", "")
103
 
104
  st.sidebar.subheader("Backend AI")
105
- # LOGICA SICURA: Se il token esiste nei secrets, mostriamo solo un badge verde.
106
- # Non mostriamo mai il token nel campo input (value=...).
107
  if env_hf_token:
108
  st.sidebar.success("✅ HF Token: Configurato da Secrets")
109
- # Se l'utente vuole sovrascriverlo, può usare questo campo opzionale
110
  hf_token_input = st.sidebar.text_input("Sovrascrivi Token (Opzionale)", type="password", key="hf_token_override")
111
- if hf_token_input:
112
- os.environ["HF_TOKEN"] = hf_token_input
113
  else:
114
  hf_token_input = st.sidebar.text_input("Inserisci HF Token", type="password")
115
- if hf_token_input:
116
- os.environ["HF_TOKEN"] = hf_token_input
117
 
118
  st.sidebar.subheader("Knowledge Graph")
119
- # URI e User non sono segreti critici, possiamo mostrarli pre-compilati
120
  uri = st.sidebar.text_input("URI", value=env_uri)
121
  user = st.sidebar.text_input("User", value=env_user)
122
 
123
- # LOGICA SICURA: Gestione Password
124
- # Non usiamo 'value=env_password' per evitare che finisca nell'HTML.
125
- pwd_placeholder = "✅ Configurato da Secrets (Lascia vuoto)" if env_password else "Inserisci Password"
126
  password_input = st.sidebar.text_input("Password", type="password", placeholder=pwd_placeholder)
127
-
128
- # Determiniamo quale password usare (Input Utente > Secret Env)
129
  password = password_input if password_input else env_password
130
 
131
  driver = None
@@ -133,7 +115,6 @@ if uri and password:
133
  driver = get_driver(uri, user, password)
134
  if driver:
135
  st.sidebar.success("🟢 Connesso a Neo4j")
136
- # Aggiorniamo l'ambiente per i moduli backend che usano os.getenv
137
  os.environ["NEO4J_URI"] = uri
138
  os.environ["NEO4J_USER"] = user
139
  os.environ["NEO4J_PASSWORD"] = password
@@ -146,9 +127,8 @@ if st.sidebar.button("🔄 Reset Pipeline", on_click=reset_pipeline):
146
 
147
  # --- MAIN HEADER ---
148
  st.title("🧠 Automated Semantic Discovery Prototype")
149
- st.markdown("**Pipeline Sequenziale Neuro-Simbolica**")
150
 
151
- # --- TAB LOGIC ---
152
  tab_gen, tab_val, tab_vis = st.tabs([
153
  "⚙️ 1. Pipeline Generativa",
154
  "🔍 2. Validazione (HITL)",
@@ -159,192 +139,176 @@ tab_gen, tab_val, tab_vis = st.tabs([
159
  # TAB 1: PIPELINE GENERATIVA (STEPPER UI)
160
  # ==============================================================================
161
  with tab_gen:
162
- # --- SELEZIONE FILE ---
163
- st.subheader("1. Sorgente Documentale")
164
- st.info("Seleziona uno degli scenari dimostrativi validati per avviare la pipeline.")
165
 
166
- selected_file = None
167
- os.makedirs("data/raw", exist_ok=True)
168
- os.makedirs("data/processed", exist_ok=True)
169
- os.makedirs("data/examples", exist_ok=True)
170
-
171
- # Logica semplificata: Solo esempi demo
172
- files = [f for f in os.listdir("data/examples") if f.endswith(".txt")]
173
- if files:
174
- choice = st.selectbox("Scenario Disponibile:", files, index=0)
175
- if choice:
176
- src = os.path.join("data/examples", choice)
177
- dst = os.path.join("data/raw", choice)
178
- shutil.copy(src, dst)
179
- selected_file = choice
180
- else:
181
- st.warning("⚠️ Nessun file trovato in data/examples. Aggiungi file .txt alla cartella per procedere.")
182
-
183
- # Logica di cambio file: se cambia il file, resetta la pipeline
184
- if selected_file and selected_file != st.session_state.current_file:
185
- st.session_state.current_file = selected_file
186
- st.session_state.pipeline_stage = 0
187
- st.rerun()
188
-
189
- if not selected_file:
190
- st.stop()
191
 
192
  st.markdown("---")
193
-
194
- # --- PROGRESS BAR ---
195
- # stage 0 -> 0%, stage 1 -> 33%, stage 2 -> 66%, stage 3 -> 100%
196
  progress_val = int((st.session_state.pipeline_stage / 3) * 100)
197
  st.progress(progress_val, text=f"Progresso Pipeline: {progress_val}%")
198
 
199
  # ==========================
200
- # FASE A: CHUNKING
201
  # ==========================
202
  with st.container():
203
- st.markdown(f"### {'✅' if st.session_state.pipeline_stage >= 1 else '1️⃣'} Fase A: Semantic Chunking")
 
 
 
 
 
204
 
205
  if st.session_state.pipeline_stage >= 1:
206
- # Stato Completato: Mostra riassunto
207
- with open("data/processed/chunks.json", "r") as f:
208
- chunks = json.load(f)
209
  st.markdown(f"""
210
  <div class="success-box">
211
- <b>Chunking completato!</b> Generati {len(chunks)} frammenti semantici.<br>
212
- Modello vettoriale utilizzato: <i>MiniLM-L12-v2</i>
213
  </div>
214
  """, unsafe_allow_html=True)
215
  with st.expander("Vedi dettagli frammenti"):
216
- st.json(chunks[:3]) # Mostra solo i primi 3 per pulizia
217
  else:
218
- # Stato Attivo: Bottone azione
219
- st.markdown("Segmentazione del testo basata sulla coerenza semantica vettoriale.")
220
- if st.button("Avvia Analisi Semantica", type="primary"):
221
- with st.spinner("Calcolo vettori e segmentazione..."):
222
  try:
223
- with open(os.path.join("data/raw", selected_file), "r", encoding="utf-8") as f:
224
- text_content = f.read()
225
-
226
  splitter = get_splitter()
227
- chunks, dists, threshold = splitter.create_chunks(text_content)
228
-
229
- with open("data/processed/chunks.json", "w", encoding="utf-8") as f:
230
- json.dump(chunks, f, ensure_ascii=False, indent=2)
231
 
 
 
232
  st.session_state.pipeline_stage = 1
233
  st.rerun()
234
  except Exception as e:
235
- st.error(f"Errore: {e}")
236
 
237
  st.markdown("⬇️")
238
 
239
  # ==========================
240
- # FASE B: EXTRACTION
241
  # ==========================
242
  is_step_b_unlocked = st.session_state.pipeline_stage >= 1
243
 
244
  with st.container():
245
- # Header grigio se bloccato, bianco (per dark mode) se attivo
246
  color = "white" if is_step_b_unlocked else "gray"
247
  icon = "✅" if st.session_state.pipeline_stage >= 2 else ("2️⃣" if is_step_b_unlocked else "🔒")
248
- st.markdown(f"<h3 style='color:{color}'>{icon} Fase B: Information Extraction</h3>", unsafe_allow_html=True)
249
-
250
- if not is_step_b_unlocked:
251
- st.caption("Completa la Fase A per sbloccare l'estrazione.")
252
-
253
- elif st.session_state.pipeline_stage >= 2:
254
- # Stato Completato
255
- with open("data/processed/triples_raw.json", "r") as f:
256
- triples = json.load(f)
257
- st.markdown(f"""
258
- <div class="success-box">
259
- <b>Estrazione completata!</b> Identificate {len(triples)} triple candidate.<br>
260
- Motore Neuro-Simbolico: <i>Llama3/Mistral + Dependecy Parsing</i>
261
- </div>
262
- """, unsafe_allow_html=True)
263
- with st.expander("Vedi esempio triple"):
264
- st.dataframe(pd.DataFrame(triples).head(5), hide_index=True)
265
- else:
266
- # Stato Attivo
267
- st.markdown("Estrazione di Entità e Relazioni tramite approccio Neuro-Simbolico.")
268
- if st.button("Avvia Estrazione Ontologica", type="primary"):
269
- with st.spinner("Processando frammenti con LLM..."):
270
- try:
271
- with open("data/processed/chunks.json", "r", encoding="utf-8") as f:
272
- chunks = json.load(f)
273
-
274
- extractor = get_extractor()
275
- all_triples = []
276
- prog_bar = st.progress(0)
277
-
278
- for i, chunk in enumerate(chunks):
279
- res = extractor.extract(chunk, source_id=selected_file)
280
- all_triples.extend([t.model_dump() for t in res.triples])
281
- prog_bar.progress((i+1)/len(chunks))
282
-
283
- with open("data/processed/triples_raw.json", "w", encoding="utf-8") as f:
284
- json.dump(all_triples, f, ensure_ascii=False, indent=2)
285
-
286
- st.session_state.pipeline_stage = 2
287
- st.rerun()
288
- except Exception as e:
289
- st.error(f"Errore: {e}")
 
 
290
 
291
  st.markdown("⬇️")
292
 
293
  # ==========================
294
- # FASE C: GRAPH POPULATION
295
  # ==========================
296
  is_step_c_unlocked = st.session_state.pipeline_stage >= 2
297
 
298
  with st.container():
299
  color = "white" if is_step_c_unlocked else "gray"
300
  icon = "✅" if st.session_state.pipeline_stage >= 3 else ("3️⃣" if is_step_c_unlocked else "🔒")
301
- st.markdown(f"<h3 style='color:{color}'>{icon} Fase C: Graph Construction</h3>", unsafe_allow_html=True)
 
 
 
 
302
 
303
  if not is_step_c_unlocked:
304
- st.caption("Completa la Fase B per popolare il grafo.")
305
-
306
  elif st.session_state.pipeline_stage >= 3:
307
  st.markdown("""
308
  <div class="success-box">
309
- <b>Grafo Aggiornato!</b> I dati sono stati caricati su Neo4j.<br>
310
- Puoi esplorarli nei tab "Validazione" e "Visualizzazione".
311
  </div>
312
  """, unsafe_allow_html=True)
313
- if st.button("Riavvia con nuovo file"):
314
- reset_pipeline()
315
- st.rerun()
316
  else:
317
- st.markdown("Entity Resolution (Deduplica) e Caricamento su Neo4j.")
318
  if not driver:
319
  st.error("⚠️ Connettiti a Neo4j (nella sidebar) per procedere.")
320
  else:
321
- if st.button("Genera Knowledge Graph", type="primary"):
322
- with st.spinner("Risoluzione entità e scrittura DB..."):
323
  try:
324
- with open("data/processed/triples_raw.json", "r", encoding="utf-8") as f:
325
- raw_data = json.load(f)
326
-
327
- triples_objs = [GraphTriple(**t) for t in raw_data]
328
 
329
  resolver = get_resolver()
330
- resolved = resolver.resolve_entities(triples_objs)
 
 
 
 
331
 
332
- persister = KnowledgeGraphPersister()
333
- persister.save_triples(resolved)
 
 
 
 
 
 
 
334
  persister.close()
335
 
336
  st.session_state.pipeline_stage = 3
337
  st.rerun()
338
  except Exception as e:
339
- st.error(f"Errore: {e}")
340
 
341
  # ==============================================================================
342
- # TAB 2: VALIDAZIONE (Codice invariato, solo stile)
343
  # ==============================================================================
344
  with tab_val:
345
  st.header("Curation & Feedback Loop")
346
  if driver:
347
- # Recupera statistiche rapide
348
  stats = run_query(driver, "MATCH (n) RETURN count(n) as nodes, count{()-->()} as rels")
349
  if stats:
350
  c1, c2 = st.columns(2)
@@ -357,58 +321,59 @@ with tab_val:
357
  COALESCE(s.label, s.name, head(labels(s))) as Soggetto,
358
  type(r) as Predicato,
359
  COALESCE(o.label, o.name, head(labels(o))) as Oggetto,
360
- COALESCE(r.confidence, 0.85) as Confidenza
361
- ORDER BY Confidenza ASC LIMIT 50
362
  """
363
  triples_data = run_query(driver, cypher_val)
364
 
365
  if triples_data:
366
  df = pd.DataFrame(triples_data)
367
- st.dataframe(df.drop(columns=["id"]), use_container_width=True, hide_index=True)
368
  else:
369
  st.info("Grafo vuoto.")
370
  else:
371
  st.warning("Database non connesso.")
372
 
373
- # ==============================================================================
374
- # TAB 3: VISUALIZZAZIONE
375
- # ==============================================================================
376
  with tab_vis:
377
  st.header("Esplorazione Topologica")
378
  if driver:
379
  col_ctrl, col_info = st.columns([1, 4])
380
  with col_ctrl:
381
  physics = st.checkbox("Abilita Fisica (Gravità)", value=True)
382
- if st.button("🔄 Ricarica Dati"):
383
- st.rerun()
384
 
385
- # Logica di visualizzazione automatica (non dipendente da un bottone)
386
- cypher_vis = """
387
- MATCH (s)-[r]->(o)
388
- RETURN COALESCE(s.label, s.name, head(labels(s))) as src,
389
- type(r) as rel,
390
- COALESCE(o.label, o.name, head(labels(o))) as dst
391
- LIMIT 100
392
- """
393
- graph_data = run_query(driver, cypher_vis)
394
-
395
- if graph_data:
396
- net = Network(height="600px", width="100%", bgcolor="#222222", font_color="white", notebook=False)
397
- for item in graph_data:
398
- src, dst, rel = str(item['src']), str(item['dst']), str(item['rel'])
399
- net.add_node(src, label=src, color="#4facfe", title=src)
400
- net.add_node(dst, label=dst, color="#00f2fe", title=dst)
401
- net.add_edge(src, dst, title=rel, label=rel)
402
-
403
- net.toggle_physics(physics)
404
- path = "data/processed/graph_viz.html"
405
- os.makedirs("data/processed", exist_ok=True)
406
- net.save_graph(path)
407
-
408
- with open(path, 'r', encoding='utf-8') as f:
409
- html_string = f.read()
410
- components.html(html_string, height=600, scrolling=True)
 
 
 
 
411
  else:
412
- st.info("Il grafo è attualmente vuoto o non raggiungibile.")
 
413
  else:
414
  st.warning("Database non connesso. Configura le credenziali nella sidebar.")
 
1
  import streamlit as st
2
  import os
3
+ import tempfile
 
 
4
  import pandas as pd
 
5
  from neo4j import GraphDatabase
6
  from pyvis.network import Network
7
  import streamlit.components.v1 as components
 
10
  # --- IMPORT MODULI SPECIFICI ---
11
  from src.ingestion.semantic_splitter import ActivaSemanticSplitter
12
  from src.extraction.extractor import NeuroSymbolicExtractor, GraphTriple
13
+ from src.validation.validator import SemanticValidator
14
  from src.graph.graph_loader import KnowledgeGraphPersister
15
  from src.graph.entity_resolver import EntityResolver
16
 
 
23
  page_icon="🧠"
24
  )
25
 
26
+ def local_css(file_name):
27
+ with open(file_name, "r") as f:
28
+ st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
29
+
30
+ local_css("assets/style.css")
31
+
32
+ # --- SESSION STATE MANAGEMENT (In-Memory per HF Spaces) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  if 'pipeline_stage' not in st.session_state:
34
+ st.session_state.pipeline_stage = 0
35
+ if 'document_text' not in st.session_state:
36
+ st.session_state.document_text = ""
37
+ if 'chunks' not in st.session_state:
38
+ st.session_state.chunks = []
39
+ if 'extraction_data' not in st.session_state:
40
+ st.session_state.extraction_data = {"entities": [], "triples": []}
41
+ if 'graph_html' not in st.session_state:
42
+ st.session_state.graph_html = None
43
 
44
  def reset_pipeline():
45
  st.session_state.pipeline_stage = 0
46
+ st.session_state.document_text = ""
47
+ st.session_state.chunks = []
48
+ st.session_state.extraction_data = {"entities": [], "triples": []}
 
 
49
 
50
  # --- CACHING RISORSE ---
51
  @st.cache_resource
52
  def get_splitter():
53
+ return ActivaSemanticSplitter(model_name="all-MiniLM-L6-v2")
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():
62
+ return EntityResolver(neo4j_driver=None, similarity_threshold=0.85)
63
+
64
+ @st.cache_resource
65
+ def get_validator():
66
+ return SemanticValidator()
67
+
68
+ #carico subito i vari oggetti così da evitare rallentamenti nelle varie fasi della pipeline
69
+ _ = get_splitter()
70
+ _ = get_extractor()
71
+ _ = get_validator()
72
 
73
  # --- FUNZIONI NEO4J ---
74
  def get_driver(uri, user, password):
75
  if not uri or not password: return None
76
  try:
77
+ driver = GraphDatabase.driver(uri, auth=(user, password))
78
+ driver.verify_connectivity()
79
+ return driver
80
  except: return None
81
 
82
  def run_query(driver, query, params=None):
 
88
  # --- UI: SIDEBAR ---
89
  st.sidebar.title("⚙️ Configurazione")
90
 
 
 
91
  env_uri = os.getenv("NEO4J_URI", "")
92
  env_user = os.getenv("NEO4J_USER", "neo4j")
93
  env_password = os.getenv("NEO4J_PASSWORD", "")
94
  env_hf_token = os.getenv("HF_TOKEN", "")
95
 
96
  st.sidebar.subheader("Backend AI")
 
 
97
  if env_hf_token:
98
  st.sidebar.success("✅ HF Token: Configurato da Secrets")
 
99
  hf_token_input = st.sidebar.text_input("Sovrascrivi Token (Opzionale)", type="password", key="hf_token_override")
100
+ if hf_token_input: os.environ["HF_TOKEN"] = hf_token_input
 
101
  else:
102
  hf_token_input = st.sidebar.text_input("Inserisci HF Token", type="password")
103
+ if hf_token_input: os.environ["HF_TOKEN"] = hf_token_input
 
104
 
105
  st.sidebar.subheader("Knowledge Graph")
 
106
  uri = st.sidebar.text_input("URI", value=env_uri)
107
  user = st.sidebar.text_input("User", value=env_user)
108
 
109
+ pwd_placeholder = "✅ Configurato (Lascia vuoto)" if env_password else "Inserisci Password"
 
 
110
  password_input = st.sidebar.text_input("Password", type="password", placeholder=pwd_placeholder)
 
 
111
  password = password_input if password_input else env_password
112
 
113
  driver = None
 
115
  driver = get_driver(uri, user, password)
116
  if driver:
117
  st.sidebar.success("🟢 Connesso a Neo4j")
 
118
  os.environ["NEO4J_URI"] = uri
119
  os.environ["NEO4J_USER"] = user
120
  os.environ["NEO4J_PASSWORD"] = password
 
127
 
128
  # --- MAIN HEADER ---
129
  st.title("🧠 Automated Semantic Discovery Prototype")
130
+ st.markdown("**Endpoint per l'ingestion testuale e l'estrazione neuro-simbolica**")
131
 
 
132
  tab_gen, tab_val, tab_vis = st.tabs([
133
  "⚙️ 1. Pipeline Generativa",
134
  "🔍 2. Validazione (HITL)",
 
139
  # TAB 1: PIPELINE GENERATIVA (STEPPER UI)
140
  # ==============================================================================
141
  with tab_gen:
142
+ st.subheader("1. Ingestion Documentale")
143
+ st.info("Inserisci il testo da analizzare nel campo sottostante.")
 
144
 
145
+ with st.form("ingestion_form"):
146
+ input_text = st.text_area("Testo del documento:", value=st.session_state.document_text, height=200)
147
+ submitted = st.form_submit_button("Salva Testo e Prepara Pipeline")
148
+
149
+ if submitted:
150
+ if input_text != st.session_state.document_text and input_text.strip() != "":
151
+ st.session_state.document_text = input_text
152
+ st.session_state.pipeline_stage = 0
153
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  st.markdown("---")
 
 
 
156
  progress_val = int((st.session_state.pipeline_stage / 3) * 100)
157
  st.progress(progress_val, text=f"Progresso Pipeline: {progress_val}%")
158
 
159
  # ==========================
160
+ # FASE 1: CHUNKING
161
  # ==========================
162
  with st.container():
163
+ st.markdown(f"### {'✅' if st.session_state.pipeline_stage >= 1 else '1️⃣'} Fase 1: Semantic Chunking")
164
+
165
+ with st.expander("ℹ️ Cosa fa questa fase?"):
166
+ st.write("Segmenta il testo in frammenti coerenti analizzando la similarità semantica vettoriale tra le frasi. " \
167
+ "A differenza di un taglio rigido per numero di parole, questo approccio garantisce che i concetti non vengano interrotti bruscamente, " \
168
+ "ottimizzando il contesto per l'LLM.")
169
 
170
  if st.session_state.pipeline_stage >= 1:
171
+ chunks = st.session_state.chunks
 
 
172
  st.markdown(f"""
173
  <div class="success-box">
174
+ <b>Chunking completato!</b> Generati {len(chunks)} frammenti semantici.
 
175
  </div>
176
  """, unsafe_allow_html=True)
177
  with st.expander("Vedi dettagli frammenti"):
178
+ st.json(chunks)
179
  else:
180
+ if st.button("Avvia Semantic Splitter", type="primary"):
181
+ with st.spinner("Creazione chunks in corso..."):
 
 
182
  try:
 
 
 
183
  splitter = get_splitter()
184
+ chunks, _, _ = splitter.create_chunks(input_text, percentile_threshold=90)
 
 
 
185
 
186
+ # Salvataggio in-memory
187
+ st.session_state.chunks = chunks
188
  st.session_state.pipeline_stage = 1
189
  st.rerun()
190
  except Exception as e:
191
+ st.error(f"Errore durante il chunking: {e}")
192
 
193
  st.markdown("⬇️")
194
 
195
  # ==========================
196
+ # FASE 2: EXTRACTION
197
  # ==========================
198
  is_step_b_unlocked = st.session_state.pipeline_stage >= 1
199
 
200
  with st.container():
 
201
  color = "white" if is_step_b_unlocked else "gray"
202
  icon = "✅" if st.session_state.pipeline_stage >= 2 else ("2️⃣" if is_step_b_unlocked else "🔒")
203
+ st.markdown(f"<h3 style='color:{color}'>{icon} Fase 2: Neuro-Symbolic Extraction</h3>", unsafe_allow_html=True)
204
+
205
+ with st.expander("ℹ️ Cosa fa questa fase?"):
206
+ st.write("Invia i frammenti al Large Language Model (es. Llama 3) per estrarre dinamicamente entità e relazioni. " \
207
+ "L'approccio Neuro-Simbolico forza l'output del modello a rispettare una struttura dati rigorosa (JSON tipizzato) prima di procedere.")
208
+
209
+ if not is_step_b_unlocked:
210
+ st.caption("Completa la Fase 1 per sbloccare l'estrazione.")
211
+ elif st.session_state.pipeline_stage >= 2:
212
+ data = st.session_state.extraction_data
213
+ st.markdown(f"""
214
+ <div class="success-box">
215
+ <b>Estrazione completata!</b> Identificate {len(data['entities'])} entità e {len(data['triples'])} triple.
216
+ </div>
217
+ """, unsafe_allow_html=True)
218
+ with st.expander("Vedi dati estratti"):
219
+ st.write("Entità Trovate:", data['entities'])
220
+ st.dataframe(pd.DataFrame(data['triples']), hide_index=True)
221
+ else:
222
+ if st.button("Avvia Estrazione Ontologica", type="primary"):
223
+ with st.spinner("Invocazione modello sui frammenti..."):
224
+ try:
225
+ chunks = st.session_state.chunks
226
+ extractor = get_extractor()
227
+ all_triples = []
228
+ all_entities = []
229
+ prog_bar = st.progress(0)
230
+
231
+ for i, chunk in enumerate(chunks):
232
+ chunk_id = f"st_req_chunk_{i+1}"
233
+ res = extractor.extract(chunk, source_id=chunk_id)
234
+
235
+ if res:
236
+ if res.triples: all_triples.extend([t.model_dump() for t in res.triples])
237
+ if res.entities: all_entities.extend(res.entities)
238
+
239
+ prog_bar.progress((i+1)/len(chunks))
240
+
241
+ # Salvataggio in-memory
242
+ st.session_state.extraction_data = {"entities": all_entities, "triples": all_triples}
243
+ st.session_state.pipeline_stage = 2
244
+ st.rerun()
245
+ except Exception as e:
246
+ st.error(f"Errore: {e}")
247
 
248
  st.markdown("⬇️")
249
 
250
  # ==========================
251
+ # FASE 3: RESOLUTION & PERSISTENCE
252
  # ==========================
253
  is_step_c_unlocked = st.session_state.pipeline_stage >= 2
254
 
255
  with st.container():
256
  color = "white" if is_step_c_unlocked else "gray"
257
  icon = "✅" if st.session_state.pipeline_stage >= 3 else ("3️⃣" if is_step_c_unlocked else "🔒")
258
+ st.markdown(f"<h3 style='color:{color}'>{icon} Fase 3: Resolution, Validation & Graph Population</h3>", unsafe_allow_html=True)
259
+
260
+ with st.expander("ℹ️ Cosa fa questa fase?"):
261
+ st.write("Unisce ed elimina i duplicati delle entità (Entity Resolution) sfruttando i Vector Index di Neo4j e chiamate esterne. " \
262
+ "Successivamente, applica regole deterministiche (SHACL) per validare le triple estratte e le salva permanentemente nel database a grafo.")
263
 
264
  if not is_step_c_unlocked:
265
+ st.caption("Completa la Fase 2 per procedere.")
 
266
  elif st.session_state.pipeline_stage >= 3:
267
  st.markdown("""
268
  <div class="success-box">
269
+ <b>Grafo Aggiornato!</b> I dati sono stati validati e caricati su Neo4j.
 
270
  </div>
271
  """, unsafe_allow_html=True)
 
 
 
272
  else:
 
273
  if not driver:
274
  st.error("⚠️ Connettiti a Neo4j (nella sidebar) per procedere.")
275
  else:
276
+ if st.button("Genera e Valida Knowledge Graph", type="primary"):
277
+ with st.spinner("Risoluzione entità, validazione SHACL e scrittura..."):
278
  try:
279
+ raw_data = st.session_state.extraction_data
280
+ all_entities = raw_data.get("entities", [])
281
+ all_triples = [GraphTriple(**t) for t in raw_data.get("triples", [])]
 
282
 
283
  resolver = get_resolver()
284
+ resolver.driver = driver
285
+ all_entities, all_triples, entities_to_save = resolver.resolve_entities(all_entities, all_triples)
286
+
287
+ validator = get_validator()
288
+ is_valid, report, _ = validator.validate_batch(entities_to_save, all_triples)
289
 
290
+ if not is_valid:
291
+ st.markdown(f"""
292
+ <div class="warning-box">
293
+ <b>Attenzione:</b> La validazione SHACL ha rilevato violazioni. Guarda il log console per i dettagli.
294
+ </div>
295
+ """, unsafe_allow_html=True)
296
+
297
+ persister = KnowledgeGraphPersister()
298
+ persister.save_entities_and_triples(entities_to_save, all_triples)
299
  persister.close()
300
 
301
  st.session_state.pipeline_stage = 3
302
  st.rerun()
303
  except Exception as e:
304
+ st.error(f"Errore critico: {e}")
305
 
306
  # ==============================================================================
307
+ # TAB 2 & 3: VALIDAZIONE E VISUALIZZAZIONE
308
  # ==============================================================================
309
  with tab_val:
310
  st.header("Curation & Feedback Loop")
311
  if driver:
 
312
  stats = run_query(driver, "MATCH (n) RETURN count(n) as nodes, count{()-->()} as rels")
313
  if stats:
314
  c1, c2 = st.columns(2)
 
321
  COALESCE(s.label, s.name, head(labels(s))) as Soggetto,
322
  type(r) as Predicato,
323
  COALESCE(o.label, o.name, head(labels(o))) as Oggetto,
324
+ COALESCE(r.confidence, 1.0) as Confidenza
325
+ ORDER BY Confidenza ASC
326
  """
327
  triples_data = run_query(driver, cypher_val)
328
 
329
  if triples_data:
330
  df = pd.DataFrame(triples_data)
331
+ st.dataframe(df.drop(columns=["id"]), width='stretch', hide_index=True)
332
  else:
333
  st.info("Grafo vuoto.")
334
  else:
335
  st.warning("Database non connesso.")
336
 
 
 
 
337
  with tab_vis:
338
  st.header("Esplorazione Topologica")
339
  if driver:
340
  col_ctrl, col_info = st.columns([1, 4])
341
  with col_ctrl:
342
  physics = st.checkbox("Abilita Fisica (Gravità)", value=True)
343
+ generate_graph = st.button("🔄 Genera / Aggiorna Grafo", type="primary")
 
344
 
345
+ if generate_graph:
346
+ with st.spinner("Estrazione dati e generazione del grafo interattivo..."):
347
+ cypher_vis = """
348
+ MATCH (s)-[r]->(o)
349
+ RETURN COALESCE(s.label, s.name, head(labels(s))) as src,
350
+ type(r) as rel,
351
+ COALESCE(o.label, o.name, head(labels(o))) as dst
352
+ """
353
+ graph_data = run_query(driver, cypher_vis)
354
+
355
+ if graph_data:
356
+ net = Network(height="600px", width="100%", bgcolor="#222222", font_color="white", notebook=False)
357
+ for item in graph_data:
358
+ src, dst, rel = str(item['src']), str(item['dst']), str(item['rel'])
359
+ net.add_node(src, label=src, color="#4facfe", title=src)
360
+ net.add_node(dst, label=dst, color="#00f2fe", title=dst)
361
+ net.add_edge(src, dst, title=rel, label=rel)
362
+
363
+ net.toggle_physics(physics)
364
+
365
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.html') as tmp:
366
+ net.save_graph(tmp.name)
367
+ with open(tmp.name, 'r', encoding='utf-8') as f:
368
+ st.session_state.graph_html = f.read()
369
+ else:
370
+ st.warning("Il grafo è attualmente vuoto.")
371
+ st.session_state.graph_html = None
372
+
373
+ if st.session_state.graph_html:
374
+ components.html(st.session_state.graph_html, height=600, scrolling=True)
375
  else:
376
+ st.info("👆 Clicca su 'Genera / Aggiorna Grafo' per visualizzare i dati attuali di Neo4j.")
377
+
378
  else:
379
  st.warning("Database non connesso. Configura le credenziali nella sidebar.")
app/ui.py DELETED
@@ -1,161 +0,0 @@
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://99ed65ab.databases.neo4j.io")
19
- USER = os.getenv("NEO4J_USER", "99ed65ab")
20
- PASSWORD = os.getenv("NEO4J_PASSWORD", "4z86xz3Zwd5D7nt_lqIgE5O1NPmghKfoad6q_lL2YGs")
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)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/style.css ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .step-card {
2
+ padding: 20px;
3
+ border-radius: 10px;
4
+ border: 1px solid #e0e0e0;
5
+ margin-bottom: 20px;
6
+ background-color: #262730;
7
+ }
8
+
9
+ .step-header {
10
+ font-size: 1.2rem;
11
+ font-weight: bold;
12
+ margin-bottom: 10px;
13
+ color: #4facfe;
14
+ }
15
+
16
+ .success-box {
17
+ padding: 10px;
18
+ background-color: rgba(76, 175, 80, 0.1);
19
+ border-left: 5px solid #4CAF50;
20
+ border-radius: 5px;
21
+ }
22
+
23
+ .warning-box {
24
+ padding: 10px;
25
+ background-color: rgba(255, 152, 0, 0.1);
26
+ border-left: 5px solid #FF9800;
27
+ border-radius: 5px;
28
+ margin-top: 10px;
29
+ }
30
+
31
+ [data-testid="stExpander"] {
32
+ background-color: rgba(38, 39, 48, 0.4);
33
+ border: 1px solid rgba(79, 172, 254, 0.3);
34
+ border-radius: 8px;
35
+ transition: border 0.3s ease, background-color 0.3s ease;
36
+ margin-top: 5px;
37
+ margin-bottom: 15px;
38
+ }
39
+
40
+ [data-testid="stExpander"]:hover {
41
+ border: 1px solid rgba(79, 172, 254, 0.8);
42
+ background-color: rgba(38, 39, 48, 0.6);
43
+ }
44
+
45
+ [data-testid="stExpander"] summary p {
46
+ color: #a0a0a0;
47
+ font-weight: 500;
48
+ font-size: 0.95rem;
49
+ }
50
+
51
+ [data-testid="stExpander"]:hover summary p {
52
+ color: #4facfe;
53
+ transition: color 0.2s ease;
54
+ }
55
+
56
+ [data-testid="stExpanderDetails"] {
57
+ color: #cccccc;
58
+ font-size: 0.9rem;
59
+ line-height: 1.6;
60
+ padding-top: 10px;
61
+ }
data/examples/intelligenza_artificiale.txt DELETED
@@ -1,9 +0,0 @@
1
- L'Intelligenza Artificiale (IA) è un ramo dell'informatica che mira a creare sistemi capaci di eseguire compiti che richiedono normalmente l'intelligenza umana.
2
- Questi compiti includono il riconoscimento vocale, la visione artificiale e la traduzione automatica.
3
-
4
- Le reti neurali profonde (Deep Learning) sono alla base dei recenti progressi nell'IA generativa.
5
- Modelli come i Transformer permettono di analizzare grandi quantità di dati testuali per estrarre significati semantici.
6
- Tuttavia, questi sistemi statistici mancano spesso di capacità di ragionamento logico formale.
7
-
8
- Per superare questo limite, si sta sviluppando l'approccio Neuro-Simbolico.
9
- Questo paradigma integra la flessibilità delle reti neurali con la precisione delle regole logiche e dei grafi di conoscenza (Knowledge Graphs), migliorando l'affidabilità e la spiegabilità dei risultati.
 
 
 
 
 
 
 
 
 
 
data/examples/la_prima_parte_della_via_appia.txt DELETED
The diff for this file is too large to render. See raw diff
 
data/examples/parco_canne_battaglia.txt DELETED
@@ -1,9 +0,0 @@
1
- Il Parco Archeologico di Canne della Battaglia si trova su un'altura lungo la riva destra del fiume Ofanto, a pochi chilometri dalla foce.
2
- Il sito è celebre per la storica battaglia del 216 a.C., dove l'esercito cartaginese guidato da Annibale accerchiò e distrusse le legioni romane, nonostante l'inferiorità numerica.
3
-
4
- L'area archeologica comprende l'Antiquarium e la Cittadella medievale.
5
- All'interno dell'Antiquarium sono conservati reperti che vanno dalla preistoria al medioevo, inclusi corredi funerari e ceramiche geometriche daunia.
6
- La Cittadella conserva i resti del castello e della basilica maggiore, testimonianza dell'importanza strategica del luogo anche in epoca successiva alla battaglia.
7
-
8
- Recenti scavi hanno portato alla luce una necropoli medievale, suggerendo che l'insediamento fosse densamente abitato fino al XII secolo.
9
- I visitatori possono percorrere i sentieri che costeggiano le mura difensive e osservare la piana dell'Ofanto, teatro dello scontro militare.
 
 
 
 
 
 
 
 
 
 
data/examples/venezia_monumentale.txt DELETED
@@ -1,10 +0,0 @@
1
- La Basilica di San Marco a Venezia è il principale monumento religioso della città e uno dei simboli dell'arte veneto-bizantina.
2
- Situata nell'omonima piazza, la basilica fungeva da cappella palatina del Palazzo Ducale ed è collegata ad esso tramite la Porta della Carta.
3
-
4
- L'edificio presenta una pianta a croce greca con cinque cupole.
5
- La facciata è decorata con mosaici dorati, bassorilievi e marmi orientali, frutto del bottino della Quarta Crociata, tra cui i celebri Cavalli di San Marco (quelli esposti all'esterno sono copie).
6
- L'interno è rivestito da oltre 8000 metri quadrati di mosaici a fondo oro che narrano storie bibliche e agiografiche.
7
-
8
- Il Palazzo Ducale, adiacente alla basilica, è un capolavoro del gotico veneziano.
9
- Fu sede del Doge e delle magistrature statali della Serenissima Repubblica.
10
- Al suo interno si trovano la Scala d'Oro e la Sala del Maggior Consiglio, che ospita il "Paradiso" di Tintoretto, una delle tele più grandi al mondo.
 
 
 
 
 
 
 
 
 
 
 
data/gold_standard/examples.json CHANGED
@@ -1,63 +1,70 @@
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
  ]
 
1
  [
2
  {
3
+ "text": "Il Menhir di Canne, situato lungo la strada provinciale, è un monolite calcareo che fungeva da segnacolo funerario o, secondo una teoria recente e dibattuta, da confine territoriale.",
4
+ "reasoning": "L'entità fisica e la localizzazione sono fatti certi (1.0). L'uso come segnacolo è consolidato ma non assoluto (0.9), mentre l'uso come confine è esplicitamente presentato come teoria incerta, quindi assegno un'ipotesi (0.6).",
5
+ "entities": [
6
+ "Menhir di Canne",
7
+ "Strada Provinciale",
8
+ "Segnacolo funerario",
9
+ "Confine territoriale"
10
+ ],
11
  "triples": [
12
+ {"subject": "Menhir di Canne", "predicate": "core:hasType", "object": "arco:ArchaeologicalProperty", "confidence": 1.0},
13
+ {"subject": "Menhir di Canne", "predicate": "a-loc:isLocatedIn", "object": "Strada Provinciale", "confidence": 1.0},
14
+ {"subject": "Menhir di Canne", "predicate": "core:hasConcept", "object": "Segnacolo funerario", "confidence": 0.9},
15
+ {"subject": "Menhir di Canne", "predicate": "core:hasConcept", "object": "Confine territoriale", "confidence": 0.6}
16
  ]
17
  },
18
  {
19
+ "text": "La Battaglia di Canne del 216 a.C. vide la vittoria dell'esercito cartaginese guidato da Annibale. Le dinamiche dell'accerchiamento fanno presumere una conoscenza pregressa del terreno fangoso da parte dei comandanti.",
20
+ "reasoning": "La battaglia, la data e gli agenti coinvolti sono certi (1.0). La conoscenza del terreno da parte di Annibale è una deduzione forte derivata dalle tattiche, quindi è un'inferenza logica (0.85).",
21
+ "entities": [
22
+ "Battaglia di Canne",
23
+ "216 a.C.",
24
+ "Esercito Cartaginese",
25
+ "Annibale",
26
+ "Conoscenza del terreno"
27
+ ],
28
  "triples": [
29
+ {"subject": "Battaglia di Canne", "predicate": "core:hasType", "object": "core:Event", "confidence": 1.0},
30
+ {"subject": "Battaglia di Canne", "predicate": "ti:atTime", "object": "216 a.C.", "confidence": 1.0},
31
+ {"subject": "Battaglia di Canne", "predicate": "ro:involvesAgent", "object": "Esercito Cartaginese", "confidence": 1.0},
32
+ {"subject": "Annibale", "predicate": "core:hasConcept", "object": "Conoscenza del terreno", "confidence": 0.85}
33
  ]
34
  },
35
  {
36
+ "text": "L'Antiquarium custodisce un prezioso corredo funerario proveniente dalla necropoli dauna. Alcuni dettagli pittorici sui vasi a figure rosse fanno sospettare un'influenza diretta della bottega del Pittore di Dario. All'ingresso della struttura è esposta anche una piccola stele iscritta.",
37
+ "reasoning": "Aggiunta un'entità isolata ('stele iscritta') che non ha relazioni esplicite nel testo con gli altri reperti, ma va comunque tracciata. L'attribuzione alla bottega rimane un'ipotesi (0.5).",
38
+ "entities": [
39
+ "Antiquarium",
40
+ "Corredo funerario",
41
+ "Vasi a figure rosse",
42
+ "Bottega del Pittore di Dario",
43
+ "Stele iscritta"
44
+ ],
45
  "triples": [
46
+ {"subject": "Antiquarium", "predicate": "core:hasType", "object": "cis:CulturalInstituteOrSite", "confidence": 1.0},
47
+ {"subject": "Corredo funerario", "predicate": "a-loc:hasCurrentLocation", "object": "Antiquarium", "confidence": 1.0},
48
+ {"subject": "Corredo funerario", "predicate": "core:hasPart", "object": "Vasi a figure rosse", "confidence": 1.0},
49
+ {"subject": "Vasi a figure rosse", "predicate": "ro:hasAuthor", "object": "Bottega del Pittore di Dario", "confidence": 0.5}
50
  ]
51
  },
52
  {
53
+ "text": "Durante i recenti scavi nell'area nord, sono state rinvenute tre monete puniche d'argento mescolate a ceneri vicino a una struttura di accampamento. In un settore adiacente è stato trovato un elmo in bronzo frammentario.",
54
+ "reasoning": "L'elmo in bronzo è un reperto rilevante ma nel testo non è relazionato direttamente a ceneri o monete. Lo estraggo come entità isolata. Le monete e le ceneri suggeriscono un accampamento cartaginese (0.8).",
55
+ "entities": [
56
+ "Area nord",
57
+ "Monete puniche d'argento",
58
+ "Struttura di accampamento",
59
+ "Accampamento Cartaginese",
60
+ "Evento di incendio",
61
+ "Elmo in bronzo"
62
+ ],
63
  "triples": [
64
+ {"subject": "Area nord", "predicate": "core:hasPart", "object": "Monete puniche d'argento", "confidence": 1.0},
65
+ {"subject": "Area nord", "predicate": "core:hasPart", "object": "Struttura di accampamento", "confidence": 1.0},
66
+ {"subject": "Struttura di accampamento", "predicate": "core:hasConcept", "object": "Accampamento Cartaginese", "confidence": 0.8},
67
+ {"subject": "Area nord", "predicate": "core:hasConcept", "object": "Evento di incendio", "confidence": 0.75}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  ]
69
  }
70
  ]
data/processed/chunks_debug.txt DELETED
@@ -1,6 +0,0 @@
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 DELETED
@@ -1,5 +0,0 @@
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 DELETED
@@ -1,13 +0,0 @@
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docker-compose.yml DELETED
@@ -1,26 +0,0 @@
1
- services:
2
- neo4j:
3
- image: neo4j:5.15.0-community
4
- container_name: activa_graph_db
5
- ports:
6
- - "7474:7474" # Browser UI
7
- - "7687:7687" # Python Driver
8
- environment:
9
- - NEO4J_AUTH=neo4j/activa_semantic_lab
10
- # Carica APOC e GDS automaticamente
11
- - NEO4J_PLUGINS=["apoc", "graph-data-science"]
12
- # CONFIGURAZIONE CRUCIALE PER N10S (Neosemantics)
13
- - NEO4J_dbms_security_procedures_unrestricted=n10s.*,apoc.*
14
- - NEO4J_dbms_security_procedures_allowlist=n10s.*,apoc.*,gds.*
15
- # Memoria
16
- - NEO4J_dbms_memory_heap_initial__size=1G
17
- - NEO4J_dbms_memory_heap_max__size=2G
18
- volumes:
19
- # Mappa le cartelle che hai creato tu nella root
20
- - ./neo4j/data:/data
21
- - ./neo4j/plugins:/plugins
22
- healthcheck:
23
- test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1"]
24
- interval: 10s
25
- timeout: 5s
26
- retries: 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
main.py DELETED
@@ -1,131 +0,0 @@
1
- import sys
2
- import os
3
- import time
4
- import glob
5
-
6
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
7
-
8
- from src.ingestion.semantic_splitter import ActivaSemanticSplitter
9
- from src.extraction.extractor import NeuroSymbolicExtractor
10
- from src.validation.validator import SemanticValidator
11
- from src.graph.graph_loader import KnowledgeGraphPersister
12
- from src.graph.entity_resolver import EntityResolver
13
-
14
- def pipeline_execution():
15
- print("\n🚀 AVVIO PIPELINE AUTOMATED DISCOVERY\n" + "="*50)
16
-
17
- raw_text = load_raw_documents()
18
-
19
- if not raw_text:
20
- print("⚠️ Nessun file trovato in data/raw/. Uso testo di default.")
21
- raw_text = """
22
- La Basilica di San Marco a Venezia è il principale luogo di culto della città.
23
- È uno degli esempi più noti di architettura italo-bizantina.
24
- """
25
-
26
- # --- FASE 1: INGESTION ---
27
- print("\n[FASE 1] Ingestion & Semantic Chunking...")
28
- try:
29
- # Usa un modello piccolo per lo splitting veloce
30
- splitter = ActivaSemanticSplitter(model_name="all-MiniLM-L6-v2")
31
- # percentile_threshold=90 significa: taglia solo quando la similarità scende molto
32
- chunks, _, _ = splitter.create_chunks(raw_text, percentile_threshold=90)
33
- save_chunks_to_processed(chunks)
34
- print(f"✅ Testo diviso in {len(chunks)} segmenti semantici.")
35
- except Exception as e:
36
- print(f"❌ Errore in Fase 1: {e}")
37
- return
38
-
39
- # --- FASE 2: EXTRACTION ---
40
- print("\n[FASE 2] Init Neuro-Symbolic Core (Llama 3)...")
41
-
42
- gold_path = os.path.join("data", "gold_standard", "examples.json")
43
-
44
- try:
45
- # Assicurati che Ollama sia attivo!
46
- extractor = NeuroSymbolicExtractor(model_name="llama3", gold_standard_path=gold_path)
47
- except Exception as e:
48
- print(f"❌ Errore connessione Ollama: {e}")
49
- return
50
-
51
- all_triples = []
52
-
53
- print(f"🔄 Avvio estrazione su {len(chunks)} chunk...")
54
- for i, chunk in enumerate(chunks):
55
- chunk_id = f"doc_sample_chunk_{i+1}"
56
-
57
- print(f"\n Processing {chunk_id} ({len(chunk)} chars)...")
58
-
59
- # Invoca Llama 3
60
- extraction_result = extractor.extract(chunk, source_id=chunk_id)
61
-
62
- if extraction_result and extraction_result.triples:
63
- count = len(extraction_result.triples)
64
- print(f" -> Estratte {count} triple.")
65
- # Aggiungiamo le triple alla lista totale
66
- all_triples.extend(extraction_result.triples)
67
- else:
68
- print(" -> Nessuna tripla trovata (o errore parsing).")
69
-
70
- print(f"\n✅ Totale triple raccolte: {len(all_triples)}")
71
-
72
- if not all_triples:
73
- print("⚠️ Nessuna tripla da salvare. Pipeline terminata.")
74
- return
75
-
76
- # --- FASE 2.5: SYMBOLIC RESOLUTION & CANONICALIZATION ---
77
- # Implementazione Sezione 4.1 del Documento
78
- print("\n[FASE 2.5] Entity Resolution & Canonicalization (DBSCAN)...")
79
- try:
80
- resolver = EntityResolver(similarity_threshold=0.85)
81
- # Sovrascriviamo le triple con quelle pulite
82
- all_triples = resolver.resolve_entities(all_triples)
83
- print("✅ Risoluzione entità completata.")
84
- except Exception as e:
85
- print(f"⚠️ Errore nel resolver (skip): {e}")
86
-
87
- print("\n[FASE 2.6] Validazione Semantica (SHACL)...")
88
- validator = SemanticValidator()
89
- is_valid, report, _ = validator.validate_batch(all_triples)
90
-
91
- if is_valid:
92
- print("✅ Validazione passata. I dati rispettano l'ontologia.")
93
- else:
94
- print("⚠️ Warning: Rilevate violazioni SHACL.")
95
- print(" (In produzione, queste triple verrebbero scartate o mandate in Human Review)")
96
- # Per ora procediamo, ma in un sistema reale fermeremmo qui le triple corrotte.
97
- print(report)
98
-
99
- # --- FASE 3: PERSISTENCE ---
100
- print("\n[FASE 3] Graph Construction & Persistence (Neo4j)...")
101
- try:
102
- persister = KnowledgeGraphPersister()
103
- persister.save_triples(all_triples)
104
- persister.close()
105
- print("\n🎉 PIPELINE COMPLETATA CON SUCCESSO!")
106
- print("👉 Vai su http://localhost:7474 ed esegui: MATCH (n)-[r]->(m) RETURN n,r,m")
107
- except Exception as e:
108
- print(f"❌ Errore in Fase 3 (Neo4j): {e}")
109
-
110
- def load_raw_documents(directory="data/raw"):
111
- """Legge tutti i file .txt nella cartella raw."""
112
- texts = []
113
- files = glob.glob(os.path.join(directory, "*.txt"))
114
- print(f"📂 Trovati {len(files)} documenti in {directory}")
115
- for f_path in files:
116
- with open(f_path, 'r', encoding='utf-8') as f:
117
- texts.append(f.read())
118
- return "\n\n".join(texts)
119
-
120
- def save_chunks_to_processed(chunks, directory="data/processed"):
121
- """Salva i chunk su disco per debug."""
122
- os.makedirs(directory, exist_ok=True)
123
- with open(os.path.join(directory, "chunks_debug.txt"), "w", encoding="utf-8") as f:
124
- for i, c in enumerate(chunks):
125
- f.write(f"--- CHUNK {i} ---\n{c}\n\n")
126
- print(f"💾 Chunk salvati in {directory}/chunks_debug.txt")
127
-
128
- if __name__ == "__main__":
129
- start_time = time.time()
130
- pipeline_execution()
131
- print(f"\n⏱️ Tempo totale esecuzione: {time.time() - start_time:.2f} secondi")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -3,30 +3,32 @@ langchain>=0.3.0
3
  langchain-community>=0.3.0
4
  langchain-ollama>=0.2.0
5
  langchain-huggingface>=0.1.0
 
6
  langchain-core
7
  huggingface_hub
8
 
9
  # --- Data Validation ---
10
  pydantic>=2.0
11
- pyshacl # Per validazione SHACL
12
 
13
  # --- NLP & Semantic Chunking ---
14
- sentence-transformers # Backend per HuggingFace
15
- scikit-learn # Per cosine similarity
16
  numpy
17
- matplotlib # Per grafici di analisi
18
- nltk # Per lo splitting linguistico avanzato
19
- pandas
20
  spacy
21
 
22
  # --- Graph Database & Semantic Web ---
23
- neo4j>=5.0.0 # Driver Python ufficiale
24
- rdflib # Gestione RDF
25
- networkx # Calcoli su grafo (usato da PyVis/Streamlit)
26
-
27
- # --- Frontend ---
28
- streamlit>=1.30.0
29
- pyvis # Visualizzazione interattiva
30
 
 
 
 
 
 
 
 
 
31
  # --- Utilities ---
32
  python-dotenv
 
3
  langchain-community>=0.3.0
4
  langchain-ollama>=0.2.0
5
  langchain-huggingface>=0.1.0
6
+ langchain-groq
7
  langchain-core
8
  huggingface_hub
9
 
10
  # --- Data Validation ---
11
  pydantic>=2.0
12
+ pyshacl
13
 
14
  # --- NLP & Semantic Chunking ---
15
+ sentence-transformers
16
+ scikit-learn
17
  numpy
18
+ nltk
 
 
19
  spacy
20
 
21
  # --- Graph Database & Semantic Web ---
22
+ neo4j>=5.0.0
23
+ rdflib
 
 
 
 
 
24
 
25
+ # --- Web & API ---
26
+ fastapi
27
+ uvicorn
28
+ requests
29
+ streamlit
30
+ pyvis
31
+ pandas
32
+
33
  # --- Utilities ---
34
  python-dotenv
src/extraction/extractor.py CHANGED
@@ -5,13 +5,17 @@ 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
 
9
- # Gestione Multi-Backend (Locale vs Cloud)
10
  from langchain_ollama import ChatOllama
11
  from langchain_huggingface import HuggingFaceEmbeddings, ChatHuggingFace, HuggingFaceEndpoint
12
  from sklearn.metrics.pairwise import cosine_similarity
 
13
 
14
- # --- 1. DEFINIZIONE DELLO SCHEMA ---
 
 
 
15
  class GraphTriple(BaseModel):
16
  subject: str = Field(..., description="Entità sorgente (Canonical).")
17
  predicate: str = Field(..., description="Relazione (snake_case).")
@@ -21,13 +25,15 @@ class GraphTriple(BaseModel):
21
 
22
  class KnowledgeGraphExtraction(BaseModel):
23
  reasoning: Optional[str] = Field(None, description="Breve ragionamento logico.")
 
24
  triples: List[GraphTriple]
25
 
26
- # --- 2. ESTRATTORE DINAMICO (Dynamic Few-Shot) ---
27
  class NeuroSymbolicExtractor:
28
  def __init__(self, model_name="llama3", temperature=0, gold_standard_path=None):
29
 
30
  hf_token = os.getenv("HF_TOKEN")
 
31
 
32
  if hf_token:
33
  print("☁️ Rilevato ambiente Cloud (HF Spaces). Utilizzo HuggingFace Inference API.")
@@ -46,6 +52,17 @@ class NeuroSymbolicExtractor:
46
  except Exception as e:
47
  print(f"❌ Errore connessione HF API: {e}. Fallback su CPU locale (sconsigliato).")
48
  raise e
 
 
 
 
 
 
 
 
 
 
 
49
  else:
50
  print(f"🏠 Ambiente Locale rilevato. Inizializzazione Ollama: {model_name}...")
51
  try:
@@ -58,11 +75,11 @@ class NeuroSymbolicExtractor:
58
  except Exception as e:
59
  print(f"⚠️ Errore Ollama: {e}")
60
 
61
- # 2. Modello Embedding per la selezione dinamica
62
  print("🧠 Caricamento modello embedding per Dynamic Selection...")
63
  self.embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
64
 
65
- # 3. Caricamento e Indicizzazione Gold Standard
66
  self.examples = []
67
  self.example_embeddings = None
68
 
@@ -74,23 +91,25 @@ class NeuroSymbolicExtractor:
74
  print("⚠️ Nessun Gold Standard trovato. Modalità Zero-Shot.")
75
 
76
  # Template Specializzato (Prompt Engineering)
77
- self.system_template_base = """Sei l'Agente Cognitivo (AC) del sistema Canusium xCH.
78
- Il tuo compito è trasformare il testo non strutturato in un Digital Twin Graph (RDF).
79
 
80
  SCHEMA JSON RICHIESTO:
81
  {{
82
  "reasoning": "Spiega brevemente perché hai scelto queste classi/relazioni...",
 
83
  "triples": [
84
  {{"subject": "Entità", "predicate": "prefix:Relazione", "object": "Entità", "confidence": 0.95}}
85
  ]
86
  }}
87
 
88
- ONTOLOGIA DI RIFERIMENTO (Usa questi prefissi):
89
- - xchh: (Heritage) -> Per oggetti fisici, siti, reperti (es. xchh:HeritageObject, xchh:Site).
90
- - crm: (CIDOC-CRM) -> Per relazioni standard (es. crm:P55_has_current_location, crm:P4_has_time-span).
91
- - xche: (Experience) -> Per sessioni AR/VR, visitatori, interazioni (es. xche:ExperienceSession).
92
- - xcha: (Agents) -> Per agenti umani o artificiali.
93
- - skos: -> Per concetti generici o gerarchie.
 
94
 
95
  ESEMPI CONTESTUALI (Dynamic Few-Shot):
96
  {selected_examples}
@@ -99,8 +118,13 @@ class NeuroSymbolicExtractor:
99
  - 1.0 (Fatto Curato): Informazione esplicita e certa nel testo.
100
  - 0.8 - 0.9 (Inferenza): Deduzione logica forte ma non esplicita.
101
  - < 0.7 (Ipotesi): Associazione probabile ma incerta (da marcare per revisione umana).
 
 
 
 
 
102
 
103
- Canonicalizza i nomi (es. "Il Parco" -> "Parco Archeologico di Canne").
104
  Rispondi ESCLUSIVAMENTE con un JSON valido.
105
  """
106
 
@@ -110,7 +134,7 @@ class NeuroSymbolicExtractor:
110
  with open(path, 'r', encoding='utf-8') as f:
111
  self.examples = json.load(f)
112
 
113
- # Estraiamo solo il testo di input per calcolare l'embedding
114
  texts = [ex['text'] for ex in self.examples]
115
  self.example_embeddings = self.embedding_model.embed_documents(texts)
116
  print(f"✅ Indicizzati {len(self.examples)} esempi di Gold Standard.")
@@ -125,13 +149,13 @@ class NeuroSymbolicExtractor:
125
  if not self.examples or self.example_embeddings is None:
126
  return "Nessun esempio disponibile."
127
 
128
- # 1. Embed del chunk attuale
129
  query_embedding = self.embedding_model.embed_query(query_text)
130
 
131
- # 2. Calcolo similarità coseno
132
  similarities = cosine_similarity([query_embedding], self.example_embeddings)[0]
133
 
134
- # 3. Selezione dei top-k
135
  top_k_indices = np.argsort(similarities)[-k:][::-1]
136
 
137
  formatted_text = ""
@@ -140,9 +164,13 @@ class NeuroSymbolicExtractor:
140
  sim_score = similarities[idx]
141
  formatted_text += f"\n--- ESEMPIO RILEVANTE #{i+1} (Sim: {sim_score:.2f}) ---\n"
142
  formatted_text += f"INPUT: {ex['text']}\n"
143
- # Gestione sicura nel caso triples manchi
144
- triples_out = ex.get('triples', [])
145
- formatted_text += f"OUTPUT: {json.dumps({'triples': triples_out}, ensure_ascii=False)}\n"
 
 
 
 
146
 
147
  return formatted_text
148
 
@@ -177,6 +205,10 @@ class NeuroSymbolicExtractor:
177
  elif "```" in content:
178
  content = content.split("```")[1].split("```")[0].strip()
179
 
 
 
 
 
180
  data = json.loads(content)
181
 
182
  # Normalizzazione output
@@ -187,6 +219,7 @@ class NeuroSymbolicExtractor:
187
  triples = [GraphTriple(**t) for t in data.get("triples", [])]
188
  validated_data = KnowledgeGraphExtraction(
189
  reasoning=data.get("reasoning", "N/A"),
 
190
  triples=triples
191
  )
192
 
 
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_groq import ChatGroq
9
 
 
10
  from langchain_ollama import ChatOllama
11
  from langchain_huggingface import HuggingFaceEmbeddings, ChatHuggingFace, HuggingFaceEndpoint
12
  from sklearn.metrics.pairwise import cosine_similarity
13
+ from dotenv import load_dotenv
14
 
15
+
16
+ load_dotenv() # in locale carica il file .env , su HF non trovando il file utilizza i secrets inseriti nella sezione settings.
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).")
 
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")
37
 
38
  if hf_token:
39
  print("☁️ Rilevato ambiente Cloud (HF Spaces). Utilizzo HuggingFace Inference API.")
 
52
  except Exception as e:
53
  print(f"❌ Errore connessione HF API: {e}. Fallback su CPU locale (sconsigliato).")
54
  raise e
55
+ elif groq_api_key:
56
+ print("☁️ Rilevato ambiente Groq Cloud!")
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:
65
+ print(f"❌ Errore Groq API {e}")
66
  else:
67
  print(f"🏠 Ambiente Locale rilevato. Inizializzazione Ollama: {model_name}...")
68
  try:
 
75
  except Exception as e:
76
  print(f"⚠️ Errore Ollama: {e}")
77
 
78
+ # Modello Embedding per la selezione dinamica
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
 
 
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}
 
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
 
 
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.")
 
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 = ""
 
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
 
 
205
  elif "```" in content:
206
  content = content.split("```")[1].split("```")[0].strip()
207
 
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
  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
 
src/graph/entity_resolver.py CHANGED
@@ -1,77 +1,133 @@
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
 
 
 
1
  import numpy as np
2
+ import requests
3
  from sklearn.cluster import DBSCAN
4
  from langchain_huggingface import HuggingFaceEmbeddings
 
5
 
6
  class EntityResolver:
7
+ def __init__(self, neo4j_driver, model_name="all-MiniLM-L6-v2", similarity_threshold=0.85):
8
+ print("🧩 Inizializzazione Entity Resolver Ibrido (Vector Search + Wikidata EL)...")
 
 
 
 
 
9
  self.embedding_model = HuggingFaceEmbeddings(model_name=model_name)
 
 
10
  self.eps = 1 - similarity_threshold
11
+ self.similarity_threshold = similarity_threshold
12
+ self.driver = neo4j_driver
13
+
14
+ def _find_canonical_in_db(self, embedding_vector):
15
+ """Interroga l'indice vettoriale di Neo4j per trovare il nodo più simile."""
16
+ if not self.driver: return None
17
+
18
+ query = """
19
+ CALL db.index.vector.queryNodes('entity_embeddings', 1, $embedding)
20
+ YIELD node, score
21
+ WHERE score >= $threshold
22
+ RETURN node.label AS canonical_label, score
23
+ """
24
+ with self.driver.session() as session:
25
+ result = session.run(query, embedding=embedding_vector, threshold=self.similarity_threshold)
26
+ record = result.single()
27
+ if record:
28
+ return record["canonical_label"]
29
+ return None
30
 
31
+ def _link_to_wikidata(self, entity_name):
32
  """
33
+ Interroga l'API di Wikidata per trovare un match diretto (Entity Linking).
34
+ Ritorna l'URI di Wikidata (es. wd:Q12345) o None.
35
  """
36
+ url = "https://www.wikidata.org/w/api.php"
37
+ params = {
38
+ "action": "wbsearchentities",
39
+ "search": entity_name,
40
+ "language": "it",
41
+ "format": "json",
42
+ "limit": 1 # Prendo solo il best match per la riconciliazione automatica
43
+ }
44
+ try:
45
+ # Timeout breve per non bloccare la pipeline se Wikidata è lento
46
+ response = requests.get(url, params=params, timeout=3.0)
47
+ if response.status_code == 200:
48
+ data = response.json()
49
+ if not data.get("search"):
50
+ print(f" [DEBUG] Wikidata non ha trovato corrispondenze per: '{entity_name}'")
51
+
52
+ if data.get("search"):
53
+ best_match = data["search"][0]
54
+ return f"wd:{best_match['id']}"
55
+ except Exception as e:
56
+ print(f" ⚠️ Errore lookup Wikidata per '{entity_name}': {e}")
57
+ return None
58
 
59
+ def resolve_entities(self, extracted_entities, triples):
60
+ if not triples and not extracted_entities:
61
+ return [], []
62
+
63
+ # Raccolgo tutte le entità uniche dal chunk corrente
64
+ chunk_entities = set(extracted_entities)
65
  for t in triples:
66
+ chunk_entities.add(t.subject)
67
+ chunk_entities.add(t.object)
68
+ unique_chunk_entities = list(chunk_entities)
 
 
69
 
70
+ if not unique_chunk_entities:
71
+ return [], triples
72
 
73
+ # Calcolo gli embedding per il batch locale
74
+ embeddings = self.embedding_model.embed_documents(unique_chunk_entities)
75
+
76
+ # Local Batch Deduplication
77
+ clustering = DBSCAN(eps=self.eps, min_samples=1, metric='cosine').fit(np.array(embeddings))
78
+
79
+ local_cluster_map = {}
80
+ for entity, emb, label in zip(unique_chunk_entities, embeddings, clustering.labels_):
81
+ if label not in local_cluster_map:
82
+ local_cluster_map[label] = []
83
+ local_cluster_map[label].append({"name": entity, "embedding": emb})
84
 
85
+ entity_replacement_map = {}
86
+ entities_to_save = [] # Array di {label, embedding, wikidata_sameAs}
 
 
87
 
88
+ # Global Database Resolution & Wikidata Linking
89
+ for label, items in local_cluster_map.items():
90
+ local_canonical_item = sorted(items, key=lambda x: len(x["name"]), reverse=True)[0]
91
+ local_canonical_name = local_canonical_item["name"]
92
+ local_canonical_emb = local_canonical_item["embedding"]
 
 
93
 
94
+ db_canonical_name = self._find_canonical_in_db(local_canonical_emb)
95
+
96
+ if db_canonical_name:
97
+ # Caso A: Neo4j conosce già questa entità (ha già il suo embedding e potenziale URI)
98
+ final_canonical = db_canonical_name
99
+ print(f" 🔗 Match Globale: '{local_canonical_name}' -> '{db_canonical_name}' (Neo4j)")
 
 
 
 
 
100
  else:
101
+ # Caso B: È un'entità veramente nuova. Tento l'Entity Linking!
102
+ final_canonical = local_canonical_name
103
+
104
+ # Chiamata a Wikidata
105
+ wikidata_uri = self._link_to_wikidata(final_canonical)
106
+
107
+ entity_dict = {
108
+ "label": final_canonical,
109
+ "embedding": local_canonical_emb,
110
+ "wikidata_sameAs": wikidata_uri
111
+ }
112
+
113
+ if wikidata_uri:
114
+ print(f" ✨ Nuova Entità: '{final_canonical}' 🌍 Linked to: {wikidata_uri}")
115
+ else:
116
+ print(f" ✨ Nuova Entità: '{final_canonical}' (No Wiki link)")
117
+
118
+ entities_to_save.append(entity_dict)
119
+
120
+ # Mappo le varianti locali al canonico
121
+ for item in items:
122
+ entity_replacement_map[item["name"]] = final_canonical
123
 
124
+ # Riscrittura Output
125
  resolved_triples = []
126
  for t in triples:
 
127
  t.subject = entity_replacement_map.get(t.subject, t.subject)
128
  t.object = entity_replacement_map.get(t.object, t.object)
129
  resolved_triples.append(t)
130
 
131
+ resolved_entities = list(set([entity_replacement_map.get(e, e) for e in extracted_entities]))
132
+
133
+ return resolved_entities, resolved_triples, entities_to_save
src/graph/graph_loader.py CHANGED
@@ -3,17 +3,16 @@ from collections import defaultdict
3
  from neo4j import GraphDatabase
4
  from dotenv import load_dotenv
5
 
6
- # Carica variabili d'ambiente
7
- load_dotenv()
8
 
9
  class KnowledgeGraphPersister:
10
  def __init__(self):
11
  """
12
  Inizializza il driver Neo4j e crea i vincoli necessari per le performance.
13
  """
14
- uri = os.getenv("NEO4J_URI", "neo4j+s://748d6c94.databases.neo4j.io")
15
- user = os.getenv("NEO4J_USER", "neo4j")
16
- password = os.getenv("NEO4J_PASSWORD", "t1bT1DiXwDOGMYfX89qR20loSN8FXurB3Dfg8bPQcTI")
17
 
18
  try:
19
  self.driver = GraphDatabase.driver(uri, auth=(user, password))
@@ -38,13 +37,26 @@ class KnowledgeGraphPersister:
38
  """
39
  if not self.driver: return
40
  query = "CREATE CONSTRAINT resource_uri_unique IF NOT EXISTS FOR (n:Resource) REQUIRE n.uri IS UNIQUE"
 
 
 
 
 
 
 
 
41
  with self.driver.session() as session:
42
  try:
43
  session.run(query)
44
- print("⚡ Vincoli/Indici Neo4j verificati.")
 
 
 
 
 
 
45
  except Exception as e:
46
- # Spesso fallisce se l'utente non ha permessi admin o se esiste già con nome diverso
47
- print(f"⚠️ Warning creazione indici: {e}")
48
 
49
  def sanitize_name(self, name):
50
  """
@@ -57,20 +69,17 @@ class KnowledgeGraphPersister:
57
  def sanitize_predicate(self, pred):
58
  """
59
  Pulisce il predicato per evitare Cypher Injection.
60
- FIX: Gestisce meglio i separatori (:, -, spazio) sostituendoli con underscore
61
- per evitare predicati illeggibili come XCHEHASOBJECT.
62
- Es. xche:has_object -> XCHE_HAS_OBJECT
63
  """
64
  if not pred: return "RELATED_TO"
65
 
66
- # 1. Normalizzazione preliminare dei separatori comuni
67
- # Sostituisce i due punti dei namespace e trattini con underscore
68
  pred = pred.replace(":", "_").replace("-", "_").replace(" ", "_")
69
 
70
- # 2. Rimozione caratteri non sicuri (mantiene solo alfanumerici e underscore)
71
  clean = "".join(x for x in pred if x.isalnum() or x == "_")
72
 
73
- # 3. Conversione in uppercase (convenzione Neo4j per Relationships)
74
  return clean.upper() if clean else "RELATED_TO"
75
 
76
  def save_triples(self, triples):
@@ -83,7 +92,7 @@ class KnowledgeGraphPersister:
83
 
84
  print(f"💾 Preparazione Batch di {len(triples)} triple...")
85
 
86
- # 1. Raggruppamento per Predicato
87
  batched_by_pred = defaultdict(list)
88
 
89
  for t in triples:
@@ -99,7 +108,7 @@ class KnowledgeGraphPersister:
99
  }
100
  batched_by_pred[safe_pred].append(item)
101
 
102
- # 2. Esecuzione Transazioni (Una per tipo di relazione)
103
  with self.driver.session() as session:
104
  for pred, data_list in batched_by_pred.items():
105
  try:
@@ -110,6 +119,39 @@ class KnowledgeGraphPersister:
110
 
111
  print("✅ Salvataggio completato.")
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  @staticmethod
114
  def _unwind_write_tx(tx, predicate, batch_data):
115
  """
 
3
  from neo4j import GraphDatabase
4
  from dotenv import load_dotenv
5
 
6
+ load_dotenv() # in locale carica il file .env , su HF non trovando il file utilizza i secrets inseriti nella sezione settings.
 
7
 
8
  class KnowledgeGraphPersister:
9
  def __init__(self):
10
  """
11
  Inizializza il driver Neo4j e crea i vincoli necessari per le performance.
12
  """
13
+ uri = os.getenv("NEO4J_URI")
14
+ user = os.getenv("NEO4J_USER")
15
+ password = os.getenv("NEO4J_PASSWORD")
16
 
17
  try:
18
  self.driver = GraphDatabase.driver(uri, auth=(user, password))
 
37
  """
38
  if not self.driver: return
39
  query = "CREATE CONSTRAINT resource_uri_unique IF NOT EXISTS FOR (n:Resource) REQUIRE n.uri IS UNIQUE"
40
+ query_vector = """
41
+ CREATE VECTOR INDEX entity_embeddings IF NOT EXISTS
42
+ FOR (n:Resource) ON (n.embedding)
43
+ OPTIONS {indexConfig: {
44
+ `vector.dimensions`: 384,
45
+ `vector.similarity_function`: 'cosine'
46
+ }}
47
+ """
48
  with self.driver.session() as session:
49
  try:
50
  session.run(query)
51
+ print("⚡ Vincolo di unicità verificato.")
52
+ except Exception as e:
53
+ print(f"⚠️ Warning vincolo unicità: {e}")
54
+
55
+ try:
56
+ session.run(query_vector)
57
+ print("⚡ Vector Index verificato.")
58
  except Exception as e:
59
+ print(f"⚠️ Warning vector index: {e}")
 
60
 
61
  def sanitize_name(self, name):
62
  """
 
69
  def sanitize_predicate(self, pred):
70
  """
71
  Pulisce il predicato per evitare Cypher Injection.
 
 
 
72
  """
73
  if not pred: return "RELATED_TO"
74
 
75
+ # Normalizzazione preliminare dei separatori comuni
76
+ # Sostituisco i due punti dei namespace e trattini con underscore
77
  pred = pred.replace(":", "_").replace("-", "_").replace(" ", "_")
78
 
79
+ # Rimozione caratteri non sicuri (mantiene solo alfanumerici e underscore)
80
  clean = "".join(x for x in pred if x.isalnum() or x == "_")
81
 
82
+ # Conversione in uppercase (convenzione Neo4j per Relationships)
83
  return clean.upper() if clean else "RELATED_TO"
84
 
85
  def save_triples(self, triples):
 
92
 
93
  print(f"💾 Preparazione Batch di {len(triples)} triple...")
94
 
95
+ # Raggruppamento per Predicato
96
  batched_by_pred = defaultdict(list)
97
 
98
  for t in triples:
 
108
  }
109
  batched_by_pred[safe_pred].append(item)
110
 
111
+ # Esecuzione Transazioni (Una per tipo di relazione)
112
  with self.driver.session() as session:
113
  for pred, data_list in batched_by_pred.items():
114
  try:
 
119
 
120
  print("✅ Salvataggio completato.")
121
 
122
+ def save_entities_and_triples(self, entities_to_save, triples):
123
+ """Salva prima i nodi isolati (con i loro vettori), poi le relazioni."""
124
+ if not self.driver: return
125
+
126
+ # Salvataggio Nodi (anche senza relazioni, includendo l'embedding)
127
+ if entities_to_save:
128
+ print(f"💾 Salvataggio di {len(entities_to_save)} nodi singoli con vettori...")
129
+
130
+ # Aggiungo il campo "uri" calcolandolo dalla label
131
+ node_batch = []
132
+ for item in entities_to_save:
133
+ item["uri"] = self.sanitize_name(item["label"])
134
+ node_batch.append(item)
135
+
136
+ with self.driver.session() as session:
137
+ session.execute_write(self._unwind_write_nodes, node_batch)
138
+
139
+ # Salvataggio Triple
140
+ if triples:
141
+ self.save_triples(triples)
142
+
143
+ @staticmethod
144
+ def _unwind_write_nodes(tx, batch_data):
145
+ query = (
146
+ "UNWIND $batch AS row "
147
+ "MERGE (n:Resource {uri: row.uri}) "
148
+ "ON CREATE SET n.label = row.label, "
149
+ " n.embedding = row.embedding, "
150
+ " n.wikidata_sameAs = row.wikidata_sameAs, "
151
+ " n.last_updated = datetime() "
152
+ )
153
+ tx.run(query, batch=batch_data)
154
+
155
  @staticmethod
156
  def _unwind_write_tx(tx, predicate, batch_data):
157
  """
src/ingestion/semantic_splitter.py CHANGED
@@ -1,32 +1,21 @@
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/all-MiniLM-L6-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
@@ -38,7 +27,7 @@ class ActivaSemanticSplitter:
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')
@@ -46,9 +35,8 @@ class ActivaSemanticSplitter:
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:
@@ -57,7 +45,6 @@ class ActivaSemanticSplitter:
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)
@@ -94,7 +81,9 @@ class ActivaSemanticSplitter:
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
 
@@ -109,8 +98,10 @@ class ActivaSemanticSplitter:
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
 
1
  import os
2
  import re
3
  import numpy as np
 
4
  from sklearn.metrics.pairwise import cosine_similarity
5
  from dotenv import load_dotenv
6
  from langchain_huggingface import HuggingFaceEmbeddings
7
 
8
+ load_dotenv() # in locale carica il file .env , su HF non trovando il file utilizza i secrets inseriti nella sezione settings.
9
 
10
  class ActivaSemanticSplitter:
11
  def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2", batch_size=32):
12
  self.batch_size = batch_size
 
13
 
14
+ print("🔄 Inizializzazione HuggingFace Embedding Engine...")
15
 
16
  try:
17
+ self.embedding_model = HuggingFaceEmbeddings(model_name=model_name)
 
 
 
 
 
 
 
 
18
  print("✅ Modello caricato correttamente.")
 
19
  except Exception as e:
20
  print(f"❌ Errore caricamento modello: {e}")
21
  raise e
 
27
  text = text.strip()
28
  try:
29
  import nltk
30
+ # Controllo che i dati ci siano
31
  try:
32
  nltk.data.find('tokenizers/punkt')
33
  nltk.data.find('tokenizers/punkt_tab')
 
35
  print("⬇️ Download risorse NLTK...")
36
  nltk.download('punkt', quiet=True)
37
  nltk.download('punkt_tab', quiet=True)
38
+
39
+ # Invece di usare sent_tokenize() che è una black box, carico l'oggetto.
 
40
  try:
41
  tokenizer = nltk.data.load('tokenizers/punkt/italian.pickle')
42
  except:
 
45
  tokenizer = PunktSentenceTokenizer()
46
 
47
  # --- LISTA ECCEZIONI ABBREVIAZIONI ---
 
48
  custom_abbrevs = ['sec', 's', 'prof', 'dott', 'avv', 'pag', 'fig', 'nr', 'art']
49
  for abbr in custom_abbrevs:
50
  tokenizer._params.abbrev_types.add(abbr)
 
81
  distances = []
82
  for i in range(len(embeddings) - 1):
83
  similarity = cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0]
84
+ #(0 = identiche, 1 = completamente diverse)
85
+ distance = 1.0 - similarity
86
+ distances.append(distance)
87
 
88
  return distances, embeddings
89
 
 
98
  if not distances:
99
  return [text], [], 0
100
 
101
+ threshold = np.percentile(distances, percentile_threshold)
102
+
103
+ # Un breakpoint avviene quando la distanza supera la soglia
104
+ indices_above_thresh = [i for i, x in enumerate(distances) if x > threshold]
105
 
106
  chunks = []
107
  start_index = 0
src/validation/validator.py CHANGED
@@ -5,7 +5,7 @@ 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
 
@@ -18,46 +18,57 @@ class SemanticValidator:
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(
 
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
 
 
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
 
62
+ def validate_batch(self, entities, triples):
63
  """
64
+ Esegue la validazione SHACL sia sulle entità isolate che sulle triple.
65
  Ritorna (is_valid, report_text, rdf_graph)
66
  """
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...")
74
  conforms, report_graph, report_text = validate(