{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RAG (Retrieval-Augmented Generation)\n",
    "## Índice de Contenidos\n",
    "\n",
    "1. [Fundamentos Teóricos de RAG](#1-fundamentos)\n",
    "2. [Embeddings: El Corazón Semántico](#2-embeddings)\n",
    "3. [Chunking: División Inteligente de Documentos](#3-chunking)\n",
    "4. [Vector Stores: Almacenamiento y Búsqueda](#4-vectorstores)\n",
    "5. [Pipeline RAG Completo](#5-pipeline)\n",
    "6. [Técnicas Avanzadas de Retrieval](#6-avanzadas)\n",
    "7. [Evaluación y Métricas](#7-evaluacion)\n",
    "8. [Prompt Engineering para RAG](#8-prompting)\n",
    "9. [Soluciones Open Source](#9-opensource)\n",
    "10. [Despliegue en Producción](#10-produccion)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Configuración del Entorno\n",
    "\n",
    "Antes de comenzar, necesitamos instalar las bibliotecas necesarias. Este notebook utiliza herramientas open source que pueden ejecutarse completamente en local, sin necesidad de APIs externas de pago.\n",
    "\n",
    "### Bibliotecas principales:\n",
    "\n",
    "- **sentence-transformers**: Modelos de embeddings de última generación\n",
    "- **chromadb**: Base de datos vectorial ligera y fácil de usar\n",
    "- **faiss-cpu**: Biblioteca de Facebook para búsqueda de similitud eficiente\n",
    "- **langchain**: Framework para construir aplicaciones con LLMs\n",
    "- **transformers**: Biblioteca de Hugging Face para modelos de lenguaje\n",
    "- **rank_bm25**: Implementación del algoritmo BM25 para búsqueda léxica"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Instalación de dependencias (ejecutar solo la primera vez)\n",
    "!pip install -q \\\n",
    "    langchain \\\n",
    "    langchain-community \\\n",
    "    langchain-huggingface \\\n",
    "    sentence-transformers \\\n",
    "    chromadb \\\n",
    "    faiss-cpu \\\n",
    "    transformers \\\n",
    "    torch \\\n",
    "    pypdf \\\n",
    "    rank_bm25 \\\n",
    "    scikit-learn \\\n",
    "    matplotlib \\\n",
    "    numpy \\\n",
    "    pandas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imports principales que usaremos a lo largo del notebook\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.metrics.pairwise import cosine_similarity\n",
    "from sklearn.decomposition import PCA\n",
    "\n",
    "print(\"✅ Entorno configurado correctamente\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"1-fundamentos\"></a>\n",
    "# 1. Fundamentos Teóricos de RAG\n",
    "\n",
    "## 1.1 ¿Qué es RAG?\n",
    "\n",
    "**RAG (Retrieval-Augmented Generation)** es una arquitectura que combina dos componentes fundamentales:\n",
    "\n",
    "1. **Retrieval (Recuperación)**: Un sistema que busca y recupera información relevante de una base de conocimiento\n",
    "2. **Generation (Generación)**: Un modelo de lenguaje (LLM) que genera respuestas basándose en la información recuperada\n",
    "\n",
    "Esta combinación permite que los LLMs accedan a información actualizada, específica y verificable, superando sus limitaciones inherentes.\n",
    "\n",
    "## 1.2 ¿Por qué necesitamos RAG?\n",
    "\n",
    "Los Modelos de Lenguaje Grande (LLMs) como GPT, Llama o Mistral tienen limitaciones importantes:\n",
    "\n",
    "### Problemas de los LLMs sin RAG:\n",
    "\n",
    "| Problema | Descripción | Impacto |\n",
    "|----------|-------------|----------|\n",
    "| **Conocimiento desactualizado** | El modelo solo conoce información hasta su fecha de entrenamiento | No puede responder sobre eventos recientes |\n",
    "| **Alucinaciones** | Genera información plausible pero falsa con total confianza | Respuestas incorrectas presentadas como hechos |\n",
    "| **Sin acceso a datos privados** | No conoce documentos internos de tu organización | No puede ayudar con información específica |\n",
    "| **Falta de citaciones** | No puede indicar de dónde proviene la información | Imposible verificar las respuestas |\n",
    "| **Contexto limitado** | Ventana de contexto finita (4K-128K tokens) | No puede procesar grandes volúmenes de documentos |\n",
    "\n",
    "### Cómo RAG resuelve estos problemas:\n",
    "\n",
    "| Problema | Solución con RAG |\n",
    "|----------|------------------|\n",
    "| Conocimiento desactualizado | Acceso a documentos actualizados en tiempo real |\n",
    "| Alucinaciones | Respuestas fundamentadas en fuentes específicas |\n",
    "| Sin datos privados | Integración con repositorios documentales internos |\n",
    "| Falta de citaciones | Referencias exactas a documentos y páginas |\n",
    "| Contexto limitado | Recuperación selectiva de información relevante |\n",
    "\n",
    "## 1.3 Arquitectura de un Sistema RAG\n",
    "\n",
    "Un sistema RAG típico consta de dos fases principales:\n",
    "\n",
    "### Fase 1: Indexación (Offline)\n",
    "\n",
    "Esta fase se ejecuta una vez (o periódicamente) para preparar los documentos:\n",
    "\n",
    "```\n",
    "┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n",
    "│ DOCUMENTOS  │────▶│  CHUNKING   │────▶│  EMBEDDING  │────▶│VECTOR STORE │\n",
    "│ (PDF, DOCX, │     │ (División   │     │ (Conversión │     │(Almacena-   │\n",
    "│  TXT, HTML) │     │  en partes) │     │ a vectores) │     │ miento)     │\n",
    "└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘\n",
    "```\n",
    "\n",
    "1. **Carga de documentos**: Se leen los documentos en diversos formatos\n",
    "2. **Chunking**: Se dividen en fragmentos manejables (típicamente 500-1000 tokens)\n",
    "3. **Embedding**: Cada fragmento se convierte en un vector numérico que captura su significado\n",
    "4. **Almacenamiento**: Los vectores se guardan en una base de datos vectorial con índices para búsqueda rápida\n",
    "\n",
    "### Fase 2: Consulta (Online)\n",
    "\n",
    "Esta fase se ejecuta cada vez que un usuario hace una pregunta:\n",
    "\n",
    "```\n",
    "┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n",
    "│  CONSULTA   │────▶│  EMBEDDING  │────▶│  BÚSQUEDA   │\n",
    "│ del usuario │     │ de consulta │     │  VECTORIAL  │\n",
    "└─────────────┘     └─────────────┘     └──────┬──────┘\n",
    "                                               │\n",
    "                                               ▼\n",
    "┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n",
    "│  RESPUESTA  │◀────│     LLM     │◀────│  CONTEXTO   │\n",
    "│ al usuario  │     │ (Generación)│     │ (Top-K docs)│\n",
    "└─────────────┘     └─────────────┘     └─────────────┘\n",
    "```\n",
    "\n",
    "1. **Embedding de consulta**: La pregunta del usuario se convierte en vector\n",
    "2. **Búsqueda**: Se encuentran los K fragmentos más similares semánticamente\n",
    "3. **Construcción de contexto**: Se prepara un prompt con los documentos recuperados\n",
    "4. **Generación**: El LLM genera una respuesta basada en el contexto\n",
    "5. **Respuesta**: Se devuelve la respuesta con las fuentes citadas\n",
    "\n",
    "## 1.4 Componentes Clave\n",
    "\n",
    "### Modelo de Embeddings\n",
    "\n",
    "Convierte texto en vectores numéricos de alta dimensión (típicamente 384-4096 dimensiones). Textos con significado similar producen vectores cercanos en el espacio vectorial.\n",
    "\n",
    "**Ejemplos de modelos:**\n",
    "- `BAAI/bge-m3` (Estado del arte, multilingüe)\n",
    "- `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` (Ligero, multilingüe)\n",
    "- `intfloat/multilingual-e5-large` (Excelente para español)\n",
    "\n",
    "### Base de Datos Vectorial\n",
    "\n",
    "Almacena los vectores y permite búsquedas eficientes por similitud.\n",
    "\n",
    "**Ejemplos:**\n",
    "- **ChromaDB**: Simple, ideal para prototipos\n",
    "- **FAISS**: Alto rendimiento, desarrollado por Facebook\n",
    "- **Qdrant**: Producción, filtros avanzados\n",
    "- **Milvus**: Escalabilidad masiva\n",
    "\n",
    "### Modelo de Lenguaje (LLM)\n",
    "\n",
    "Genera la respuesta final basándose en el contexto recuperado.\n",
    "\n",
    "**Modelos Open Source recomendados:**\n",
    "- **Llama 3.1** (8B-70B): Propósito general, excelente rendimiento\n",
    "- **Mistral/Mixtral**: Eficiente, buen razonamiento\n",
    "- **Qwen 2.5**: Excelente soporte multilingüe\n",
    "\n",
    "## 1.5 Ventajas de RAG para Investigación Científica\n",
    "\n",
    "Para instituciones como el CSIC, RAG ofrece ventajas específicas:\n",
    "\n",
    "1. **Soberanía de datos**: Los documentos nunca salen de la infraestructura propia\n",
    "2. **Reproducibilidad**: Las respuestas son verificables y citables\n",
    "3. **Actualización continua**: Se pueden añadir nuevas publicaciones sin reentrenar\n",
    "4. **Cumplimiento normativo**: Compatible con RGPD al no enviar datos a terceros\n",
    "5. **Personalización**: Adaptable a terminología y dominios específicos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"2-embeddings\"></a>\n",
    "# 2. Embeddings: El Corazón Semántico de RAG\n",
    "\n",
    "## 2.1 ¿Qué son los Embeddings?\n",
    "\n",
    "Los **embeddings** son representaciones numéricas (vectores) de texto que capturan el **significado semántico**. A diferencia de métodos tradicionales como TF-IDF o bag-of-words, los embeddings:\n",
    "\n",
    "- Capturan **relaciones semánticas** entre palabras y frases\n",
    "- Representan el **significado** más allá de las palabras exactas\n",
    "- Permiten **comparar similitud** entre textos de forma matemática\n",
    "\n",
    "### Intuición Visual\n",
    "\n",
    "Imagina un espacio de muchas dimensiones donde cada punto representa un texto:\n",
    "\n",
    "- \"El gato duerme en el sofá\" y \"El felino descansa en el sillón\" estarían **cerca** (mismo significado)\n",
    "- \"La proteína se pliega\" y \"El gato duerme\" estarían **lejos** (significados diferentes)\n",
    "\n",
    "### Propiedades Matemáticas\n",
    "\n",
    "Los embeddings tienen propiedades algebraicas interesantes:\n",
    "\n",
    "```\n",
    "vector(\"rey\") - vector(\"hombre\") + vector(\"mujer\") ≈ vector(\"reina\")\n",
    "```\n",
    "\n",
    "Esta propiedad demuestra que los embeddings capturan relaciones semánticas complejas.\n",
    "\n",
    "## 2.2 Cómo Funcionan los Modelos de Embeddings\n",
    "\n",
    "Los modelos de embeddings modernos se basan en arquitecturas **Transformer** (como BERT) entrenados con técnicas de **aprendizaje contrastivo**:\n",
    "\n",
    "1. **Pares positivos**: Textos relacionados (pregunta-respuesta, paráfrasis) deben tener embeddings similares\n",
    "2. **Pares negativos**: Textos no relacionados deben tener embeddings diferentes\n",
    "3. **Entrenamiento**: El modelo aprende a maximizar la similitud de pares positivos y minimizar la de negativos\n",
    "\n",
    "### Tipos de Modelos de Embeddings\n",
    "\n",
    "| Tipo | Descripción | Ejemplo |\n",
    "|------|-------------|----------|\n",
    "| **Bi-encoder** | Codifica query y documento independientemente. Rápido para búsqueda. | Sentence-BERT, BGE |\n",
    "| **Cross-encoder** | Procesa query y documento juntos. Más preciso pero más lento. | ms-marco-MiniLM |\n",
    "| **Sparse embeddings** | Vectores dispersos, interpretables. | SPLADE |\n",
    "| **Dense embeddings** | Vectores densos, capturan semántica profunda. | E5, BGE-M3 |\n",
    "\n",
    "## 2.3 Métricas de Similitud\n",
    "\n",
    "Para comparar embeddings usamos métricas de distancia o similitud:\n",
    "\n",
    "### Similitud Coseno (más común)\n",
    "\n",
    "Mide el ángulo entre dos vectores, ignorando su magnitud:\n",
    "\n",
    "$$\\text{cosine\\_similarity}(A, B) = \\frac{A \\cdot B}{\\|A\\| \\|B\\|}$$\n",
    "\n",
    "- **Valor 1**: Vectores idénticos en dirección (muy similares)\n",
    "- **Valor 0**: Vectores perpendiculares (sin relación)\n",
    "- **Valor -1**: Vectores opuestos (significados opuestos)\n",
    "\n",
    "### Distancia Euclidiana\n",
    "\n",
    "Mide la distancia directa entre dos puntos:\n",
    "\n",
    "$$\\text{euclidean\\_distance}(A, B) = \\sqrt{\\sum_{i=1}^{n}(A_i - B_i)^2}$$\n",
    "\n",
    "### Producto Punto (Dot Product)\n",
    "\n",
    "Similar al coseno pero considera la magnitud:\n",
    "\n",
    "$$\\text{dot\\_product}(A, B) = \\sum_{i=1}^{n} A_i \\times B_i$$\n",
    "\n",
    "**Recomendación**: Para RAG, usar **similitud coseno** o **producto punto con vectores normalizados**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.4 Práctica: Trabajando con Embeddings\n",
    "\n",
    "Vamos a explorar cómo funcionan los embeddings en la práctica. Usaremos `sentence-transformers`, una biblioteca que facilita el uso de modelos de embeddings de última generación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sentence_transformers import SentenceTransformer\n",
    "\n",
    "# Cargar un modelo de embeddings multilingüe\n",
    "# Este modelo funciona bien con español y es relativamente ligero\n",
    "print(\"Cargando modelo de embeddings...\")\n",
    "embedding_model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')\n",
    "\n",
    "# Información del modelo\n",
    "dimension = embedding_model.get_sentence_embedding_dimension()\n",
    "print(f\"✅ Modelo cargado correctamente\")\n",
    "print(f\"📐 Dimensión de los embeddings: {dimension}\")\n",
    "print(f\"📝 Esto significa que cada texto se convierte en un vector de {dimension} números\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo 1: Generando Embeddings\n",
    "\n",
    "Veamos cómo convertir textos en vectores y explorar sus propiedades:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Textos de ejemplo sobre diferentes temas\n",
    "textos = [\n",
    "    \"El gato duerme plácidamente en el sofá\",           # Tema: animales/hogar\n",
    "    \"El felino descansa sobre el sillón de la sala\",    # Paráfrasis del anterior\n",
    "    \"Las proteínas chaperonas ayudan al plegamiento\",   # Tema: biología molecular\n",
    "    \"Hsp70 asiste en el correcto doblez de proteínas\",  # Paráfrasis del anterior\n",
    "    \"Python es un lenguaje de programación versátil\",   # Tema: programación\n",
    "]\n",
    "\n",
    "# Generar embeddings para todos los textos\n",
    "embeddings = embedding_model.encode(textos)\n",
    "\n",
    "# Explorar la estructura de los embeddings\n",
    "print(\"Estructura de los embeddings generados:\")\n",
    "print(f\"  - Tipo: {type(embeddings)}\")\n",
    "print(f\"  - Shape: {embeddings.shape}\")\n",
    "print(f\"  - Tenemos {embeddings.shape[0]} textos, cada uno con {embeddings.shape[1]} dimensiones\")\n",
    "print(f\"\\nPrimeras 10 dimensiones del primer embedding:\")\n",
    "print(f\"  {embeddings[0][:10]}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo 2: Matriz de Similitud\n",
    "\n",
    "Ahora calculemos la similitud coseno entre todos los pares de textos. Esto nos mostrará qué textos son semánticamente similares:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calcular la matriz de similitud coseno\n",
    "similarity_matrix = cosine_similarity(embeddings)\n",
    "\n",
    "# Crear una visualización clara\n",
    "print(\"MATRIZ DE SIMILITUD COSENO\")\n",
    "print(\"=\" * 80)\n",
    "print(\"\\nValores cercanos a 1.0 = textos muy similares\")\n",
    "print(\"Valores cercanos a 0.0 = textos no relacionados\\n\")\n",
    "\n",
    "# Mostrar los textos con índices\n",
    "print(\"Textos:\")\n",
    "for i, texto in enumerate(textos):\n",
    "    print(f\"  T{i}: {texto}\")\n",
    "\n",
    "print(\"\\nMatriz de similitud:\")\n",
    "print(\"     \", end=\"\")\n",
    "for i in range(len(textos)):\n",
    "    print(f\"  T{i}  \", end=\"\")\n",
    "print()\n",
    "\n",
    "for i in range(len(textos)):\n",
    "    print(f\"T{i}  \", end=\"\")\n",
    "    for j in range(len(textos)):\n",
    "        sim = similarity_matrix[i][j]\n",
    "        # Colorear según similitud (usando símbolos)\n",
    "        if i == j:\n",
    "            print(f\" 1.00 \", end=\"\")\n",
    "        elif sim > 0.7:\n",
    "            print(f\" {sim:.2f}*\", end=\"\")\n",
    "        else:\n",
    "            print(f\" {sim:.2f} \", end=\"\")\n",
    "    print()\n",
    "\n",
    "print(\"\\n* = Alta similitud (>0.7)\")\n",
    "print(\"\\n💡 Observaciones:\")\n",
    "print(\"   - T0 y T1 (gato/felino) tienen alta similitud: son paráfrasis\")\n",
    "print(\"   - T2 y T3 (proteínas/Hsp70) tienen alta similitud: mismo tema científico\")\n",
    "print(\"   - T4 (Python) tiene baja similitud con todos: tema diferente\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo 3: Visualización en 2D\n",
    "\n",
    "Aunque los embeddings tienen muchas dimensiones (384 en este caso), podemos usar **PCA (Análisis de Componentes Principales)** para proyectarlos a 2D y visualizar cómo se agrupan los textos similares:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Reducir a 2 dimensiones para visualización\n",
    "pca = PCA(n_components=2)\n",
    "embeddings_2d = pca.fit_transform(embeddings)\n",
    "\n",
    "# Configurar el gráfico\n",
    "plt.figure(figsize=(12, 8))\n",
    "\n",
    "# Colores por tema\n",
    "colores = ['#e74c3c', '#c0392b',  # Rojo: gato/felino\n",
    "           '#3498db', '#2980b9',  # Azul: proteínas\n",
    "           '#27ae60']             # Verde: programación\n",
    "\n",
    "# Dibujar puntos\n",
    "for i, (x, y) in enumerate(embeddings_2d):\n",
    "    plt.scatter(x, y, c=colores[i], s=200, alpha=0.7, edgecolors='white', linewidth=2)\n",
    "    \n",
    "    # Etiqueta abreviada\n",
    "    etiqueta = textos[i][:25] + \"...\" if len(textos[i]) > 25 else textos[i]\n",
    "    plt.annotate(f\"T{i}: {etiqueta}\", (x, y), \n",
    "                 fontsize=9, \n",
    "                 xytext=(10, 10), \n",
    "                 textcoords='offset points',\n",
    "                 bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.3))\n",
    "\n",
    "# Dibujar líneas entre textos similares\n",
    "plt.plot([embeddings_2d[0][0], embeddings_2d[1][0]], \n",
    "         [embeddings_2d[0][1], embeddings_2d[1][1]], \n",
    "         'r--', alpha=0.5, linewidth=2, label='Paráfrasis (gato/felino)')\n",
    "plt.plot([embeddings_2d[2][0], embeddings_2d[3][0]], \n",
    "         [embeddings_2d[2][1], embeddings_2d[3][1]], \n",
    "         'b--', alpha=0.5, linewidth=2, label='Paráfrasis (proteínas)')\n",
    "\n",
    "plt.title(\"Visualización de Embeddings en 2D (PCA)\\nTextos similares aparecen cercanos\", fontsize=14)\n",
    "plt.xlabel(f\"Componente Principal 1 ({pca.explained_variance_ratio_[0]*100:.1f}% varianza)\")\n",
    "plt.ylabel(f\"Componente Principal 2 ({pca.explained_variance_ratio_[1]*100:.1f}% varianza)\")\n",
    "plt.legend(loc='best')\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "print(\"📊 El gráfico muestra cómo los textos con significados similares\")\n",
    "print(\"   se agrupan en el espacio vectorial, incluso usando diferentes palabras.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo 4: Búsqueda Semántica Simple\n",
    "\n",
    "Implementemos una búsqueda semántica básica para entender cómo RAG encuentra documentos relevantes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def busqueda_semantica(consulta, documentos, modelo, top_k=3):\n",
    "    \"\"\"\n",
    "    Realiza una búsqueda semántica simple.\n",
    "    \n",
    "    Args:\n",
    "        consulta: La pregunta o búsqueda del usuario\n",
    "        documentos: Lista de documentos donde buscar\n",
    "        modelo: Modelo de embeddings\n",
    "        top_k: Número de resultados a devolver\n",
    "    \n",
    "    Returns:\n",
    "        Lista de tuplas (documento, score) ordenada por relevancia\n",
    "    \"\"\"\n",
    "    # Generar embedding de la consulta\n",
    "    embedding_consulta = modelo.encode([consulta])\n",
    "    \n",
    "    # Generar embeddings de los documentos\n",
    "    embeddings_docs = modelo.encode(documentos)\n",
    "    \n",
    "    # Calcular similitud coseno\n",
    "    similitudes = cosine_similarity(embedding_consulta, embeddings_docs)[0]\n",
    "    \n",
    "    # Ordenar por similitud descendente\n",
    "    indices_ordenados = np.argsort(similitudes)[::-1][:top_k]\n",
    "    \n",
    "    # Devolver resultados\n",
    "    resultados = [(documentos[i], similitudes[i]) for i in indices_ordenados]\n",
    "    return resultados\n",
    "\n",
    "\n",
    "# Base de conocimiento de ejemplo (simulando documentos científicos)\n",
    "base_conocimiento = [\n",
    "    \"Las proteínas chaperonas son moléculas que asisten en el plegamiento correcto de otras proteínas.\",\n",
    "    \"Hsp70 reconoce secuencias hidrofóbicas de 7-8 aminoácidos en proteínas desplegadas.\",\n",
    "    \"El mal plegamiento de proteínas está asociado con enfermedades neurodegenerativas como Alzheimer.\",\n",
    "    \"CRISPR-Cas9 permite editar secuencias de ADN con alta precisión.\",\n",
    "    \"La PCR amplifica secuencias específicas de ADN para su análisis.\",\n",
    "    \"Los ribosomas son los orgánulos encargados de sintetizar proteínas.\",\n",
    "    \"Hsp90 es esencial para la maduración de receptores de hormonas esteroides.\",\n",
    "    \"Las enfermedades por priones son causadas por proteínas mal plegadas infecciosas.\",\n",
    "]\n",
    "\n",
    "# Realizar una búsqueda\n",
    "consulta = \"¿Qué proteínas ayudan a otras proteínas a plegarse correctamente?\"\n",
    "\n",
    "print(f\"🔍 Consulta: \\\"{consulta}\\\"\")\n",
    "print(\"\\n📄 Resultados de la búsqueda semántica:\\n\")\n",
    "\n",
    "resultados = busqueda_semantica(consulta, base_conocimiento, embedding_model, top_k=3)\n",
    "\n",
    "for i, (doc, score) in enumerate(resultados, 1):\n",
    "    print(f\"{i}. [Similitud: {score:.4f}]\")\n",
    "    print(f\"   {doc}\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.5 Modelos de Embeddings Recomendados\n",
    "\n",
    "La elección del modelo de embeddings es crucial para el rendimiento de RAG. Aquí una comparativa detallada:\n",
    "\n",
    "### Modelos Multilingües (Recomendados para español)\n",
    "\n",
    "| Modelo | Dimensión | Contexto | Características | Caso de Uso |\n",
    "|--------|-----------|----------|-----------------|-------------|\n",
    "| **BAAI/bge-m3** | 1024 | 8192 tokens | Estado del arte, soporta dense + sparse | Producción, máxima calidad |\n",
    "| **intfloat/multilingual-e5-large** | 1024 | 512 tokens | Muy buen rendimiento en español | RAG multilingüe |\n",
    "| **sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2** | 384 | 128 tokens | Ligero y rápido | Prototipos, recursos limitados |\n",
    "\n",
    "### Modelos Específicos para Español\n",
    "\n",
    "| Modelo | Dimensión | Características |\n",
    "|--------|-----------|----------------|\n",
    "| **jinaai/jina-embeddings-v2-base-es** | 768 | Optimizado para español |\n",
    "| **hiiamsid/sentence_similarity_spanish_es** | 768 | Entrenado específicamente en español |\n",
    "\n",
    "### Criterios de Selección\n",
    "\n",
    "1. **Idioma**: Priorizar modelos multilingües o específicos para español\n",
    "2. **Dimensión**: Mayor dimensión = más capacidad semántica pero más memoria\n",
    "3. **Contexto máximo**: Importante si los chunks son largos\n",
    "4. **Velocidad**: Considerar latencia en producción\n",
    "5. **Licencia**: Verificar compatibilidad con uso comercial/académico"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"3-chunking\"></a>\n",
    "# 3. Chunking: División Inteligente de Documentos\n",
    "\n",
    "## 3.1 ¿Por qué es Necesario el Chunking?\n",
    "\n",
    "El **chunking** (fragmentación) es el proceso de dividir documentos largos en fragmentos más pequeños. Es necesario por varias razones:\n",
    "\n",
    "### Limitaciones Técnicas\n",
    "\n",
    "1. **Límite de tokens del modelo de embeddings**: La mayoría de modelos tienen un máximo de 512-8192 tokens\n",
    "2. **Ventana de contexto del LLM**: Aunque algunos LLMs aceptan 128K tokens, el rendimiento decrece con contextos muy largos\n",
    "3. **Memoria y velocidad**: Procesar documentos completos es ineficiente\n",
    "\n",
    "### Beneficios para la Calidad\n",
    "\n",
    "1. **Precisión en la búsqueda**: Fragmentos pequeños permiten recuperar información más específica\n",
    "2. **Relevancia del contexto**: El LLM recibe solo la información pertinente, no todo el documento\n",
    "3. **Mejor citación**: Se puede indicar exactamente qué parte del documento se usó\n",
    "\n",
    "## 3.2 El Dilema del Tamaño del Chunk\n",
    "\n",
    "Elegir el tamaño correcto es un balance:\n",
    "\n",
    "```\n",
    "Chunks MUY PEQUEÑOS              Chunks MUY GRANDES\n",
    "        ◄─────────────────────────────────────────►\n",
    "        \n",
    "✓ Alta precisión en búsqueda    ✗ Baja precisión en búsqueda\n",
    "✗ Pérdida de contexto           ✓ Contexto completo\n",
    "✗ Fragmentación de ideas        ✓ Ideas completas\n",
    "✓ Menos tokens por consulta     ✗ Más tokens por consulta\n",
    "```\n",
    "\n",
    "### Recomendaciones Generales\n",
    "\n",
    "| Caso de Uso | Tamaño Recomendado | Overlap |\n",
    "|-------------|-------------------|----------|\n",
    "| Documentos técnicos/científicos | 500-1000 tokens | 10-20% |\n",
    "| FAQs y documentos cortos | 200-500 tokens | 5-10% |\n",
    "| Código fuente | 100-300 tokens | 0-10% |\n",
    "| Conversaciones/chat | 300-500 tokens | 20-30% |\n",
    "\n",
    "## 3.3 Estrategias de Chunking\n",
    "\n",
    "### 1. Fixed Size Chunking (Tamaño Fijo)\n",
    "\n",
    "La estrategia más simple: dividir por número de caracteres o tokens.\n",
    "\n",
    "**Ventajas:**\n",
    "- Simple de implementar\n",
    "- Chunks de tamaño predecible\n",
    "\n",
    "**Desventajas:**\n",
    "- Puede cortar a mitad de oración o párrafo\n",
    "- No respeta la estructura del documento\n",
    "\n",
    "### 2. Recursive Character Splitting (Recomendado)\n",
    "\n",
    "Divide usando una jerarquía de separadores: primero intenta dividir por párrafos, luego por oraciones, luego por palabras.\n",
    "\n",
    "**Ventajas:**\n",
    "- Respeta mejor las unidades semánticas\n",
    "- Balance entre simplicidad y calidad\n",
    "\n",
    "**Desventajas:**\n",
    "- Tamaños de chunks menos uniformes\n",
    "\n",
    "### 3. Semantic Chunking\n",
    "\n",
    "Usa embeddings para detectar cambios de tema y dividir en esos puntos.\n",
    "\n",
    "**Ventajas:**\n",
    "- Chunks coherentes temáticamente\n",
    "- Ideal para documentos con múltiples temas\n",
    "\n",
    "**Desventajas:**\n",
    "- Más costoso computacionalmente\n",
    "- Requiere calibración\n",
    "\n",
    "### 4. Document Structure Chunking\n",
    "\n",
    "Respeta la estructura del documento (títulos, secciones, párrafos).\n",
    "\n",
    "**Ventajas:**\n",
    "- Preserva la organización original\n",
    "- Mantiene metadatos de sección\n",
    "\n",
    "**Desventajas:**\n",
    "- Requiere documentos bien estructurados\n",
    "- Tamaños muy variables\n",
    "\n",
    "## 3.4 El Concepto de Overlap (Solapamiento)\n",
    "\n",
    "El **overlap** es la cantidad de texto que se repite entre chunks consecutivos. Es crucial para no perder contexto en los bordes:\n",
    "\n",
    "```\n",
    "Sin overlap:\n",
    "┌────────────────┐ ┌────────────────┐ ┌────────────────┐\n",
    "│    Chunk 1     │ │    Chunk 2     │ │    Chunk 3     │\n",
    "└────────────────┘ └────────────────┘ └────────────────┘\n",
    "                  ▲                  ▲\n",
    "            Información perdida en los bordes\n",
    "\n",
    "Con overlap del 20%:\n",
    "┌────────────────────┐\n",
    "│      Chunk 1       │\n",
    "└──────────┬─────────┘\n",
    "     ┌─────┴─────────────┐\n",
    "     │      Chunk 2      │\n",
    "     └──────────┬────────┘\n",
    "          ┌─────┴────────────┐\n",
    "          │     Chunk 3      │\n",
    "          └──────────────────┘\n",
    "```\n",
    "\n",
    "El overlap asegura que la información que aparece en la frontera entre chunks no se pierda."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.5 Práctica: Implementando Diferentes Estrategias de Chunking\n",
    "\n",
    "Vamos a implementar y comparar diferentes estrategias de chunking con un documento científico de ejemplo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Documento científico de ejemplo\n",
    "documento_cientifico = \"\"\"\n",
    "# Proteínas Chaperonas: Guardianes del Plegamiento Celular\n",
    "\n",
    "## Introducción\n",
    "\n",
    "Las proteínas chaperonas representan una clase fundamental de moléculas que asisten en el \n",
    "plegamiento correcto de otras proteínas. Descubiertas inicialmente como proteínas de choque \n",
    "térmico (Heat Shock Proteins, HSPs), hoy sabemos que desempeñan funciones esenciales en \n",
    "condiciones normales y de estrés celular.\n",
    "\n",
    "El plegamiento de proteínas es un proceso crítico: una cadena polipeptídica debe adoptar \n",
    "una estructura tridimensional específica para ser funcional. Errores en este proceso pueden \n",
    "llevar a enfermedades graves, incluyendo Alzheimer, Parkinson y otras patologías \n",
    "neurodegenerativas.\n",
    "\n",
    "## Mecanismo de Acción\n",
    "\n",
    "Las chaperonas funcionan reconociendo regiones hidrofóbicas que quedan expuestas durante \n",
    "el plegamiento. En una proteína correctamente plegada, estas regiones están ocultas en el \n",
    "interior; su exposición indica un estado de plegamiento incorrecto o incompleto.\n",
    "\n",
    "El mecanismo general implica:\n",
    "\n",
    "1. Reconocimiento de la proteína sustrato desplegada\n",
    "2. Unión a través de dominios específicos de unión a sustrato\n",
    "3. Ciclos de unión y liberación dependientes de ATP\n",
    "4. Facilitación del plegamiento correcto o direccionamiento a degradación\n",
    "\n",
    "## Familia Hsp70\n",
    "\n",
    "La familia Hsp70 es una de las más conservadas evolutivamente y mejor caracterizadas. \n",
    "Sus miembros se encuentran en prácticamente todos los organismos, desde bacterias hasta \n",
    "humanos.\n",
    "\n",
    "Características principales de Hsp70:\n",
    "\n",
    "- Reconoce segmentos hidrofóbicos de 7-8 aminoácidos\n",
    "- Requiere co-chaperonas como Hsp40 (DnaJ) para su actividad óptima\n",
    "- Funciona mediante un ciclo ATPasa regulado\n",
    "- Participa en múltiples procesos: plegamiento, translocación, desensamblaje de complejos\n",
    "\n",
    "## Familia Hsp90\n",
    "\n",
    "Hsp90 es esencial para la maduración de un conjunto específico de proteínas cliente, \n",
    "muchas de las cuales son factores de señalización celular.\n",
    "\n",
    "Entre sus clientes se incluyen:\n",
    "\n",
    "- Receptores de hormonas esteroides\n",
    "- Quinasas de señalización\n",
    "- Factores de transcripción\n",
    "- Proteínas del ciclo celular\n",
    "\n",
    "## Relevancia Clínica\n",
    "\n",
    "Las chaperonas tienen una relevancia clínica significativa en múltiples contextos:\n",
    "\n",
    "### Enfermedades Neurodegenerativas\n",
    "\n",
    "El mal plegamiento y agregación de proteínas es la causa subyacente de numerosas \n",
    "enfermedades neurodegenerativas:\n",
    "\n",
    "- Alzheimer: agregados de beta-amiloide y tau hiperfosforilada\n",
    "- Parkinson: agregados de alfa-sinucleína (cuerpos de Lewy)\n",
    "- Huntington: agregados de huntingtina con expansión de poliglutamina\n",
    "- Enfermedades por priones: agregados de PrPSc\n",
    "\n",
    "### Cáncer\n",
    "\n",
    "Las células tumorales frecuentemente muestran dependencia de chaperonas, particularmente \n",
    "Hsp90. Esta \"adicción a chaperonas\" ha llevado al desarrollo de inhibidores de Hsp90 \n",
    "como estrategia terapéutica anticancerígena.\n",
    "\n",
    "## Conclusiones\n",
    "\n",
    "Las proteínas chaperonas son componentes esenciales de la maquinaria celular de control \n",
    "de calidad proteica. Su estudio continúa revelando nuevos mecanismos y posibilidades \n",
    "terapéuticas para enfermedades asociadas al mal plegamiento de proteínas.\n",
    "\"\"\"\n",
    "\n",
    "print(f\"📄 Documento cargado\")\n",
    "print(f\"   - Longitud total: {len(documento_cientifico)} caracteres\")\n",
    "print(f\"   - Palabras aproximadas: {len(documento_cientifico.split())}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.text_splitter import (\n",
    "    CharacterTextSplitter,\n",
    "    RecursiveCharacterTextSplitter,\n",
    "    MarkdownHeaderTextSplitter,\n",
    ")\n",
    "\n",
    "# ============================================================\n",
    "# ESTRATEGIA 1: Character Text Splitter (División simple)\n",
    "# ============================================================\n",
    "\n",
    "print(\"=\"*70)\n",
    "print(\"ESTRATEGIA 1: Character Text Splitter\")\n",
    "print(\"=\"*70)\n",
    "print(\"\\nDivide por un separador específico (ej: salto de línea)\")\n",
    "print(\"Simple pero puede cortar oraciones a mitad.\\n\")\n",
    "\n",
    "char_splitter = CharacterTextSplitter(\n",
    "    separator=\"\\n\",      # Dividir por saltos de línea\n",
    "    chunk_size=500,      # Máximo 500 caracteres por chunk\n",
    "    chunk_overlap=50,    # 50 caracteres de overlap\n",
    "    length_function=len,\n",
    ")\n",
    "\n",
    "chunks_char = char_splitter.split_text(documento_cientifico)\n",
    "\n",
    "print(f\"Número de chunks generados: {len(chunks_char)}\")\n",
    "print(f\"\\nEjemplo - Chunk 1 ({len(chunks_char[0])} chars):\")\n",
    "print(\"-\" * 50)\n",
    "print(chunks_char[0][:300] + \"...\" if len(chunks_char[0]) > 300 else chunks_char[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# ESTRATEGIA 2: Recursive Character Text Splitter (RECOMENDADA)\n",
    "# ============================================================\n",
    "\n",
    "print(\"=\"*70)\n",
    "print(\"ESTRATEGIA 2: Recursive Character Text Splitter (RECOMENDADA)\")\n",
    "print(\"=\"*70)\n",
    "print(\"\\nIntenta dividir jerárquicamente: párrafos → oraciones → palabras\")\n",
    "print(\"Mejor preservación de unidades semánticas.\\n\")\n",
    "\n",
    "recursive_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size=500,\n",
    "    chunk_overlap=100,  # 20% de overlap\n",
    "    length_function=len,\n",
    "    separators=[\"\\n\\n\", \"\\n\", \". \", \", \", \" \", \"\"]  # Jerarquía de separadores\n",
    ")\n",
    "\n",
    "chunks_recursive = recursive_splitter.split_text(documento_cientifico)\n",
    "\n",
    "print(f\"Número de chunks generados: {len(chunks_recursive)}\")\n",
    "print(f\"\\nTamaños de los primeros 5 chunks:\")\n",
    "for i, chunk in enumerate(chunks_recursive[:5]):\n",
    "    print(f\"  Chunk {i+1}: {len(chunk)} caracteres\")\n",
    "\n",
    "print(f\"\\nEjemplo - Chunk 3:\")\n",
    "print(\"-\" * 50)\n",
    "print(chunks_recursive[2])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# ESTRATEGIA 3: Markdown Header Text Splitter\n",
    "# ============================================================\n",
    "\n",
    "print(\"=\"*70)\n",
    "print(\"ESTRATEGIA 3: Markdown Header Text Splitter\")\n",
    "print(\"=\"*70)\n",
    "print(\"\\nDivide respetando la estructura de encabezados Markdown.\")\n",
    "print(\"Preserva metadatos de sección para mejor contexto.\\n\")\n",
    "\n",
    "# Definir los headers que queremos usar para dividir\n",
    "headers_to_split = [\n",
    "    (\"#\", \"titulo\"),\n",
    "    (\"##\", \"seccion\"),\n",
    "    (\"###\", \"subseccion\"),\n",
    "]\n",
    "\n",
    "markdown_splitter = MarkdownHeaderTextSplitter(\n",
    "    headers_to_split_on=headers_to_split\n",
    ")\n",
    "\n",
    "chunks_markdown = markdown_splitter.split_text(documento_cientifico)\n",
    "\n",
    "print(f\"Número de chunks generados: {len(chunks_markdown)}\")\n",
    "print(f\"\\nChunks con sus metadatos de sección:\")\n",
    "print(\"-\" * 50)\n",
    "\n",
    "for i, chunk in enumerate(chunks_markdown[:4]):\n",
    "    print(f\"\\nChunk {i+1}:\")\n",
    "    print(f\"  📁 Metadatos: {chunk.metadata}\")\n",
    "    contenido_preview = chunk.page_content[:150].replace('\\n', ' ')\n",
    "    print(f\"  📝 Contenido: {contenido_preview}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.6 Combinando Estrategias: Pipeline de Chunking Óptimo\n",
    "\n",
    "En la práctica, la mejor aproximación es combinar estrategias: primero dividir por estructura del documento, luego aplicar chunking recursivo a secciones grandes.\n",
    "\n",
    "Además, es importante **enriquecer cada chunk con metadatos** que ayuden en la recuperación y citación:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def chunking_optimizado(documento, chunk_size=500, chunk_overlap=100):\n",
    "    \"\"\"\n",
    "    Pipeline de chunking optimizado que combina estructura y tamaño.\n",
    "    \n",
    "    1. Primero divide por estructura (headers markdown)\n",
    "    2. Luego aplica chunking recursivo a secciones grandes\n",
    "    3. Preserva metadatos de origen\n",
    "    \"\"\"\n",
    "    from langchain.schema import Document\n",
    "    \n",
    "    # Paso 1: Dividir por estructura\n",
    "    headers = [(\"#\", \"titulo\"), (\"##\", \"seccion\"), (\"###\", \"subseccion\")]\n",
    "    md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)\n",
    "    chunks_estructura = md_splitter.split_text(documento)\n",
    "    \n",
    "    # Paso 2: Aplicar chunking recursivo a secciones grandes\n",
    "    recursive_splitter = RecursiveCharacterTextSplitter(\n",
    "        chunk_size=chunk_size,\n",
    "        chunk_overlap=chunk_overlap,\n",
    "        separators=[\"\\n\\n\", \"\\n\", \". \", \" \"]\n",
    "    )\n",
    "    \n",
    "    chunks_finales = []\n",
    "    \n",
    "    for chunk in chunks_estructura:\n",
    "        contenido = chunk.page_content\n",
    "        metadata = chunk.metadata.copy()\n",
    "        \n",
    "        if len(contenido) > chunk_size:\n",
    "            # Sección grande: dividir más\n",
    "            sub_chunks = recursive_splitter.split_text(contenido)\n",
    "            for i, sub_chunk in enumerate(sub_chunks):\n",
    "                new_metadata = metadata.copy()\n",
    "                new_metadata['parte'] = i + 1\n",
    "                new_metadata['total_partes'] = len(sub_chunks)\n",
    "                chunks_finales.append(Document(\n",
    "                    page_content=sub_chunk,\n",
    "                    metadata=new_metadata\n",
    "                ))\n",
    "        else:\n",
    "            # Sección pequeña: mantener como está\n",
    "            metadata['parte'] = 1\n",
    "            metadata['total_partes'] = 1\n",
    "            chunks_finales.append(Document(\n",
    "                page_content=contenido,\n",
    "                metadata=metadata\n",
    "            ))\n",
    "    \n",
    "    return chunks_finales\n",
    "\n",
    "\n",
    "# Aplicar el chunking optimizado\n",
    "chunks_optimizados = chunking_optimizado(documento_cientifico, chunk_size=400, chunk_overlap=80)\n",
    "\n",
    "print(\"CHUNKING OPTIMIZADO: Resultados\")\n",
    "print(\"=\" * 70)\n",
    "print(f\"\\nTotal de chunks generados: {len(chunks_optimizados)}\")\n",
    "print(f\"\\nDetalle de cada chunk:\")\n",
    "print(\"-\" * 70)\n",
    "\n",
    "for i, chunk in enumerate(chunks_optimizados):\n",
    "    seccion = chunk.metadata.get('seccion', 'Principal')\n",
    "    parte = chunk.metadata.get('parte', 1)\n",
    "    total = chunk.metadata.get('total_partes', 1)\n",
    "    \n",
    "    print(f\"\\nChunk {i+1}:\")\n",
    "    print(f\"  📁 Sección: {seccion}\")\n",
    "    print(f\"  📄 Parte: {parte}/{total}\")\n",
    "    print(f\"  📏 Longitud: {len(chunk.page_content)} caracteres\")\n",
    "    print(f\"  📝 Preview: {chunk.page_content[:80].replace(chr(10), ' ')}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.7 Mejores Prácticas de Chunking\n",
    "\n",
    "### Lista de Verificación para Chunking de Calidad\n",
    "\n",
    "1. **Preservar unidades semánticas**\n",
    "   - No cortar a mitad de oración\n",
    "   - Preferir cortes en límites de párrafo\n",
    "   - Respetar estructura del documento cuando sea posible\n",
    "\n",
    "2. **Configurar overlap adecuado**\n",
    "   - 10-20% del tamaño del chunk es lo recomendado\n",
    "   - Más overlap para documentos con ideas que fluyen entre párrafos\n",
    "   - Menos overlap para documentos con secciones independientes\n",
    "\n",
    "3. **Enriquecer con metadatos**\n",
    "   - Fuente del documento (archivo, URL)\n",
    "   - Número de página o sección\n",
    "   - Fecha de creación/actualización\n",
    "   - Autor o departamento\n",
    "   - Categoría o tags\n",
    "\n",
    "4. **Limpiar el texto**\n",
    "   - Eliminar headers/footers repetitivos\n",
    "   - Corregir errores de OCR si aplica\n",
    "   - Normalizar espacios y saltos de línea\n",
    "   - Manejar tablas y figuras apropiadamente\n",
    "\n",
    "5. **Adaptar al dominio**\n",
    "   - Documentos científicos: chunks más grandes para mantener contexto\n",
    "   - FAQs: chunks más pequeños, uno por pregunta\n",
    "   - Código: respetar funciones/clases como unidades\n",
    "\n",
    "### Parámetros Recomendados por Tipo de Documento\n",
    "\n",
    "| Tipo de Documento | chunk_size | chunk_overlap | Estrategia |\n",
    "|-------------------|------------|---------------|------------|\n",
    "| Papers científicos | 800-1200 | 150-200 | Recursive + Structure |\n",
    "| Documentación técnica | 500-800 | 100-150 | Recursive |\n",
    "| Normativas/Legal | 600-1000 | 100-200 | Structure-aware |\n",
    "| FAQs | 200-400 | 50 | Por pregunta |\n",
    "| Código fuente | 200-500 | 50-100 | Por función/clase |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"4-vectorstores\"></a>\n",
    "# 4. Vector Stores: Almacenamiento y Búsqueda Semántica\n",
    "\n",
    "## 4.1 ¿Qué es un Vector Store?\n",
    "\n",
    "Un **Vector Store** (o base de datos vectorial) es un sistema especializado en almacenar y buscar vectores de alta dimensión. A diferencia de las bases de datos tradicionales que buscan por coincidencia exacta, los vector stores permiten **búsqueda por similitud**: encontrar los vectores más parecidos a uno dado.\n",
    "\n",
    "### ¿Por qué no usar una base de datos tradicional?\n",
    "\n",
    "Las bases de datos relacionales (PostgreSQL, MySQL) o documentales (MongoDB) no están optimizadas para:\n",
    "\n",
    "1. **Almacenar vectores de alta dimensión** (384-4096 dimensiones)\n",
    "2. **Búsqueda por similitud** en tiempo sublineal\n",
    "3. **Escalabilidad** con millones de vectores\n",
    "\n",
    "Los vector stores utilizan estructuras de datos especializadas como **HNSW (Hierarchical Navigable Small World)** o **IVF (Inverted File Index)** que permiten búsquedas aproximadas muy rápidas.\n",
    "\n",
    "## 4.2 Algoritmos de Indexación\n",
    "\n",
    "### Búsqueda Exacta (Brute Force)\n",
    "\n",
    "Compara el vector de consulta con **todos** los vectores almacenados.\n",
    "\n",
    "- **Complejidad**: O(n) donde n = número de vectores\n",
    "- **Ventaja**: 100% de precisión (recall perfecto)\n",
    "- **Desventaja**: Muy lento para grandes colecciones\n",
    "- **Uso**: Colecciones pequeñas (<10,000 vectores)\n",
    "\n",
    "### HNSW (Hierarchical Navigable Small World)\n",
    "\n",
    "Construye un grafo multicapa donde cada nodo está conectado a sus vecinos más cercanos.\n",
    "\n",
    "```\n",
    "Capa 3:    ○───────────────○  (pocos nodos, conexiones largas)\n",
    "           │               │\n",
    "Capa 2:    ○───○───────○───○  (más nodos)\n",
    "           │   │       │   │\n",
    "Capa 1:    ○─○─○─○───○─○─○─○  (más nodos aún)\n",
    "           │ │ │ │   │ │ │ │\n",
    "Capa 0:    ○○○○○○○○○○○○○○○○○  (todos los nodos)\n",
    "```\n",
    "\n",
    "- **Complejidad**: O(log n) para búsqueda\n",
    "- **Ventaja**: Muy rápido, alta precisión (>95%)\n",
    "- **Desventaja**: Mayor uso de memoria\n",
    "- **Uso**: La mayoría de aplicaciones en producción\n",
    "\n",
    "### IVF (Inverted File Index)\n",
    "\n",
    "Divide el espacio vectorial en clusters (regiones). Para buscar, primero identifica los clusters relevantes y luego busca solo dentro de ellos.\n",
    "\n",
    "- **Complejidad**: O(n/k) donde k = número de clusters visitados\n",
    "- **Ventaja**: Buen balance velocidad/memoria\n",
    "- **Desventaja**: Requiere entrenamiento previo\n",
    "- **Uso**: Colecciones muy grandes (>1M vectores)\n",
    "\n",
    "## 4.3 Comparativa de Soluciones\n",
    "\n",
    "| Solución | Licencia | Tipo | Mejor Para | Características |\n",
    "|----------|----------|------|------------|----------------|\n",
    "| **ChromaDB** | Apache 2.0 | Embebido | Prototipos, desarrollo | Simple, Python-native, sin servidor |\n",
    "| **FAISS** | MIT | Biblioteca | Alto rendimiento | Facebook, muy optimizado, bajo nivel |\n",
    "| **Qdrant** | Apache 2.0 | Servidor | Producción | Filtros, REST API, escalable |\n",
    "| **Milvus** | Apache 2.0 | Servidor | Enterprise | Cloud-native, escalabilidad masiva |\n",
    "| **Weaviate** | BSD-3 | Servidor | Búsqueda híbrida | GraphQL, módulos ML integrados |\n",
    "| **Pinecone** | Propietario | Cloud | Managed service | Sin operaciones, serverless |\n",
    "| **pgvector** | PostgreSQL | Extensión | Integración SQL | Usa PostgreSQL existente |\n",
    "\n",
    "### Recomendaciones por Caso de Uso\n",
    "\n",
    "- **Aprendizaje/Prototipos**: ChromaDB (simple, sin configuración)\n",
    "- **Proyecto pequeño/mediano**: Qdrant o ChromaDB persistente\n",
    "- **Producción con alto rendimiento**: Qdrant, Milvus o FAISS\n",
    "- **Ya tienes PostgreSQL**: pgvector\n",
    "- **Sin recursos para operar**: Pinecone (cloud managed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.4 Práctica: Implementación con ChromaDB\n",
    "\n",
    "ChromaDB es ideal para aprender y prototipar porque:\n",
    "- Se ejecuta en memoria o con persistencia local\n",
    "- No requiere servidor externo\n",
    "- API simple e intuitiva\n",
    "- Integración nativa con modelos de embeddings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import chromadb\n",
    "from chromadb.utils import embedding_functions\n",
    "\n",
    "# ============================================================\n",
    "# Crear cliente de ChromaDB\n",
    "# ============================================================\n",
    "\n",
    "# Opción 1: En memoria (se pierde al cerrar)\n",
    "client = chromadb.Client()\n",
    "\n",
    "# Opción 2: Persistente (descomentar para usar)\n",
    "# client = chromadb.PersistentClient(path=\"./chroma_db\")\n",
    "\n",
    "print(\"✅ Cliente ChromaDB creado\")\n",
    "\n",
    "# ============================================================\n",
    "# Configurar función de embeddings\n",
    "# ============================================================\n",
    "\n",
    "# Usar sentence-transformers (mismo modelo que usamos antes)\n",
    "embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(\n",
    "    model_name=\"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2\"\n",
    ")\n",
    "\n",
    "print(\"✅ Función de embeddings configurada\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Crear una colección\n",
    "# ============================================================\n",
    "\n",
    "# Las colecciones son como \"tablas\" en bases de datos relacionales\n",
    "# Cada colección tiene su propio índice y puede tener diferentes configuraciones\n",
    "\n",
    "collection = client.get_or_create_collection(\n",
    "    name=\"documentos_cientificos\",\n",
    "    embedding_function=embedding_fn,\n",
    "    metadata={\n",
    "        \"hnsw:space\": \"cosine\",  # Métrica de distancia: cosine, l2, ip\n",
    "        \"description\": \"Colección de documentos científicos sobre biología molecular\"\n",
    "    }\n",
    ")\n",
    "\n",
    "print(f\"✅ Colección '{collection.name}' creada/obtenida\")\n",
    "print(f\"   Documentos actuales: {collection.count()}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Añadir documentos a la colección\n",
    "# ============================================================\n",
    "\n",
    "# Documentos de ejemplo (simulando chunks de un paper científico)\n",
    "documentos = [\n",
    "    \"Las proteínas chaperonas son moléculas esenciales que asisten en el plegamiento correcto de otras proteínas mediante ciclos de unión y liberación dependientes de ATP.\",\n",
    "    \"La familia Hsp70 reconoce secuencias hidrofóbicas de 7-8 aminoácidos en proteínas parcialmente desplegadas o mal plegadas.\",\n",
    "    \"Hsp90 es crucial para la maduración de receptores de hormonas esteroides, quinasas de señalización y factores de transcripción.\",\n",
    "    \"El mal plegamiento de proteínas está asociado con enfermedades neurodegenerativas como Alzheimer, Parkinson y enfermedades por priones.\",\n",
    "    \"CRISPR-Cas9 es una herramienta de edición genética que permite modificar secuencias de ADN con alta precisión usando un ARN guía.\",\n",
    "    \"La reacción en cadena de la polimerasa (PCR) permite amplificar secuencias específicas de ADN millones de veces para su análisis.\",\n",
    "    \"Los ribosomas son complejos macromoleculares responsables de la síntesis de proteínas, traduciendo el ARN mensajero a cadenas polipeptídicas.\",\n",
    "    \"La espectroscopía de masas es una técnica analítica que permite identificar proteínas y caracterizar sus modificaciones post-traduccionales.\",\n",
    "    \"Las co-chaperonas como Hsp40 (DnaJ) regulan la actividad ATPasa de Hsp70 y ayudan en el reconocimiento de sustratos.\",\n",
    "    \"Los inhibidores de Hsp90 son una estrategia terapéutica prometedora en cáncer debido a la dependencia de las células tumorales en esta chaperona.\"\n",
    "]\n",
    "\n",
    "# Metadatos para cada documento\n",
    "metadatos = [\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"general\", \"fuente\": \"review_chaperonas.pdf\", \"pagina\": 1},\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"hsp70\", \"fuente\": \"review_chaperonas.pdf\", \"pagina\": 5},\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"hsp90\", \"fuente\": \"review_chaperonas.pdf\", \"pagina\": 8},\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"patologia\", \"fuente\": \"neurodegeneration.pdf\", \"pagina\": 12},\n",
    "    {\"tema\": \"genetica\", \"subtema\": \"crispr\", \"fuente\": \"gene_editing.pdf\", \"pagina\": 3},\n",
    "    {\"tema\": \"genetica\", \"subtema\": \"pcr\", \"fuente\": \"molecular_techniques.pdf\", \"pagina\": 7},\n",
    "    {\"tema\": \"biologia_celular\", \"subtema\": \"ribosomas\", \"fuente\": \"cell_biology.pdf\", \"pagina\": 45},\n",
    "    {\"tema\": \"proteomica\", \"subtema\": \"espectrometria\", \"fuente\": \"proteomics_handbook.pdf\", \"pagina\": 23},\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"cochaperonas\", \"fuente\": \"review_chaperonas.pdf\", \"pagina\": 6},\n",
    "    {\"tema\": \"chaperonas\", \"subtema\": \"terapeutica\", \"fuente\": \"cancer_therapy.pdf\", \"pagina\": 15},\n",
    "]\n",
    "\n",
    "# IDs únicos para cada documento\n",
    "ids = [f\"doc_{i:03d}\" for i in range(len(documentos))]\n",
    "\n",
    "# Añadir a la colección\n",
    "collection.add(\n",
    "    documents=documentos,\n",
    "    metadatas=metadatos,\n",
    "    ids=ids\n",
    ")\n",
    "\n",
    "print(f\"✅ {len(documentos)} documentos añadidos a la colección\")\n",
    "print(f\"   Total de documentos en la colección: {collection.count()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.5 Búsqueda Semántica con ChromaDB\n",
    "\n",
    "Ahora que tenemos documentos indexados, podemos realizar diferentes tipos de búsqueda:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Búsqueda semántica básica\n",
    "# ============================================================\n",
    "\n",
    "consulta = \"¿Qué moléculas ayudan a las proteínas a plegarse correctamente?\"\n",
    "\n",
    "resultados = collection.query(\n",
    "    query_texts=[consulta],  # ChromaDB genera el embedding automáticamente\n",
    "    n_results=3,              # Top 3 resultados\n",
    "    include=[\"documents\", \"metadatas\", \"distances\"]  # Qué incluir en resultados\n",
    ")\n",
    "\n",
    "print(f\"🔍 BÚSQUEDA SEMÁNTICA BÁSICA\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Consulta: \\\"{consulta}\\\"\")\n",
    "print(f\"\\n📄 Resultados (Top 3):\\n\")\n",
    "\n",
    "for i in range(len(resultados['documents'][0])):\n",
    "    doc = resultados['documents'][0][i]\n",
    "    meta = resultados['metadatas'][0][i]\n",
    "    dist = resultados['distances'][0][i]\n",
    "    similitud = 1 - dist  # Convertir distancia a similitud\n",
    "    \n",
    "    print(f\"{i+1}. [Similitud: {similitud:.4f}]\")\n",
    "    print(f\"   📁 Fuente: {meta['fuente']}, página {meta['pagina']}\")\n",
    "    print(f\"   🏷️  Tema: {meta['tema']} > {meta['subtema']}\")\n",
    "    print(f\"   📝 {doc[:100]}...\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Búsqueda con filtros de metadatos\n",
    "# ============================================================\n",
    "\n",
    "# Podemos filtrar por metadatos ANTES de la búsqueda semántica\n",
    "# Esto es muy útil para acotar el dominio de búsqueda\n",
    "\n",
    "consulta = \"mecanismo de acción molecular\"\n",
    "\n",
    "print(f\"🔍 BÚSQUEDA CON FILTROS DE METADATOS\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Consulta: \\\"{consulta}\\\"\")\n",
    "print(f\"Filtro: Solo documentos del tema 'chaperonas'\\n\")\n",
    "\n",
    "resultados_filtrados = collection.query(\n",
    "    query_texts=[consulta],\n",
    "    n_results=5,\n",
    "    where={\"tema\": \"chaperonas\"},  # Filtro de metadatos\n",
    "    include=[\"documents\", \"metadatas\", \"distances\"]\n",
    ")\n",
    "\n",
    "print(f\"📄 Resultados (filtrados por tema='chaperonas'):\\n\")\n",
    "\n",
    "for i in range(len(resultados_filtrados['documents'][0])):\n",
    "    doc = resultados_filtrados['documents'][0][i]\n",
    "    meta = resultados_filtrados['metadatas'][0][i]\n",
    "    dist = resultados_filtrados['distances'][0][i]\n",
    "    \n",
    "    print(f\"{i+1}. [{meta['subtema']}] {doc[:80]}...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Filtros avanzados con operadores lógicos\n",
    "# ============================================================\n",
    "\n",
    "print(f\"🔍 FILTROS AVANZADOS\")\n",
    "print(f\"=\"*70)\n",
    "\n",
    "# Ejemplo 1: OR - documentos de chaperonas O genética\n",
    "print(\"\\n1️⃣ Filtro OR: tema = 'chaperonas' O tema = 'genetica'\")\n",
    "resultados_or = collection.query(\n",
    "    query_texts=[\"técnicas de laboratorio\"],\n",
    "    n_results=4,\n",
    "    where={\n",
    "        \"$or\": [\n",
    "            {\"tema\": \"chaperonas\"},\n",
    "            {\"tema\": \"genetica\"}\n",
    "        ]\n",
    "    },\n",
    "    include=[\"documents\", \"metadatas\"]\n",
    ")\n",
    "\n",
    "for i, doc in enumerate(resultados_or['documents'][0]):\n",
    "    tema = resultados_or['metadatas'][0][i]['tema']\n",
    "    print(f\"   {i+1}. [{tema}] {doc[:60]}...\")\n",
    "\n",
    "# Ejemplo 2: AND + comparación numérica\n",
    "print(\"\\n2️⃣ Filtro AND: tema = 'chaperonas' Y pagina < 10\")\n",
    "resultados_and = collection.query(\n",
    "    query_texts=[\"función proteica\"],\n",
    "    n_results=4,\n",
    "    where={\n",
    "        \"$and\": [\n",
    "            {\"tema\": \"chaperonas\"},\n",
    "            {\"pagina\": {\"$lt\": 10}}  # $lt = less than\n",
    "        ]\n",
    "    },\n",
    "    include=[\"documents\", \"metadatas\"]\n",
    ")\n",
    "\n",
    "for i, doc in enumerate(resultados_and['documents'][0]):\n",
    "    pagina = resultados_and['metadatas'][0][i]['pagina']\n",
    "    print(f\"   {i+1}. [pág. {pagina}] {doc[:60]}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.6 Implementación con FAISS\n",
    "\n",
    "**FAISS** (Facebook AI Similarity Search) es una biblioteca de bajo nivel que ofrece máximo rendimiento. Aunque requiere más código, es ideal cuando se necesita escala o control fino sobre el índice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import faiss\n",
    "import numpy as np\n",
    "from sentence_transformers import SentenceTransformer\n",
    "\n",
    "# Cargar modelo de embeddings (reusar el anterior si ya está cargado)\n",
    "if 'embedding_model' not in dir():\n",
    "    embedding_model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')\n",
    "\n",
    "# Generar embeddings para los documentos\n",
    "embeddings_docs = embedding_model.encode(documentos)\n",
    "embeddings_docs = embeddings_docs.astype('float32')  # FAISS requiere float32\n",
    "\n",
    "# Normalizar para usar producto punto (equivalente a coseno)\n",
    "faiss.normalize_L2(embeddings_docs)\n",
    "\n",
    "# Crear índice FAISS\n",
    "dimension = embeddings_docs.shape[1]\n",
    "index = faiss.IndexFlatIP(dimension)  # IP = Inner Product (coseno con normalización)\n",
    "\n",
    "# Añadir vectores al índice\n",
    "index.add(embeddings_docs)\n",
    "\n",
    "print(f\"✅ Índice FAISS creado\")\n",
    "print(f\"   Dimensión: {dimension}\")\n",
    "print(f\"   Vectores indexados: {index.ntotal}\")\n",
    "print(f\"   Tipo de índice: Flat (búsqueda exacta)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Búsqueda con FAISS\n",
    "# ============================================================\n",
    "\n",
    "def buscar_faiss(consulta, index, modelo, documentos, k=3):\n",
    "    \"\"\"Realiza búsqueda semántica usando FAISS.\"\"\"\n",
    "    # Generar y normalizar embedding de consulta\n",
    "    query_embedding = modelo.encode([consulta]).astype('float32')\n",
    "    faiss.normalize_L2(query_embedding)\n",
    "    \n",
    "    # Buscar los k vectores más cercanos\n",
    "    scores, indices = index.search(query_embedding, k)\n",
    "    \n",
    "    # Formatear resultados\n",
    "    resultados = []\n",
    "    for i, (idx, score) in enumerate(zip(indices[0], scores[0])):\n",
    "        resultados.append({\n",
    "            'documento': documentos[idx],\n",
    "            'score': score,\n",
    "            'indice': idx\n",
    "        })\n",
    "    \n",
    "    return resultados\n",
    "\n",
    "\n",
    "# Realizar búsqueda\n",
    "consulta = \"proteínas relacionadas con enfermedades cerebrales\"\n",
    "\n",
    "print(f\"🔍 BÚSQUEDA CON FAISS\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Consulta: \\\"{consulta}\\\"\\n\")\n",
    "\n",
    "resultados_faiss = buscar_faiss(consulta, index, embedding_model, documentos, k=3)\n",
    "\n",
    "for i, r in enumerate(resultados_faiss, 1):\n",
    "    print(f\"{i}. [Score: {r['score']:.4f}]\")\n",
    "    print(f\"   {r['documento'][:80]}...\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.7 Mejores Prácticas para Vector Stores\n",
    "\n",
    "### Diseño de Colecciones\n",
    "\n",
    "1. **Separar por dominio o tipo de documento**\n",
    "   - Una colección para papers científicos\n",
    "   - Otra para documentación técnica\n",
    "   - Esto optimiza las búsquedas y permite configuraciones específicas\n",
    "\n",
    "2. **Metadatos estratégicos**\n",
    "   - Incluir información que permita filtrar (fecha, autor, categoría)\n",
    "   - Guardar referencia a la fuente original (archivo, URL, página)\n",
    "   - Considerar metadatos para control de acceso si es necesario\n",
    "\n",
    "### Optimización de Rendimiento\n",
    "\n",
    "| Factor | Recomendación |\n",
    "|--------|---------------|\n",
    "| < 10K docs | Índice Flat (exacto) |\n",
    "| 10K - 1M docs | HNSW con parámetros default |\n",
    "| > 1M docs | IVF + tuning de parámetros |\n",
    "| Filtros frecuentes | Crear índices en campos de metadatos |\n",
    "\n",
    "### Mantenimiento\n",
    "\n",
    "1. **Actualizaciones incrementales**: Añadir nuevos documentos sin reindexar todo\n",
    "2. **Versionado**: Mantener versiones del índice para rollback\n",
    "3. **Backups**: Respaldar tanto los vectores como los metadatos\n",
    "4. **Monitorización**: Tracking de latencias y calidad de resultados"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"5-pipeline\"></a>\n",
    "# 5. Pipeline RAG Completo\n",
    "\n",
    "Ahora que entendemos cada componente (embeddings, chunking, vector stores), vamos a integrar todo en un **pipeline RAG funcional**.\n",
    "\n",
    "## 5.1 Arquitectura del Pipeline\n",
    "\n",
    "Un pipeline RAG completo tiene las siguientes etapas:\n",
    "\n",
    "```\n",
    "                    FASE DE INDEXACIÓN\n",
    "┌──────────────────────────────────────────────────────────┐\n",
    "│                                                          │\n",
    "│  [Documentos] → [Chunking] → [Embedding] → [VectorStore] │\n",
    "│                                                          │\n",
    "└──────────────────────────────────────────────────────────┘\n",
    "\n",
    "                    FASE DE CONSULTA\n",
    "┌──────────────────────────────────────────────────────────┐\n",
    "│                                                          │\n",
    "│  [Query] → [Embedding] → [Retrieval] → [Contexto]       │\n",
    "│                              │                           │\n",
    "│                              ↓                           │\n",
    "│  [Respuesta] ← [LLM] ← [Prompt con Contexto]            │\n",
    "│                                                          │\n",
    "└──────────────────────────────────────────────────────────┘\n",
    "```\n",
    "\n",
    "## 5.2 Implementación Paso a Paso\n",
    "\n",
    "Vamos a construir el pipeline de forma modular, componente por componente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# CLASE RAGPipeline: Implementación completa\n",
    "# ============================================================\n",
    "\n",
    "from typing import List, Dict, Optional\n",
    "from dataclasses import dataclass\n",
    "import chromadb\n",
    "from sentence_transformers import SentenceTransformer\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "\n",
    "@dataclass\n",
    "class Documento:\n",
    "    \"\"\"Representa un documento con su contenido y metadatos.\"\"\"\n",
    "    contenido: str\n",
    "    metadata: Dict = None\n",
    "    \n",
    "    def __post_init__(self):\n",
    "        if self.metadata is None:\n",
    "            self.metadata = {}\n",
    "\n",
    "\n",
    "@dataclass\n",
    "class ResultadoBusqueda:\n",
    "    \"\"\"Representa un resultado de búsqueda.\"\"\"\n",
    "    contenido: str\n",
    "    score: float\n",
    "    metadata: Dict\n",
    "\n",
    "\n",
    "class RAGPipeline:\n",
    "    \"\"\"\n",
    "    Pipeline RAG completo con indexación, búsqueda y generación.\n",
    "    \n",
    "    Esta implementación está diseñada para ser didáctica y mostrar\n",
    "    claramente cada paso del proceso RAG.\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(\n",
    "        self, \n",
    "        embedding_model: str = \"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2\",\n",
    "        collection_name: str = \"rag_collection\",\n",
    "        chunk_size: int = 500,\n",
    "        chunk_overlap: int = 100\n",
    "    ):\n",
    "        \"\"\"\n",
    "        Inicializa el pipeline RAG.\n",
    "        \n",
    "        Args:\n",
    "            embedding_model: Nombre del modelo de embeddings\n",
    "            collection_name: Nombre de la colección en ChromaDB\n",
    "            chunk_size: Tamaño máximo de cada chunk en caracteres\n",
    "            chunk_overlap: Solapamiento entre chunks\n",
    "        \"\"\"\n",
    "        print(\"🔧 Inicializando RAG Pipeline...\")\n",
    "        \n",
    "        # Modelo de embeddings\n",
    "        print(f\"   📐 Cargando modelo de embeddings: {embedding_model}\")\n",
    "        self.embedding_model = SentenceTransformer(embedding_model)\n",
    "        \n",
    "        # Splitter para chunking\n",
    "        self.text_splitter = RecursiveCharacterTextSplitter(\n",
    "            chunk_size=chunk_size,\n",
    "            chunk_overlap=chunk_overlap,\n",
    "            separators=[\"\\n\\n\", \"\\n\", \". \", \" \"]\n",
    "        )\n",
    "        \n",
    "        # Vector store (ChromaDB)\n",
    "        print(f\"   📦 Configurando ChromaDB: {collection_name}\")\n",
    "        self.client = chromadb.Client()\n",
    "        \n",
    "        # Función de embedding para ChromaDB\n",
    "        self.embedding_fn = chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction(\n",
    "            model_name=embedding_model\n",
    "        )\n",
    "        \n",
    "        self.collection = self.client.get_or_create_collection(\n",
    "            name=collection_name,\n",
    "            embedding_function=self.embedding_fn,\n",
    "            metadata={\"hnsw:space\": \"cosine\"}\n",
    "        )\n",
    "        \n",
    "        # Contador para IDs\n",
    "        self._doc_counter = 0\n",
    "        \n",
    "        print(\"✅ Pipeline inicializado correctamente\\n\")\n",
    "    \n",
    "    def indexar_documentos(self, documentos: List[Documento]) -> int:\n",
    "        \"\"\"\n",
    "        Indexa una lista de documentos en el vector store.\n",
    "        \n",
    "        Proceso:\n",
    "        1. Divide cada documento en chunks\n",
    "        2. Genera embeddings (automático por ChromaDB)\n",
    "        3. Almacena en el vector store\n",
    "        \n",
    "        Returns:\n",
    "            Número de chunks indexados\n",
    "        \"\"\"\n",
    "        print(f\"📥 Indexando {len(documentos)} documentos...\")\n",
    "        \n",
    "        todos_chunks = []\n",
    "        todos_metadatos = []\n",
    "        todos_ids = []\n",
    "        \n",
    "        for doc in documentos:\n",
    "            # Dividir en chunks\n",
    "            chunks = self.text_splitter.split_text(doc.contenido)\n",
    "            \n",
    "            for i, chunk in enumerate(chunks):\n",
    "                # Preparar metadatos del chunk\n",
    "                chunk_metadata = doc.metadata.copy()\n",
    "                chunk_metadata['chunk_index'] = i\n",
    "                chunk_metadata['total_chunks'] = len(chunks)\n",
    "                \n",
    "                todos_chunks.append(chunk)\n",
    "                todos_metadatos.append(chunk_metadata)\n",
    "                todos_ids.append(f\"doc_{self._doc_counter:05d}\")\n",
    "                self._doc_counter += 1\n",
    "        \n",
    "        # Añadir a ChromaDB\n",
    "        if todos_chunks:\n",
    "            self.collection.add(\n",
    "                documents=todos_chunks,\n",
    "                metadatas=todos_metadatos,\n",
    "                ids=todos_ids\n",
    "            )\n",
    "        \n",
    "        print(f\"✅ {len(todos_chunks)} chunks indexados\")\n",
    "        return len(todos_chunks)\n",
    "    \n",
    "    def buscar(self, consulta: str, k: int = 3, filtros: Dict = None) -> List[ResultadoBusqueda]:\n",
    "        \"\"\"\n",
    "        Busca los documentos más relevantes para una consulta.\n",
    "        \n",
    "        Args:\n",
    "            consulta: Pregunta o texto de búsqueda\n",
    "            k: Número de resultados a devolver\n",
    "            filtros: Diccionario de filtros de metadatos (opcional)\n",
    "        \n",
    "        Returns:\n",
    "            Lista de ResultadoBusqueda ordenados por relevancia\n",
    "        \"\"\"\n",
    "        resultados = self.collection.query(\n",
    "            query_texts=[consulta],\n",
    "            n_results=k,\n",
    "            where=filtros,\n",
    "            include=[\"documents\", \"metadatas\", \"distances\"]\n",
    "        )\n",
    "        \n",
    "        # Convertir a objetos ResultadoBusqueda\n",
    "        busquedas = []\n",
    "        for i in range(len(resultados['documents'][0])):\n",
    "            busquedas.append(ResultadoBusqueda(\n",
    "                contenido=resultados['documents'][0][i],\n",
    "                score=1 - resultados['distances'][0][i],  # Convertir distancia a similitud\n",
    "                metadata=resultados['metadatas'][0][i]\n",
    "            ))\n",
    "        \n",
    "        return busquedas\n",
    "    \n",
    "    def construir_contexto(self, resultados: List[ResultadoBusqueda]) -> str:\n",
    "        \"\"\"\n",
    "        Construye el contexto para el LLM a partir de los resultados.\n",
    "        \n",
    "        Formatea los documentos recuperados de forma estructurada\n",
    "        para que el LLM pueda utilizarlos efectivamente.\n",
    "        \"\"\"\n",
    "        partes_contexto = []\n",
    "        \n",
    "        for i, res in enumerate(resultados, 1):\n",
    "            fuente = res.metadata.get('fuente', 'Desconocida')\n",
    "            pagina = res.metadata.get('pagina', 'N/A')\n",
    "            \n",
    "            parte = f\"[Documento {i} - Fuente: {fuente}, Pág. {pagina}]\\n{res.contenido}\"\n",
    "            partes_contexto.append(parte)\n",
    "        \n",
    "        return \"\\n\\n\".join(partes_contexto)\n",
    "    \n",
    "    def generar_prompt(self, consulta: str, contexto: str) -> str:\n",
    "        \"\"\"\n",
    "        Genera el prompt completo para el LLM.\n",
    "        \n",
    "        Este prompt está diseñado para:\n",
    "        - Instruir al LLM a usar solo el contexto proporcionado\n",
    "        - Solicitar citaciones de las fuentes\n",
    "        - Manejar casos donde no hay información suficiente\n",
    "        \"\"\"\n",
    "        prompt = f\"\"\"Eres un asistente de investigación científica. Tu tarea es responder \n",
    "preguntas basándote ÚNICAMENTE en el contexto proporcionado.\n",
    "\n",
    "REGLAS IMPORTANTES:\n",
    "1. Usa SOLO la información del contexto para responder\n",
    "2. Si la información no está en el contexto, di: \"No tengo información sobre esto en los documentos disponibles\"\n",
    "3. Cita las fuentes usando el formato [Documento X]\n",
    "4. Sé preciso y conciso en tu respuesta\n",
    "5. Si hay información contradictoria, menciona ambas perspectivas\n",
    "\n",
    "CONTEXTO:\n",
    "---\n",
    "{contexto}\n",
    "---\n",
    "\n",
    "PREGUNTA: {consulta}\n",
    "\n",
    "RESPUESTA:\"\"\"\n",
    "        \n",
    "        return prompt\n",
    "    \n",
    "    def consultar(self, pregunta: str, k: int = 3, filtros: Dict = None) -> Dict:\n",
    "        \"\"\"\n",
    "        Ejecuta el pipeline RAG completo.\n",
    "        \n",
    "        Esta función combina todos los pasos:\n",
    "        1. Búsqueda de documentos relevantes\n",
    "        2. Construcción del contexto\n",
    "        3. Generación del prompt\n",
    "        \n",
    "        Returns:\n",
    "            Diccionario con prompt, contexto y documentos fuente\n",
    "        \"\"\"\n",
    "        # Paso 1: Buscar documentos relevantes\n",
    "        resultados = self.buscar(pregunta, k=k, filtros=filtros)\n",
    "        \n",
    "        # Paso 2: Construir contexto\n",
    "        contexto = self.construir_contexto(resultados)\n",
    "        \n",
    "        # Paso 3: Generar prompt\n",
    "        prompt = self.generar_prompt(pregunta, contexto)\n",
    "        \n",
    "        return {\n",
    "            'prompt': prompt,\n",
    "            'contexto': contexto,\n",
    "            'documentos_fuente': resultados,\n",
    "            'num_documentos': len(resultados)\n",
    "        }\n",
    "\n",
    "\n",
    "print(\"✅ Clase RAGPipeline definida correctamente\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Crear y usar el pipeline\n",
    "# ============================================================\n",
    "\n",
    "# Inicializar el pipeline\n",
    "rag = RAGPipeline(\n",
    "    embedding_model=\"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2\",\n",
    "    collection_name=\"demo_rag\",\n",
    "    chunk_size=400,\n",
    "    chunk_overlap=80\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Preparar e indexar documentos\n",
    "# ============================================================\n",
    "\n",
    "# Crear documentos de ejemplo\n",
    "docs_ejemplo = [\n",
    "    Documento(\n",
    "        contenido=\"\"\"Las proteínas chaperonas son moléculas esenciales para la vida celular.\n",
    "        Funcionan como \"guardianes\" del plegamiento proteico, asistiendo a otras proteínas\n",
    "        para que adopten su estructura tridimensional correcta. Sin las chaperonas, muchas\n",
    "        proteínas se plegarían incorrectamente y formarían agregados tóxicos.\n",
    "        \n",
    "        El mecanismo de acción implica ciclos de unión y liberación dependientes de ATP.\n",
    "        Las chaperonas reconocen regiones hidrofóbicas expuestas, que normalmente estarían\n",
    "        ocultas en el interior de una proteína correctamente plegada.\"\"\",\n",
    "        metadata={\"fuente\": \"manual_bioquimica.pdf\", \"pagina\": 245, \"tema\": \"chaperonas\"}\n",
    "    ),\n",
    "    Documento(\n",
    "        contenido=\"\"\"La familia Hsp70 es una de las más importantes y conservadas evolutivamente.\n",
    "        Estas chaperonas reconocen segmentos hidrofóbicos de 7-8 aminoácidos en proteínas\n",
    "        desplegadas. Para funcionar óptimamente, Hsp70 requiere co-chaperonas como Hsp40\n",
    "        (también conocida como DnaJ), que estimulan su actividad ATPasa.\n",
    "        \n",
    "        El ciclo funcional de Hsp70 incluye:\n",
    "        1. Unión del sustrato en estado ATP\n",
    "        2. Hidrólisis de ATP estimulada por Hsp40\n",
    "        3. Liberación del sustrato tras intercambio de nucleótido\"\"\",\n",
    "        metadata={\"fuente\": \"manual_bioquimica.pdf\", \"pagina\": 248, \"tema\": \"chaperonas\"}\n",
    "    ),\n",
    "    Documento(\n",
    "        contenido=\"\"\"El mal plegamiento de proteínas está implicado en numerosas enfermedades\n",
    "        neurodegenerativas. En la enfermedad de Alzheimer, los agregados de beta-amiloide\n",
    "        y tau hiperfosforilada forman placas y ovillos neurofibrilares respectivamente.\n",
    "        \n",
    "        En Parkinson, la proteína alfa-sinucleína se agrega formando los cuerpos de Lewy.\n",
    "        Las enfermedades por priones son causadas por PrPSc, una forma mal plegada de la\n",
    "        proteína priónica que puede \"contagiar\" su conformación a proteínas normales.\"\"\",\n",
    "        metadata={\"fuente\": \"neurologia_molecular.pdf\", \"pagina\": 89, \"tema\": \"patologia\"}\n",
    "    ),\n",
    "    Documento(\n",
    "        contenido=\"\"\"CRISPR-Cas9 ha revolucionado la edición genética. Este sistema utiliza\n",
    "        un ARN guía para dirigir la nucleasa Cas9 a una secuencia específica del genoma,\n",
    "        donde realiza un corte de doble hebra. La célula repara este corte mediante\n",
    "        unión de extremos no homólogos (NHEJ) o recombinación homóloga (HDR).\n",
    "        \n",
    "        Aplicaciones incluyen: corrección de mutaciones genéticas, creación de modelos\n",
    "        de enfermedad, desarrollo de terapias génicas y mejora de cultivos.\"\"\",\n",
    "        metadata={\"fuente\": \"genetica_moderna.pdf\", \"pagina\": 156, \"tema\": \"genetica\"}\n",
    "    ),\n",
    "]\n",
    "\n",
    "# Indexar documentos\n",
    "num_chunks = rag.indexar_documentos(docs_ejemplo)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Realizar consultas con el pipeline\n",
    "# ============================================================\n",
    "\n",
    "pregunta = \"¿Cómo funcionan las proteínas chaperonas y qué enfermedades están relacionadas con su mal funcionamiento?\"\n",
    "\n",
    "print(\"🔍 CONSULTA RAG\")\n",
    "print(\"=\"*70)\n",
    "print(f\"Pregunta: {pregunta}\\n\")\n",
    "\n",
    "# Ejecutar pipeline\n",
    "resultado = rag.consultar(pregunta, k=3)\n",
    "\n",
    "# Mostrar documentos recuperados\n",
    "print(\"📄 DOCUMENTOS RECUPERADOS:\")\n",
    "print(\"-\"*70)\n",
    "for i, doc in enumerate(resultado['documentos_fuente'], 1):\n",
    "    print(f\"\\n{i}. [Similitud: {doc.score:.4f}]\")\n",
    "    print(f\"   Fuente: {doc.metadata.get('fuente')}, pág. {doc.metadata.get('pagina')}\")\n",
    "    print(f\"   {doc.contenido[:100]}...\")\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"📝 PROMPT GENERADO PARA EL LLM:\")\n",
    "print(\"-\"*70)\n",
    "print(resultado['prompt'][:1500] + \"...\\n\")\n",
    "\n",
    "print(\"💡 Este prompt se enviaría al LLM (Llama, Mistral, etc.) para generar la respuesta final.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"6-avanzadas\"></a>\n",
    "# 6. Técnicas Avanzadas de Retrieval\n",
    "\n",
    "El RAG básico (\"Naive RAG\") funciona bien en muchos casos, pero existen técnicas avanzadas que pueden mejorar significativamente la calidad de las respuestas.\n",
    "\n",
    "## 6.1 Limitaciones del RAG Básico\n",
    "\n",
    "| Limitación | Descripción | Impacto |\n",
    "|------------|-------------|----------|\n",
    "| **Búsqueda solo semántica** | No captura coincidencias de términos exactos | Falla con términos técnicos específicos |\n",
    "| **Sin reordenamiento** | Primer resultado puede no ser el mejor | Contexto subóptimo para el LLM |\n",
    "| **Query única** | La consulta del usuario puede ser ambigua | Baja cobertura de documentos relevantes |\n",
    "| **Sin verificación** | El LLM puede ignorar el contexto | Posibles alucinaciones |\n",
    "\n",
    "## 6.2 Hybrid Search: Combinando Semántica y Keywords\n",
    "\n",
    "### El Problema\n",
    "\n",
    "La búsqueda puramente semántica puede fallar con:\n",
    "- **Términos técnicos**: \"Hsp70\" vs \"proteína de choque térmico 70\"\n",
    "- **Acrónimos**: \"PCR\", \"ADN\", \"ARNm\"\n",
    "- **Nombres propios**: \"CRISPR-Cas9\", \"Alzheimer\"\n",
    "\n",
    "### La Solución: Búsqueda Híbrida\n",
    "\n",
    "Combina dos tipos de búsqueda:\n",
    "\n",
    "1. **Búsqueda Vectorial (Semántica)**: Captura el significado\n",
    "2. **Búsqueda Léxica (BM25)**: Captura coincidencias de términos exactos\n",
    "\n",
    "```\n",
    "Score_final = α × Score_vectorial + (1-α) × Score_BM25\n",
    "\n",
    "donde α ∈ [0, 1] controla el balance:\n",
    "  α = 1.0 → Solo vectorial\n",
    "  α = 0.0 → Solo BM25\n",
    "  α = 0.5 → Balance igual (recomendado como punto de partida)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Implementación de Búsqueda Híbrida\n",
    "# ============================================================\n",
    "\n",
    "from rank_bm25 import BM25Okapi\n",
    "import re\n",
    "import numpy as np\n",
    "from typing import List, Tuple\n",
    "\n",
    "class BusquedaHibrida:\n",
    "    \"\"\"\n",
    "    Implementa búsqueda híbrida combinando vectorial + BM25.\n",
    "    \n",
    "    BM25 (Best Matching 25) es un algoritmo de ranking que considera:\n",
    "    - Frecuencia del término en el documento (TF)\n",
    "    - Frecuencia inversa del documento (IDF)\n",
    "    - Longitud del documento\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(self, embedding_model):\n",
    "        self.embedding_model = embedding_model\n",
    "        self.documentos = []\n",
    "        self.embeddings = None\n",
    "        self.bm25 = None\n",
    "        self.tokenized_docs = []\n",
    "    \n",
    "    def _tokenizar(self, texto: str) -> List[str]:\n",
    "        \"\"\"Tokenización simple para BM25.\"\"\"\n",
    "        # Convertir a minúsculas y extraer palabras\n",
    "        tokens = re.findall(r'\\w+', texto.lower())\n",
    "        return tokens\n",
    "    \n",
    "    def indexar(self, documentos: List[str]):\n",
    "        \"\"\"Indexa documentos para búsqueda híbrida.\"\"\"\n",
    "        self.documentos = documentos\n",
    "        \n",
    "        # Índice vectorial\n",
    "        print(\"   Generando embeddings...\")\n",
    "        self.embeddings = self.embedding_model.encode(documentos)\n",
    "        \n",
    "        # Índice BM25\n",
    "        print(\"   Construyendo índice BM25...\")\n",
    "        self.tokenized_docs = [self._tokenizar(doc) for doc in documentos]\n",
    "        self.bm25 = BM25Okapi(self.tokenized_docs)\n",
    "        \n",
    "        print(f\"✅ {len(documentos)} documentos indexados para búsqueda híbrida\")\n",
    "    \n",
    "    def buscar(self, query: str, k: int = 5, alpha: float = 0.5) -> List[Tuple[str, float, dict]]:\n",
    "        \"\"\"\n",
    "        Realiza búsqueda híbrida.\n",
    "        \n",
    "        Args:\n",
    "            query: Consulta del usuario\n",
    "            k: Número de resultados\n",
    "            alpha: Peso de búsqueda vectorial (0-1)\n",
    "                   1.0 = solo vectorial, 0.0 = solo BM25\n",
    "        \n",
    "        Returns:\n",
    "            Lista de (documento, score_hibrido, detalles)\n",
    "        \"\"\"\n",
    "        # Búsqueda vectorial\n",
    "        query_embedding = self.embedding_model.encode([query])\n",
    "        vector_scores = cosine_similarity(query_embedding, self.embeddings)[0]\n",
    "        \n",
    "        # Búsqueda BM25\n",
    "        tokenized_query = self._tokenizar(query)\n",
    "        bm25_scores = self.bm25.get_scores(tokenized_query)\n",
    "        \n",
    "        # Normalizar scores a rango [0, 1]\n",
    "        vector_norm = self._normalizar(vector_scores)\n",
    "        bm25_norm = self._normalizar(bm25_scores)\n",
    "        \n",
    "        # Combinar scores\n",
    "        hybrid_scores = alpha * vector_norm + (1 - alpha) * bm25_norm\n",
    "        \n",
    "        # Obtener top-k\n",
    "        top_indices = np.argsort(hybrid_scores)[::-1][:k]\n",
    "        \n",
    "        resultados = []\n",
    "        for idx in top_indices:\n",
    "            resultados.append((\n",
    "                self.documentos[idx],\n",
    "                hybrid_scores[idx],\n",
    "                {\n",
    "                    'score_vectorial': vector_scores[idx],\n",
    "                    'score_bm25': bm25_scores[idx],\n",
    "                    'score_vectorial_norm': vector_norm[idx],\n",
    "                    'score_bm25_norm': bm25_norm[idx]\n",
    "                }\n",
    "            ))\n",
    "        \n",
    "        return resultados\n",
    "    \n",
    "    def _normalizar(self, scores):\n",
    "        \"\"\"Normaliza scores al rango [0, 1].\"\"\"\n",
    "        min_s, max_s = scores.min(), scores.max()\n",
    "        if max_s - min_s < 1e-6:\n",
    "            return np.zeros_like(scores)\n",
    "        return (scores - min_s) / (max_s - min_s)\n",
    "\n",
    "\n",
    "# Crear instancia\n",
    "print(\"🔧 Inicializando búsqueda híbrida...\")\n",
    "hibrida = BusquedaHibrida(embedding_model)\n",
    "\n",
    "# Indexar documentos de ejemplo\n",
    "docs_hibrida = [\n",
    "    \"Las proteínas chaperonas Hsp70 y Hsp90 son esenciales para el plegamiento proteico.\",\n",
    "    \"La técnica CRISPR-Cas9 permite edición genética precisa del ADN.\",\n",
    "    \"El mal plegamiento de proteínas causa enfermedades como Alzheimer.\",\n",
    "    \"Hsp70 reconoce secuencias hidrofóbicas de 7-8 aminoácidos.\",\n",
    "    \"La PCR amplifica secuencias específicas de ADN usando Taq polimerasa.\",\n",
    "    \"Los agregados de alfa-sinucleína causan la enfermedad de Parkinson.\",\n",
    "]\n",
    "\n",
    "hibrida.indexar(docs_hibrida)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Comparación: Vectorial vs BM25 vs Híbrido\n",
    "# ============================================================\n",
    "\n",
    "# Query con término técnico específico\n",
    "query = \"Hsp70 aminoácidos hidrofóbicos\"\n",
    "\n",
    "print(f\"🔍 COMPARACIÓN DE MÉTODOS DE BÚSQUEDA\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Query: \\\"{query}\\\"\\n\")\n",
    "\n",
    "# Solo vectorial (alpha=1.0)\n",
    "print(\"1️⃣ SOLO VECTORIAL (alpha=1.0):\")\n",
    "res_vec = hibrida.buscar(query, k=3, alpha=1.0)\n",
    "for i, (doc, score, _) in enumerate(res_vec, 1):\n",
    "    print(f\"   {i}. [{score:.3f}] {doc[:60]}...\")\n",
    "\n",
    "# Solo BM25 (alpha=0.0)\n",
    "print(\"\\n2️⃣ SOLO BM25 (alpha=0.0):\")\n",
    "res_bm25 = hibrida.buscar(query, k=3, alpha=0.0)\n",
    "for i, (doc, score, _) in enumerate(res_bm25, 1):\n",
    "    print(f\"   {i}. [{score:.3f}] {doc[:60]}...\")\n",
    "\n",
    "# Híbrido (alpha=0.5)\n",
    "print(\"\\n3️⃣ HÍBRIDO (alpha=0.5):\")\n",
    "res_hyb = hibrida.buscar(query, k=3, alpha=0.5)\n",
    "for i, (doc, score, detalles) in enumerate(res_hyb, 1):\n",
    "    print(f\"   {i}. [{score:.3f}] (vec:{detalles['score_vectorial_norm']:.2f}, bm25:{detalles['score_bm25_norm']:.2f})\")\n",
    "    print(f\"      {doc[:55]}...\")\n",
    "\n",
    "print(\"\\n💡 Observa cómo BM25 encuentra mejor el documento con 'Hsp70' y 'aminoácidos',\")\n",
    "print(\"   mientras que vectorial captura el significado general.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.3 Reranking: Mejorando la Precisión\n",
    "\n",
    "### El Concepto\n",
    "\n",
    "El **reranking** es un paso adicional que reordena los resultados iniciales usando un modelo más preciso pero más lento.\n",
    "\n",
    "```\n",
    "Query → Retrieval (rápido, top-20) → Reranker (preciso, top-5) → LLM\n",
    "```\n",
    "\n",
    "### Bi-Encoder vs Cross-Encoder\n",
    "\n",
    "| Aspecto | Bi-Encoder | Cross-Encoder |\n",
    "|---------|------------|---------------|\n",
    "| **Cómo funciona** | Codifica query y doc por separado | Procesa query + doc juntos |\n",
    "| **Velocidad** | Muy rápido (O(1) por comparación) | Lento (procesa cada par) |\n",
    "| **Precisión** | Buena | Excelente |\n",
    "| **Uso** | Retrieval inicial | Reranking |\n",
    "\n",
    "### Flujo Típico\n",
    "\n",
    "1. **Retrieval**: Bi-encoder recupera top-20 candidatos (rápido)\n",
    "2. **Reranking**: Cross-encoder reordena → top-5 (preciso)\n",
    "3. **Generación**: LLM usa los 5 mejores documentos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Implementación de Reranking con Cross-Encoder\n",
    "# ============================================================\n",
    "\n",
    "from sentence_transformers import CrossEncoder\n",
    "\n",
    "# Cargar modelo de reranking\n",
    "print(\"🔧 Cargando modelo de reranking (Cross-Encoder)...\")\n",
    "reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')\n",
    "print(\"✅ Modelo de reranking cargado\\n\")\n",
    "\n",
    "\n",
    "def rerank(query: str, documentos: List[str], top_k: int = 3) -> List[Tuple[str, float]]:\n",
    "    \"\"\"\n",
    "    Reordena documentos usando Cross-Encoder.\n",
    "    \n",
    "    El Cross-Encoder evalúa directamente la relevancia de cada par\n",
    "    (query, documento), lo que es más preciso que comparar embeddings.\n",
    "    \"\"\"\n",
    "    # Crear pares (query, documento)\n",
    "    pares = [[query, doc] for doc in documentos]\n",
    "    \n",
    "    # Obtener scores del reranker\n",
    "    scores = reranker.predict(pares)\n",
    "    \n",
    "    # Ordenar por score descendente\n",
    "    doc_scores = list(zip(documentos, scores))\n",
    "    doc_scores.sort(key=lambda x: x[1], reverse=True)\n",
    "    \n",
    "    return doc_scores[:top_k]\n",
    "\n",
    "\n",
    "# Ejemplo de reranking\n",
    "query = \"enfermedades causadas por proteínas que no se pliegan bien\"\n",
    "\n",
    "print(f\"🔍 DEMOSTRACIÓN DE RERANKING\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Query: \\\"{query}\\\"\\n\")\n",
    "\n",
    "# Orden original (simulando retrieval inicial)\n",
    "print(\"📄 ORDEN ORIGINAL (del retrieval):\")\n",
    "for i, doc in enumerate(docs_hibrida, 1):\n",
    "    print(f\"   {i}. {doc[:60]}...\")\n",
    "\n",
    "# Después de reranking\n",
    "print(\"\\n📄 DESPUÉS DE RERANKING:\")\n",
    "reranked = rerank(query, docs_hibrida, top_k=3)\n",
    "for i, (doc, score) in enumerate(reranked, 1):\n",
    "    print(f\"   {i}. [Score: {score:.4f}] {doc[:50]}...\")\n",
    "\n",
    "print(\"\\n💡 El reranker identifica correctamente los documentos sobre\")\n",
    "print(\"   enfermedades y mal plegamiento como los más relevantes.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.4 Multi-Query RAG: Expandiendo la Búsqueda\n",
    "\n",
    "### El Problema\n",
    "\n",
    "Las consultas de los usuarios pueden ser:\n",
    "- **Ambiguas**: \"¿Cómo funcionan las proteínas?\" (muy general)\n",
    "- **Incompletas**: No usan todos los términos relevantes\n",
    "- **Mal formuladas**: No coinciden con cómo está escrita la información\n",
    "\n",
    "### La Solución: Múltiples Consultas\n",
    "\n",
    "Generar variaciones de la consulta original y fusionar los resultados:\n",
    "\n",
    "```\n",
    "Query original: \"¿Qué hacen las chaperonas?\"\n",
    "                        │\n",
    "                        ▼\n",
    "        ┌───────────────┼───────────────┐\n",
    "        ▼               ▼               ▼\n",
    "   \"función de      \"cómo ayudan     \"proteínas Hsp70\n",
    "    chaperonas\"      las chaperonas\"   Hsp90 función\"\n",
    "        │               │               │\n",
    "        ▼               ▼               ▼\n",
    "    [Retrieval]    [Retrieval]    [Retrieval]\n",
    "        │               │               │\n",
    "        └───────────────┼───────────────┘\n",
    "                        ▼\n",
    "                   [Fusión RRF]\n",
    "                        │\n",
    "                        ▼\n",
    "              Resultados combinados\n",
    "```\n",
    "\n",
    "### Reciprocal Rank Fusion (RRF)\n",
    "\n",
    "Técnica para combinar rankings de múltiples fuentes:\n",
    "\n",
    "$$RRF(d) = \\sum_{q \\in Q} \\frac{1}{k + rank_q(d)}$$\n",
    "\n",
    "Donde:\n",
    "- $d$ = documento\n",
    "- $Q$ = conjunto de queries\n",
    "- $rank_q(d)$ = posición del documento $d$ en los resultados de query $q$\n",
    "- $k$ = constante (típicamente 60)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Implementación de Multi-Query RAG\n",
    "# ============================================================\n",
    "\n",
    "def generar_queries_alternativas(query_original: str) -> List[str]:\n",
    "    \"\"\"\n",
    "    Genera variaciones de la consulta original.\n",
    "    \n",
    "    En producción, usarías un LLM para generar estas variaciones.\n",
    "    Aquí simulamos algunas transformaciones comunes.\n",
    "    \"\"\"\n",
    "    variaciones = [query_original]  # Siempre incluir la original\n",
    "    \n",
    "    # Ejemplo de variaciones manuales (en producción, usar LLM)\n",
    "    if \"chaperona\" in query_original.lower():\n",
    "        variaciones.extend([\n",
    "            \"proteínas de choque térmico HSP función\",\n",
    "            \"Hsp70 Hsp90 plegamiento proteico\",\n",
    "            \"cómo asisten las chaperonas moleculares\"\n",
    "        ])\n",
    "    elif \"enfermedad\" in query_original.lower():\n",
    "        variaciones.extend([\n",
    "            \"patologías proteínas mal plegadas\",\n",
    "            \"Alzheimer Parkinson agregados proteicos\"\n",
    "        ])\n",
    "    \n",
    "    return variaciones\n",
    "\n",
    "\n",
    "def reciprocal_rank_fusion(rankings: List[List[str]], k: int = 60) -> List[Tuple[str, float]]:\n",
    "    \"\"\"\n",
    "    Fusiona múltiples rankings usando Reciprocal Rank Fusion.\n",
    "    \n",
    "    Args:\n",
    "        rankings: Lista de rankings (cada uno es una lista de documentos ordenados)\n",
    "        k: Constante de suavizado (default: 60)\n",
    "    \n",
    "    Returns:\n",
    "        Lista de (documento, score_rrf) ordenada por score\n",
    "    \"\"\"\n",
    "    rrf_scores = {}\n",
    "    \n",
    "    for ranking in rankings:\n",
    "        for rank, doc in enumerate(ranking, 1):\n",
    "            if doc not in rrf_scores:\n",
    "                rrf_scores[doc] = 0\n",
    "            rrf_scores[doc] += 1 / (k + rank)\n",
    "    \n",
    "    # Ordenar por score RRF\n",
    "    sorted_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)\n",
    "    return sorted_docs\n",
    "\n",
    "\n",
    "# Demostración\n",
    "query_original = \"¿Qué hacen las chaperonas moleculares?\"\n",
    "\n",
    "print(f\"🔍 MULTI-QUERY RAG\")\n",
    "print(f\"=\"*70)\n",
    "print(f\"Query original: \\\"{query_original}\\\"\\n\")\n",
    "\n",
    "# Generar queries alternativas\n",
    "queries = generar_queries_alternativas(query_original)\n",
    "print(\"📝 Queries generadas:\")\n",
    "for i, q in enumerate(queries, 1):\n",
    "    print(f\"   {i}. {q}\")\n",
    "\n",
    "# Realizar búsquedas con cada query\n",
    "print(\"\\n🔍 Resultados por query:\")\n",
    "rankings = []\n",
    "for q in queries:\n",
    "    resultados = hibrida.buscar(q, k=4, alpha=0.5)\n",
    "    ranking = [doc for doc, _, _ in resultados]\n",
    "    rankings.append(ranking)\n",
    "\n",
    "# Fusionar con RRF\n",
    "fusion = reciprocal_rank_fusion(rankings)\n",
    "\n",
    "print(\"\\n📄 RESULTADOS FUSIONADOS (RRF):\")\n",
    "for i, (doc, score) in enumerate(fusion[:4], 1):\n",
    "    print(f\"   {i}. [RRF: {score:.4f}] {doc[:55]}...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.5 Resumen de Técnicas Avanzadas\n",
    "\n",
    "| Técnica | Cuándo Usar | Mejora |\n",
    "|---------|-------------|--------|\n",
    "| **Hybrid Search** | Términos técnicos, acrónimos, nombres propios | Mejor recall para términos específicos |\n",
    "| **Reranking** | Alta precisión necesaria, tienes recursos computacionales | Mejor precisión en top-k |\n",
    "| **Multi-Query** | Consultas ambiguas, baja cobertura | Mayor cobertura de documentos relevantes |\n",
    "| **Query Expansion** | Vocabulario variado en documentos | Mejor matching de términos |\n",
    "\n",
    "### Pipeline Avanzado Recomendado\n",
    "\n",
    "```\n",
    "Query → [Multi-Query] → [Hybrid Search] → [Reranking] → [LLM]\n",
    "         (expandir)      (recuperar)       (precisar)   (generar)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"7-evaluacion\"></a>\n",
    "# 7. Evaluación y Métricas de RAG\n",
    "\n",
    "## 7.1 ¿Por qué Evaluar?\n",
    "\n",
    "Sin evaluación sistemática, no puedes:\n",
    "- Saber si tu sistema RAG está funcionando bien\n",
    "- Comparar diferentes configuraciones\n",
    "- Detectar regresiones al hacer cambios\n",
    "- Justificar mejoras ante stakeholders\n",
    "\n",
    "## 7.2 Métricas de Retrieval\n",
    "\n",
    "Evalúan la calidad de los documentos recuperados:\n",
    "\n",
    "### Precision@k\n",
    "\n",
    "¿Qué proporción de los top-k documentos recuperados son relevantes?\n",
    "\n",
    "$$Precision@k = \\frac{|\\text{documentos relevantes en top-k}|}{k}$$\n",
    "\n",
    "**Ejemplo**: Si de los top-5 resultados, 3 son relevantes → Precision@5 = 0.6\n",
    "\n",
    "### Recall@k\n",
    "\n",
    "¿Qué proporción de todos los documentos relevantes fueron recuperados?\n",
    "\n",
    "$$Recall@k = \\frac{|\\text{documentos relevantes en top-k}|}{|\\text{total documentos relevantes}|}$$\n",
    "\n",
    "**Ejemplo**: Si hay 10 docs relevantes y recuperamos 3 en top-5 → Recall@5 = 0.3\n",
    "\n",
    "### MRR (Mean Reciprocal Rank)\n",
    "\n",
    "¿Qué tan arriba aparece el primer documento relevante?\n",
    "\n",
    "$$MRR = \\frac{1}{|Q|} \\sum_{i=1}^{|Q|} \\frac{1}{rank_i}$$\n",
    "\n",
    "**Ejemplo**: Si el primer doc relevante está en posición 2 → RR = 0.5\n",
    "\n",
    "### Hit Rate (Success Rate)\n",
    "\n",
    "¿Hay al menos un documento relevante en los top-k?\n",
    "\n",
    "$$HitRate@k = \\frac{|\\text{queries con al menos 1 doc relevante en top-k}|}{|\\text{total queries}|}$$\n",
    "\n",
    "## 7.3 Métricas de Generación\n",
    "\n",
    "Evalúan la calidad de la respuesta del LLM:\n",
    "\n",
    "### Faithfulness (Fidelidad)\n",
    "\n",
    "¿La respuesta se basa en el contexto proporcionado?\n",
    "- Detecta alucinaciones\n",
    "- Verifica que no invente información\n",
    "\n",
    "### Answer Relevancy\n",
    "\n",
    "¿La respuesta contesta la pregunta del usuario?\n",
    "- No basta con ser fiel al contexto\n",
    "- Debe ser pertinente a la consulta\n",
    "\n",
    "### Context Precision\n",
    "\n",
    "¿Los documentos del contexto eran realmente necesarios?\n",
    "- Penaliza incluir información irrelevante"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Implementación de Métricas de Retrieval\n",
    "# ============================================================\n",
    "\n",
    "def precision_at_k(retrieved: List[str], relevant: set, k: int) -> float:\n",
    "    \"\"\"\n",
    "    Calcula Precision@k.\n",
    "    \n",
    "    Args:\n",
    "        retrieved: Lista de documentos recuperados (ordenados por ranking)\n",
    "        relevant: Conjunto de documentos relevantes (ground truth)\n",
    "        k: Número de documentos a considerar\n",
    "    \"\"\"\n",
    "    retrieved_k = retrieved[:k]\n",
    "    relevant_in_k = sum(1 for doc in retrieved_k if doc in relevant)\n",
    "    return relevant_in_k / k\n",
    "\n",
    "\n",
    "def recall_at_k(retrieved: List[str], relevant: set, k: int) -> float:\n",
    "    \"\"\"Calcula Recall@k.\"\"\"\n",
    "    if not relevant:\n",
    "        return 0.0\n",
    "    retrieved_k = retrieved[:k]\n",
    "    relevant_in_k = sum(1 for doc in retrieved_k if doc in relevant)\n",
    "    return relevant_in_k / len(relevant)\n",
    "\n",
    "\n",
    "def mrr(retrieved: List[str], relevant: set) -> float:\n",
    "    \"\"\"Calcula Mean Reciprocal Rank (para una query).\"\"\"\n",
    "    for rank, doc in enumerate(retrieved, 1):\n",
    "        if doc in relevant:\n",
    "            return 1 / rank\n",
    "    return 0.0\n",
    "\n",
    "\n",
    "def hit_rate_at_k(retrieved: List[str], relevant: set, k: int) -> float:\n",
    "    \"\"\"Calcula Hit Rate@k (1 si hay al menos un relevante, 0 si no).\"\"\"\n",
    "    retrieved_k = retrieved[:k]\n",
    "    return 1.0 if any(doc in relevant for doc in retrieved_k) else 0.0\n",
    "\n",
    "\n",
    "# Ejemplo de evaluación\n",
    "print(\"📊 EVALUACIÓN DE RETRIEVAL\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "# Simulamos resultados de retrieval\n",
    "retrieved_docs = [\"doc_A\", \"doc_B\", \"doc_C\", \"doc_D\", \"doc_E\"]\n",
    "relevant_docs = {\"doc_A\", \"doc_C\", \"doc_F\", \"doc_G\"}  # Ground truth\n",
    "\n",
    "print(f\"\\nDocumentos recuperados: {retrieved_docs}\")\n",
    "print(f\"Documentos relevantes (ground truth): {relevant_docs}\")\n",
    "print()\n",
    "\n",
    "for k in [1, 3, 5]:\n",
    "    p = precision_at_k(retrieved_docs, relevant_docs, k)\n",
    "    r = recall_at_k(retrieved_docs, relevant_docs, k)\n",
    "    hr = hit_rate_at_k(retrieved_docs, relevant_docs, k)\n",
    "    print(f\"k={k}: Precision={p:.2f}, Recall={r:.2f}, Hit Rate={hr:.2f}\")\n",
    "\n",
    "print(f\"\\nMRR: {mrr(retrieved_docs, relevant_docs):.2f}\")\n",
    "print(\"   (El primer doc relevante 'doc_A' está en posición 1 → MRR = 1.0)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"8-prompting\"></a>\n",
    "# 8. Prompt Engineering para RAG\n",
    "\n",
    "## 8.1 La Importancia del Prompt\n",
    "\n",
    "El prompt es la interfaz entre el contexto recuperado y la respuesta generada. Un buen prompt debe:\n",
    "\n",
    "1. **Instruir claramente** al modelo sobre su rol y tarea\n",
    "2. **Estructurar el contexto** de forma que sea fácil de procesar\n",
    "3. **Guiar el formato** de la respuesta esperada\n",
    "4. **Manejar edge cases** (información insuficiente, contradicciones)\n",
    "\n",
    "## 8.2 Estructura de un Prompt RAG Efectivo\n",
    "\n",
    "```\n",
    "┌─────────────────────────────────────────────────────────────┐\n",
    "│ SYSTEM PROMPT                                               │\n",
    "│ - Rol del asistente                                        │\n",
    "│ - Instrucciones de comportamiento                          │\n",
    "│ - Reglas para usar el contexto                             │\n",
    "│ - Formato de respuesta esperado                            │\n",
    "├─────────────────────────────────────────────────────────────┤\n",
    "│ CONTEXTO                                                    │\n",
    "│ - Documentos recuperados                                   │\n",
    "│ - Metadatos (fuente, página, fecha)                        │\n",
    "│ - Delimitadores claros entre documentos                    │\n",
    "├─────────────────────────────────────────────────────────────┤\n",
    "│ PREGUNTA                                                    │\n",
    "│ - La consulta del usuario                                  │\n",
    "├─────────────────────────────────────────────────────────────┤\n",
    "│ INSTRUCCIONES DE RESPUESTA                                  │\n",
    "│ - Cómo estructurar la respuesta                            │\n",
    "│ - Requisitos de citación                                   │\n",
    "│ - Qué hacer si no hay información                          │\n",
    "└─────────────────────────────────────────────────────────────┘\n",
    "```\n",
    "\n",
    "## 8.3 Plantillas de Prompt por Caso de Uso"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# Plantillas de Prompts para RAG\n",
    "# ============================================================\n",
    "\n",
    "PROMPTS_RAG = {\n",
    "    # Prompt para investigación científica\n",
    "    \"cientifico\": '''Eres un asistente de investigación científica especializado en {dominio}.\n",
    "\n",
    "INSTRUCCIONES:\n",
    "1. Responde ÚNICAMENTE basándote en el contexto proporcionado\n",
    "2. Si la información no está en el contexto, di claramente: \"No tengo información sobre esto en los documentos disponibles\"\n",
    "3. Cita siempre las fuentes usando el formato [Documento X, pág. Y]\n",
    "4. Usa terminología científica precisa\n",
    "5. Si hay información contradictoria, menciona ambas perspectivas\n",
    "6. Distingue entre hechos establecidos y hipótesis\n",
    "\n",
    "CONTEXTO:\n",
    "---\n",
    "{contexto}\n",
    "---\n",
    "\n",
    "PREGUNTA: {pregunta}\n",
    "\n",
    "RESPUESTA:''',\n",
    "\n",
    "    # Prompt para documentación técnica\n",
    "    \"tecnico\": '''Eres un asistente técnico que ayuda con documentación de sistemas y procedimientos.\n",
    "\n",
    "FORMATO DE RESPUESTA:\n",
    "1. Resumen breve (1-2 oraciones)\n",
    "2. Pasos detallados si es un procedimiento\n",
    "3. Advertencias o notas importantes\n",
    "4. Referencias a documentos específicos\n",
    "\n",
    "REGLAS:\n",
    "- Usa solo la información del contexto\n",
    "- Sé preciso con comandos, configuraciones y parámetros\n",
    "- Indica la versión del sistema si está disponible\n",
    "\n",
    "DOCUMENTACIÓN DISPONIBLE:\n",
    "---\n",
    "{contexto}\n",
    "---\n",
    "\n",
    "CONSULTA: {pregunta}\n",
    "\n",
    "RESPUESTA:''',\n",
    "\n",
    "    # Prompt con output estructurado (JSON)\n",
    "    \"estructurado\": '''Analiza el contexto y responde la pregunta en formato JSON.\n",
    "\n",
    "CONTEXTO:\n",
    "---\n",
    "{contexto}\n",
    "---\n",
    "\n",
    "PREGUNTA: {pregunta}\n",
    "\n",
    "Responde con el siguiente formato JSON:\n",
    "{{\n",
    "    \"respuesta\": \"Tu respuesta basada en el contexto\",\n",
    "    \"confianza\": \"alta|media|baja\",\n",
    "    \"fuentes_usadas\": [\"lista de fuentes citadas\"],\n",
    "    \"informacion_faltante\": \"qué información adicional sería útil (o null si la respuesta es completa)\"\n",
    "}}\n",
    "\n",
    "JSON:''',\n",
    "\n",
    "    # Prompt simple para casos básicos\n",
    "    \"simple\": '''Contexto:\n",
    "{contexto}\n",
    "\n",
    "Pregunta: {pregunta}\n",
    "\n",
    "Responde basándote solo en el contexto. Si no hay información suficiente, dilo claramente.\n",
    "\n",
    "Respuesta:'''\n",
    "}\n",
    "\n",
    "print(\"📝 PLANTILLAS DE PROMPTS DISPONIBLES:\")\n",
    "print(\"=\"*50)\n",
    "for nombre in PROMPTS_RAG.keys():\n",
    "    print(f\"   ✅ {nombre}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8.4 Técnicas Avanzadas de Prompting\n",
    "\n",
    "### Chain of Thought (CoT)\n",
    "\n",
    "Instruir al modelo a razonar paso a paso:\n",
    "\n",
    "```\n",
    "\"Antes de responder, analiza el contexto paso a paso:\n",
    "1. Identifica los conceptos clave en la pregunta\n",
    "2. Localiza información relevante en cada documento\n",
    "3. Sintetiza la información encontrada\n",
    "4. Formula una respuesta coherente\"\n",
    "```\n",
    "\n",
    "### Few-Shot Learning\n",
    "\n",
    "Proporcionar ejemplos de respuestas deseadas:\n",
    "\n",
    "```\n",
    "\"Ejemplos de respuestas correctas:\n",
    "\n",
    "Pregunta: ¿Qué es X?\n",
    "Respuesta: X es... [Documento 1, pág. 5]\n",
    "\n",
    "Pregunta: ¿Cómo funciona Y?\n",
    "Respuesta: Y funciona mediante... [Documento 2, pág. 12]\"\n",
    "```\n",
    "\n",
    "### Self-Consistency\n",
    "\n",
    "Generar múltiples respuestas y seleccionar la más consistente.\n",
    "\n",
    "### Guardrails\n",
    "\n",
    "Validar que la respuesta cumpla criterios específicos:\n",
    "- ¿Cita fuentes?\n",
    "- ¿Responde la pregunta?\n",
    "- ¿Es coherente con el contexto?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"9-opensource\"></a>\n",
    "# 9. Soluciones Open Source para RAG\n",
    "\n",
    "## 9.1 Frameworks de Desarrollo\n",
    "\n",
    "### LangChain\n",
    "\n",
    "El framework más popular para construir aplicaciones con LLMs.\n",
    "\n",
    "| Aspecto | Detalle |\n",
    "|---------|----------|\n",
    "| **Licencia** | MIT |\n",
    "| **Lenguajes** | Python, JavaScript |\n",
    "| **Integraciones** | 100+ (LLMs, vectorstores, tools) |\n",
    "| **Documentación** | Excelente |\n",
    "| **Comunidad** | Muy activa |\n",
    "| **Ideal para** | Prototipos rápidos, producción |\n",
    "\n",
    "### LlamaIndex\n",
    "\n",
    "Especializado en indexación y recuperación de datos.\n",
    "\n",
    "| Aspecto | Detalle |\n",
    "|---------|----------|\n",
    "| **Licencia** | MIT |\n",
    "| **Fortaleza** | Manejo de documentos complejos |\n",
    "| **Características** | Índices avanzados, soporte PDF/tablas |\n",
    "| **Ideal para** | Repositorios documentales, papers científicos |\n",
    "\n",
    "### Haystack\n",
    "\n",
    "Framework europeo con enfoque en pipelines modulares.\n",
    "\n",
    "| Aspecto | Detalle |\n",
    "|---------|----------|\n",
    "| **Licencia** | Apache 2.0 |\n",
    "| **Origen** | Alemania (deepset) |\n",
    "| **Fortaleza** | Pipelines, sensibilidad RGPD |\n",
    "| **Ideal para** | Enterprise, requisitos de auditoría |\n",
    "\n",
    "## 9.2 Aplicaciones Listas para Usar\n",
    "\n",
    "### Open WebUI\n",
    "\n",
    "Interfaz web similar a ChatGPT, self-hosted.\n",
    "\n",
    "```bash\n",
    "# Despliegue rápido con Docker\n",
    "docker run -d -p 3000:8080 -v open-webui:/app/backend/data \\\n",
    "  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \\\n",
    "  ghcr.io/open-webui/open-webui:main\n",
    "```\n",
    "\n",
    "**Características:**\n",
    "- RAG integrado (subida de documentos)\n",
    "- Gestión de usuarios y permisos\n",
    "- Múltiples modelos\n",
    "- Interfaz intuitiva\n",
    "\n",
    "### PrivateGPT\n",
    "\n",
    "100% offline, máxima privacidad.\n",
    "\n",
    "**Características:**\n",
    "- Sin conexión a internet requerida\n",
    "- API compatible con OpenAI\n",
    "- Ideal para datos confidenciales\n",
    "\n",
    "### AnythingLLM\n",
    "\n",
    "Enfoque empresarial con multi-tenancy.\n",
    "\n",
    "**Características:**\n",
    "- Espacios de trabajo separados\n",
    "- Múltiples proveedores de LLM\n",
    "- Gestión de permisos granular\n",
    "\n",
    "## 9.3 Modelos LLM Open Source\n",
    "\n",
    "| Modelo | Tamaños | Licencia | Fortalezas |\n",
    "|--------|---------|----------|------------|\n",
    "| **Llama 3.1** | 8B, 70B, 405B | Llama 3 | Propósito general, multilingüe |\n",
    "| **Mistral/Mixtral** | 7B, 8x7B, 8x22B | Apache 2.0 | Eficiencia, razonamiento |\n",
    "| **Qwen 2.5** | 0.5B-72B | Apache 2.0 | Multilingüe, código |\n",
    "| **Gemma 2** | 2B, 9B, 27B | Gemma | Ligero, edge computing |\n",
    "\n",
    "## 9.4 Servidores de Inferencia\n",
    "\n",
    "### Ollama\n",
    "\n",
    "La forma más simple de ejecutar LLMs localmente.\n",
    "\n",
    "```bash\n",
    "# Instalación\n",
    "curl -fsSL https://ollama.com/install.sh | sh\n",
    "\n",
    "# Descargar y ejecutar un modelo\n",
    "ollama pull llama3.1:8b\n",
    "ollama run llama3.1:8b\n",
    "```\n",
    "\n",
    "### vLLM\n",
    "\n",
    "Alto rendimiento para producción.\n",
    "\n",
    "```bash\n",
    "# Servidor de inferencia\n",
    "python -m vllm.entrypoints.openai.api_server \\\n",
    "  --model meta-llama/Llama-3.1-8B-Instruct\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<a id=\"10-produccion\"></a>\n",
    "# 10. Despliegue en Producción\n",
    "\n",
    "## 10.1 Checklist de Producción\n",
    "\n",
    "### Preparación de Datos\n",
    "\n",
    "- [ ] Chunking optimizado para el dominio\n",
    "- [ ] Metadatos enriquecidos (fuente, fecha, autor)\n",
    "- [ ] Limpieza de texto (OCR, formatos)\n",
    "- [ ] Pipeline de actualización definido\n",
    "\n",
    "### Retrieval\n",
    "\n",
    "- [ ] Hybrid search implementado si hay términos técnicos\n",
    "- [ ] Reranking para alta precisión\n",
    "- [ ] Filtros de metadatos configurados\n",
    "- [ ] Número óptimo de documentos (k) validado\n",
    "\n",
    "### Generación\n",
    "\n",
    "- [ ] Prompts probados y optimizados\n",
    "- [ ] Manejo de \"no sé\" implementado\n",
    "- [ ] Citaciones funcionando correctamente\n",
    "- [ ] Guardrails contra alucinaciones\n",
    "\n",
    "### Infraestructura\n",
    "\n",
    "- [ ] Autenticación (LDAP/SSO)\n",
    "- [ ] Cifrado (TLS en tránsito, en reposo)\n",
    "- [ ] Logs de auditoría\n",
    "- [ ] Backups de vectorstore\n",
    "- [ ] Monitorización de métricas\n",
    "\n",
    "### Evaluación\n",
    "\n",
    "- [ ] Conjunto de test creado\n",
    "- [ ] Métricas baseline establecidas\n",
    "- [ ] Sistema de feedback de usuarios\n",
    "- [ ] A/B testing configurado\n",
    "\n",
    "## 10.2 Arquitectura Recomendada\n",
    "\n",
    "### Stack Básico (Para Empezar)\n",
    "\n",
    "```\n",
    "┌─────────────────────────────────────────────────────────┐\n",
    "│                    STACK BÁSICO                         │\n",
    "├─────────────────────────────────────────────────────────┤\n",
    "│ LLM:        Llama 3.1 8B o Qwen 2.5 7B (via Ollama)    │\n",
    "│ Embeddings: paraphrase-multilingual-MiniLM o BGE-M3    │\n",
    "│ VectorDB:   ChromaDB o Qdrant                          │\n",
    "│ Framework:  LangChain o LlamaIndex                     │\n",
    "│ Interfaz:   Open WebUI o Gradio                        │\n",
    "│ Hardware:   1x GPU 16GB (RTX 4090, A4000)              │\n",
    "└─────────────────────────────────────────────────────────┘\n",
    "```\n",
    "\n",
    "### Stack Avanzado (Escala)\n",
    "\n",
    "```\n",
    "┌─────────────────────────────────────────────────────────┐\n",
    "│                   STACK AVANZADO                        │\n",
    "├─────────────────────────────────────────────────────────┤\n",
    "│ LLM:         Llama 3.1 70B o Mixtral 8x22B             │\n",
    "│ Servidor:    vLLM con balanceo de carga                │\n",
    "│ VectorDB:    Qdrant cluster o Milvus                   │\n",
    "│ Orquestación: Kubernetes + Helm                        │\n",
    "│ Caché:       Redis para respuestas frecuentes          │\n",
    "│ Monitorización: Prometheus + Grafana                    │\n",
    "│ Hardware:    Múltiples GPUs A100/H100                  │\n",
    "└─────────────────────────────────────────────────────────┘\n",
    "```\n",
    "\n",
    "## 10.3 Próximos Pasos\n",
    "\n",
    "1. **Prueba de Concepto**\n",
    "   - Instalar Ollama + Open WebUI\n",
    "   - Probar con un conjunto pequeño de documentos\n",
    "   - Validar con usuarios piloto\n",
    "\n",
    "2. **Iteración**\n",
    "   - Crear conjunto de evaluación\n",
    "   - Optimizar chunking y prompts\n",
    "   - Implementar técnicas avanzadas según necesidad\n",
    "\n",
    "3. **Producción**\n",
    "   - Implementar seguridad y autenticación\n",
    "   - Configurar monitorización\n",
    "   - Establecer procesos de actualización"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "# 🎯 Conclusión y Recursos\n",
    "\n",
    "## Resumen de lo Aprendido\n",
    "\n",
    "En esta guía hemos cubierto:\n",
    "\n",
    "1. **Fundamentos**: Qué es RAG, por qué es necesario, arquitectura\n",
    "2. **Embeddings**: Representación semántica, modelos, métricas\n",
    "3. **Chunking**: Estrategias, parámetros óptimos, metadatos\n",
    "4. **Vector Stores**: ChromaDB, FAISS, búsqueda y filtros\n",
    "5. **Pipeline RAG**: Construcción completa paso a paso\n",
    "6. **Técnicas Avanzadas**: Hybrid search, reranking, multi-query\n",
    "7. **Evaluación**: Métricas de retrieval y generación\n",
    "8. **Prompting**: Plantillas y técnicas efectivas\n",
    "9. **Soluciones Open Source**: Frameworks y aplicaciones\n",
    "10. **Producción**: Checklist y arquitecturas recomendadas\n",
    "\n",
    "## Recursos Adicionales\n",
    "\n",
    "### Documentación Oficial\n",
    "- LangChain: https://langchain.com\n",
    "- LlamaIndex: https://llamaindex.ai\n",
    "- ChromaDB: https://www.trychroma.com\n",
    "- Ollama: https://ollama.ai\n",
    "\n",
    "### Papers Fundamentales\n",
    "- \"Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks\" (Lewis et al., 2020)\n",
    "- \"A Survey on Retrieval-Augmented Text Generation\" (Gao et al., 2023)\n",
    "\n",
    "### Modelos en Hugging Face\n",
    "- https://huggingface.co/models\n",
    "\n",
    "---\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
