Volver al Blog
IA & ML · 16 min lectura

WhatsApp Agent: IA Local-First con Fallback Cloud

Cómo construimos un agente WhatsApp que escucha notas de voz, mira imágenes, genera audio y dibujos, embebe en local con EmbeddingGemma 300M y decide entre LM Studio, WebLLM en WebGPU o proxy cloud — sin que el usuario tenga que pensar en ello. Y cómo encaja con resonia.cadences.app y vilasenvento.cadences.app para formar el stack conversacional completo de Cadences.

Gonzalo Monzón

Gonzalo Monzón

· Fundador & CTO

Local-first AI on desktop
Electron LM Studio WebLLM ONNX Whisper Flux SQLite

Casi todos los agentes "IA para WhatsApp" del mercado son lo mismo: un servidor centralizado al que envías el número de teléfono y la conversación, y que decide por ti — con tu cuenta, en su data center, leyendo tus mensajes. Es cómodo, pero rompe el modelo mental que importa: WhatsApp es privado, tu agente también debería serlo.

El planteamiento: una app Electron que abre WhatsApp Web en su propio Chromium controlado por Playwright, persiste todo en SQLite local, y monta una pila de IA local-first con cinco servicios — LLM, embeddings, STT, TTS, ITT y TTI — donde lo local se prefiere y la nube es fallback, no requisito.

Arquitectura

Cinco servicios de IA, dos modos de ejecución

El agente tiene cinco capacidades multimedia y cada una elige independientemente si corre en local o en la nube. La regla es la misma para todas: si hay recurso local, se usa; si no, fallback a la nube, sin pedir API keys al usuario.

Capacidad Local primero Fallback nube Trigger automático
LLM LM Studio :1234/v1 · WebLLM (MLC) en WebGPU DeepSeek · Groq · Gemini · Cloudflare AI Cada mensaje entrante
Embeddings EmbeddingGemma-300M ONNX q8 (768 dim) — (siempre local) Indexado de chats y docs
STT (roadmap) Whisper ONNX in-process Cloudflare Whisper · Groq · OpenAI Nota de voz entrante
TTS — (calidad insuficiente local) Cloudflare MeloTTS (gratis) Usuario pide "dímelo con voz"
ITT (visión) — (modelos VLM aún muy pesados) Gemini · GPT-4o · Cloudflare LLaVA Imagen entrante
TTI — (no factible en CPU del usuario) Cloudflare Flux (gratis) con seed "haz una imagen de..."

Para los servicios que tienen variante local plausible (LLM y embeddings), la decisión se toma por petición. Para los demás, vamos al proxy de Cadences, que abstrae cinco proveedores detrás de una sola URL. Esto resuelve el problema más aburrido de cualquier agente multimedia: no pedirle al usuario seis API keys distintas para que su asistente funcione.

LLM Router

LM Studio, WebLLM o proxy: la decisión en 4 ramas

El cliente LLM detecta cada 30 segundos si hay un endpoint compatible OpenAI corriendo en localhost:1234. Si lo hay, lista los modelos y elige el primero. Si no, va al proxy cloud con el modelo por defecto del proveedor seleccionado. WebLLM es un caso aparte: corre dentro del proceso renderer del Electron, sobre WebGPU, y los modelos viven en Cache Storage tras la primera descarga.

llmClient.chat() — routing
// 1. ¿Hay LM Studio escuchando? (cache 30s)
const detected = await this.detect();   // GET :1234/v1/models

if (provider === 'local' && detected.ok) {
  return await _chatViaLocal(messages, detected.models[0]);
}

// 2. WebLLM corre en el renderer (IPC bridge)
if (provider === 'webllm') {
  return await _chatViaWebllm(messages, model);
}

// 3. Proxy cloud — y aquí el truco anti-foot-gun:
//    si por error nos llega un model MLC para una llamada cloud,
//    lo cambiamos al default del provider en lugar de fallar.
if (model && isWebllmModel(model)) {
  model = getProvider(provider).defaultModel;
}
return await _chatViaCadences({ messages, model, provider });

El proxy cloud vive en cadences.pages.dev/api/ai-providers/chat. Recibe un provider + model, resuelve la API key del lado servidor (jamás baja al cliente) y devuelve el chat completion estándar OpenAI. Los seis proveedores soportados son los mismos que usa el resto del ecosistema:

Provider Modelo por defecto Cuándo brilla
DeepSeekdeepseek-chatDefault. Calidad/€ imbatible en español
Groqllama-3.3-70b-versatileLatencia: ~250 ms primera token
Geminigemini-2.0-flashContexto largo + multimodal
Cloudflare AI@cf/meta/llama-3.1-8b-instruct-fp8-fastTier gratuito generoso
LM StudioAuto-detectado /v1/modelsPrivacidad absoluta + sin Internet
WebLLMQwen3.5-2B-q4f16_1-MLCCero instalación, corre en WebGPU
Pipeline multimedia

STT, TTS, ITT y TTI: enriquecimiento automático del mensaje

