Files
reformix-hackaton/mvp/Whatsapp-bot

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
WhatsApp 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_NUMBER es 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_audio en OpenRouter. El resto del pipeline sigue usando Claude.
  • Si MODEL está definido, sirve como fallback para MODEL_GENERADOR cuando 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:

  1. Limpiar leads inactivos: Marca como perdido los leads en en_proceso sin actualización > 48h.
  2. Apertura de leads nuevos: Busca leads con estado_actual = 'nuevo', los marca como en_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_audio usando Gemini 2.5 Flash (modelo configurable via MODEL_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_url usando 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.
  • 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_baileys y 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 mensajeFallback segú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