From f082351b434a9319a93f032509d43755c4f01710 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 3 Jun 2026 23:09:37 -0400 Subject: [PATCH] actualizacion de readme agente Luisa --- mvp/Whatsapp-bot/README.md | 361 +++++++++++++++++++++++++++++-------- 1 file changed, 287 insertions(+), 74 deletions(-) diff --git a/mvp/Whatsapp-bot/README.md b/mvp/Whatsapp-bot/README.md index 805ce29..35a291f 100644 --- a/mvp/Whatsapp-bot/README.md +++ b/mvp/Whatsapp-bot/README.md @@ -1,31 +1,165 @@ # Reformix Luisa Bot 🤖 -Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de forma conversacional, recoge 5 datos clave 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, recoge 5 datos clave (espacio, metros, estilo, urgencia, presupuesto) y cierra el flujo según el flag viable/no_viable. + +--- ## Stack -- **NestJS** — framework principal -- **Baileys** — conexión con WhatsApp (sin API oficial) -- **PostgreSQL** — base de datos via TypeORM -- **Claude 4.5** via **OpenRouter** — LLM con soporte de texto, audio e imagen +| 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 ``` -/src - /whatsapp ← Módulo Baileys: conexión, QR, recepción y envío - /leads ← Módulo de leads: CRUD y lógica de estados - /conversacion ← Módulo de historial de mensajes por lead - /scheduler ← Cron cada 5 min: dispara apertura a leads nuevos - /claude ← Construye el contexto y llama a Claude 4.5 - /media ← Procesa audio e imagen antes de pasar a Claude - -/prompts - luisa_core.md ← Identidad y personalidad de Luisa ← RELLENAR - luisa_flujo.md ← Flujo de cualificación paso a paso ← RELLENAR - luisa_casos.md ← Casos edge y ejemplos ← RELLENAR +/ +├── 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 @@ -34,32 +168,33 @@ Agente de WhatsApp **Luisa** para **Reformix** — cualifica leads de reforma de cp .env.example .env ``` -Edita `.env`: +Edita `.env` con tus valores reales: ```env -OPENROUTER_API_KEY=sk-or-... -MODEL=anthropic/claude-sonnet-4-5 -DATABASE_URL=postgresql://user:password@localhost:5432/reformix_luisa +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, TypeORM creará las tablas automáticamente al arrancar. +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, desactiva `synchronize` y usa migrations: +En producción, cambia `synchronize: false` y usa migrations: ```bash -npm run migration:generate +npm run migration:generate -- src/migrations/Init npm run migration:run ``` ### 3. Prompts de Luisa -Rellena los 3 archivos en `/prompts` antes de arrancar: +Los 3 archivos de prompts ya están creados y contienen la configuración completa: -- `luisa_core.md` — identidad, tono, límites -- `luisa_flujo.md` — estados, preguntas por estado, condiciones de avance -- `luisa_casos.md` — casos edge, fallbacks, ejemplos de conversación +- **`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 @@ -68,62 +203,140 @@ npm install npm run start:dev ``` -Escanea el **QR** que aparece en la terminal con WhatsApp. +Aparecerá un **código QR** en la terminal. Escanéalo con WhatsApp → **WhatsApp Web**. -Luisa queda conectada y lista. +Luisa queda conectada y lista para recibir mensajes. -## Flujo de mensajes +--- -``` -Mensaje entrante (texto / audio / imagen) - ↓ -Identificar lead por teléfono (crear si no existe) - ↓ -Si audio → Claude 4.5 transcripción -Si imagen → Claude 4.5 Vision (prompt según estado) -Si texto → directo - ↓ -Guardar mensaje usuario en DB - ↓ -Construir contexto: estado, datos del lead, historial, prompts MD - ↓ -Llamar Claude 4.5 via OpenRouter - ↓ -Extraer entidades del turno → actualizar lead en DB - ↓ -Evaluar flag viable → cambiar estado si aplica - ↓ -Guardar respuesta de Claude en DB - ↓ -Enviar respuesta por Baileys -``` - -## Scheduler (cron cada 5 min) - -- Busca leads con `estado_actual = 'nuevo'` -- Marca como `en_proceso` antes de actuar -- Genera y envía el mensaje de APERTURA de Luisa -- Ignora leads en `completado`, `no_viable`, `perdido` -- Marca como `perdido` leads en `en_proceso` sin actividad > 48h - -## Estados del lead +## Máquina de estados del lead | Estado | Descripción | |--------|-------------| | `nuevo` | Lead creado, aún no contactado | -| `en_proceso` | Luisa le ha enviado el primer mensaje | -| `recopilando_datos` | Conversación activa | -| `completado` | Todos los datos recogidos, viable=true | -| `no_viable` | Lead descartado, viable=false | +| `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 | -## Qué NO hace este servicio +**Secuencia completa:** `nuevo → apertura → espacio → tamano → estilo → urgencia → presupuesto → fin_viable/fin_no_viable` -- No genera el presupuesto (lo hace otro worker) -- No renderiza el PDF -- No envía la URL (la inserta el worker en `url_presupuesto`) -- No tiene panel del reformista +### 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. --- -Desarrollado para Reformix © 2025 +## 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 \ No newline at end of file