274 lines
10 KiB
Markdown
274 lines
10 KiB
Markdown
# 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. Toda la persistencia va por **API HTTP** contra la app principal (`POST /api/leads/:id/perfil`, `conversacion`, etc.), no escribe a Postgres directamente.
|
|
|
|
---
|
|
|
|
## Stack
|
|
|
|
| Capa | Tecnología |
|
|
|------|-----------|
|
|
| **Framework** | NestJS 10 |
|
|
| **WhatsApp** | Baileys 7 (`@whiskeysockets/baileys`) + `baileys-antiban` |
|
|
| **Persistencia** | API HTTP contra `REFORMIX_API_URL` con `Authorization: Bearer` |
|
|
| **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
|
|
│ ├── api/
|
|
│ │ ├── api-client.service.ts ← Cliente HTTP para endpoints de la app Reformix
|
|
│ │ └── api.module.ts
|
|
│ ├── 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
|
|
│ │ └── leads.service.ts ← Máquina de estados, viabilidad (sin BD)
|
|
│ ├── conversacion/
|
|
│ │ ├── conversacion.module.ts
|
|
│ │ └── conversacion.service.ts ← Historial via API HTTP
|
|
│ ├── 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
|
|
│ └── webhook/
|
|
│ ├── webhook.module.ts
|
|
│ └── webhook-listener.ts ← Servidor HTTP para recibir señales de la app
|
|
├── .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 │
|
|
│ • Verificar lead en sesión │
|
|
│ (llega via webhook, no por │
|
|
│ teléfono) │
|
|
│ • Si audio → transcripción │
|
|
│ (Gemini 2.5 Flash via │
|
|
│ OpenRouter) │
|
|
│ • Si imagen → Vision │
|
|
│ (Claude Sonnet via │
|
|
│ OpenRouter) + enviar a │
|
|
│ /ingesta │
|
|
│ • Si texto → directo │
|
|
│ • Guardar mensaje en │
|
|
│ /conversacion (API HTTP) │
|
|
└───────────┬───────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ CAPA 1: CLASIFICADOR (Haiku) │
|
|
└───────────┬───────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ CAPA 2: VALIDADOR (código) │
|
|
└───────────┬───────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ CAPA 3: GENERADOR (Sonnet) │
|
|
└───────────┬───────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ CAPA 4: REGLAS (Haiku) │
|
|
└───────────┬───────────────────┘
|
|
↓
|
|
Guardar respuesta en /conversacion (API HTTP)
|
|
↓
|
|
Persistir datos en /perfil (API HTTP)
|
|
↓
|
|
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
|
|
API_BASE_URL=https://reformix.dv3.com.es # (REQUERIDA) URL de la app Reformix
|
|
FUNNEL_API_KEY= # (REQUERIDA) API key compartida
|
|
WEBHOOK_PORT=3001 # (OPCIONAL) Puerto para webhooks entrantes
|
|
ALLOWED_NUMBER= # (OPCIONAL) Restringe el bot a un solo número
|
|
```
|
|
|
|
**Notas:**
|
|
- `API_BASE_URL` + `FUNNEL_API_KEY` reemplazan a la antigua `DATABASE_URL`. El bot ya no escribe a Postgres directamente.
|
|
- `WEBHOOK_PORT` define dónde escucha el servidor HTTP para recibir señales de la app (`/whatsapp-start`, `/whatsapp-pdf`).
|
|
- Una vez escaneado el QR, Luisa queda en espera. La app le enviará leads vía `WHATSAPP_START_WEBHOOK_URL`.
|
|
|
|
---
|
|
|
|
## Configuración rápida
|
|
|
|
### 1. Variables de entorno
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edita `.env` con tus valores reales.
|
|
|
|
### 2. Prompts de Luisa
|
|
|
|
Los 3 archivos de prompts ya están creados y contienen la configuración completa. Puedes modificarlos para ajustar el tono o comportamiento de Luisa.
|
|
|
|
### 3. 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 escuchando webhooks de la app (por defecto en puerto 3001).
|
|
|
|
---
|
|
|
|
## Máquina de estados del lead
|
|
|
|
| Estado | Descripción |
|
|
|--------|-------------|
|
|
| `nuevo` | Lead creado, aún no contactado |
|
|
| `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` | Lead viable (presupuesto >= 5000€) |
|
|
| `fin_no_viable` | Lead no viable (presupuesto < 5000€) |
|
|
|
|
### Datos recolectados por estado
|
|
|
|
| Estado | Campo perfil | Valores válidos |
|
|
|--------|-------------|-----------------|
|
|
| `espacio` | `espacio` | `cocina`, `bano`, `salon`, `comedor`, `integral`, `otro` |
|
|
| `tamano` | `rangoM2` | `menos10`, `10a20`, `20a40`, `mas40` |
|
|
| `estilo` | `estilo` | `funcional`, `cuidado`, `exclusivo` |
|
|
| `urgencia` | `urgencia` | `alta`, `media`, `baja` |
|
|
| `presupuesto` | `presupuestoDeclarado` | Cifra o rango en euros |
|
|
|
|
---
|
|
|
|
## Cómo se conecta con la app
|
|
|
|
```
|
|
App Reformix Bot (este proyecto)
|
|
│ │
|
|
│ POST /webhook/whatsapp-start │
|
|
│ { leadId, telefono, nombre, empresa }────►│ Guarda sesión
|
|
│ │
|
|
│ │ Cliente escribe a Luisa
|
|
│ │
|
|
│ ◄── POST /api/leads/:id/conversacion ──── │ Guarda turno
|
|
│ ◄── POST /api/leads/:id/perfil ────────── │ Actualiza datos
|
|
│ ◄── POST /api/leads/:id/intento ───────── │ Registra contacto
|
|
│ ◄── POST /api/leads/:id/ingesta ───────── │ Sube fotos del lead
|
|
│ │
|
|
│ POST /webhook/whatsapp-pdf │
|
|
│ { leadId, telefono, pdfBase64 }──────────►│ Envía PDF al cliente
|
|
```
|
|
|
|
## Flujo de webhooks
|
|
|
|
| Webhook | Dirección | Puerto por defecto |
|
|
|---------|-----------|-------------------|
|
|
| `WHATSAPP_START_WEBHOOK_URL` | App → Bot | `http://bot:3001/whatsapp-start` |
|
|
| `WHATSAPP_WEBHOOK_URL` | App → Bot | `http://bot:3001/whatsapp-pdf` |
|
|
|
|
Configurar en el `.env` de la app Reformix.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## 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**.
|
|
- La transcripción conserva coloquialismos y jerga madrileña.
|
|
|
|
### Imagen
|
|
- Se descarga el buffer y se envía a OpenRouter con `image_url` usando **Claude Sonnet**.
|
|
- Además se envía a `/ingesta` de la app Reformix para persistirla.
|
|
- Si la imagen tiene caption, se combina con la inferencia.
|
|
|
|
---
|
|
|
|
## Manejo de errores y reconexión
|
|
|
|
- Reconexión automática a WhatsApp tras 5 segundos (excepto logout).
|
|
- Cada mensaje se procesa en un bloque `try/catch`.
|
|
- Si Claude falla al clasificar, se usa un fallback conservador.
|
|
- Las llamadas a la API de Reformix son **best-effort** (nunca lanzan error, loguean y continúan).
|
|
|
|
---
|
|
|
|
## 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 lint # Ejecutar ESLint
|
|
npm run test # Ejecutar tests con Jest
|
|
```
|
|
|
|
---
|
|
|
|
## Desarrollo
|
|
|
|
### Requisitos
|
|
|
|
- Node.js >= 20
|
|
- Cuenta en [OpenRouter](https://openrouter.ai) con API key
|
|
- App Reformix corriendo con `FUNNEL_API_KEY` configurada
|
|
|
|
---
|
|
|
|
Desarrollado para Reformix © 2026
|