# Módulo RAG

Paquete independiente de Retrieval-Augmented Generation (RAG) para Python
Permite indexar documentos locales (`.txt`, `.md`, `.pdf`) y consultar los fragmentos más relevantes mediante similitud semántica, sin depender de un ningún servicio externo.

Los modelos de lenguaje solo conocen lo que había en su entrenamiento, RAG soluciona esto:

1. Convierte los documentos en vectores numéricos (embeddings) y los guarda en una base de datos vectorial local
2. Cuando el usuario hace una pregunta, la convierte también en vector y busca los fragmentos del índice que más se le parecen
3. Esos fragmentos se entregan al modelo como contexto, de modo que puede responder con información actualizada y específica del proyecto

## Arquitectura y flujo de datos

```
Documentos (.txt / .md / .pdf)
        │
        ▼
   [loader.py]          Lee archivos, detecta codificación, extrae texto de PDFs
        │
        ▼
   [chunker.py]         Divide el texto en trozos de ≤512 caracteres con solapamiento
        │
        ▼
   [embedder.py]        Convierte cada trozo en un vector de 384 dimensiones
        │                usando sentence-transformers (modelo multilingüe)
        ▼
   [store.py]           Persiste los vectores en ChromaDB (carpeta rag_db/)
```

El orquestador de indexación es `indexer.py`, ejecutando el flujo anterior en un solo paso

## Descripción de cada archivo

### `config.py`

Centraliza todas las constantes ajustables del módulo. Cambiar un valor aquí afecta a todo el sistema

| Variable               | Valor por defecto                       | Efecto                                                                                      |
| ---------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `EMBEDDING_MODEL`      | `paraphrase-multilingual-MiniLM-L12-v2` | Modelo de sentence-transformers. Soporta español.                                           |
| `CHROMA_PERSIST_DIR`   | `./rag_db`                              | Carpeta donde ChromaDB guarda los vectores en disco.                                        |
| `DEFAULT_COLLECTION`   | `"default"`                             | Nombre de la colección vectorial por defecto                                                |
| `CHUNK_SIZE`           | `512`                                   | Máximo de caracteres por fragmento.                                                         |
| `CHUNK_OVERLAP`        | `64`                                    | Caracteres que se repiten entre fragmentos contiguos para no perder contexto en los bordes. |
| `DEFAULT_TOP_K`        | `3`                                     | Número de fragmentos que devuelve cada consulta.                                            |
| `MIN_SCORE`            | `0.30`                                  | Similitud mínima (coseno). Resultados por debajo se descartan.                              |
| `SUPPORTED_EXTENSIONS` | `{".txt", ".md", ".pdf"}`               | Tipos de archivo que el loader acepta.                                                      |
| `MIN_CHUNK_LENGTH`     | `50`                                    | Fragmentos más cortos que esto se descartan.                                                |

---

### `loader.py`

Carga documentos desde el sistema de archivos y los devuelve como objetos `Document`.

#### Funciones principales:

- load(source): Ruta a un archivo o a un directorio. Si es directorio, recorre buscando archivos con extensiones soportadas.
- \_load_text(path): Lee `.txt` y `.md`. Intenta `utf-8` primero, luego `latin-1` como respaldo.
- \_load_pdf(path): Extrae texto página a página usando `PyPDF2`. Si la librería no está instalada, omite el archivo con una advertencia

### `chunker.py`

Divide textos largos en fragmentos manejables usando una estrategia recursiva por separadores.

#### Algoritmo (split):

1. Intenta separar por párrafos (`\n\n`), luego por líneas (`\n`), luego por oraciones (`. `), y como último recurso corta por caracteres.
2. Junta partes pequeñas hasta llenar `CHUNK_SIZE`. Si una parte ya supera el límite, la subdivide recursivamente.
3. Aplica solapamiento (`CHUNK_OVERLAP`): los últimos 64 caracteres del chunk anterior se pegan al inicio del siguiente, evitando perder contexto en los límites.
4. Descarta fragmentos menores a `MIN_CHUNK_LENGTH` (ruido, líneas vacías, etc.).

Fragmentos demasiado grandes pierden precisión en la búsqueda y demasiado pequeños pierden contexto

### `embedder.py`

Convierte texto en vectores numéricos usando un modelo de sentence-transformers.