La idea operativa es que el LLM nunca debe verse limitado por el formato del mensaje entrante. Si llega una nota de voz, se transcribe y entra al prompt como texto. Si llega una foto, se describe y entra como texto. El LLM responde una sola vez, pero el agente puede contestar con audio o imagen si el usuario lo pide.

🎤

STT — Transcripción automática de notas de voz

El messageWatcher detecta type === 'audio', extrae el blob OGG/Opus desde el DOM, lo manda al proxy de Cadences que enruta a Cloudflare Whisper (1500 req/día gratis) o Groq Whisper si el primero está saturado. La transcripción se inyecta en el evento message como transcription.text, y el autoReplyService la usa como query sin que ninguna otra capa se entere de que era audio.

🖼️

ITT — Análisis de imágenes entrantes

Mismo patrón con imágenes: extracción de blob, base64, llamada a Gemini Vision (preferido) o LLaVA-1.5 de Cloudflare como fallback. Prompt fijo: "Describe esta imagen en detalle. Si contiene texto, transcríbelo. Si es un documento, ticket o factura, extrae los datos relevantes." Esto resuelve sin extra coste el caso "te envío la factura por foto" — habitual en pymes y autónomos que llevan WhatsApp como CRM real.

🔊

TTS — Respuesta por audio cuando se pide

Un detector regex en el mensaje entrante busca "dímelo con voz", "mándame un audio", "responde por audio". Si dispara, el LLM genera la respuesta normalmente y después MeloTTS (Cloudflare) sintetiza el MP3. Se encolan dos mensajes: texto primero (por accesibilidad y log), audio después. El flag es local al scope de la petición — no hay estado compartido entre contactos, así que dos personas pidiendo audio simultáneamente no se pisan.

🎨

TTI — Generación de imágenes con memoria de seed

Detectamos peticiones del tipo "hazme una imagen de...", "dibuja...", "genera un logo de...". Llamada a Cloudflare Flux vía proxy, con seed opcional. Truco clave: tras enviar la imagen, el agente guarda el {prompt, seed, model, aspectRatio} en un Map<phone, ...> con ventana de 30 min. Si el usuario contesta "modifícala", "igual pero más oscura", "estilo cómic" o "ahora con un perro", se detecta como modificación, se combina con el prompt original y se reusa la seed. Es la diferencia entre "una imagen aleatoria" y "una conversación visual".

Embeddings locales

EmbeddingGemma-300M sobre ONNX: 768 dimensiones, 0 dependencias cloud

Aquí es donde el "local-first" deja de ser slogan y se vuelve infraestructura. El agente carga onnx-community/embeddinggemma-300m-ONNX quantizado a 8 bits (~338 MB en disco, ~1.4 GB en RAM) usando @huggingface/transformers. Es el mismo runtime que usaría Transformers.js en el navegador, pero corriendo en Node dentro del Electron, lo que permite usar SIMD nativo en CPU sin tener que pelear con WASM threading.

Latencia real

Cold load (primera llamada): ~7s mientras se mapea el modelo a RAM. Warm: ~26 ms por frase. Suficiente para indexar conversaciones en background sin bloquear UI.

💾

Persistencia

Vectores guardados como BLOB Float32 en SQLite. 3072 bytes por embedding. Una conversación de 10k mensajes ocupa ~30 MB de vectores — perfectamente manejable sin sqlite-vec.

🔍

Búsqueda híbrida

BM25 (FTS5 de SQLite) + KNN coseno in-memory, fusionados con Reciprocal Rank Fusion. ~15 ms para sweeps de hasta 50k chunks. Sin GPU, sin Pinecone, sin Qdrant.

🛡️

Privacidad real

Tras la primera descarga del modelo, el indexado de tus chats nunca sale del equipo. Funciona en avión. El backend de Cadences ignora por completo qué contenido tienes.

Esto alimenta un sistema de contexto en cuatro capas que el contextBuilder ensambla antes de cada llamada al LLM:

Capa Contenido Presupuesto
L0 — IdentidadQuién es el agente, beliefs, vocabulario, estilo~1200 tok
L0.5 — PersonaTono concreto: casual / formal / welcome / short~200 tok
L1 — Memoria contactoTop-N chunks por importancia × recencia~400 tok
L1-CrossHistoria 1↔1 de remitentes activos del grupo~400 tok
L3 — RAG híbridoBM25 + KNN sobre toda la base, RRF~600 tok
Clima conversacional

Las 10 capas de ResonIA, también en texto

ResonIA es nuestro estudio de QA conductual para llamadas: subes un audio y obtienes un mapa temporal de 10 capas semánticas — Intensidad, Fricción, Bucle, Handoff, Reparación, Apertura, Retirada, Dominancia, Sincronía, Críticos — más hallazgos como "Bucle de intención no resuelta" o "Contexto incompleto antes del handoff". Sin diagnosticar a las personas, sin juzgar: mostrando evidencia para que el equipo decida.

