GaetanoParente's picture
riviste le varie sezioni e i commenti
c1b1880
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
import os
import time
import hashlib
from contextlib import asynccontextmanager
from src.ingestion.semantic_splitter import ActivaSemanticSplitter
from src.extraction.extractor import NeuroSymbolicExtractor
from src.validation.validator import SemanticValidator
from src.graph.graph_loader import KnowledgeGraphPersister
from src.graph.entity_resolver import EntityResolver
# --- GESTORE DEGLI STATI GLOBALI ---
# Usiamo un dizionario globale per tenere in RAM i pesi dei modelli.
ml_models = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Nel mondo FastAPI il lifespan è il modo più pulito per fare il setup.
# Mi permette di caricare i modelli di embedding e l'LLM all'avvio del worker, una sola volta.
print("⏳ Inizializzazione modelli (SentenceTransformers e Llama3) nel Lifespan...")
ml_models["splitter"] = ActivaSemanticSplitter(model_name="all-MiniLM-L6-v2")
schema_path = os.path.join("data", "schemas", "ARCO_schema.json")
ml_models["extractor"] = NeuroSymbolicExtractor(model_name="llama3", schema_path=schema_path)
ml_models["persister"] = KnowledgeGraphPersister()
ml_models["resolver"] = EntityResolver(neo4j_driver=ml_models["persister"].driver, similarity_threshold=0.85)
ml_models["validator"] = SemanticValidator()
print("✅ Modelli caricati e pronti a ricevere richieste!")
yield # Qui l'API inizia ad ascoltare le chiamate in ingresso
# Chiusura pulita delle connessioni. Evita query appese su Neo4j quando killiamo il container.
print("🛑 Spegnimento in corso... chiusura connessioni e pulizia memoria.")
if "persister" in ml_models and ml_models["persister"]:
ml_models["persister"].close()
ml_models.clear()
app = FastAPI(
title="Automated Semantic Discovery API",
description="Endpoint per l'ingestion testuale e l'estrazione neuro-simbolica",
version="1.0",
lifespan=lifespan
)
class DiscoveryRequest(BaseModel):
documentText: str
@app.post("/api/discover")
def run_discovery(payload: DiscoveryRequest):
start_time = time.time()
raw_text = payload.documentText
if not raw_text or not raw_text.strip():
raise HTTPException(status_code=400, detail="Il testo fornito è vuoto.")
# Recupero le istanze
splitter = ml_models["splitter"]
extractor = ml_models["extractor"]
validator = ml_models["validator"]
resolver = ml_models["resolver"]
persister = ml_models["persister"]
# --- FASE 1: INGESTION ---
# Taglio il testo in modo semantico per non sforare la context window dell'LLM
chunks, _, _ = splitter.create_chunks(raw_text, percentile_threshold=90)
# --- FASE 2: EXTRACTION ---
# Invocazione del motore neuro-simbolico per ogni blocco di testo
all_triples = []
all_entities = []
for i, chunk in enumerate(chunks):
chunk_id = f"api_req_chunk_{i+1}"
extraction_result = extractor.extract(chunk, source_id=chunk_id)
if extraction_result:
if extraction_result.triples:
all_triples.extend(extraction_result.triples)
if hasattr(extraction_result, 'entities') and extraction_result.entities:
all_entities.extend(extraction_result.entities)
if not all_triples:
return {
"status": "success",
"message": "Nessuna entità trovata.",
"graph_data": []
}
# --- FASE 2.1: SYMBOLIC RESOLUTION ---
# Deduplica in RAM e linking verso Wikidata e Neo4j (Entity Resolution)
entities_to_save = []
try:
all_entities, all_triples, entities_to_save = resolver.resolve_entities(all_entities, all_triples)
except Exception as e:
print(f"⚠️ Errore nel resolver (skip): {e}")
# --- FASE 2.2: VALIDATION ---
# Prima di salvare nel DB, verifico con SHACL
# se l'LLM ha generato allucinazioni o violato i vincoli dell'ontologia.
is_valid, report, _ = validator.validate_batch(entities_to_save, all_triples)
if not is_valid:
print("\n❌ [SHACL VALIDATION FAILED] Rilevate entità o relazioni non conformi all'ontologia:")
print(report)
print("-" * 60)
else:
print("\n✅ [SHACL VALIDATION SUCCESS] Tutte le triple ed entità rispettano i vincoli.")
# --- FASE 3: PERSISTENCE (Neo4j) ---
try:
persister.save_entities_and_triples(entities_to_save, all_triples)
except Exception as e:
print(f"⚠️ Errore salvataggio Neo4j: {e}")
# Preparazione payload di risposta
graph_data = []
for t in all_triples:
subj = getattr(t, 'subject', t[0] if isinstance(t, tuple) else str(t))
pred = getattr(t, 'predicate', t[1] if isinstance(t, tuple) else '')
obj = getattr(t, 'object', t[2] if isinstance(t, tuple) else '')
if isinstance(t, tuple) and len(t) > 3:
conf = t[3]
else:
conf = getattr(t, 'confidence', 1.0)
subj_str = str(subj)
pred_str = str(pred)
obj_str = str(obj)
# Genero un ID stabile per facilitare il rendering dei nodi lato client
node_id = hashlib.md5(subj_str.encode('utf-8')).hexdigest()
graph_data.append({
"start_node_id": node_id,
"start_node_label": subj_str,
"relationship_type": pred_str,
"end_node_label": obj_str,
"confidence": float(conf)
})
return {
"status": "success",
"message": "Estrazione semantica completata",
"execution_time_seconds": round(time.time() - start_time, 2),
"chunks_processed": len(chunks),
"triples_extracted": len(graph_data),
"shacl_valid": is_valid,
"graph_data": graph_data
}
if __name__ == "__main__":
uvicorn.run("api:app", host="0.0.0.0", port=5000, reload=True)