File size: 14,446 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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 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 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | # # NOTA: Este archivo es la versión original del RAG.
# # Está reemplazado por graph_rag_engine.py que añade
# # el grafo de conocimiento. Se mantiene como referencia.
# # =====================================================================
# # 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
# # =====================================================================
# # os.path.dirname(__file__) → carpeta de este script (project-ai-llms-V01/src/)
# # "../.." → subimos hasta la raíz del proyecto
# RUTA_VECTORSTORE = os.path.join(
# os.path.dirname(__file__), "..", "..", "vectorstore_ia"
# )
# RUTA_GRAFO = os.path.join(
# os.path.dirname(__file__), "..", "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
# =====================================================================
# MOTOR RAG — Versión original (v1.0)
#
# NOTA: Este archivo es la versión SIMPLE del RAG.
# Está reemplazado por graph_rag_engine.py que añade el grafo
# de conocimiento. Se mantiene como referencia histórica.
#
# Diferencia con graph_rag_engine.py:
# rag_engine.py → solo busca en FAISS (vectores)
# graph_rag_engine.py → busca en FAISS + navega el grafo NetworkX
# =====================================================================
import logging
import os
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("rag_engine")
# =====================================================================
# RUTA DE LA BASE DE DATOS VECTORIAL EN DISCO
# Apuntamos a la carpeta vectorstore_ia/ que creó build_vectorstore.py
# os.path.dirname(__file__) → carpeta de este script (src/tools/)
# "../.." → subimos hasta la raíz del proyecto
# =====================================================================
RUTA_VECTORSTORE = os.path.join(
os.path.dirname(__file__), "..", "..", "vectorstore_ia"
)
# =====================================================================
# CARGAMOS EL MODELO DE EMBEDDINGS UNA SOLA VEZ AL ARRANCAR
# Si lo cargáramos dentro de la función, se recargaría cada vez
# que el usuario pregunta (muy lento).
# Cargándolo aquí, se carga una vez y queda en memoria.
# =====================================================================
logger.info("Cargando modelo de embeddings en memoria...")
try:
MODELO_EMBEDDINGS = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
logger.info("✅ Modelo de embeddings cargado correctamente")
except Exception as e:
logger.error(f"❌ Error cargando modelo de embeddings: {str(e)}")
MODELO_EMBEDDINGS = None
@tool
def investigar_y_sintetizar_ciencia(query: str) -> str:
"""
Busca información científica sobre Inteligencia Artificial en una base
de datos local de papers académicos y devuelve los fragmentos más relevantes.
Útil para responder preguntas sobre IA, machine learning, deep learning,
redes neuronales, NLP y temas relacionados.
"""
logger.info(f"RAG iniciado. Query recibida: '{query}'")
# ------------------------------------------------------------------
# PASO 1: COMPROBAMOS QUE LA BASE DE DATOS EXISTE EN DISCO
# Si el usuario no ha ejecutado build_vectorstore.py todavía,
# le avisamos con un mensaje claro en vez de dar un error raro
# ------------------------------------------------------------------
logger.info("PASO 1: Comprobando que existe la base de datos vectorial...")
if not os.path.exists(RUTA_VECTORSTORE):
logger.error(f"PASO 1 ERROR: No existe '{RUTA_VECTORSTORE}'")
return (
"⚠️ La base de datos científica no está construida todavía. "
"Ejecuta primero: python src/tools/build_vectorstore.py"
)
logger.info(f"PASO 1 OK: Base de datos encontrada")
# ------------------------------------------------------------------
# PASO 2: CARGAMOS LA BASE DE DATOS DESDE DISCO
# allow_dangerous_deserialization=True es necesario para cargar
# archivos .pkl de FAISS — es seguro porque son nuestros propios archivos
# ------------------------------------------------------------------
logger.info("PASO 2: Cargando base de datos vectorial desde disco...")
try:
vector_store = FAISS.load_local(
RUTA_VECTORSTORE,
MODELO_EMBEDDINGS,
allow_dangerous_deserialization=True
)
logger.info("PASO 2 OK: Base de datos cargada correctamente")
except Exception as e:
logger.error(f"PASO 2 ERROR: {str(e)}")
return f"Error al cargar la base de datos vectorial: {str(e)}"
# ------------------------------------------------------------------
# PASO 3: RECUPERACIÓN (Retrieval)
# Buscamos los 3 chunks cuyo significado sea más similar a la query
# Esto es instantáneo porque todo está en disco, sin llamadas a internet
# ------------------------------------------------------------------
logger.info(f"PASO 3: Buscando chunks más relevantes para '{query}'...")
try:
chunks_relevantes = vector_store.similarity_search(query, k=3)
logger.info(f"PASO 3 OK: Encontrados {len(chunks_relevantes)} chunks relevantes")
except Exception as e:
logger.error(f"PASO 3 ERROR: {str(e)}")
return f"Error al buscar en la base de datos: {str(e)}"
# ------------------------------------------------------------------
# PASO 4: EMPAQUETADO FINAL
# Juntamos los mejores fragmentos en un texto que el LLM usará
# como contexto para generar su respuesta
# ------------------------------------------------------------------
logger.info("PASO 4: Empaquetando contexto final para el LLM...")
contexto_final = "Fragmentos extraídos de papers científicos sobre IA:\n\n"
for i, chunk in enumerate(chunks_relevantes):
titulo = chunk.metadata.get("Title", "Paper científico")
contexto_final += f"--- FRAGMENTO {i+1} (de: {titulo}) ---\n"
contexto_final += f"{chunk.page_content}\n\n"
logger.info(f"PASO 4 OK: RAG completado ✅ ({len(contexto_final)} caracteres)")
return contexto_final |