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_City fijado en el contenedor; la hora de disparo se calcula además con zoneinfo de forma explícita en bot.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 en 127.0.0.1:11434. Modelo qwen2.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:

TagSignificado
#HH:MMHora de envío (24h)
#hoyEnviar hoy
#mañanaEnviar 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 → antepone 52; si ya trae 52/+52 lo deja igual; convierte el viejo formato 521XXXXXXXXXX (WhatsApp México pre-2021) a 52XXXXXXXXXX.

Véase también

  • Docker — por qué network_mode: host en este contenedor
  • Glances — mismo host, mismo patrón de Docker
  • Trilium — ídem