diff --git a/docs/arquitectura-integracion.md b/docs/arquitectura-integracion.md index 118775d..4632043 100644 --- a/docs/arquitectura-integracion.md +++ b/docs/arquitectura-integracion.md @@ -519,10 +519,11 @@ El bot DEBE: El worker DEBE: -1. **Escuchar en la URL que configures como `PERFIL_WEBHOOK_URL`**. +1. **Escuchar en la URL que configures como `PERFIL_WEBHOOK_URL`** (POST /perfil-completo). 2. **Recibir el payload** con `leadId`, `cliente`, `reforma`, `zonas` (con fotos "antes"). -3. **Generar renders** para cada zona usando un modelo image-to-image. +3. **Generar renders** para cada zona: Etapa 1 (Claude Haiku → prompt), Etapa 2 (Gemini Flash → imagen), Etapa 3 (Claude Haiku Vision → validación). Todo via OpenRouter. 4. **Devolver los renders** llamando a `POST /api/leads/:id/ingesta` con: +5. **Autenticarse** con `Authorization: Bearer `. ```json { "items": [ diff --git a/mvp/Whatsapp-bot/README.md b/mvp/Whatsapp-bot/README.md index 35a291f..75b7dca 100644 --- a/mvp/Whatsapp-bot/README.md +++ b/mvp/Whatsapp-bot/README.md @@ -1,6 +1,6 @@ -# Reformix Luisa 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. +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. --- @@ -10,7 +10,7 @@ Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de |------|-----------| | **Framework** | NestJS 10 | | **WhatsApp** | Baileys 7 (`@whiskeysockets/baileys`) + `baileys-antiban` | -| **Base de datos** | PostgreSQL via TypeORM con `synchronize: true` (dev) / migrations (prod) | +| **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` | @@ -21,37 +21,38 @@ Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de ``` / -├── auth_info_baileys/ ← Estado de sesión de WhatsApp (se genera automáticamente) -├── dist/ ← Compilación +├── 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 +├── 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 +│ ├── 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 -│ │ ├── lead.entity.ts ← Entidad Lead con todos los campos -│ │ └── leads.service.ts ← CRUD, máquina de estados, viabilidad +│ │ └── leads.service.ts ← Máquina de estados, viabilidad (sin BD) │ ├── conversacion/ │ │ ├── conversacion.module.ts -│ │ ├── conversacion.entity.ts ← Entidad Conversacion (historial) -│ │ └── conversacion.service.ts ← Guardado y recuperación de historial +│ │ └── 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 -│ └── scheduler/ -│ ├── scheduler.module.ts -│ └── scheduler.service.ts ← Cron cada 5 min: apertura a leads nuevos + limpieza +│ └── webhook/ +│ ├── webhook.module.ts +│ └── webhook-listener.ts ← Servidor HTTP para recibir señales de la app ├── .env.example ├── nest-cli.json ├── package.json @@ -70,68 +71,40 @@ Mensaje entrante (texto / audio / imagen) ↓ ┌───────────────────────────────┐ │ PREPROCESAMIENTO │ - │ • Identificar lead por teléfono│ + │ • 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) │ + │ (Claude Sonnet via │ + │ OpenRouter) + enviar a │ + │ /ingesta │ │ • Si texto → directo │ - │ • Guardar mensaje usuario en │ - │ DB │ + │ • Guardar mensaje en │ + │ /conversacion (API HTTP) │ └───────────┬───────────────────┘ ↓ ┌───────────────────────────────┐ │ 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 + Guardar respuesta en /conversacion (API HTTP) + ↓ + Persistir datos en /perfil (API HTTP) ↓ Enviar por Baileys ``` @@ -148,15 +121,17 @@ MODEL_GENERADOR=anthropic/claude-sonnet-4-5 # Modelo para generar respuestas 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 +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:** -- `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. +- `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`. --- @@ -168,35 +143,13 @@ ALLOWED_NUMBER= # (OPCIONAL) Restringe el bot a u cp .env.example .env ``` -Edita `.env` con tus valores reales: +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. Prompts de Luisa -### 2. Base de datos +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. -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 +### 3. Arrancar ```bash npm install @@ -205,7 +158,7 @@ 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. +Luisa queda conectada y escuchando webhooks de la app (por defecto en puerto 3001). --- @@ -214,56 +167,60 @@ Luisa queda conectada y lista para recibir mensajes. | 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` +| `fin_viable` | Lead viable (presupuesto >= 5000€) | +| `fin_no_viable` | Lead no viable (presupuesto < 5000€) | ### Datos recolectados por estado -| Estado | Campo DB | Valores válidos | -|--------|----------|-----------------| -| `espacio` | `espacio` | `cocina`, `bano`, `salon`, `integral`, `otro` | -| `tamano` | `rango_m2` | `menos10`, `10a20`, `20a40`, `mas40` | +| 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` | `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. +| `urgencia` | `urgencia` | `alta`, `media`, `baja` | +| `presupuesto` | `presupuestoDeclarado` | Cifra o rango en euros | --- -## Scheduler (cron cada 5 min) +## Cómo se conecta con la app -El servicio `SchedulerService` se ejecuta cada 5 minutos vía `@nestjs/schedule` y realiza dos tareas: +``` +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 +``` -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. +## 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. 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. +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. --- @@ -272,27 +229,22 @@ Si se define `ALLOWED_NUMBER` en `.env`, el bot **ignora cualquier mensaje que n ### 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`). +- Se envía a OpenRouter con `input_audio` usando **Gemini 2.5 Flash**. - 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. +- 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 -- 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. +- 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). --- @@ -302,41 +254,20 @@ Si se define `ALLOWED_NUMBER` en `.env`, el bot **ignora cualquier mensaje que n 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 -``` +- App Reformix corriendo con `FUNNEL_API_KEY` configurada --- -Desarrollado para Reformix © 2025 \ No newline at end of file +Desarrollado para Reformix © 2026