Commit ·
cc3f780
1
Parent(s): fc23ce5
integrata riconciliazione semantica ed estrazione singole entità
Browse files- .env.example +22 -0
- .gitignore +2 -0
- Dockerfile +3 -11
- README.md +84 -109
- api.py +127 -0
- app.py +179 -214
- app/ui.py +0 -161
- assets/style.css +61 -0
- data/examples/intelligenza_artificiale.txt +0 -9
- data/examples/la_prima_parte_della_via_appia.txt +0 -0
- data/examples/parco_canne_battaglia.txt +0 -9
- data/examples/venezia_monumentale.txt +0 -10
- data/gold_standard/examples.json +52 -45
- data/processed/chunks_debug.txt +0 -6
- data/raw/menhir_test.txt +0 -5
- data/raw/venezia_arte.doc +0 -13
- docker-compose.yml +0 -26
- main.py +0 -131
- requirements.txt +15 -13
- src/extraction/extractor.py +54 -21
- src/graph/entity_resolver.py +108 -52
- src/graph/graph_loader.py +59 -17
- src/ingestion/semantic_splitter.py +13 -22
- src/validation/validator.py +37 -26
.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 |
-
|
| 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
|
| 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:
|
| 10 |
---
|
| 11 |
|
| 12 |
# Automated Semantic Discovery – Prototype
|
| 13 |
|
| 14 |
-

