Reformix Luisa Bot 🤖
Agente de WhatsApp Luisa para Reformix — cualifica leads de reforma de forma conversacional siguiendo una máquina de estados de 7 pasos, recoge 5 datos clave (espacio, metros, estilo, urgencia, presupuesto) y cierra el flujo según el flag viable/no_viable.
Stack
| Capa | Tecnología |
|---|---|
| Framework | NestJS 10 |
Baileys 7 (@whiskeysockets/baileys) + baileys-antiban |
|
| Base de datos | PostgreSQL via TypeORM con synchronize: true (dev) / migrations (prod) |
| LLM | Claude 4.5 Sonnet/Haiku + Gemini 2.5 Flash via OpenRouter |
| Logging | Pino |
| QR | qrcode-terminal |
Estructura del proyecto
/
├── auth_info_baileys/ ← Estado de sesión de WhatsApp (se genera automáticamente)
├── dist/ ← Compilación
├── node_modules/
├── prompts/ ← Prompts del sistema para Claude
│ ├── luisa_core.md ← Identidad, personalidad y máquina de estados
│ ├── luisa_flujo.md ← Flujo de cualificación paso a paso
│ └── luisa_casos.md ← Casos edge y ejemplos
├── src/
│ ├── main.ts ← Punto de entrada
│ ├── app.module.ts ← Módulo raíz con TypeORM y Schedule
│ ├── whatsapp/
│ │ ├── whatsapp.module.ts
│ │ ├── whatsapp.service.ts ← Conexión Baileys, recepción/envío
│ │ └── whatsapp-debounce.service.ts ← Debounce de 3s para coalescer mensajes rápidos
│ ├── leads/
│ │ ├── leads.module.ts
│ │ ├── lead.entity.ts ← Entidad Lead con todos los campos
│ │ └── leads.service.ts ← CRUD, máquina de estados, viabilidad
│ ├── conversacion/
│ │ ├── conversacion.module.ts
│ │ ├── conversacion.entity.ts ← Entidad Conversacion (historial)
│ │ └── conversacion.service.ts ← Guardado y recuperación de historial
│ ├── claude/
│ │ ├── claude.module.ts
│ │ └── claude.service.ts ← Arquitectura de 4 capas con Claude
│ ├── media/
│ │ ├── media.module.ts
│ │ └── media.service.ts ← Transcripción de audio + análisis de imagen
│ └── scheduler/
│ ├── scheduler.module.ts
│ └── scheduler.service.ts ← Cron cada 5 min: apertura a leads nuevos + limpieza
├── .env.example
├── nest-cli.json
├── package.json
├── tsconfig.json
└── tsconfig.build.json
Arquitectura de procesamiento (4 capas con Claude)
Cada mensaje entrante pasa por 4 capas antes de responder:
Mensaje entrante (texto / audio / imagen)
↓
┌───────────────────────────────┐
│ PREPROCESAMIENTO │
│ • Identificar lead por teléfono│
│ • Si audio → transcripción │
│ (Gemini 2.5 Flash via │
│ OpenRouter) │
│ • Si imagen → Vision │
│ (Claude Sonnet via │
│ OpenRouter) │
│ • Si texto → directo │
│ • Guardar mensaje usuario en │
│ DB │
└───────────┬───────────────────┘
↓
┌───────────────────────────────┐
│ CAPA 1: CLASIFICADOR (Haiku) │
│ Extrae intención y valor │
│ del mensaje: │
│ • responde_pregunta: bool │
│ • valor_extraido: string|null │
│ • es_desvio: bool │
│ • intencion: respuesta | │
│ desvio | despedida | │
│ insulto | pregunta │
└───────────┬───────────────────┘
↓
┌───────────────────────────────┐
│ CAPA 2: VALIDADOR (código) │
│ Valida contra valores │
│ permitidos del estado actual │
│ • espacios: cocina, bano, │
│ salon, integral, otro │
│ • tamaño: menos10, 10a20, │
│ 20a40, mas40 │
│ • estilo: funcional, cuidado, │
│ exclusivo │
│ • urgencia: urgente, │
│ medio_plazo, frio │
│ • presupuesto: evalúa │
│ viabilidad (>= 5000€) │
└───────────┬───────────────────┘
↓
┌───────────────────────────────┐
│ CAPA 3: GENERADOR (Sonnet) │
│ Construye contexto y genera │
│ borrador de respuesta │
│ • Estado del lead │
│ • Datos capturados │
│ • Historial completo │
│ • Clasificación y validación │
│ • Reintentos │
└───────────┬───────────────────┘
↓
┌───────────────────────────────┐
│ CAPA 4: REGLAS (Haiku) │
│ Corrige el borrador para │
│ cumplir identidad de Luisa: │
│ • Sin menciones a IA │
│ • Máximo 2 líneas │
│ • Sin emojis │
│ • Tono Madrid/España │
└───────────┬───────────────────┘
↓
Guardar respuesta en DB
↓
Enviar por Baileys
Variables de entorno
.env.example
OPENROUTER_API_KEY= # (REQUERIDA) API key de OpenRouter
MODEL_GENERADOR=anthropic/claude-sonnet-4-5 # Modelo para generar respuestas (Capa 3)
MODEL_CLASIFICADOR=anthropic/claude-haiku-4-5 # Modelo para clasificar mensajes (Capa 1)
MODEL_REGLAS=anthropic/claude-haiku-4-5 # Modelo para aplicar reglas (Capa 4)
MODEL_TRANSCRIPCION=google/gemini-2.5-flash # Modelo para transcripción de audio
MODEL=anthropic/claude-sonnet-4-5 # Fallback general si no se especifica otro
DATABASE_URL= # (REQUERIDA) PostgreSQL connection string
ALLOWED_NUMBER= # (OPCIONAL) Restringe el bot a un solo número
Notas:
ALLOWED_NUMBERes opcional. Si se define, el bot solo atiende mensajes de ese número. Si se deja vacío, acepta mensajes de cualquier número de teléfono.- La transcripción de audio usa Gemini 2.5 Flash por defecto porque Claude no soporta
input_audioen OpenRouter. El resto del pipeline sigue usando Claude. - Si
MODELestá definido, sirve como fallback paraMODEL_GENERADORcuando no se especifica.
Configuración rápida
1. Variables de entorno
cp .env.example .env
Edita .env con tus valores reales:
OPENROUTER_API_KEY=sk-or-v1-tu-api-key-aqui
DATABASE_URL=postgresql://user:password@localhost:5432/reformix
2. Base de datos
El proyecto usa synchronize: true en modo desarrollo (definido en src/app.module.ts). TypeORM creará las tablas automáticamente al arrancar.
En producción, cambia synchronize: false y usa migrations:
npm run migration:generate -- src/migrations/Init
npm run migration:run
3. Prompts de Luisa
Los 3 archivos de prompts ya están creados y contienen la configuración completa:
prompts/luisa_core.md— Identidad de Luisa, personalidad (español de Madrid, cercana, directa), máquina de estados obligatoria con mensajes exactos por estado, extracción de datos con bloque JSON<DATOS_EXTRAIDOS>, manejo de casos especiales (desvíos, reintentos, inactividad, multimedia, tono defensivo), y ejemplos few-shot.prompts/luisa_flujo.md— Secuencia de estados, campos DB con valores permitidos, y mensajes por estado (versión simplificada).prompts/luisa_casos.md— Casos edge: desvíos, reintentos (máx 2), inactividad (24h/48h), manejo de audio/imagen/sticker, tono defensivo, usuario que no da presupuesto.
Puedes modificarlos libremente para ajustar el tono o comportamiento de Luisa.
4. Arrancar
npm install
npm run start:dev
Aparecerá un código QR en la terminal. Escanéalo con WhatsApp → WhatsApp Web.
Luisa queda conectada y lista para recibir mensajes.
Máquina de estados del lead
| Estado | Descripción |
|---|---|
nuevo |
Lead creado, aún no contactado |
en_proceso |
El scheduler le ha enviado el primer mensaje (transición interna) |
apertura |
Luisa se presenta y pregunta disponibilidad |
espacio |
Pregunta: ¿qué espacio quieres reformar? |
tamano |
Pregunta: ¿rango de metros cuadrados? |
estilo |
Pregunta: ¿tipo de acabado? |
urgencia |
Pregunta: ¿cuándo quieres empezar? |
presupuesto |
Pregunta: ¿presupuesto aproximado? |
fin_viable |
Transición interna → completado si presupuesto >= 5000€ |
fin_no_viable |
Transición interna → no_viable si presupuesto < 5000€ |
recopilando_datos |
Estado legacy, se normaliza a apertura |
completado |
Todos los datos recogidos y lead viable |
no_viable |
Lead descartado por presupuesto insuficiente |
perdido |
Sin actividad > 48h |
Secuencia completa: nuevo → apertura → espacio → tamano → estilo → urgencia → presupuesto → fin_viable/fin_no_viable
Datos recolectados por estado
| Estado | Campo DB | Valores válidos |
|---|---|---|
espacio |
espacio |
cocina, bano, salon, integral, otro |
tamano |
rango_m2 |
menos10, 10a20, 20a40, mas40 |
estilo |
estilo |
funcional, cuidado, exclusivo |
urgencia |
urgencia |
urgente, medio_plazo, frio |
presupuesto |
presupuesto_declarado |
Cifra o rango en euros |
Evaluación de viabilidad
Un presupuesto es viable si su valor numérico es >= 5000€. Si no se puede determinar el valor, se asume viable.
Scheduler (cron cada 5 min)
El servicio SchedulerService se ejecuta cada 5 minutos vía @nestjs/schedule y realiza dos tareas:
- Limpiar leads inactivos: Marca como
perdidolos leads enen_procesosin actualización > 48h. - Apertura de leads nuevos: Busca leads con
estado_actual = 'nuevo', los marca comoen_proceso, llama a Claude para generar el mensaje de apertura, lo guarda en el historial y lo envía por WhatsApp.
Debounce de mensajes
El servicio WhatsappDebounceService agrupa mensajes rápidos de un mismo usuario en una ventana de 3 segundos. Si el usuario envía varios mensajes cortos seguidos, se concatenan en un solo texto antes de procesarlos. Esto evita que Claude reciba múltiples requests simultáneas por el mismo lead.
Restricción por número (ALLOWED_NUMBER)
Si se define ALLOWED_NUMBER en .env, el bot ignora cualquier mensaje que no venga de ese número. Esto es útil para pruebas o para desplegar una instancia dedicada a un solo cliente.
Soporte multimedia
Audio
- Se descarga el buffer del mensaje de audio via Baileys.
- Se detecta el formato real por magic bytes (Ogg, MP3, WAV).
- Se envía a OpenRouter con
input_audiousando Gemini 2.5 Flash (modelo configurable viaMODEL_TRANSCRIPCION). - La transcripción conserva coloquialismos y jerga madrileña.
- Si falla o el buffer es muy pequeño, se responde con un mensaje de cortesía pidiendo repetición.
Imagen
- Se descarga el buffer y se envía a OpenRouter con
image_urlusando Claude Sonnet. - El prompt de análisis varía según el estado actual del lead:
- En
espacio/tamano: infiere tipo de espacio y metros cuadrados. - En
estilo: infiere el estilo o calidad de acabados. - En otros estados: descripción general.
- En
- Si la imagen tiene caption, se combina con la inferencia.
Manejo de errores y reconexión
- Si la conexión de WhatsApp se cierra por cualquier motivo que no sea logout, se reintenta automáticamente tras 5 segundos.
- Si hay un logout (sesión cerrada), se muestra un error pidiendo eliminar
auth_info_baileysy reiniciar. - Cada mensaje se procesa en un bloque
try/catch; si falla, se loguea el error y se continúa con el siguiente mensaje. - Si Claude falla repetidamente al clasificar, se usa un fallback conservador (desvío) para no bloquear la conversación.
- Si la respuesta generada contiene frases prohibidas ("soy un asistente", "ChatGPT", etc.), se reemplaza automáticamente con un
mensajeFallbacksegún el estado actual.
Scripts disponibles
npm run build # Compilar con NestJS
npm run start # Iniciar en producción
npm run start:dev # Iniciar en desarrollo con watch
npm run start:debug # Iniciar en modo debug
npm run lint # Ejecutar ESLint
npm run test # Ejecutar tests con Jest
npm run migration:generate # Generar migración de TypeORM
npm run migration:run # Ejecutar migraciones pendientes
Qué NO hace este servicio
- ❌ No genera el presupuesto (lo hace otro worker externo)
- ❌ No renderiza el PDF
- ❌ No envía la URL del presupuesto (la inserta el worker en
url_presupuesto) - ❌ No tiene panel del reformista
- ❌ No maneja conversaciones grupales (@g.us) — se ignoran explícitamente
Desarrollo
Requisitos
- Node.js >= 20
- PostgreSQL >= 14
- Cuenta en OpenRouter con API key
Tests
npm run test
npm run test:watch # Modo watch
npm run test:cov # Con cobertura
Desarrollado para Reformix © 2025