NexuxAI / src /tools /graph_rag_engine.py
Gemita284's picture
feat: deploy inicial NexusAI Agent Hub
5688f8f
Raw
History Blame Contribute Delete
8.23 kB
# src/tools/graph_rag_engine.py
# =====================================================================
# MOTOR DE GRAPH RAG
#
# Combina dos fuentes de conocimiento:
# 1. FAISS (vectorstore) → fragmentos de texto relevantes
# 2. NetworkX (grafo) → conceptos relacionados factualmente
#
# El resultado es un contexto más rico que el RAG normal,
# porque no solo encuentra fragmentos similares sino que también
# navega las conexiones entre conceptos del grafo.
# =====================================================================
import logging
import os
import pickle
import networkx as nx
from langchain_core.tools import tool
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("graph_rag_engine")
# =====================================================================
# RUTAS
# =====================================================================
RAIZ_PROYECTO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
RUTA_VECTORSTORE = os.path.join(RAIZ_PROYECTO, "vectorstore_ia")
RUTA_GRAFO = os.path.join(RAIZ_PROYECTO, "knowledge_graph.pkl")
# =====================================================================
# CARGAMOS RECURSOS UNA SOLA VEZ AL ARRANCAR LA APP
# Igual que en rag_engine.py, cargamos fuera de la función
# para no recargar en cada pregunta
# =====================================================================
logger.info("Cargando modelo de embeddings para Graph RAG...")
try:
MODELO_EMBEDDINGS = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
logger.info("✅ Modelo de embeddings cargado")
except Exception as e:
logger.error(f"❌ Error cargando embeddings: {e}")
MODELO_EMBEDDINGS = None
logger.info("Cargando grafo de conocimiento desde disco...")
try:
with open(RUTA_GRAFO, "rb") as f:
GRAFO = pickle.load(f)
logger.info(f"✅ Grafo cargado: {GRAFO.number_of_nodes()} nodos, "
f"{GRAFO.number_of_edges()} aristas")
except Exception as e:
logger.error(f"❌ Error cargando grafo: {e}")
GRAFO = None
def buscar_en_grafo(query: str, max_nodos: int = 5) -> str:
"""
Busca en el grafo de conocimiento los conceptos relacionados
con la query del usuario.
Estrategia:
1. Busca nodos del grafo cuyo nombre aparezca en la query
2. Para cada nodo encontrado, recoge sus vecinos (conceptos relacionados)
3. Devuelve las relaciones como texto para el LLM
Ejemplo:
Query: "explícame los transformers"
Nodo encontrado: "Transformer"
Vecinos: "Red Neuronal" (es_tipo_de), "Atención" (usa), "BERT" (base_de)
"""
if GRAFO is None:
return "Grafo de conocimiento no disponible."
query_lower = query.lower()
contexto_grafo = ""
nodos_encontrados = []
# Buscamos qué nodos del grafo aparecen mencionados en la query
for nodo in GRAFO.nodes():
if nodo.lower() in query_lower or query_lower in nodo.lower():
nodos_encontrados.append(nodo)
# Si no encontramos nodos directos, buscamos por palabras clave
if not nodos_encontrados:
palabras_query = query_lower.split()
for nodo in GRAFO.nodes():
nodo_lower = nodo.lower()
for palabra in palabras_query:
if len(palabra) > 4 and palabra in nodo_lower:
nodos_encontrados.append(nodo)
break
if not nodos_encontrados:
logger.info(" No se encontraron nodos relevantes en el grafo para esta query")
return ""
logger.info(f" Nodos encontrados en el grafo: {nodos_encontrados[:max_nodos]}")
# Para cada nodo encontrado, recogemos sus conexiones
contexto_grafo = "Contexto factual del grafo de conocimiento:\n\n"
for nodo in nodos_encontrados[:max_nodos]:
contexto_grafo += f"Concepto: '{nodo}'\n"
# Vecinos de salida — lo que este concepto ES o HACE
vecinos_salida = list(GRAFO.successors(nodo))
if vecinos_salida:
for vecino in vecinos_salida[:4]:
relacion = GRAFO.edges[nodo, vecino].get("label", "relacionado_con")
contexto_grafo += f" → {relacion} → '{vecino}'\n"
# Vecinos de entrada — qué otros conceptos apuntan a este
vecinos_entrada = list(GRAFO.predecessors(nodo))
if vecinos_entrada:
for vecino in vecinos_entrada[:4]:
relacion = GRAFO.edges[vecino, nodo].get("label", "relacionado_con")
contexto_grafo += f" ← '{vecino}' ← {relacion}\n"
contexto_grafo += "\n"
return contexto_grafo
@tool
def investigar_con_graph_rag(query: str) -> str:
"""
Versión mejorada del motor RAG científico que combina:
1. Búsqueda vectorial en papers académicos (FAISS)
2. Navegación del grafo de conocimiento (NetworkX)
Úsala para responder preguntas sobre Inteligencia Artificial,
machine learning, deep learning, redes neuronales y temas relacionados.
Proporciona respuestas más precisas y con mayor contexto factual
que el RAG tradicional.
"""
logger.info(f"Graph RAG iniciado. Query: '{query}'")
# ------------------------------------------------------------------
# PASO 1: BÚSQUEDA VECTORIAL (igual que rag_engine.py)
# Buscamos los fragmentos de texto más relevantes en FAISS
# ------------------------------------------------------------------
logger.info("PASO 1: Búsqueda vectorial en FAISS...")
contexto_vectorial = ""
if not os.path.exists(RUTA_VECTORSTORE):
logger.error("No existe el vectorstore — ejecuta build_vectorstore.py primero")
contexto_vectorial = "Base de datos vectorial no disponible."
else:
try:
vector_store = FAISS.load_local(
RUTA_VECTORSTORE,
MODELO_EMBEDDINGS,
allow_dangerous_deserialization=True
)
chunks = vector_store.similarity_search(query, k=3)
contexto_vectorial = "Fragmentos relevantes de papers científicos:\n\n"
for i, chunk in enumerate(chunks):
titulo = chunk.metadata.get("Title", "Paper científico")
contexto_vectorial += f"--- FRAGMENTO {i+1} (de: {titulo}) ---\n"
contexto_vectorial += f"{chunk.page_content}\n\n"
logger.info(f"PASO 1 OK: {len(chunks)} fragmentos encontrados en FAISS")
except Exception as e:
logger.error(f"PASO 1 ERROR: {e}")
contexto_vectorial = f"Error en búsqueda vectorial: {e}"
# ------------------------------------------------------------------
# PASO 2: BÚSQUEDA EN EL GRAFO
# Navegamos el grafo para encontrar conceptos relacionados
# ------------------------------------------------------------------
logger.info("PASO 2: Buscando en el grafo de conocimiento...")
contexto_grafo = buscar_en_grafo(query)
if contexto_grafo:
logger.info("PASO 2 OK: Contexto del grafo obtenido")
else:
logger.info("PASO 2: No se encontró contexto relevante en el grafo")
# ------------------------------------------------------------------
# PASO 3: COMBINAR LOS DOS CONTEXTOS
# Juntamos el contexto vectorial y el del grafo en un solo texto
# que el LLM usará para generar su respuesta
# ------------------------------------------------------------------
logger.info("PASO 3: Combinando contextos vectorial y de grafo...")
contexto_final = "=" * 50 + "\n"
contexto_final += "CONTEXTO ENRIQUECIDO (Graph RAG)\n"
contexto_final += "=" * 50 + "\n\n"
# Primero el contexto del grafo (hechos concretos y relaciones)
if contexto_grafo:
contexto_final += contexto_grafo
contexto_final += "\n" + "-" * 40 + "\n\n"
# Luego el contexto vectorial (fragmentos de papers)
contexto_final += contexto_vectorial
logger.info(f"PASO 3 OK: Contexto final listo ({len(contexto_final)} caracteres) ✅")
return contexto_final