- Singleton: el modelo se carga una sola vez en memoria (`_model`) y se reutiliza en todas las llamadas. La primera vez tarda unos segundos; el resto es instantáneo.
- embed(texts): Vectoriza una lista de textos. Devuelve vectores normalizados, lo que permite usar distancia coseno directamente.
- embed_one(text): Atajo para vectorizar un único texto (usado por el retriever en cada consulta).
- Modelo elegido: paraphrase-multilingual-MiniLM-L12-v2 es compacto (118 MB), rápido en CPU y entiende español, inglés y otros idiomas simultáneamente.

### `store.py`

Abstracción sobre ChromaDB: la base de datos vectorial que persiste los embeddings en disco.

#### Operaciones:

- get_collection(name): Obtiene o crea una colección ChromaDB con espacio métrico coseno.
- upsert(...): Inserta o actualiza chunks. Comprueba cuáles ya existen para reportar cuántos son nuevos vs. omitidos. Los IDs son determinísticos, así que reindexar el mismo archivo no crea duplicados.
- similarity_search(collection, query_embedding, top_k): Busca los `top_k` vectores más cercanos al query. Convierte la distancia coseno en score (0–1) y filtra por `MIN_SCORE`.
- reset(collection): Borra completamente una colección (usado al reindexar con `force=True`).

ChromaDB guarda los datos en la carpeta `rag_db/` (configurable). Los datos persisten ante reinicios del proceso.

### `indexer.py`

Orquesta el pipeline completo de indexación: `loader → chunker → embedder → store`.

- index_documents(source, collection, force): Recorre los documentos cargados por el loader, los trocea, vectoriza y almacena. Si `force=True`, borra la colección antes de empezar.
- Genera IDs determinísticos por chunk (\_chunk_id) usando SHA-256 sobre fuente + índice + primeros 50 chars. Dos ejecuciones sobre el mismo archivo producen los mismos IDs, evitando duplicados.
- Devuelve un `IndexResult` con contadores de documentos procesados, chunks añadidos, chunks omitidos y errores.

### `retriever.py`

Responde consultas en lenguaje natural buscando los chunks más relevantes en el índice.

- query(text, collection, top_k): Vectoriza la pregunta con embedder.embed_one, luego llama a store.similarity_search.
- Devuelve un QueryResult con una lista de objetos Chunk, cada uno con su texto, score de similitud, fuente y metadatos.
- Si la colección está vacía o el score de todos los resultados es menor que `MIN_SCORE`, devuelve lista vacía en lugar de respuestas irrelevantes.

### `__init__.py`

Define la API pública del módulo: las tres funciones que los consumidores externos (el chatbot, scripts, etc.) deben usar.

```python
from rag import index_documents, query, reset
```

### `requirements.txt`

Dependencias del módulo:

| Paquete                 | Para qué se usa                                |
| ----------------------- | ---------------------------------------------- |
| `sentence-transformers` | Modelo de embeddings multilingüe               |
| `chromadb`              | Base de datos vectorial persistente            |
| `PyPDF2`                | Extracción de texto de archivos PDF (opcional) |

## Instalación

```bash
# Desde la raíz del proyecto
pip install -r rag/requirements.txt
```

## Uso rápido

### Indexar documentos

```python
import rag

# Indexar un directorio completo (.txt, .md, .pdf)
result = rag.index_documents("docs/")
print(f"Docs: {result.docs_processed}, chunks: {result.chunks_added}")

# Reindexar desde cero (borra el índice anterior)
rag.index_documents("docs/", force=True)
```

### Consultar

```python
import rag

result = rag.query("¿Qué temas cubre el manual de usuario?")
for chunk in result.chunks:
    print(f"{chunk.score} - {chunk.source}: {chunk.text[:120]}…")
```

### Borrar colección

```python
import rag

rag.reset() # borra la colección "default"
rag.reset("mi-col")  # borra una colección específica
```

## Notas importantes

- Indexar el mismo directorio dos veces no duplica chunks gracias a los IDs determinísticos.
- Cambiar `EMBEDDING_MODEL` en `config.py` requiere reindexar con `force=True`, porque los vectores del modelo nuevo son incompatibles con los del anterior.
- Ejecuta el chatbot y los scripts desde la raíz del proyecto para que `./rag_db` apunte siempre al mismo directorio.
