File size: 8,225 Bytes
5688f8f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | # 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 |