Captura de ResonIA mostrando las 10 capas conductuales sobre una conversación real
Así se ven las 10 capas en ResonIA: cada fila es una dimensión conductual, segmentada por tramos temporales de la llamada. El mismo vocabulario es el que reutilizamos como heurística rápida dentro del WhatsApp Agent.

En el WhatsApp Agent reutilizamos la misma tipología de 10 capas, pero adaptada a texto y sin LLM por mensaje (sería caro). Una capa heurística — conversationClimate.js — corre regex y léxico sobre el mensaje actual, puntúa cada capa con un peso, y si la dominante supera un threshold (≥14) inyecta una directiva al prompt antes del LLM:

Ejemplo de directiva inyectada
// Mensaje del usuario detectado como capa 'loop' (repetición)
// Score: 18, dominante

"El cliente está repitiendo la misma intención por tercera vez."
"Reconoce explícitamente que has entendido qué quiere antes"
"de proponer cualquier acción. No vuelvas al menú general."

Es deliberadamente conservador: no pretende sustituir el análisis de ResonIA (que sí usa prosodia, diarización y LLM dedicado), sino dar al agente un instinto básico para no caer en los patrones que ResonIA ya identifica como destructivos en llamadas. La misma teoría conversacional, dos puntos de aplicación: ResonIA observa post-hoc, el WhatsApp Agent actúa en tiempo real.

Stack conversacional

Vila Sen Vento, ResonIA y WhatsApp Agent: una misma idea, tres canales

vilasenvento.cadences.app es la vitrina pública del modelo. Una tienda demo de despensa artesanal gallega cerca de Santiago, pensada como prueba de concepto comercial: asistente de texto en streaming (SSE) con DeepSeek V4 + function calling (5 tools), asistente de voz con ElevenLabs Conversational AI, y handoff voz↔chat (si cuelgas la llamada, el chat retoma la conversación con todo el contexto). RAG sobre catálogo, memoria entre sesiones, OTP + Twilio para callbacks programados.

Las tres piezas resuelven el mismo problema desde ángulos distintos:

🌐

Vila Sen Vento — el cliente nuevo te encuentra en la web

Astro estático en Cloudflare Pages, chat SSE + voz ElevenLabs. Optimizado para descubrimiento (SEO, Open Graph) y conversión inmediata. Lo que un visitante anónimo prueba en su primera sesión.

💬

WhatsApp Agent — el cliente conocido te escribe por WhatsApp

Electron en el escritorio del dueño/equipo, memoria local persistente. El mismo modelo conversacional que en Vila Sen Vento, ahora con historia real del contacto, capacidades multimedia completas y privacidad absoluta de la base de datos.

📊

ResonIA — el equipo aprende qué funciona y qué no

React + Whisper local + DeepSeek. Subes la grabación de una llamada (voicebot, humana o mixta) y obtienes el mapa de 10 capas + hallazgos accionables. Cierra el ciclo: lo que ResonIA detecta en las llamadas mejora los prompts del WhatsApp Agent y del chat de Vila Sen Vento.

Lecciones

Tres cosas que aprendimos construyéndolo

1. Local-first no es "todo en local"

Forzar STT/TTS/TTI/ITT en local hoy es una mala decisión: los modelos buenos no caben en el equipo del usuario medio, y los que caben dan resultados pobres. La línea correcta es: lo que se puede correr local con calidad aceptable (LLM ligero, embeddings) se hace local; lo demás va al proxy. Y el usuario nunca ve la diferencia.

2. El proxy resuelve el problema de las API keys

Pedirle a un autónomo que configure cinco API keys para que su WhatsApp responda solo es no haber entendido el producto. El proxy de Cadences abstrae DeepSeek, Groq, Gemini, Cloudflare AI, MeloTTS, Whisper, Flux y Vision detrás de una URL única, con resolución de claves en backend. El usuario configura cero claves y todo funciona.

3. La memoria es lo que convierte un chatbot en un agente

Sin embeddings persistentes, sin memoria de seed para iterar imágenes, sin tipología conversacional para no caer en bucles — tienes un chatbot. Con ellos, tienes algo que recuerda al cliente, sabe qué le ha enviado antes, y entiende que "modifícala más oscura" se refiere a la imagen anterior y no requiere generar otra desde cero. La diferencia no se ve en el demo. Se ve en el uso real, a los tres días.

¿Cómo lo pruebas?

Vila Sen Vento está abierto en vilasenvento.cadences.app — chatea o haz una llamada. ResonIA en resonia.cadences.app acepta tus audios privados (procesamiento local, no se suben). El WhatsApp Agent es la app Electron descargable desde cadences.app, pensada para autónomos y pymes que quieren convertir su WhatsApp en un canal con IA real sin perder la propiedad de los datos.

Newsletter

No te pierdas ninguna historia

Suscríbete para recibir nuevos lanzamientos, capítulos exclusivos y contenido detrás de cámaras.

  • Insights y artículos semanales
  • Contenido exclusivo y acceso anticipado
  • Sin spam, cancela cuando quieras

Respetamos tu privacidad. Puedes darte de baja cuando quieras.