Chatbot de ventas y soporte con RAG 100% local: responde desde tu propia documentación, deriva a un humano cuando no sabe, y trae bandeja de conversaciones en vivo y un CRM básico.
Sin costo por mensaje y sin que los datos salgan de tu servidor.
Widget web → FastAPI → ┌─ embeddings ──→ Ollama (embeddinggemma)
├─ búsqueda ────→ SQLite + sqlite-vec + FTS5
└─ respuesta ───→ Ollama (gemma3:4b)
↓
Panel: bandeja · CRM · contenido · métricas
- RAG sobre tus documentos en Markdown, con reindexado desde el panel.
- Búsqueda híbrida: vectorial + léxica (FTS5), fusionadas con Reciprocal Rank Fusion.
- Guardarraíles: no inventa precios ni datos; si no sabe, deriva a una persona.
- Chat en vivo: un operador toma la conversación y el bot se calla.
- CRM: contactos con etapas, notas e historial, editables desde el propio chat.
- Panel web con autenticación, métricas y edición del contenido.
- Widget autónomo sin dependencias, para pegar en cualquier sitio.
- Linux con Ollama
- Python 3.11+
- GPU con 4 GB de VRAM basta (
gemma3:4b). También corre en CPU, más lento.
ollama pull embeddinggemma
ollama pull gemma3:4bgit clone https://github.com/leoses03/ChatBotIA-Local-RAG.git
cd ChatBotIA-Local-RAG
curl -LsSf https://astral.sh/uv/install.sh | sh # sin sudo
uv sync
cp .env.example .env # edita BRAND, ADMIN_PASSWORD y los tokens
python -c "import secrets; print(secrets.token_urlsafe(32))" # para los secretos
uv run python ingest.py # indexa corpus/
uv run uvicorn app:app --host 127.0.0.1 --port 8000Panel en http://127.0.0.1:8000/admin.
# ~/.config/systemd/user/chatbot-rag.service
[Unit]
Description=ChatBot IA Local RAG
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/ChatBotIA-Local-RAG
ExecStart=%h/.local/bin/uv run uvicorn app:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=default.targetloginctl enable-linger $USER # sobrevive reinicios
systemctl --user enable --now chatbot-ragCon Cloudflare Tunnel,
sin abrir puertos en el router. Ajusta ALLOWED_ORIGIN al dominio de tu sitio.
<script>window.CHAT_API = "https://chat.tu-dominio.com";</script>
<script src="/ruta/chat-widget.js" defer></script>corpus/*.md— tu contenido. Es lo que más importa: el bot solo sabe lo que está ahí..env—BRAND,BRAND_DESC,BRAND_REGIONy el resto de la configuración.prompt.py— tono y reglas.widget/chat-widget.js— colores y tipografías (arriba del archivo).
Estos puntos costaron horas de depuración. Si vas a tocar el código, léelos.
Devuelve distancia L2 sobre vectores sin normalizar. Medido con embeddinggemma:
| Caso | Distancia |
|---|---|
| Acierto claro | 0.84 – 0.98 |
| Acierto justo (pregunta corta) | ~1.10 |
| Fuera de tema | 1.12 – 1.27 |
Con un umbral de 0.75 (asumiendo escala 0-1) todo se derivaba. Si cambias de modelo de embeddings, vuelve a medir: hay un script de diagnóstico en el panel.
Buscar "planes" no encontraba "Plan Pro". Por eso la consulta léxica usa prefijos de
4 letras con comodín (plan*). Sin esto, la pregunta "¿qué planes tienen?" no
recuperaba el documento de planes y el modelo inventaba nombres de planes.
Moraleja general: en un RAG, una alucinación casi siempre es un fallo de recuperación, no del prompt.
Se midió: una consulta válida daba -8.5 y una fuera de tema -4.2. No separa. La
búsqueda léxica mejora qué se recupera; no decide si hay respuesta. Usarla para
eso dejó al bot sin derivar nunca.
La derivación usa dos señales independientes: la distancia vectorial, y detectar cuándo el
modelo mismo admite que no puede ayudar (NEEDS_HUMAN_RE).
Describir un plan como "todo lo del Esencial más X" hacía que el modelo mezclara y le atribuyera funciones equivocadas. Al listar cada plan por completo, se corrigió. Con modelos pequeños, evita contenido que obligue a inferir.
El prompt puede prohibir el voseo, pero a un modelo de 4B se le escapa. prompt.clean_reply()
lo corrige tras generar: determinista y no depende de que el modelo obedezca.
"¿Cuánto sale?" no significa nada aislado y se derivaba sin motivo. Si el mensaje es corto y hay historial, la búsqueda concatena el mensaje anterior del visitante.
La bandeja se actualiza cada 5 s. La barra del CRM no se redibuja si el operador tiene el foco dentro, ni si nada cambió.
- El panel cambia lo que el bot responde a tus clientes: nunca lo expongas sin contraseña.
- Cookie de sesión firmada con HMAC,
HttpOnly+Secure, 8 horas. - Los nombres de archivo del corpus se validan contra path traversal.
- La configuración editable desde el panel excluye los secretos.
- CORS restringido a tu dominio y límite de mensajes por IP.
- Para una capa extra, pon Cloudflare Access delante de
/admin.
MIT