Wasquedul (wa-scheduler)
Bot de Telegram (@Waschedule54_bot) que programa avisos de WhatsApp y tiene personalidad conversacional vía un LLM local. Corre en Docker con network_mode: host en vm-playground — mismo patrón que usan Glances y Trilium en este host, aquí para poder llegar a Ollama en 127.0.0.1:11434 del propio host sin exponerlo en ninguna IP de red.
El bot nunca manda el WhatsApp directo: a la hora programada entrega un link wa.me/<numero>?text=<mensaje> listo, y el envío final lo hace Esteban tocando el link y presionando enviar. No usa la API oficial de WhatsApp. Solo responde al OWNER_ID configurado.
Stack
- Python 3.12 + python-telegram-bot (long polling, sin webhook).
- Sin persistencia — todo vive en memoria del proceso. Un reinicio del contenedor borra tanto los envíos programados como el historial de charla.
TZ=America/Mexico_Cityfijado en el contenedor; la hora de disparo se calcula además conzoneinfode forma explícita enbot.py, independiente de la TZ del sistema.- Ollama nativo en el host (no en Docker, para no competir por los recursos limitados de la VM), como servicio systemd (
ollama.service, enabled), bindeado únicamente en127.0.0.1:11434. Modeloqwen2.5:0.5b— chico, corre bien en 1 OCPU sin GPU; la calidad de respuesta es limitada por el tamaño del modelo, es lo esperable.
Dos formas de programar un envío
Flujo clásico por botones: se comparte un contacto de Telegram o se escribe un número mexicano de 10 dígitos directamente (con o sin 52) — se detecta solo, sin comando. El bot pregunta el mensaje, luego la hora si no vino incluida como tag, y confirma con botones inline (✅ Confirmar / ✏️ Editar hora / ❌ Cancelar) antes de agendar. El mensaje admite tags en cualquier parte del texto, que se extraen por regex y no llegan a WhatsApp:
| Tag | Significado |
|---|---|
#HH:MM | Hora de envío (24h) |
#hoy | Enviar hoy |
#mañana | Enviar mañana (default si no se especifica día) |
#2d, #3d, … | Enviar en N días |
Flujo por lenguaje natural: se le puede pedir directo en una sola frase (ej. “mándale a 5511223344 que ya casi llego, mañana a las 3pm”) y el LLM decide invocar una herramienta — programar_whatsapp (extrae teléfono, mensaje y fecha/hora, y siempre muestra un botón de confirmación con lo que entendió antes de agendar) o listar_programados (responde qué hay pendiente). Este paso de confirmación es obligatorio, no cosmético: un modelo de 0.5B a veces reformula el mensaje en vez de mandarlo tal cual, o calcula mal fechas relativas. También existe un fallback por palabras clave (programad, pendient, agendad) para cuando el modelo no invoca listar_programados aunque se le pregunte directamente. Los dos flujos coexisten sin cambios entre sí — el clásico de botones sigue funcionando igual, independiente del LLM.
En corto: la extracción de datos con un modelo tan chico no es 100% confiable; el diseño asume eso y siempre pide confirmación humana antes de programar un envío.
Personalidad LLM (chat libre)
Cualquier texto libre que no dispare ni pertenezca a una conversación activa de programación se reenvía al LLM local con un system prompt de personalidad casual en español, y la respuesta vuelve como mensaje normal de Telegram. El system prompt incluye el conteo de pendientes en cada turno, para que el bot tenga contexto ambiente sin tener que preguntar. Si Ollama no está disponible, el resto del bot sigue funcionando normal para programar wa.me — el chat libre simplemente responde con un mensaje de error genérico.
Variables de entorno (.env)
TG_TOKEN= # token de BotFather
OWNER_ID= # chat ID de Telegram; el bot ignora a cualquier otro usuario
TZ=America/Mexico_City
# Opcionales, pisan los defaults del código:
#OLLAMA_URL=http://127.0.0.1:11434/api/chat
#OLLAMA_MODEL=qwen2.5:0.5b
.env está en .gitignore, nunca se commitea.
Despliegue
cd ~/wa-scheduler
cp .env.example .env # y llenar TG_TOKEN
docker compose up --build -d
Requiere Ollama instalado y corriendo en el host (curl -fsSL https://ollama.com/install.sh | sh, luego ollama pull qwen2.5:0.5b).
docker compose logs -f # ver logs
docker compose down # detener
Notas técnicas
clean_phone()normaliza el número: 10 dígitos → antepone52; si ya trae52/+52lo deja igual; convierte el viejo formato521XXXXXXXXXX(WhatsApp México pre-2021) a52XXXXXXXXXX.