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