# Rony Chat Bot — Portfolio Bot HTTP > 🌐 **Idioma:** [English](README.md) | [Español](README.es.md) > 🤖 **Chatbot HTTP que presenta tu portfolio y responde preguntas sobre tus proyectos.** **Rony Chat Bot** es un chatbot basado en [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) que se integra con un sitio Astro/React para responder preguntas sobre Victor Hugo Vargas y sus proyectos, usando **RAG sobre archivos markdown**. ## ✨ Features - 🌐 **HTTP server** con streaming SSE (Server-Sent Events) - 🧠 **RAG híbrido sobre markdown/MDX** — búsqueda por palabras con SQLite FTS5 fusionada con embeddings multilingües (Reciprocal Rank Fusion) - 🎭 **Persona customizable** — responde como "asistente de Victor" - ⚡ **Self-hosted** con llama.cpp (default) u Ollama (no requiere API key de cloud) - 💬 **Widget de chat drop-in** — vanilla JS, sin build step, funciona en cualquier sitio - 🛡️ **Rate limiting** y logging estructurado - 📦 **Portable** — se puede adaptar a otros contextos (clientes, productos, etc.) ## 🚀 Quick start ```bash # 1. Instalar git clone https://github.com/VictorVargas/rony-chat-bot.git cd rony-chat-bot # 2. Resolver dependencias (crea go.sum con hashes) go mod tidy # 3. Descargá los modelos: un LLM instruct y un embebedor multilingüe # https://huggingface.co/Qwen/Qwen2.5-3B-Instruct-GGUF (~2 GB) # https://huggingface.co/nomic-ai/nomic-embed-text-v2-moe-GGUF (~370 MB) export RONY_MODELS_PATH=/path/to/models # 4. Cargar tus proyectos en data/projects/ echo "# Mi Proyecto Cool\nDescripción..." > data/projects/mi-proyecto.md # 5. Build go build -o bin/chat-bot ./cmd/chat-bot # 6. Arrancar el LLM (CPU, target 2 cores — ajustá --threads a tu host) llama-server \ -m $RONY_MODELS_PATH/Qwen2.5/qwen2.5-3b-instruct-q4_k_m.gguf \ --port 9100 --ctx-size 4096 --parallel 1 \ --device none --threads 2 --mlock \ --temp 0.7 --top-k 20 --top-p 0.8 --repeat-penalty 1.05 # 7. Arrancar el embebedor (segunda terminal) llama-server \ -m $RONY_MODELS_PATH/embeddings/nomic-embed-v2-moe.Q5_K_M.gguf \ --port 9200 --embedding --pooling mean \ --ctx-size 2048 --parallel 1 --device none --threads 2 # 8. Construir el índice (necesita el embebedor arriba) y servir ./bin/chat-bot reindex ./bin/chat-bot serve # → Sirve en http://localhost:7331 ``` **Hardware mínimo:** 2 CPU cores, 8 GB RAM, sin GPU. Memoria residente medida en CPU con este setup: **3,64 GB** el LLM, **0,91 GB** el embebedor, **0,02 GB** el bot — unos **4,6 GB**, dejando ~3,4 GB para el resto del host. La generación va a 21 tok/s con 2 threads una vez que el system prompt está caliente en la caché de prompts de llama-server. Tres flags son fáciles de errar y cada una te cuesta calidad real: - **`--device none`** — llama.cpp levanta el backend de GPU compilado aunque pases `-ngl 0`, y en un host sin GPU esos buffers salen de la RAM del sistema. Medido en qwen2.5-3b: 2,54 GB con una GPU absorbiéndolos, 3,66 GB sin ella. Presupuestá con el segundo número, y pasá la flag para que la medición coincida con lo que hace producción. - **`--parallel 1`** — `--ctx-size` se reparte entre slots y llama-server abre 4 por defecto, así que `--ctx-size 4096` sin esto le deja a cada request solo 1024 tokens. El rate limiter del bot ya limita la concurrencia. - **`--temp` / `--top-k` / `--top-p`** — usá los valores que publican los autores del modelo, no los defaults de llama.cpp. Los de arriba son los de Qwen para chat instruct. Errar esto no es sutil: gemma-3-1b con `temperature 0.7` y el resto sin setear devolvía respuestas de 16 tokens. **`--pooling mean` es obligatoria en el embebedor.** Sin ella el endpoint no devuelve un vector por entrada y el cliente rechaza la respuesta. Mantené `context_size` en `configs/portfolio-bot.yaml` igual a `--ctx-size`; el bot calcula sus presupuestos de RAG y compactación a partir de ese número y no le pregunta al servidor qué tiene en realidad. Si los desacoplás, el bot armará prompts que el servidor rechaza. **Por qué 4096 alcanza.** Medido sobre 20 peticiones reales, el prompt más grande que este bot llegó a construir fue de **1255 tokens** — system prompt, catálogo de proyectos, cinco chunks recuperados y la pregunta. La compactación recién arranca al 75% de la ventana (~3070 tokens), así que hay 2,4x de margen antes de que empiece. Bajar de 8192 a 4096 ahorró **212 MB** de memoria residente sin un solo truncamiento y con el mismo rendimiento (21,0 tok/s en ambos casos): el contexto extra estaba reservado y nunca se usaba. ## 📚 Proyectos vs. documentos de referencia El índice tiene dos tipos de fuente, y ambas aceptan `.md` y `.mdx`: ```yaml rag: data_path: ./data/projects # proyectos → aparecen en el catálogo docs_path: ./data/docs # referencia → se busca, nunca se lista ``` Todo lo que está en `data_path` es un proyecto tuyo y se anuncia en el catálogo que el bot inyecta en cada prompt. Todo lo que está en `docs_path` es evidencia buscable que *no* es un proyecto: tu CV, una página "sobre mí", un FAQ. Tu CV va en `docs_path`. Es el documento que responde lo que de verdad pregunta quien está evaluando contratarte ("¿sabe Kubernetes?", "¿dónde ha trabajado?"), y nada de eso es recuperable mientras viva solo en tu sitio. Enlazalo para mantener una sola copia: ```bash mkdir -p data/docs ln -s ../../../portfolio/src/content/cv/cv.mdx data/docs/cv.mdx ./bin/chat-bot reindex ``` Sin esta distinción el CV tendría que ir en `data_path` para ser buscable, y entonces el bot lista alegremente "cv" como uno de tus proyectos. Actualizar una instalación existente no requiere migración: la tabla de chunks es dato derivado, así que el store la reconstruye al abrir y el siguiente `reindex` la repuebla. ## 🔍 Recuperación: keyword + embeddings La recuperación es híbrida, y las dos mitades hacen falta. **Keyword (SQLite FTS5)** hace coincidencia exacta de palabras: sin stemming, sin traducción. Preciso para nombres propios raros, inútil entre idiomas. El corpus está en inglés y los visitantes preguntan en español, así que las palabras con carga semántica puntúan cero: medido sobre el corpus real, `"paga"` aparece 0 veces en un documento que dice *"Payments: Stripe"*, y `"trabajado"` 0 veces en uno que dice *"worked"*. La pregunta *"¿Con qué se paga en la tienda de ropa?"* no recuperaba **nada**. **Embeddings** (`nomic-embed-v2-moe`, multilingüe) cierran ese hueco: esa misma pregunta pone los tres chunks de `tienda-ropa` arriba. Son más difusos que BM25 ante un término raro exacto, y por eso se conservan ambos y se fusionan con Reciprocal Rank Fusion — RRF ordena por acuerdo entre los dos rankings, evitando comparar una puntuación BM25 con un coseno, magnitudes sin escala común. Se activa en `configs/portfolio-bot.yaml` bajo `embeddings:` y hay que re-ejecutar `reindex` — los vectores se construyen al indexar. Si el endpoint se cae o se desactiva, la recuperación degrada a keyword-only en vez de fallar. Dos detalles que cuestan precisión y son fáciles de pasar por alto: - **El frontmatter se excluye de la recuperación.** Son metadatos densos (title, tags, repo, location) en un chunk muy corto, lo que lo convierte en imán de consultas breves. El campo `location:` de un CV hacía que *"¿Dónde ha trabajado Victor?"* recuperara el frontmatter en vez del historial laboral, porque "dónde" casa con una ubicación. - **Las secciones largas se cortan en encabezados `###`, no por bytes.** La sección Experience de un CV es una lista de empleos; el corte por tamaño partía una entrada a mitad de palabra, dejando un chunk que empezaba *"... id app for an on-demand ride-sharing service"* y el nombre del empleador huérfano en el trozo anterior. Ahora cada chunk es un empleo, nombrado `Experience — Metrimex — Frontend Developer`. ## 📁 Estructura ``` rony-chat-bot/ ├── cmd/chat-bot/ # Entry point (CLI) ├── internal/ │ ├── server/ # HTTP handlers + SSE │ ├── agent/ # LLM client + RAG + persona runner │ ├── portfolio/ # Data loader (markdown → RAG) │ ├── persona/ # Persona override │ ├── streaming/ # SSE helpers │ └── i18n/ # Detección de idioma (EN/ES) ├── web/ # ← WIDGET DE CHAT DROP-IN │ ├── chat-widget.js │ ├── chat-widget.css │ └── example.html ├── data/projects/ # ← TUS PROYECTOS EN MARKDOWN │ ├── rony-tui.md │ ├── rony-llm-agent.md │ └── ... ├── configs/ │ └── portfolio-bot.yaml # Provider + RAG + persona config ├── docs/ │ └── architecture.md # ← Especificación técnica completa └── go.mod # require rony-llm-agent ``` ## 🎯 Embebido en cualquier sitio El bot viene con un widget de chat drop-in. Agrega dos archivos y un tag ` ``` Ver [`web/README.md`](./web/README.md) para la referencia completa de configuración y snippets de integración con Astro/Next.js. Arquitectura completa en [`docs/architecture.md`](./docs/architecture.md) §5. ## 🔄 Adaptar a otro cliente Este bot está diseñado para ser **atómico** y reusable. Para adaptarlo (ej. chatbot para un concesionario): 1. Fork/clone este repo 2. Reemplaza `data/projects/` con `data/inventory/` (u otro dominio) 3. Actualiza `configs/portfolio-bot.yaml` con la nueva persona 4. Deploy La librería `rony-llm-agent` no cambia. ## 📚 Documentación - [**Architecture doc**](./docs/architecture.md) — Especificación técnica completa - [Library: `rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — Core reutilizable - [Harness](https://github.com/VictorVargas/rony-harness) — El otro proyecto que usa la misma librería ## 📄 Licencia MIT — ver [`LICENSE`](./LICENSE). ## 🔗 Proyectos del workspace - [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — Librería core - [`harness`](https://github.com/VictorVargas/rony-harness) — AI agent harness (TUI) - [`portfolio`](https://github.com/VictorVargas/portfolio) — Astro + React site (integra este bot)