|
| 16 |
-
**.
|
| 65 |
|
| 66 |
## Struttura del repository
|
| 67 |
|
|
@@ -69,134 +62,116 @@ La pipeline è organizzata in **moduli indipendenti e sequenziali**.
|
|
| 69 |
prototipo/
|
| 70 |
│
|
| 71 |
├── data/
|
| 72 |
-
│
|
| 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 |
-
│
|
|
|
|
| 84 |
│
|
| 85 |
-
├──
|
| 86 |
-
├── .
|
| 87 |
-
├──
|
|
|
|
|
|
|
| 88 |
└── README.md
|
| 89 |
```
|
| 90 |
|
| 91 |
## Tech Stack & Requisiti
|
| 92 |
|
| 93 |
-
- **Linguaggio**: Python 3.
|
| 94 |
-
- **Database**: Neo4j (
|
|
|
|
| 95 |
|
| 96 |
### Core Libraries
|
| 97 |
|
| 98 |
-
- **Neuro /
|
| 99 |
-
`
|
| 100 |
|
| 101 |
-
- **
|
| 102 |
-
`
|
| 103 |
|
| 104 |
-
- **
|
| 105 |
-
`
|
| 106 |
|
| 107 |
> Le dipendenze complete sono elencate in `requirements.txt`.
|
| 108 |
|
| 109 |
-
## Configurazione
|
| 110 |
|
| 111 |
-
|
| 112 |
|
| 113 |
```env
|
| 114 |
-
NEO4J_URI=
|
| 115 |
NEO4J_USER=neo4j
|
| 116 |
-
NEO4J_PASSWORD=
|
|
|
|
|
|
|
| 117 |
```
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
## Installazione
|
| 122 |
|
| 123 |
```bash
|
| 124 |
-
|
|
|
|
| 125 |
cd prototipo
|
| 126 |
|
|
|
|
| 127 |
python -m venv venv
|
| 128 |
source venv/bin/activate # Linux / macOS
|
| 129 |
-
# venv\
|
| 130 |
|
|
|
|
| 131 |
pip install -r requirements.txt
|
| 132 |
```
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
### 1. Inserimento dei documenti
|
| 137 |
-
|
| 138 |
-
Copiare i documenti in `data/raw/`.
|
| 139 |
-
|
| 140 |
-
### 2. Segmentazione semantica
|
| 141 |
|
| 142 |
```bash
|
| 143 |
-
|
| 144 |
```
|
| 145 |
|
| 146 |
-
|
| 147 |
|
| 148 |
-
|
| 149 |
-
python src/extraction/extractor.py
|
| 150 |
-
```
|
| 151 |
|
| 152 |
-
|
| 153 |
|
| 154 |
```bash
|
| 155 |
-
python
|
| 156 |
```
|
| 157 |
|
| 158 |
-
|
| 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 |
-
##
|
| 181 |
|
| 182 |
-
|
| 183 |
-
<img src="docs/graph.png" alt="Grafo risultante su Neo4j" width="90%">
|
| 184 |
-
</p>
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
| 188 |
|
| 189 |
## Limiti noti
|
| 190 |
|
| 191 |
-
- **
|
| 192 |
-
- **
|
| 193 |
-
- **LLM**: uso intenzionalmente limitato per privilegiare determinismo e spiegabilità.
|
| 194 |
|
| 195 |
## Possibili estensioni future
|
| 196 |
|
| 197 |
-
-
|
| 198 |
-
-
|
| 199 |
-
-
|
|
|
|
| 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 |
+

|
| 15 |
+

|
| 16 |
+

|
| 17 |

|
| 18 |
+

|
| 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
|
| 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 |
-
|
| 29 |
-
|
| 30 |
-
st.markdown(""
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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
|
| 57 |
-
if '
|
| 58 |
-
st.session_state.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
def reset_pipeline():
|
| 61 |
st.session_state.pipeline_stage = 0
|
| 62 |
-
st.session_state.
|
| 63 |
-
|
| 64 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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("**
|
| 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 |
-
|
| 163 |
-
st.
|
| 164 |
-
st.info("Seleziona uno degli scenari dimostrativi validati per avviare la pipeline.")
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 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
|
| 201 |
# ==========================
|
| 202 |
with st.container():
|
| 203 |
-
st.markdown(f"### {'✅' if st.session_state.pipeline_stage >= 1 else '1️⃣'} Fase
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
if st.session_state.pipeline_stage >= 1:
|
| 206 |
-
|
| 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.
|
| 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
|
| 217 |
else:
|
| 218 |
-
|
| 219 |
-
|
| 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,
|
| 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
|
| 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
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
st.
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
<
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
| 290 |
|
| 291 |
st.markdown("⬇️")
|
| 292 |
|
| 293 |
# ==========================
|
| 294 |
-
# FASE
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
if not is_step_c_unlocked:
|
| 304 |
-
st.caption("Completa la Fase
|
| 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.
|
| 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
|
| 323 |
try:
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
triples_objs = [GraphTriple(**t) for t in raw_data]
|
| 328 |
|
| 329 |
resolver = get_resolver()
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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,
|
| 361 |
-
ORDER BY Confidenza ASC
|
| 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"]),
|
| 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 |
-
|
| 383 |
-
st.rerun()
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
else:
|
| 412 |
-
st.info("
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"triples": [
|
| 5 |
-
{"subject": "Menhir di Canne", "predicate": "
|
| 6 |
-
{"subject": "Menhir di Canne", "predicate": "
|
| 7 |
-
{"subject": "Menhir di Canne", "predicate": "
|
| 8 |
-
{"subject": "Menhir di Canne", "predicate": "
|
| 9 |
]
|
| 10 |
},
|
| 11 |
{
|
| 12 |
-
"text": "La Battaglia di Canne del 216 a.C. vide la vittoria dell'esercito cartaginese guidato da Annibale
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"triples": [
|
| 14 |
-
{"subject": "Battaglia di Canne", "predicate": "
|
| 15 |
-
{"subject": "Battaglia di Canne", "predicate": "
|
| 16 |
-
{"subject": "Battaglia di Canne", "predicate": "
|
| 17 |
-
{"subject": "Annibale", "predicate": "
|
| 18 |
]
|
| 19 |
},
|
| 20 |
{
|
| 21 |
-
"text": "L'Antiquarium custodisce un prezioso corredo funerario proveniente dalla necropoli dauna
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"triples": [
|
| 23 |
-
{"subject": "Antiquarium", "predicate": "
|
| 24 |
-
{"subject": "Corredo funerario", "predicate": "
|
| 25 |
-
{"subject": "Corredo funerario", "predicate": "
|
| 26 |
-
{"subject": "Vasi a figure rosse", "predicate": "
|
| 27 |
]
|
| 28 |
},
|
| 29 |
{
|
| 30 |
-
"text": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"triples": [
|
| 32 |
-
{"subject": "
|
| 33 |
-
{"subject": "
|
| 34 |
-
{"subject": "
|
| 35 |
-
{"subject": "
|
| 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
|
| 12 |
|
| 13 |
# --- NLP & Semantic Chunking ---
|
| 14 |
-
sentence-transformers
|
| 15 |
-
scikit-learn
|
| 16 |
numpy
|
| 17 |
-
|
| 18 |
-
nltk # Per lo splitting linguistico avanzato
|
| 19 |
-
pandas
|
| 20 |
spacy
|
| 21 |
|
| 22 |
# --- Graph Database & Semantic Web ---
|
| 23 |
-
neo4j>=5.0.0
|
| 24 |
-
rdflib
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
# ---
|
| 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 |
-
#
|
| 62 |
print("🧠 Caricamento modello embedding per Dynamic Selection...")
|
| 63 |
self.embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
| 64 |
|
| 65 |
-
#
|
| 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
|
| 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 |
-
-
|
| 90 |
-
-
|
| 91 |
-
-
|
| 92 |
-
-
|
| 93 |
-
-
|
|
|
|
| 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 |
-
#
|
| 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 |
-
#
|
| 129 |
query_embedding = self.embedding_model.embed_query(query_text)
|
| 130 |
|
| 131 |
-
#
|
| 132 |
similarities = cosine_similarity([query_embedding], self.example_embeddings)[0]
|
| 133 |
|
| 134 |
-
#
|
| 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 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 20 |
"""
|
| 21 |
-
|
|
|
|
| 22 |
"""
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
for t in triples:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
unique_entities = list(all_entities)
|
| 33 |
-
print(f" Analisi di {len(unique_entities)} entità uniche per deduplica...")
|
| 34 |
|
| 35 |
-
if
|
| 36 |
-
return triples
|
| 37 |
|
| 38 |
-
#
|
| 39 |
-
embeddings = self.embedding_model.embed_documents(
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
clustering = DBSCAN(eps=self.eps, min_samples=1, metric='cosine').fit(X)
|
| 45 |
-
labels = clustering.labels_
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
cluster_map[label] = []
|
| 53 |
-
cluster_map[label].append(entity)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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"
|
| 15 |
-
user = os.getenv("NEO4J_USER"
|
| 16 |
-
password = os.getenv("NEO4J_PASSWORD"
|
| 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("⚡
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
except Exception as e:
|
| 46 |
-
|
| 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 |
-
#
|
| 67 |
-
#
|
| 68 |
pred = pred.replace(":", "_").replace("-", "_").replace(" ", "_")
|
| 69 |
|
| 70 |
-
#
|
| 71 |
clean = "".join(x for x in pred if x.isalnum() or x == "_")
|
| 72 |
|
| 73 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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(
|
| 17 |
|
| 18 |
try:
|
| 19 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
| 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,
|
| 113 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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
|
| 23 |
g = Graph()
|
| 24 |
g.bind("skos", SKOS)
|
| 25 |
g.bind("ex", self.EX)
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
pred = self.EX[t.predicate]
|
| 47 |
|
| 48 |
-
|
| 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 |
-
|
|
|
|
| 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(
|