diff --git a/README.es.md b/README.es.md new file mode 100644 index 0000000..c862c0f --- /dev/null +++ b/README.es.md @@ -0,0 +1,109 @@ +# 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 sobre markdown** — indexa automáticamente los `.md` en `data/projects/` +- 🎭 **Persona customizable** — responde como "asistente de Victor" +- ⚡ **Self-hosted** con Ollama o llama.cpp (no requiere API key de cloud) +- 🔌 **Integrable** con Astro/React via proxy HTTP +- 🛡️ **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 chat-bot + +# 2. Resolver dependencias (crea go.sum con hashes) +go mod tidy + +# 3. Configurar provider (ejemplo: Ollama) +# Asegúrate de tener Ollama corriendo: ollama serve +# Modelo descargado: ollama pull qwen2.5:1.5b + +# 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. Run +./bin/chat-bot serve +# → Sirve en http://localhost:7331 +``` + +## 📁 Estructura + +``` +chat-bot/ +├── cm./rony-chat-bot/ # Entry point (CLI) +├── internal/ +│ ├── server/ # HTTP handlers + SSE +│ ├── portfolio/ # Data loader (markdown → RAG) +│ ├── persona/ # Persona override +│ └── streaming/ # SSE helpers +├── data/projects/ # ← TUS PROYECTOS EN MARKDOWN +│ ├── rony-tui.md +│ ├── rony-llm-agent.md +│ └── ... +├── configs/ +│ └── portfolio-bot.yaml # Provider config +├── docs/ +│ └── architecture.md # ← Especificación técnica completa +└── go.mod # require rony-llm-agent +``` + +## 🎯 Uso desde Astro + +Ver [`docs/architecture.md`](./docs/architecture.md) §5 — patrón recomendado de proxy. + +```typescript +// portfolio/src/pages/api/chat.ts +export const POST: APIRoute = async ({ request }) => { + const body = await request.json(); + const resp = await fetch('http://localhost:7331/api/chat', { + method: 'POST', + body: JSON.stringify(body), + }); + return new Response(resp.body, { + headers: { 'Content-Type': 'text/event-stream' }, + }); +}; +``` + +## 🔄 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) \ No newline at end of file diff --git a/README.md b/README.md index 0e3104b..9ef83e7 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,70 @@ -# Rony Chat Bot — Portfolio Bot HTTP +# Rony Chat Bot — HTTP Portfolio Bot -> 🤖 **Chatbot HTTP que presenta tu portfolio y responde preguntas sobre tus proyectos.** +> 🌐 **Language:** [English](./README.md) | [Español](./README.es.md) +> +> 🤖 **HTTP chatbot that presents your portfolio and answers questions about your projects.** -**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**. +**Rony Chat Bot** is a chatbot based on [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) that integrates with an Astro/React site to answer questions about Victor Hugo Vargas and his projects, using **RAG over markdown files**. ## ✨ Features -- 🌐 **HTTP server** con streaming SSE (Server-Sent Events) -- 🧠 **RAG sobre markdown** — indexa automáticamente los `.md` en `data/projects/` -- 🎭 **Persona customizable** — responde como "asistente de Victor" -- ⚡ **Self-hosted** con Ollama o llama.cpp (no requiere API key de cloud) -- 🔌 **Integrable** con Astro/React via proxy HTTP -- 🛡️ **Rate limiting** y logging estructurado -- 📦 **Portable** — se puede adaptar a otros contextos (clientes, productos, etc.) +- 🌐 **HTTP server** with SSE (Server-Sent Events) streaming +- 🧠 **RAG over markdown** — automatically indexes `.md` in `data/projects/` +- 🎭 **Customizable persona** — responds as "Victor's assistant" +- ⚡ **Self-hosted** with Ollama or llama.cpp (no cloud API key required) +- 🔌 **Integrable** with Astro/React via HTTP proxy +- 🛡️ **Rate limiting** and structured logging +- 📦 **Portable** — adaptable to other contexts (clients, products, etc.) ## 🚀 Quick start ```bash -# 1. Instalar +# 1. Install git clone https://github.com/VictorVargas/rony-chat-bot.git -cd chat-bot +cd rony-chat-bot -# 2. Resolver dependencias (crea go.sum con hashes) +# 2. Resolve dependencies (creates go.sum with hashes) go mod tidy -# 3. Configurar provider (ejemplo: Ollama) -# Asegúrate de tener Ollama corriendo: ollama serve -# Modelo descargado: ollama pull qwen2.5:1.5b +# 3. Configure provider (e.g., Ollama) +# Make sure Ollama is running: ollama serve +# Downloaded model: ollama pull qwen2.5:1.5b -# 4. Cargar tus proyectos en data/projects/ -echo "# Mi Proyecto Cool\nDescripción..." > data/projects/mi-proyecto.md +# 4. Load your projects in data/projects/ +echo "# My Cool Project\nDescription..." > data/projects/my-project.md # 5. Build go build -o bin/chat-bot ./cmd/chat-bot # 6. Run ./bin/chat-bot serve -# → Sirve en http://localhost:7331 +# → Serves on http://localhost:7331 ``` -## 📁 Estructura +## 📁 Structure ``` chat-bot/ -├── cm./rony-chat-bot/ # Entry point (CLI) +├── cmd/chat-bot/ # Entry point (CLI) ├── internal/ │ ├── server/ # HTTP handlers + SSE │ ├── portfolio/ # Data loader (markdown → RAG) │ ├── persona/ # Persona override │ └── streaming/ # SSE helpers -├── data/projects/ # ← TUS PROYECTOS EN MARKDOWN -│ ├── rony-tui.md +├── data/projects/ # ← YOUR PROJECTS IN MARKDOWN +│ ├── rony-harness.md │ ├── rony-llm-agent.md │ └── ... ├── configs/ │ └── portfolio-bot.yaml # Provider config ├── docs/ -│ └── architecture.md # ← Especificación técnica completa -└── go.mod # require rony-llm-agent +│ └── architecture.md # ← Complete technical specification +└── go.mod # require rony-llm-agent ``` -## 🎯 Uso desde Astro +## 🎯 Use from Astro -Ver [`docs/architecture.md`](./docs/architecture.md) §5 — patrón recomendado de proxy. +See [`docs/architecture.md`](./docs/architecture.md) §5 — recommended proxy pattern. ```typescript // portfolio/src/pages/api/chat.ts @@ -70,37 +72,43 @@ export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const resp = await fetch('http://localhost:7331/api/chat', { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return new Response(resp.body, { - headers: { 'Content-Type': 'text/event-stream' }, + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, }); }; ``` -## 🔄 Adaptar a otro cliente +## 🔄 Adapt to another client -Este bot está diseñado para ser **atómico** y reusable. Para adaptarlo (ej. chatbot para un concesionario): +This bot is designed to be **atomic** and reusable. To adapt it (e.g., chatbot for a car dealership): -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 +1. Fork/clone this repo +2. Replace `data/projects/` with `data/inventory/` (or another domain) +3. Update `configs/portfolio-bot.yaml` with the new persona 4. Deploy -La librería `rony-llm-agent` no cambia. +The `rony-llm-agent` library doesn't change. -## 📚 Documentación +## 📚 Documentation -- [**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 +- [**Architecture doc**](./docs/architecture.md) — Complete technical specification +- [Library: `rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — Reusable core +- [Rony Harness](https://github.com/VictorVargas/rony-harness) — The other project using the same library -## 📄 Licencia +## 📄 License -MIT — ver [`LICENSE`](./LICENSE). +MIT — see [`LICENSE`](./LICENSE). -## 🔗 Proyectos del workspace +## 🔗 Workspace projects -- [`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) \ No newline at end of file +- [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — Core library +- [`rony-harness`](https://github.com/VictorVargas/rony-harness) — AI agent harness (TUI) +- [`portfolio`](https://github.com/VictorVargas/portfolio) — Astro + React site (integrates this bot) \ No newline at end of file diff --git a/configs/portfolio-bot.es.yaml b/configs/portfolio-bot.es.yaml new file mode 100644 index 0000000..4528669 --- /dev/null +++ b/configs/portfolio-bot.es.yaml @@ -0,0 +1,82 @@ +# Configuración del Portfolio Bot +# Documentación: https://github.com/VictorVargas/rony-llm-agent/pkg/llm + +server: + host: "0.0.0.0" + port: 7331 + read_timeout_ms: 30000 + cors_origins: + - "http://localhost:4321" # Astro dev server + - "https://victorvargas.dev" # Producción (cuando exista) + rate_limit: + requests_per_minute: 30 # Por IP + burst: 5 + +# Providers LLM (al menos uno configurado) +providers: + # === Ollama (recomendado para desarrollo) === + - name: ollama-local + type: ollama + model: qwen2.5:1.5b # Modelo pequeño para Q&A + endpoint: http://localhost:11434 + default: true + + # === llama.cpp directo (GGUF) === + - name: llamacpp-local + type: llamacpp + model_path: ${RONY_MODELS_PATH}/qwen2.5-1.5b-instruct-q5_k_m.gguf + context_size: 4096 + n_gpu_layers: 999 + + # === Anthropic (si quieres calidad > privacidad) === + - name: anthropic-api + type: anthropic + model: claude-haiku-4 # Modelo barato + api_key_env: ANTHROPIC_API_KEY + +# RAG: cómo se indexan los proyectos +rag: + enabled: true + data_path: ./data/projects # Directorio con .md + chunk_size: 500 # caracteres por chunk + chunk_overlap: 50 + embedding_provider: ollama # o llamacpp + embedding_model: nomic-embed-text + vector_db_path: ./chroma # Persistencia local + top_k: 5 # Documentos a recuperar por query + rerank: false # Phase 2 + +# Persona: quién es el bot +persona: + name: "Rony Chat Bot" + tone: "Profesional, conocedor, amable" + language: "Español" + constraints: + - "Solo responder sobre Victor y sus proyectos" + - "Si no sabes, decir 'No tengo esa información'" + - "Ser conciso pero informativo" + - "Usar formato markdown para listas y código" + intro: "¡Hola! Soy Rony, el asistente virtual de Victor Hugo Vargas. Pregúntame sobre sus proyectos, skills o experiencia." + +# System prompt base (concatenado con el contenido RAG) +system_prompt: | + Eres Rony Chat Bot, el asistente virtual de Victor Hugo Vargas, un ingeniero de software mexicano. + + Tu trabajo es responder preguntas sobre: + - Los proyectos de Victor (ver archivos en data/projects/) + - Su experiencia y skills técnicas + - Su enfoque de trabajo + + Responde en español, con tono profesional pero accesible. + Si te preguntan algo que no está en tu contexto, dilo honestamente. + + Formato recomendado: + - Usa markdown para listas, código, y énfasis + - Sé conciso (máximo 2-3 párrafos por respuesta) + - Incluye links a repos cuando sea relevante + +# Logging +logging: + level: info # debug | info | warn | error + format: json # json | text + output: stderr \ No newline at end of file diff --git a/configs/portfolio-bot.yaml b/configs/portfolio-bot.yaml index 4528669..c312b42 100644 --- a/configs/portfolio-bot.yaml +++ b/configs/portfolio-bot.yaml @@ -1,5 +1,5 @@ -# Configuración del Portfolio Bot -# Documentación: https://github.com/VictorVargas/rony-llm-agent/pkg/llm +# Portfolio Bot Configuration +# Documentation: https://github.com/VictorVargas/rony-llm-agent/pkg/llm server: host: "0.0.0.0" @@ -7,73 +7,73 @@ server: read_timeout_ms: 30000 cors_origins: - "http://localhost:4321" # Astro dev server - - "https://victorvargas.dev" # Producción (cuando exista) + - "https://victorvargas.dev" # Production (when it exists) rate_limit: - requests_per_minute: 30 # Por IP + requests_per_minute: 30 # Per IP burst: 5 -# Providers LLM (al menos uno configurado) +# LLM providers (at least one configured) providers: - # === Ollama (recomendado para desarrollo) === + # === Ollama (recommended for development) === - name: ollama-local type: ollama - model: qwen2.5:1.5b # Modelo pequeño para Q&A + model: qwen2.5:1.5b # Small model for Q&A endpoint: http://localhost:11434 default: true - # === llama.cpp directo (GGUF) === + # === llama.cpp direct (GGUF) === - name: llamacpp-local type: llamacpp model_path: ${RONY_MODELS_PATH}/qwen2.5-1.5b-instruct-q5_k_m.gguf context_size: 4096 n_gpu_layers: 999 - # === Anthropic (si quieres calidad > privacidad) === + # === Anthropic (if you want quality > privacy) === - name: anthropic-api type: anthropic - model: claude-haiku-4 # Modelo barato + model: claude-haiku-4 # Cheap model api_key_env: ANTHROPIC_API_KEY -# RAG: cómo se indexan los proyectos +# RAG: how projects are indexed rag: enabled: true - data_path: ./data/projects # Directorio con .md - chunk_size: 500 # caracteres por chunk + data_path: ./data/projects # Directory with .md + chunk_size: 500 # characters per chunk chunk_overlap: 50 - embedding_provider: ollama # o llamacpp + embedding_provider: ollama # or llamacpp embedding_model: nomic-embed-text - vector_db_path: ./chroma # Persistencia local - top_k: 5 # Documentos a recuperar por query + vector_db_path: ./chroma # Local persistence + top_k: 5 # Documents to retrieve per query rerank: false # Phase 2 -# Persona: quién es el bot +# Persona: who the bot is persona: name: "Rony Chat Bot" - tone: "Profesional, conocedor, amable" - language: "Español" + tone: "Professional, knowledgeable, friendly" + language: "English" constraints: - - "Solo responder sobre Victor y sus proyectos" - - "Si no sabes, decir 'No tengo esa información'" - - "Ser conciso pero informativo" - - "Usar formato markdown para listas y código" - intro: "¡Hola! Soy Rony, el asistente virtual de Victor Hugo Vargas. Pregúntame sobre sus proyectos, skills o experiencia." + - "Only answer about Victor and his projects" + - "If you don't know, say 'I don't have that information'" + - "Be concise but informative" + - "Use markdown format for lists and code" + intro: "Hi! I'm Rony, Victor Hugo Vargas's virtual assistant. Ask me about his projects, skills or experience." -# System prompt base (concatenado con el contenido RAG) +# Base system prompt (concatenated with RAG content) system_prompt: | - Eres Rony Chat Bot, el asistente virtual de Victor Hugo Vargas, un ingeniero de software mexicano. + You are Rony Chat Bot, the virtual assistant of Victor Hugo Vargas, a Mexican software engineer. - Tu trabajo es responder preguntas sobre: - - Los proyectos de Victor (ver archivos en data/projects/) - - Su experiencia y skills técnicas - - Su enfoque de trabajo + Your job is to answer questions about: + - Victor's projects (see files in data/projects/) + - His experience and technical skills + - His work approach - Responde en español, con tono profesional pero accesible. - Si te preguntan algo que no está en tu contexto, dilo honestamente. + Respond in English, with professional but accessible tone. + If you're asked something not in your context, say it honestly. - Formato recomendado: - - Usa markdown para listas, código, y énfasis - - Sé conciso (máximo 2-3 párrafos por respuesta) - - Incluye links a repos cuando sea relevante + Recommended format: + - Use markdown for lists, code, and emphasis + - Be concise (max 2-3 paragraphs per response) + - Include links to repos when relevant # Logging logging: diff --git a/data/projects/README.es.md b/data/projects/README.es.md new file mode 100644 index 0000000..6e053b7 --- /dev/null +++ b/data/projects/README.es.md @@ -0,0 +1,52 @@ +# Proyectos del Portfolio + +> 🌐 **Idioma:** [English](README.md) | [Español](README.es.md) + + +Coloca aquí un archivo `.md` por cada proyecto que quieras que el bot pueda responder. + +## Convención de nombres + +- Un archivo por proyecto: `nombre-del-proyecto.md` +- Nombre en kebab-case (minúsculas con guiones) +- Ejemplo: `rony-tui.md`, `rony-llm-agent.md`, `portfolio-astro.md` + +## Frontmatter (opcional pero recomendado) + +```markdown +--- +title: "Rony TUI" +date: 2026-06 +status: "active" # active | archived | wip +tags: ["go", "ai", "cli"] +repo: "https://github.com/VictorVargas/rony-harness" +demo: "https://..." # opcional +--- + +# Rony TUI + +AI agent harness para desarrollo de software... +``` + +## Cómo se procesan + +1. El bot escanea este directorio al arrancar +2. Cada `.md` se divide en chunks de ~500 caracteres +3. Cada chunk se convierte a embedding con Ollama +4. Los embeddings se guardan en ChromaDB +5. Cuando alguien pregunta, se buscan los top-5 chunks más relevantes +6. Esos chunks se inyectan al contexto del LLM + +## Re-indexar + +Si modificas los `.md`, ejecuta: + +```bash +./bin/chat-bot reindex +``` + +Esto reconstruye ChromaDB desde cero. + +## Ejemplo de proyecto + +Ver [`example-project.md`](./example-project.md) para una plantilla. \ No newline at end of file diff --git a/data/projects/README.md b/data/projects/README.md index 3f4e34a..b183c6b 100644 --- a/data/projects/README.md +++ b/data/projects/README.md @@ -1,49 +1,49 @@ -# Proyectos del Portfolio +# Portfolio Projects -Coloca aquí un archivo `.md` por cada proyecto que quieras que el bot pueda responder. +Place a `.md` file here for each project you want the bot to be able to answer about. -## Convención de nombres +## File naming convention -- Un archivo por proyecto: `nombre-del-proyecto.md` -- Nombre en kebab-case (minúsculas con guiones) -- Ejemplo: `rony-tui.md`, `rony-llm-agent.md`, `portfolio-astro.md` +- One file per project: `project-name.md` +- Name in kebab-case (lowercase with hyphens) +- Example: `rony-harness.md`, `rony-llm-agent.md`, `portfolio-astro.md` -## Frontmatter (opcional pero recomendado) +## Frontmatter (optional but recommended) ```markdown --- -title: "Rony TUI" +title: "Rony Harness" date: 2026-06 status: "active" # active | archived | wip tags: ["go", "ai", "cli"] repo: "https://github.com/VictorVargas/rony-harness" -demo: "https://..." # opcional +demo: "https://..." # optional --- -# Rony TUI +# Rony Harness -AI agent harness para desarrollo de software... +AI agent harness for software development... ``` -## Cómo se procesan +## How they're processed -1. El bot escanea este directorio al arrancar -2. Cada `.md` se divide en chunks de ~500 caracteres -3. Cada chunk se convierte a embedding con Ollama -4. Los embeddings se guardan en ChromaDB -5. Cuando alguien pregunta, se buscan los top-5 chunks más relevantes -6. Esos chunks se inyectan al contexto del LLM +1. The bot scans this directory on startup +2. Each `.md` is split into chunks of ~500 characters +3. Each chunk is converted to embedding with Ollama +4. Embeddings are stored in ChromaDB +5. When someone asks a question, the top-5 most relevant chunks are searched +6. Those chunks are injected into the LLM context -## Re-indexar +## Re-index -Si modificas los `.md`, ejecuta: +If you modify the `.md` files, run: ```bash ./bin/chat-bot reindex ``` -Esto reconstruye ChromaDB desde cero. +This rebuilds ChromaDB from scratch. -## Ejemplo de proyecto +## Project example -Ver [`example-project.md`](./example-project.md) para una plantilla. \ No newline at end of file +See [`example-project.md`](./example-project.md) for a template. \ No newline at end of file diff --git a/data/projects/example-project.es.md b/data/projects/example-project.es.md new file mode 100644 index 0000000..1bb958b --- /dev/null +++ b/data/projects/example-project.es.md @@ -0,0 +1,45 @@ +--- +title: "Proyecto de Ejemplo" +date: 2026-06 +status: "active" +tags: ["ejemplo", "plantilla"] +--- + +> 🌐 **Idioma:** [English](example-project.md) | [Español](example-project.es.md) +repo: "" +demo: "" +--- + +# Proyecto de Ejemplo + +> 🌐 **Language:** [English](example-project.md) | [Español](example-project.es.md) + + +Este es un template. Reemplaza con la información real de tu proyecto. + +## Descripción + +Qué hace el proyecto, en 1-2 párrafos. Evita jerga innecesaria. + +## Stack técnico + +- **Lenguaje:** Go 1.26 +- **Framework:** Ninguno (stdlib) +- **Base de datos:** SQLite +- **Deployment:** Fly.io + +## Features principales + +1. Feature uno — descripción breve +2. Feature dos — descripción breve +3. Feature tres — descripción breve + +## Aprendizajes + +Qué aprendiste, qué challenges tuviste, qué harías diferente. + +## Links + +- Repo: github.com/VictorVargas/proyecto +- Demo: proyecto.example.com +- Docs: docs.proyecto.example.com \ No newline at end of file diff --git a/data/projects/example-project.md b/data/projects/example-project.md index 0759a38..d50f6be 100644 --- a/data/projects/example-project.md +++ b/data/projects/example-project.md @@ -1,39 +1,39 @@ --- -title: "Proyecto de Ejemplo" +title: "Example Project" date: 2026-06 status: "active" -tags: ["ejemplo", "plantilla"] +tags: ["example", "template"] repo: "" demo: "" --- -# Proyecto de Ejemplo +# Example Project -Este es un template. Reemplaza con la información real de tu proyecto. +This is a template. Replace with the real information about your project. -## Descripción +## Description -Qué hace el proyecto, en 1-2 párrafos. Evita jerga innecesaria. +What the project does, in 1-2 paragraphs. Avoid unnecessary jargon. -## Stack técnico +## Tech stack -- **Lenguaje:** Go 1.26 -- **Framework:** Ninguno (stdlib) -- **Base de datos:** SQLite +- **Language:** Go 1.26 +- **Framework:** None (stdlib) +- **Database:** SQLite - **Deployment:** Fly.io -## Features principales +## Main features -1. Feature uno — descripción breve -2. Feature dos — descripción breve -3. Feature tres — descripción breve +1. Feature one — brief description +2. Feature two — brief description +3. Feature three — brief description -## Aprendizajes +## Learnings -Qué aprendiste, qué challenges tuviste, qué harías diferente. +What you learned, what challenges you faced, what you'd do differently. ## Links -- Repo: github.com/VictorVargas/proyecto -- Demo: proyecto.example.com -- Docs: docs.proyecto.example.com \ No newline at end of file +- Repo: github.com/VictorVargas/project +- Demo: project.example.com +- Docs: docs.project.example.com \ No newline at end of file diff --git a/docs/architecture.es.md b/docs/architecture.es.md new file mode 100644 index 0000000..578f9ee --- /dev/null +++ b/docs/architecture.es.md @@ -0,0 +1,1044 @@ +# 📋 Rony Chat Bot — Technical Design Document + +> 🌐 **Idioma:** [English](architecture.md) | [Español](architecture.es.md) + + +**Versión:** 1.0 +**Autor:** Victor Hugo Vargas +**Fecha:** 2026-06-28 +**Estado:** Especificación completa para implementación +**Path:** `rony-chat-bot/docs/architecture.md` + +> 📚 **Workspace:** Este proyecto es parte del workspace `Rony/`. Ver [`../README.md`](../../README.md). +> +> 🔑 **Depende de:** [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — librería core que provee agent loop, LLM clients, RAG, persona system. +> +> 📐 **Metodología:** Este proyecto sigue el enfoque **SDD + DDD + Hexagonal Architecture**. Los Requisitos Funcionales se numeran como `CRF-XXX`. Ver [`../../METHODOLOGY.md`](../../METHODOLOGY.md). + +--- + +## 🎯 1. Visión del Proyecto + +### 1.1 ¿Qué es Chat-Bot? + +Un **chatbot HTTP** que responde preguntas sobre Victor Hugo Vargas y sus proyectos. Usa **RAG (Retrieval-Augmented Generation)** sobre archivos markdown que describen cada proyecto, y un LLM local (o cloud) para generar respuestas. + +### 1.2 Caso de uso primario + +Victor tiene un portfolio web (Astro + React). En el sitio hay un widget de chat donde visitantes pueden preguntar: +- "¿Qué proyectos ha hecho Victor?" +- "¿Cuál es su experiencia con Go?" +- "¿Cómo funciona Rony TUI?" +- "¿Victor ha trabajado con PostgreSQL?" + +El bot responde con información precisa extraída de los archivos markdown de proyectos + bio + skills. + +### 1.3 Casos de uso secundarios (futuro) + +- **Adaptación a clientes:** El mismo bot, con otra data y otra persona, sirve para concesionarios, restaurantes, etc. +- **Standalone CLI:** `./chat-bot ask "¿qué sabes de X?"` para uso desde terminal. +- **Slack/Discord bot:** Wrapper que consume el HTTP API. + +### 1.4 Filosofía + +- **Self-hosted por defecto** — funciona 100% local con Ollama + modelos 1-3B +- **Cloud opcional** — si se necesita más calidad, swap a Anthropic API +- **Portable** — fácil de fork/customizar para otros contextos +- **Streaming** — respuestas token-por-token con SSE (no espera a respuesta completa) +- **Reutiliza `rony-llm-agent`** — no reinventar el agent loop + +--- + +## 🏗️ 2. Arquitectura + +### 2.1 Vista general + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Browser (Astro site) │ +│ ↓ HTTP POST /api/chat │ +│ Astro SSR (proxy) ←────────── Sirve portfolio + proxy chat │ +│ ↓ HTTP POST /api/chat │ +│ Chat-Bot HTTP server (:7331) │ +│ ↓ │ +│ Agent loop (rony-llm-agent) │ +│ ↓ │ +│ RAG retrieval → ChromaDB sobre data/projects/*.md │ +│ ↓ │ +│ LLM (Ollama local / Anthropic cloud) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Componentes principales + +| Componente | Path | Responsabilidad | +|---|---|---| +| **HTTP server** | `internal/server/` | Gin/chi handlers, SSE streaming | +| **Agent runner** | `internal/agent/` | Wrapper sobre `rony-llm-agent` con config específica | +| **Portfolio loader** | `internal/portfolio/` | Lee `data/projects/*.md`, indexa en ChromaDB | +| **Persona** | `internal/persona/` | Carga persona desde `configs/portfolio-bot.yaml` | +| **CLI** | `cm./rony-chat-bot/` | Comandos: `serve`, `reindex`, `ask`, `version` | + +### 2.3 Stack tecnológico + +| Capa | Tecnología | Razón | +|---|---|---| +| **Lenguaje** | Go 1.26+ | Mismo que `harness`, aprovechar `os.Root`, `iter.Seq` | +| **HTTP router** | `net/http` + `chi` | Stdlib + chi para middleware (CORS, logging) | +| **SSE** | `net/http` Flusher | Stdlib es suficiente, no necesita librería externa | +| **Config** | `gopkg.in/yaml.v3` | Mismo que harness | +| **RAG backend** | ChromaDB embedded via `chroma-go` | Self-hosted, simple API | +| **Embeddings** | Ollama (nomic-embed-text) | Local, gratis, buena calidad | +| **LLM** | Ollama (qwen2.5:1.5b) o llama.cpp | Self-hosted por defecto | +| **Tests** | stdlib + testify | Consistencia con el resto | + +--- + +## 🔌 3. HTTP API + +### 3.1 Endpoints + +#### `POST /api/chat` — Chat con streaming SSE + +**Request:** +```json +{ + "messages": [ + {"role": "user", "content": "¿Qué proyectos tiene Victor?"} + ], + "stream": true +} +``` + +**Response (SSE):** +``` +data: {"type":"start","conversation_id":"abc123"} + +data: {"type":"chunk","content":"Victor"} +data: {"type":"chunk","content":" tiene"} +data: {"type":"chunk","content":" varios"} +data: {"type":"chunk","content":" proyectos"} + +data: {"type":"sources","documents":["rony-tui.md","rony-llm-agent.md"]} + +data: {"type":"done","usage":{"input_tokens":245,"output_tokens":38}} +``` + +**Sin streaming** (`"stream": false`): +```json +{ + "content": "Victor tiene varios proyectos...", + "sources": ["rony-tui.md", "rony-llm-agent.md"], + "usage": {"input_tokens": 245, "output_tokens": 38} +} +``` + +#### `POST /api/reindex` — Re-indexar portfolio + +Útil cuando se modifican archivos en `data/projects/`. + +**Request:** vacío +**Response:** +```json +{ + "indexed_files": 12, + "total_chunks": 87, + "duration_ms": 4321 +} +``` + +#### `GET /api/health` — Health check + +```json +{ + "status": "ok", + "version": "1.0.0", + "providers": ["ollama-local"], + "rag": { + "documents": 12, + "chunks": 87, + "last_index": "2026-06-28T10:23:45Z" + } +} +``` + +#### `GET /api/info` — Metadata del bot + +```json +{ + "name": "Asistente de Victor Hugo Vargas", + "model": "qwen2.5:1.5b", + "persona": "...", + "topics": ["proyectos", "experiencia", "skills técnicas"] +} +``` + +### 3.2 SSE Implementation + +```go +// internal/server/chat.go +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "github.com/VictorVargas/rony-llm-agent/pkg/agent" +) + +func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { + // Headers SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "SSE no soportado", http.StatusInternalServerError) + return + } + + // Parse request + var req ChatRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, flusher, "invalid request", err) + return + } + + // Start event + writeSSE(w, flusher, "start", map[string]string{ + "conversation_id": generateConvID(), + }) + + // Run agent con streaming + sources := []string{} + for chunk, err := range s.agent.RunStream(r.Context(), req.Messages) { + if err != nil { + writeSSE(w, flusher, "error", map[string]string{"message": err.Error()}) + return + } + if chunk.Type == "source" { + sources = append(sources, chunk.Source) + } + writeSSE(w, flusher, chunk.Type, chunk.Data) + } + + // Done event + writeSSE(w, flusher, "done", map[string]any{ + "usage": map[string]int{ + "input_tokens": 245, + "output_tokens": 38, + }, + }) +} + +func writeSSE(w http.ResponseWriter, flusher http.Flusher, eventType string, data any) { + payload, _ := json.Marshal(data) + fmt.Fprintf(w, "data: {\"type\":%q,\"data\":%s}\n\n", eventType, payload) + flusher.Flush() +} +``` + +### 3.3 Middleware + +```go +// internal/server/middleware.go +package server + +func (s *Server) loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + // Wrap response writer para capturar status + rw := &statusRecorder{ResponseWriter: w, status: 200} + next.ServeHTTP(rw, r) + + slog.Info("http.request", + "method", r.Method, + "path", r.URL.Path, + "status", rw.status, + "duration_ms", time.Since(start).Milliseconds(), + "ip", r.RemoteAddr, + ) + }) +} + +func (s *Server) corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + for _, allowed := range s.config.Server.CORSOrigins { + if origin == allowed { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + break + } + } + if r.Method == "OPTIONS" { + w.WriteHeader(204) + return + } + next.ServeHTTP(w, r) + }) +} + +func (s *Server) rateLimitMiddleware(next http.Handler) http.Handler { + limiter := rate.NewLimiter(rate.Every(time.Minute/time.Duration(s.config.Server.RateLimit.RequestsPerMinute)), s.config.Server.RateLimit.Burst) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !limiter.Allow() { + http.Error(w, "rate limit exceeded", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) +} +``` + +--- + +## 🧠 4. RAG (Retrieval-Augmented Generation) + +### 4.1 Pipeline de indexación + +``` +data/projects/*.md + ↓ (read all files) +Raw markdown content + ↓ (split into chunks, ~500 chars, 50 overlap) +Chunks [] + ↓ (embed each chunk via Ollama nomic-embed-text) +Vectors [][]float32 + ↓ (store in ChromaDB collection "portfolio") +Indexed corpus +``` + +**Cuándo se ejecuta:** +- Al arrancar el bot (si `--reindex-on-start` flag) +- Manualmente: `./chat-bot reindex` +- Vía HTTP: `POST /api/reindex` + +### 4.2 Pipeline de retrieval + +``` +User query "¿qué proyectos tiene Victor?" + ↓ (embed query) +Query vector + ↓ (cosine similarity search en ChromaDB, top_k=5) +Top 5 chunks relevantes + ↓ (format as context block) +System prompt += chunks relevantes + ↓ (send to LLM) +LLM generates answer +``` + +### 4.3 Implementación + +```go +// internal/portfolio/indexer.go +package portfolio + +import ( + "context" + "os" + "path/filepath" + "strings" + "github.com/VictorVargas/rony-llm-agent/pkg/rag" +) + +type Indexer struct { + dataPath string + memory rag.Memory + embedder rag.Embedder + chunkSize int + chunkOverlap int +} + +func (i *Indexer) IndexAll(ctx context.Context) (int, error) { + files, err := filepath.Glob(filepath.Join(i.dataPath, "*.md")) + if err != nil { + return 0, err + } + + totalChunks := 0 + for _, file := range files { + chunks, err := i.indexFile(ctx, file) + if err != nil { + slog.Warn("failed to index file", "file", file, "err", err) + continue + } + totalChunks += chunks + } + + return totalChunks, nil +} + +func (i *Indexer) indexFile(ctx context.Context, path string) (int, error) { + content, err := os.ReadFile(path) + if err != nil { + return 0, err + } + + projectID := strings.TrimSuffix(filepath.Base(path), ".md") + chunks := splitIntoChunks(string(content), i.chunkSize, i.chunkOverlap) + + for idx, chunk := range chunks { + embedding, err := i.embedder.Embed(ctx, chunk) + if err != nil { + return idx, err + } + + fragment := rag.Fragment{ + ID: fmt.Sprintf("%s-chunk-%d", projectID, idx), + Content: chunk, + Vector: embedding, + ProjectID: projectID, + Metadata: map[string]string{ + "source_file": path, + "chunk_index": fmt.Sprint(idx), + }, + } + + if err := i.memory.Add(ctx, fragment); err != nil { + return idx, err + } + } + + return len(chunks), nil +} + +func splitIntoChunks(text string, size, overlap int) []string { + // Implementación simple: split por tamaño con overlap + // Versión production usa tokenizer-aware chunking + var chunks []string + for i := 0; i < len(text); i += size - overlap { + end := i + size + if end > len(text) { + end = len(text) + } + chunks = append(chunks, text[i:end]) + } + return chunks +} +``` + +### 4.4 Retrieval en el agent loop + +```go +// internal/agent/runner.go +package agent + +func (r *Runner) buildSystemPrompt(ctx context.Context, query string) (string, error) { + // 1. Base persona prompt + basePrompt := r.persona.SystemPrompt + + // 2. Retrieve relevant chunks + fragments, err := r.memory.Search(ctx, query, r.config.RAG.TopK) + if err != nil { + return "", err + } + + // 3. Format as context + var contextBlock strings.Builder + contextBlock.WriteString(basePrompt) + contextBlock.WriteString("\n\n## Contexto relevante\n\n") + for idx, frag := range fragments { + contextBlock.WriteString(fmt.Sprintf("### Fuente: %s\n%s\n\n", + frag.Metadata["source_file"], frag.Content)) + } + + return contextBlock.String(), nil +} + +func (r *Runner) RunStream(ctx context.Context, messages []llm.Message) iter.Seq2[Chunk, error] { + return func(yield func(Chunk, error) bool) { + // Build prompt with RAG context + lastUserMsg := getLastUserMessage(messages) + systemPrompt, err := r.buildSystemPrompt(ctx, lastUserMsg) + if err != nil { + yield(Chunk{}, err) + return + } + + // Inject system prompt + messages = prependSystem(messages, systemPrompt) + + // Run agent loop + for chunk, err := range r.loop.RunStream(ctx, messages) { + if !yield(chunk, err) { + return + } + } + } +} +``` + +--- + +## 🌐 5. Integración con Astro (Portfolio) + +### 5.1 Patrón recomendado: Astro proxy + +``` +[Browser] ←→ [Astro SSR :4321] ←→ [Chat-Bot :7331] +``` + +**Por qué proxy y no llamada directa del browser al chat-bot:** +- ✅ Single domain (no CORS) +- ✅ Astro maneja auth/sesión si se necesita +- ✅ Puede haber rate limiting centralizado en Astro +- ✅ El chat-bot queda en red privada (no expuesto a internet directamente) + +### 5.2 Astro: API route del proxy + +```typescript +// portfolio/src/pages/api/chat.ts +import type { APIRoute } from 'astro'; + +const CHAT_BOT_URL = process.env.CHAT_BOT_URL || 'http://localhost:7331'; + +export const POST: APIRoute = async ({ request }) => { + const body = await request.json(); + + const resp = await fetch(`${CHAT_BOT_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!resp.ok) { + return new Response('Chat bot error', { status: resp.status }); + } + + // Stream SSE de vuelta al browser + return new Response(resp.body, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); +}; +``` + +### 5.3 React: Componente del chat + +```tsx +// portfolio/src/components/Chat.tsx +import { useState, useRef } from 'react'; + +interface Message { + role: 'user' | 'assistant'; + content: string; +} + +export default function Chat() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [streaming, setStreaming] = useState(false); + const abortRef = useRef(null); + + const send = async () => { + if (!input.trim() || streaming) return; + + const userMsg: Message = { role: 'user', content: input }; + setMessages(prev => [...prev, userMsg]); + setInput(''); + setStreaming(true); + + // Placeholder para streaming + const assistantMsg: Message = { role: 'assistant', content: '' }; + setMessages(prev => [...prev, assistantMsg]); + + abortRef.current = new AbortController(); + + try { + const resp = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [...messages, userMsg], + stream: true, + }), + signal: abortRef.current.signal, + }); + + const reader = resp.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.startsWith('data: ')) continue; + const event = JSON.parse(line.slice(6)); + + if (event.type === 'chunk') { + setMessages(prev => { + const updated = [...prev]; + updated[updated.length - 1].content += event.data.content; + return updated; + }); + } + } + } + } catch (err) { + if ((err as Error).name !== 'AbortError') { + console.error(err); + } + } finally { + setStreaming(false); + abortRef.current = null; + } + }; + + const stop = () => abortRef.current?.abort(); + + return ( +
+
+ {messages.map((m, i) => ( +
+ {m.content || (streaming && i === messages.length - 1 ? '...' : '')} +
+ ))} +
+
+ setInput(e.target.value)} + onKeyDown={e => e.key === 'Enter' && send()} + placeholder="Pregunta sobre Victor..." + disabled={streaming} + /> + {streaming ? ( + + ) : ( + + )} +
+
+ ); +} +``` + +--- + +## 🤖 6. Self-hosting con Ollama + +### 6.1 Setup + +```bash +# 1. Instalar Ollama +curl -fsSL https://ollama.com/install.sh | sh + +# 2. Descargar modelo de chat +ollama pull qwen2.5:1.5b + +# 3. Descargar modelo de embeddings +ollama pull nomic-embed-text + +# 4. Verificar +ollama list +``` + +### 6.2 Configuración por defecto + +`configs/portfolio-bot.yaml` ya viene con Ollama como default. Solo necesitas: + +```bash +# Asegurar que Ollama está corriendo +ollama serve + +# Arrancar el bot +./bin/chat-bot serve +``` + +### 6.3 Alternativa: llama.cpp directo + +Para más control o si Ollama no funciona en tu setup: + +```yaml +providers: + - name: llamacpp-local + type: llamacpp + model_path: ${RONY_MODELS_PATH}/qwen2.5-1.5b-instruct-q5_k_m.gguf + context_size: 4096 + n_gpu_layers: 999 # offload todo a GPU + default: true +``` + +El adapter `llamacpp` se importa desde `rony-llm-agent/pkg/llm/providers/llamacpp` y se compila contra `llama.cpp` vía CGO o binario externo. + +--- + +## 📦 7. CLI del bot + +### 7.1 Comandos + +```bash +# Arrancar servidor HTTP +chat-bot serve [--port 7331] [--host 0.0.0.0] [--reindex-on-start] + +# Re-indexar portfolio (lee data/projects/*.md → ChromaDB) +chat-bot reindex + +# Pregunta única (sin servidor, útil para tests) +chat-bot ask "¿Qué proyectos tiene Victor?" [--no-rag] + +# Validar config +chat-bot config validate + +# Health check (útil para monitoring) +chat-bot health + +# Versión +chat-bot version +``` + +### 7.2 Implementación con Cobra + +```go +// cm./rony-chat-bot/main.go +package main + +import ( + "github.com/spf13/cobra" +) + +func main() { + root := &cobra.Command{ + Use: "chat-bot", + Short: "Portfolio chatbot HTTP server", + } + + root.AddCommand(serveCmd()) + root.AddCommand(reindexCmd()) + root.AddCommand(askCmd()) + root.AddCommand(configCmd()) + root.AddCommand(healthCmd()) + root.AddCommand(versionCmd()) + + if err := root.Execute(); err != nil { + os.Exit(1) + } +} + +func serveCmd() *cobra.Command { + var port int + var host string + var reindexOnStart bool + + cmd := &cobra.Command{ + Use: "serve", + Short: "Start HTTP server", + RunE: func(cmd *cobra.Command, args []string) error { + return server.Serve(server.Config{ + Port: port, + Host: host, + ReindexOnStart: reindexOnStart, + }) + }, + } + + cmd.Flags().IntVar(&port, "port", 7331, "HTTP port") + cmd.Flags().StringVar(&host, "host", "0.0.0.0", "HTTP host") + cmd.Flags().BoolVar(&reindexOnStart, "reindex-on-start", false, "Re-index RAG before serving") + + return cmd +} +``` + +--- + +## 🚀 8. Deployment + +### 8.1 Recomendación: Self-hosted en VPS + +```bash +# 1. Instalar dependencias +sudo apt install golang-go ollama +ollama pull qwen2.5:1.5b +ollama pull nomic-embed-text + +# 2. Build +go build -o /usr/local/bin/chat-bot ./cmd/chat-bot + +# 3. systemd service +cat > /etc/systemd/system/chat-bot.service < 📚 **Workspace:** Este proyecto es parte del workspace `Rony/`. Ver [`../README.md`](../../README.md). +> 🌐 **Language:** [English](./architecture.md) | [Español](./architecture.es.md) > -> 🔑 **Depende de:** [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — librería core que provee agent loop, LLM clients, RAG, persona system. +> 📚 **Workspace:** This project is part of the `Rony/` workspace. See [`../README.md`](../../README.md). > -> 📐 **Metodología:** Este proyecto sigue el enfoque **SDD + DDD + Hexagonal Architecture**. Los Requisitos Funcionales se numeran como `CRF-XXX`. Ver [`../../METHODOLOGY.md`](../../METHODOLOGY.md). +> 🔑 **Depends on:** [`rony-llm-agent`](https://github.com/VictorVargas/rony-llm-agent) — core library that provides agent loop, LLM clients, RAG, persona system. +> +> 📐 **Methodology:** This project follows the **SDD + DDD + Hexagonal Architecture** approach. Functional Requirements are numbered as `CRF-XXX`. See [`../../METHODOLOGY.md`](../../METHODOLOGY.md). --- -## 🎯 1. Visión del Proyecto +## 🎯 1. Project Vision -### 1.1 ¿Qué es Chat-Bot? +### 1.1 What is Chat-Bot? -Un **chatbot HTTP** que responde preguntas sobre Victor Hugo Vargas y sus proyectos. Usa **RAG (Retrieval-Augmented Generation)** sobre archivos markdown que describen cada proyecto, y un LLM local (o cloud) para generar respuestas. +An **HTTP chatbot** that answers questions about Victor Hugo Vargas and his projects. Uses **RAG (Retrieval-Augmented Generation)** over markdown files describing each project, and a local LLM (or cloud) to generate responses. -### 1.2 Caso de uso primario +### 1.2 Primary use case -Victor tiene un portfolio web (Astro + React). En el sitio hay un widget de chat donde visitantes pueden preguntar: -- "¿Qué proyectos ha hecho Victor?" -- "¿Cuál es su experiencia con Go?" -- "¿Cómo funciona Rony TUI?" -- "¿Victor ha trabajado con PostgreSQL?" +Victor has a portfolio website (Astro + React). On the site there's a chat widget where visitors can ask: -El bot responde con información precisa extraída de los archivos markdown de proyectos + bio + skills. +- "What projects has Victor done?" +- "What's his experience with Go?" +- "How does Rony Harness work?" +- "Has Victor worked with PostgreSQL?" -### 1.3 Casos de uso secundarios (futuro) +The bot responds with accurate information extracted from the projects' markdown files + bio + skills. -- **Adaptación a clientes:** El mismo bot, con otra data y otra persona, sirve para concesionarios, restaurantes, etc. -- **Standalone CLI:** `./chat-bot ask "¿qué sabes de X?"` para uso desde terminal. -- **Slack/Discord bot:** Wrapper que consume el HTTP API. +### 1.3 Secondary use cases (future) -### 1.4 Filosofía +- **Client adaptation:** The same bot, with other data and another persona, serves car dealerships, restaurants, etc. +- **Standalone CLI:** `./chat-bot ask "what do you know about X?"` for terminal use. +- **Slack/Discord bot:** Wrapper that consumes the HTTP API. -- **Self-hosted por defecto** — funciona 100% local con Ollama + modelos 1-3B -- **Cloud opcional** — si se necesita más calidad, swap a Anthropic API -- **Portable** — fácil de fork/customizar para otros contextos -- **Streaming** — respuestas token-por-token con SSE (no espera a respuesta completa) -- **Reutiliza `rony-llm-agent`** — no reinventar el agent loop +### 1.4 Philosophy + +- **Self-hosted by default** — works 100% local with Ollama + 1-3B models +- **Cloud optional** — if you need more quality, swap to Anthropic API +- **Portable** — easy to fork/customize for other contexts +- **Streaming** — token-by-token responses with SSE (no waiting for complete response) +- **Reuses `rony-llm-agent`** — doesn't reinvent the agent loop --- -## 🏗️ 2. Arquitectura +## 🏗️ 2. Architecture -### 2.1 Vista general +### 2.1 Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ Browser (Astro site) │ │ ↓ HTTP POST /api/chat │ -│ Astro SSR (proxy) ←────────── Sirve portfolio + proxy chat │ +│ Astro SSR (proxy) ←────────── Serves portfolio + proxy chat │ │ ↓ HTTP POST /api/chat │ │ Chat-Bot HTTP server (:7331) │ │ ↓ │ │ Agent loop (rony-llm-agent) │ │ ↓ │ -│ RAG retrieval → ChromaDB sobre data/projects/*.md │ +│ RAG retrieval → ChromaDB over data/projects/*.md │ │ ↓ │ │ LLM (Ollama local / Anthropic cloud) │ └─────────────────────────────────────────────────────────────────┘ ``` -### 2.2 Componentes principales +### 2.2 Main components -| Componente | Path | Responsabilidad | +| Component | Path | Responsibility | |---|---|---| | **HTTP server** | `internal/server/` | Gin/chi handlers, SSE streaming | -| **Agent runner** | `internal/agent/` | Wrapper sobre `rony-llm-agent` con config específica | -| **Portfolio loader** | `internal/portfolio/` | Lee `data/projects/*.md`, indexa en ChromaDB | -| **Persona** | `internal/persona/` | Carga persona desde `configs/portfolio-bot.yaml` | -| **CLI** | `cm./rony-chat-bot/` | Comandos: `serve`, `reindex`, `ask`, `version` | +| **Agent runner** | `internal/agent/` | Wrapper over `rony-llm-agent` with specific config | +| **Portfolio loader** | `internal/portfolio/` | Reads `data/projects/*.md`, indexes in ChromaDB | +| **Persona** | `internal/persona/` | Loads persona from `configs/portfolio-bot.yaml` | +| **CLI** | `cmd/chat-bot/` | Commands: `serve`, `reindex`, `ask`, `version` | -### 2.3 Stack tecnológico +### 2.3 Tech stack -| Capa | Tecnología | Razón | +| Layer | Technology | Reason | |---|---|---| -| **Lenguaje** | Go 1.26+ | Mismo que `harness`, aprovechar `os.Root`, `iter.Seq` | -| **HTTP router** | `net/http` + `chi` | Stdlib + chi para middleware (CORS, logging) | -| **SSE** | `net/http` Flusher | Stdlib es suficiente, no necesita librería externa | -| **Config** | `gopkg.in/yaml.v3` | Mismo que harness | +| **Language** | Go 1.26+ | Same as rony-harness, leverage `os.Root`, `iter.Seq` | +| **HTTP router** | `net/http` + `chi` | Stdlib + chi for middleware (CORS, logging) | +| **SSE** | `net/http` Flusher | Stdlib is enough, no external library needed | +| **Config** | `gopkg.in/yaml.v3` | Same as harness | | **RAG backend** | ChromaDB embedded via `chroma-go` | Self-hosted, simple API | -| **Embeddings** | Ollama (nomic-embed-text) | Local, gratis, buena calidad | -| **LLM** | Ollama (qwen2.5:1.5b) o llama.cpp | Self-hosted por defecto | -| **Tests** | stdlib + testify | Consistencia con el resto | +| **Embeddings** | Ollama (nomic-embed-text) | Local, free, good quality | +| **LLM** | Ollama (qwen2.5:1.5b) or llama.cpp | Self-hosted by default | +| **Tests** | stdlib + testify | Consistency with the rest | --- @@ -95,13 +98,13 @@ El bot responde con información precisa extraída de los archivos markdown de p ### 3.1 Endpoints -#### `POST /api/chat` — Chat con streaming SSE +#### `POST /api/chat` — Chat with SSE streaming **Request:** ```json { "messages": [ - {"role": "user", "content": "¿Qué proyectos tiene Victor?"} + {"role": "user", "content": "What projects does Victor have?"} ], "stream": true } @@ -112,29 +115,29 @@ El bot responde con información precisa extraída de los archivos markdown de p data: {"type":"start","conversation_id":"abc123"} data: {"type":"chunk","content":"Victor"} -data: {"type":"chunk","content":" tiene"} -data: {"type":"chunk","content":" varios"} -data: {"type":"chunk","content":" proyectos"} +data: {"type":"chunk","content":" has"} +data: {"type":"chunk","content":" several"} +data: {"type":"chunk","content":" projects"} -data: {"type":"sources","documents":["rony-tui.md","rony-llm-agent.md"]} +data: {"type":"sources","documents":["rony-harness.md","rony-llm-agent.md"]} data: {"type":"done","usage":{"input_tokens":245,"output_tokens":38}} ``` -**Sin streaming** (`"stream": false`): +**Without streaming** (`"stream": false`): ```json { - "content": "Victor tiene varios proyectos...", - "sources": ["rony-tui.md", "rony-llm-agent.md"], + "content": "Victor has several projects...", + "sources": ["rony-harness.md", "rony-llm-agent.md"], "usage": {"input_tokens": 245, "output_tokens": 38} } ``` -#### `POST /api/reindex` — Re-indexar portfolio +#### `POST /api/reindex` — Re-index portfolio -Útil cuando se modifican archivos en `data/projects/`. +Useful when files in `data/projects/` are modified. -**Request:** vacío +**Request:** empty **Response:** ```json { @@ -159,14 +162,14 @@ data: {"type":"done","usage":{"input_tokens":245,"output_tokens":38}} } ``` -#### `GET /api/info` — Metadata del bot +#### `GET /api/info` — Bot metadata ```json { - "name": "Asistente de Victor Hugo Vargas", + "name": "Rony Chat Bot", "model": "qwen2.5:1.5b", "persona": "...", - "topics": ["proyectos", "experiencia", "skills técnicas"] + "topics": ["projects", "experience", "technical skills"] } ``` @@ -184,7 +187,7 @@ import ( ) func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { - // Headers SSE + // SSE headers w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") @@ -192,7 +195,7 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { - http.Error(w, "SSE no soportado", http.StatusInternalServerError) + http.Error(w, "SSE not supported", http.StatusInternalServerError) return } @@ -208,7 +211,7 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { "conversation_id": generateConvID(), }) - // Run agent con streaming + // Run agent with streaming sources := []string{} for chunk, err := range s.agent.RunStream(r.Context(), req.Messages) { if err != nil { @@ -246,7 +249,7 @@ package server func (s *Server) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() - // Wrap response writer para capturar status + // Wrap response writer to capture status rw := &statusRecorder{ResponseWriter: w, status: 200} next.ServeHTTP(rw, r) @@ -295,7 +298,7 @@ func (s *Server) rateLimitMiddleware(next http.Handler) http.Handler { ## 🧠 4. RAG (Retrieval-Augmented Generation) -### 4.1 Pipeline de indexación +### 4.1 Indexing pipeline ``` data/projects/*.md @@ -309,26 +312,26 @@ Vectors [][]float32 Indexed corpus ``` -**Cuándo se ejecuta:** -- Al arrancar el bot (si `--reindex-on-start` flag) -- Manualmente: `./chat-bot reindex` -- Vía HTTP: `POST /api/reindex` +**When it runs:** +- On bot startup (if `--reindex-on-start` flag) +- Manually: `./chat-bot reindex` +- Via HTTP: `POST /api/reindex` -### 4.2 Pipeline de retrieval +### 4.2 Retrieval pipeline ``` -User query "¿qué proyectos tiene Victor?" +User query "what projects does Victor have?" ↓ (embed query) Query vector - ↓ (cosine similarity search en ChromaDB, top_k=5) -Top 5 chunks relevantes + ↓ (cosine similarity search in ChromaDB, top_k=5) +Top 5 relevant chunks ↓ (format as context block) -System prompt += chunks relevantes +System prompt += relevant chunks ↓ (send to LLM) LLM generates answer ``` -### 4.3 Implementación +### 4.3 Implementation ```go // internal/portfolio/indexer.go @@ -404,8 +407,8 @@ func (i *Indexer) indexFile(ctx context.Context, path string) (int, error) { } func splitIntoChunks(text string, size, overlap int) []string { - // Implementación simple: split por tamaño con overlap - // Versión production usa tokenizer-aware chunking + // Simple implementation: split by size with overlap + // Production version uses tokenizer-aware chunking var chunks []string for i := 0; i < len(text); i += size - overlap { end := i + size @@ -418,7 +421,7 @@ func splitIntoChunks(text string, size, overlap int) []string { } ``` -### 4.4 Retrieval en el agent loop +### 4.4 Retrieval in the agent loop ```go // internal/agent/runner.go @@ -437,9 +440,9 @@ func (r *Runner) buildSystemPrompt(ctx context.Context, query string) (string, e // 3. Format as context var contextBlock strings.Builder contextBlock.WriteString(basePrompt) - contextBlock.WriteString("\n\n## Contexto relevante\n\n") + contextBlock.WriteString("\n\n## Relevant context\n\n") for idx, frag := range fragments { - contextBlock.WriteString(fmt.Sprintf("### Fuente: %s\n%s\n\n", + contextBlock.WriteString(fmt.Sprintf("### Source: %s\n%s\n\n", frag.Metadata["source_file"], frag.Content)) } @@ -471,31 +474,30 @@ func (r *Runner) RunStream(ctx context.Context, messages []llm.Message) iter.Seq --- -## 🌐 5. Integración con Astro (Portfolio) +## 🌐 5. Integration with Astro (Portfolio) -### 5.1 Patrón recomendado: Astro proxy +### 5.1 Recommended pattern: Astro proxy ``` [Browser] ←→ [Astro SSR :4321] ←→ [Chat-Bot :7331] ``` -**Por qué proxy y no llamada directa del browser al chat-bot:** +**Why proxy and not direct browser call to chat-bot:** - ✅ Single domain (no CORS) -- ✅ Astro maneja auth/sesión si se necesita -- ✅ Puede haber rate limiting centralizado en Astro -- ✅ El chat-bot queda en red privada (no expuesto a internet directamente) +- ✅ Astro handles auth/session if needed +- ✅ There can be centralized rate limiting in Astro +- ✅ The chat-bot stays on private network (not exposed to internet directly) -### 5.2 Astro: API route del proxy +### 5.2 Astro: API route of the proxy ```typescript // portfolio/src/pages/api/chat.ts import type { APIRoute } from 'astro'; -const CHAT_BOT_URL = process.env.CHAT_BOT_URL || 'http://localhost:7331'; +const CHAT_BOT_URL = import.meta.env.CHAT_BOT_URL || 'http://localhost:7331'; export const POST: APIRoute = async ({ request }) => { const body = await request.json(); - const resp = await fetch(`${CHAT_BOT_URL}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -506,7 +508,7 @@ export const POST: APIRoute = async ({ request }) => { return new Response('Chat bot error', { status: resp.status }); } - // Stream SSE de vuelta al browser + // Stream SSE back to browser return new Response(resp.body, { status: 200, headers: { @@ -518,7 +520,7 @@ export const POST: APIRoute = async ({ request }) => { }; ``` -### 5.3 React: Componente del chat +### 5.3 React: Chat component ```tsx // portfolio/src/components/Chat.tsx @@ -543,7 +545,7 @@ export default function Chat() { setInput(''); setStreaming(true); - // Placeholder para streaming + // Placeholder for streaming const assistantMsg: Message = { role: 'assistant', content: '' }; setMessages(prev => [...prev, assistantMsg]); @@ -611,7 +613,7 @@ export default function Chat() { value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} - placeholder="Pregunta sobre Victor..." + placeholder="Ask about Victor..." disabled={streaming} /> {streaming ? ( @@ -627,39 +629,39 @@ export default function Chat() { --- -## 🤖 6. Self-hosting con Ollama +## 🤖 6. Self-hosting with Ollama ### 6.1 Setup ```bash -# 1. Instalar Ollama +# 1. Install Ollama curl -fsSL https://ollama.com/install.sh | sh -# 2. Descargar modelo de chat +# 2. Download chat model ollama pull qwen2.5:1.5b -# 3. Descargar modelo de embeddings +# 3. Download embeddings model ollama pull nomic-embed-text -# 4. Verificar +# 4. Verify ollama list ``` -### 6.2 Configuración por defecto +### 6.2 Default configuration -`configs/portfolio-bot.yaml` ya viene con Ollama como default. Solo necesitas: +`configs/portfolio-bot.yaml` already comes with Ollama as default. You only need: ```bash -# Asegurar que Ollama está corriendo +# Make sure Ollama is running ollama serve -# Arrancar el bot +# Start the bot ./bin/chat-bot serve ``` -### 6.3 Alternativa: llama.cpp directo +### 6.3 Alternative: llama.cpp direct -Para más control o si Ollama no funciona en tu setup: +For more control or if Ollama doesn't work in your setup: ```yaml providers: @@ -667,42 +669,42 @@ providers: type: llamacpp model_path: ${RONY_MODELS_PATH}/qwen2.5-1.5b-instruct-q5_k_m.gguf context_size: 4096 - n_gpu_layers: 999 # offload todo a GPU + n_gpu_layers: 999 # offload all to GPU default: true ``` -El adapter `llamacpp` se importa desde `rony-llm-agent/pkg/llm/providers/llamacpp` y se compila contra `llama.cpp` vía CGO o binario externo. +The `llamacpp` adapter is imported from `rony-llm-agent/pkg/llm/providers/llamacpp` and is compiled against `llama.cpp` via CGO or external binary. --- -## 📦 7. CLI del bot +## 📦 7. Bot CLI -### 7.1 Comandos +### 7.1 Commands ```bash -# Arrancar servidor HTTP +# Start HTTP server chat-bot serve [--port 7331] [--host 0.0.0.0] [--reindex-on-start] -# Re-indexar portfolio (lee data/projects/*.md → ChromaDB) +# Re-index portfolio (reads data/projects/*.md → ChromaDB) chat-bot reindex -# Pregunta única (sin servidor, útil para tests) -chat-bot ask "¿Qué proyectos tiene Victor?" [--no-rag] +# Single question (no server, useful for tests) +chat-bot ask "What projects does Victor have?" [--no-rag] -# Validar config +# Validate config chat-bot config validate -# Health check (útil para monitoring) +# Health check (useful for monitoring) chat-bot health -# Versión +# Version chat-bot version ``` -### 7.2 Implementación con Cobra +### 7.2 Implementation with Cobra ```go -// cm./rony-chat-bot/main.go +// cmd/chat-bot/main.go package main import ( @@ -756,10 +758,10 @@ func serveCmd() *cobra.Command { ## 🚀 8. Deployment -### 8.1 Recomendación: Self-hosted en VPS +### 8.1 Recommendation: Self-hosted on VPS ```bash -# 1. Instalar dependencias +# 1. Install dependencies sudo apt install golang-go ollama ollama pull qwen2.5:1.5b ollama pull nomic-embed-text @@ -800,7 +802,7 @@ chat.victorvargas.dev { ### 8.3 Monitoring ```bash -# Health check periódico +# Health check periodic curl -s http://localhost:7331/api/health | jq # Logs @@ -821,7 +823,7 @@ func TestHandleChat_ValidRequest(t *testing.T) { s := newTestServer(t) req := httptest.NewRequest("POST", "/api/chat", strings.NewReader(`{ - "messages": [{"role": "user", "content": "hola"}] + "messages": [{"role": "user", "content": "hello"}] }`)) req.Header.Set("Content-Type", "application/json") @@ -838,31 +840,31 @@ func TestHandleChat_RateLimit(t *testing.T) { }) // First request OK - req1 := newChatRequest("hola") + req1 := newChatRequest("hello") w1 := httptest.NewRecorder() s.handleChat(w1, req1) assert.Equal(t, 200, w1.Code) // Second request denied - req2 := newChatRequest("hola de nuevo") + req2 := newChatRequest("hello again") w2 := httptest.NewRecorder() s.handleChat(w2, req2) assert.Equal(t, 429, w2.Code) } ``` -### 9.2 Integration tests con mock LLM +### 9.2 Integration tests with mock LLM ```go // internal/agent/runner_test.go func TestRunner_RAGContextIsInjected(t *testing.T) { mockLLM := mock.New(mock.Responses{ - {Match: "proyectos", Response: "Victor tiene varios proyectos..."}, + {Match: "projects", Response: "Victor has several projects..."}, }) memory := newMockMemoryWithDocs(t, []rag.Fragment{ - {Content: "Rony TUI: AI agent harness...", ProjectID: "rony-tui"}, - {Content: "rony-llm-agent: librería Go...", ProjectID: "rony-llm-agent"}, + {Content: "Rony Harness: AI agent harness...", ProjectID: "rony-harness"}, + {Content: "go-llm-agent: Go library...", ProjectID: "rony-llm-agent"}, }) runner := agent.NewRunner(agent.Config{ @@ -872,36 +874,36 @@ func TestRunner_RAGContextIsInjected(t *testing.T) { }) resp, _ := runner.Run(context.Background(), []llm.Message{ - {Role: llm.RoleUser, Content: "¿qué proyectos tiene Victor?"}, + {Role: llm.RoleUser, Content: "what projects does Victor have?"}, }) // Verify LLM received context chunks in system prompt lastReq := mockLLM.LastRequest() - assert.Contains(t, lastReq.Messages[0].Content, "Rony TUI") - assert.Contains(t, lastReq.Messages[0].Content, "rony-llm-agent") + assert.Contains(t, lastReq.Messages[0].Content, "Rony Harness") + assert.Contains(t, lastReq.Messages[0].Content, "go-llm-agent") } ``` -### 9.3 E2E test con Astro +### 9.3 E2E test with Astro ```bash -# 1. Arrancar chat-bot en :7331 +# 1. Start chat-bot on :7331 ./bin/chat-bot serve & -# 2. Arrancar Astro en :4321 +# 2. Start Astro on :4321 cd ../portfolio && npm run dev & -# 3. Hacer request al proxy de Astro +# 3. Make request to Astro's proxy curl -X POST http://localhost:4321/api/chat \ -H "Content-Type: application/json" \ - -d '{"messages":[{"role":"user","content":"hola"}]}' + -d '{"messages":[{"role":"user","content":"hello"}]}' -# 4. Verificar SSE stream +# 4. Verify SSE stream ``` --- -## 📂 10. Estructura del Proyecto +## 📂 10. Project Structure ``` chat-bot/ @@ -918,21 +920,21 @@ chat-bot/ │ │ ├── middleware.go # logging, CORS, rate limit │ │ └── sse.go # SSE helpers │ │ -│ ├── agent/ # Wrapper sobre rony-llm-agent -│ │ ├── runner.go # RunStream con RAG injection +│ ├── agent/ # Wrapper over rony-llm-agent +│ │ ├── runner.go # RunStream with RAG injection │ │ └── prompts.go # System prompt builder │ │ │ ├── portfolio/ # Data loader -│ │ ├── indexer.go # Lee .md, chunks, embed, store +│ │ ├── indexer.go # Reads .md, chunks, embed, store │ │ ├── retriever.go # Query → top-k chunks │ │ └── chunker.go # Text splitting │ │ │ └── persona/ # Persona override -│ └── loader.go # Carga persona desde YAML +│ └── loader.go # Loads persona from YAML │ ├── data/ -│ └── projects/ # ← Markdown por proyecto -│ ├── rony-tui.md +│ └── projects/ # ← Markdown per project +│ ├── rony-harness.md │ ├── rony-llm-agent.md │ └── example-project.md │ @@ -940,7 +942,7 @@ chat-bot/ │ └── portfolio-bot.yaml # Provider + RAG + persona config │ ├── docs/ -│ └── architecture.md # ← ESTE ARCHIVO +│ └── architecture.md # ← THIS FILE │ ├── go.mod └── README.md @@ -950,83 +952,83 @@ chat-bot/ ## 📅 11. Roadmap -### Fase 1: MVP (2-3 semanas) +### Phase 1: MVP (2-3 weeks) -- [ ] Setup proyecto (`go mod init`, estructura) -- [ ] HTTP server básico con un endpoint `/api/chat` -- [ ] SSE streaming funcional -- [ ] RAG indexer (lee `data/projects/*.md` → ChromaDB) +- [ ] Project setup (`go mod init`, structure) +- [ ] Basic HTTP server with `/api/chat` endpoint +- [ ] Functional SSE streaming +- [ ] RAG indexer (reads `data/projects/*.md` → ChromaDB) - [ ] RAG retriever (query → top-k chunks) -- [ ] Persona loader desde YAML -- [ ] Integración con Ollama (qwen2.5:1.5b) +- [ ] Persona loader from YAML +- [ ] Ollama integration (qwen2.5:1.5b) - [ ] CLI: `serve`, `reindex`, `ask` -- [ ] Tests básicos +- [ ] Basic tests -### Fase 2: Integración con Astro (1 semana) +### Phase 2: Integration with Astro (1 week) -- [ ] Astro API route del proxy -- [ ] React component del chat widget -- [ ] E2E test: Astro → chat-bot → respuesta -- [ ] Styling del widget (TailwindCSS) +- [ ] Astro API route of the proxy +- [ ] React component of the chat widget +- [ ] E2E test: Astro → chat-bot → response +- [ ] Widget styling (TailwindCSS) -### Fase 3: Polish (1 semana) +### Phase 3: Polish (1 week) -- [ ] Rate limiting robusto -- [ ] Logging estructurado (JSON) -- [ ] Health checks para monitoring +- [ ] Robust rate limiting +- [ ] Structured logging (JSON) +- [ ] Health checks for monitoring - [ ] systemd service file -- [ ] README + docs de deployment +- [ ] README + deployment docs -### Fase 4: Opcionales +### Phase 4: Optionals -- [ ] Soporte para múltiples conversaciones (session ID) -- [ ] Historial de chats persistido -- [ ] Análisis de preguntas frecuentes -- [ ] Multi-idioma (EN/ES switch) -- [ ] Versión standalone CLI más pulida (`chat-bot ask`) +- [ ] Support for multiple conversations (session ID) +- [ ] Persisted chat history +- [ ] Analysis of frequent questions +- [ ] Multi-language (EN/ES switch) +- [ ] More polished standalone CLI version (`chat-bot ask`) --- -## 📐 12. Especificaciones de Calidad +## 📐 12. Quality Specifications -### 12.1 Métricas de rendimiento +### 12.1 Performance metrics -| Métrica | Target | +| Metric | Target | |---|---| -| TTFT (Time-to-first-token) | <500ms con Ollama local | -| End-to-end (pregunta → respuesta completa) | <3s para respuestas típicas | -| Memoria en reposo | <150MB | -| RAG indexing speed | ~100 docs/segundo | -| Retrieval latency | <50ms para top-5 | +| TTFT (Time-to-first-token) | <500ms with Ollama local | +| End-to-end (question → complete response) | <3s for typical responses | +| Memory at rest | <150MB | +| RAG indexing speed | ~100 docs/second | +| Retrieval latency | <50ms for top-5 | -### 12.2 Pruebas requeridas +### 12.2 Required tests -- Unit tests: cobertura ≥70% -- Integration tests: con mock LLM + mock ChromaDB -- E2E: al menos un flujo completo Astro → chat-bot +- Unit tests: coverage ≥70% +- Integration tests: with mock LLM + mock ChromaDB +- E2E: at least one complete Astro → chat-bot flow --- -## 🔒 13. Seguridad +## 🔒 13. Security -### 13.1 Implementado +### 13.1 Implemented -- **Rate limiting** por IP (default 30 req/min) -- **CORS restrictivo** — solo origins configurados -- **Input validation** — JSON schema validation en requests -- **No PII storage** — no guardamos conversaciones por default -- **Local-only por default** — sin llamadas a APIs cloud +- **Rate limiting** per IP (default 30 req/min) +- **Restrictive CORS** — only configured origins +- **Input validation** — JSON schema validation on requests +- **No PII storage** — we don't save conversations by default +- **Local-only by default** — no calls to cloud APIs -### 13.2 Diferido / Opcional +### 13.2 Deferred / Optional -- Auth con API key (para uso privado) -- Logging de queries para analytics -- Anonymization de IPs en logs +- Auth with API key (for private use) +- Query logging for analytics +- IP anonymization in logs - HTTPS via reverse proxy (Caddy/nginx) --- -## 📚 14. Referencias +## 📚 14. References - **SSE Spec:** https://html.spec.whatwg.org/multipage/server-sent-events.html - **Ollama API:** https://github.com/ollama/ollama/blob/main/docs/api.md @@ -1038,4 +1040,4 @@ chat-bot/ --- -**Documento listo para implementación. 🚀** \ No newline at end of file +**Document ready for implementation. 🚀** \ No newline at end of file