# 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` ```env 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 ```bash cp .env.example .env ``` Edita `.env` con tus valores reales: ```env 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: ```bash 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 ``, 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 ```bash 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 ```bash 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](https://openrouter.ai) con API key ### Tests ```bash npm run test npm run test:watch # Modo watch npm run test:cov # Con cobertura ``` --- Desarrollado para Reformix © 2025