# 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 `<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

```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