meta-optimizer/CLAUDE.md
José Manuel Gómez 17f80842be Add CLAUDE.md project documentation
Documents architecture, all components, Baserow tables, Slack integration,
dashboard, automation, env vars, and key implementation notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:29:13 +02:00

304 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meta Optimizer — Documentación del Proyecto
Agente autónomo de optimización de campañas Meta Ads (Facebook/Instagram) para generación de leads.
Analiza rendimiento diario, propone acciones y las ejecuta tras aprobación humana vía Slack.
---
## Arquitectura general
```
Meta Ads API
run.py ──► agent.py (Claude Haiku) ──► Baserow (persistencia)
│ │
└──────────────────────────────────────► Slack (notificaciones + aprobación)
approval_server.py (FastAPI)
Baserow (ejecutar acción)
Meta Ads API (aplicar)
Consulta manual:
dashboard.py (Streamlit) ──► Meta Ads API + Baserow
```
---
## Componentes
### `run.py` — Orquestador principal
Punto de entrada diario. Ejecuta el ciclo completo:
1. Carga objetivos CPL por vertical desde Baserow
2. Obtiene métricas de ayer + ventanas 3d y 7d desde Meta API
3. Para cada campaña activa (prefijo `VIVIFUL`):
- Llama a `agent.decide()` → propone acción (PAUSE / REDUCE_BUDGET / INCREASE_BUDGET / MAINTAIN)
- Analiza top-5 adsets (ventana 3d) con `agent.analyze_unit()`
- Analiza top-5 anuncios ACTIVOS (ventanas 3d + 7d fusionadas) con `agent.analyze_unit()`
- Detecta ABO vs CBO: si la campaña no tiene presupuesto a nivel campaña (ABO), omite INCREASE/REDUCE_BUDGET
- Guarda snapshot diario en Baserow
4. Envía informe consolidado a Slack
5. Guarda log de ejecución en Baserow
6. Si `DRY_RUN=false`: ejecuta acciones previamente aprobadas del día anterior
**Modo:** `DRY_RUN=true` por defecto (no ejecuta nada en Meta). Cambiar a `false` para producción.
**Log:** cada ejecución genera un archivo en `logs/YYYYMMDD_HHMMSS.log`
---
### `agent.py` — Agente de decisión (Claude Haiku)
Tres funciones principales:
**`decide(analysis)`** — Decisión a nivel campaña
- Modelo: `claude-haiku-4-5-20251001`
- Input: métricas 1d/3d/7d, max_cpl, CTR, nombre de campaña
- Output: `{action, parameter, justification, advice, alert, confidence}`
- Reglas clave:
- Ventana principal: cpl_3d (últimos 3 días)
- Tendencia: comparar cpl_3d vs cpl_7d (si mejora → más permisivo)
- PAUSE solo si leads_3d == 0 Y spend_3d >= 3× max_cpl
- INCREASE/REDUCE_BUDGET: cambios máximos del 15-20%
- Nunca PAUSE por CPL alto → usar REDUCE_BUDGET
**`analyze_unit(metrics, level)`** — Análisis de adset o anuncio
- Modelo: `claude-haiku-4-5-20251001`
- Para adsets (level="adset"): evalúa rendimiento vs cap de coste, 3d window
- Para anuncios (level="ad"): recibe métricas 3d + 7d fusionadas; usa 7d para PAUSE
- PAUSE de anuncio solo si leads_7d == 0 Y gasto 7d > 3× max_cpl
- Si cpl_3d alto pero cpl_7d OK → MAINTAIN (ruido estadístico)
- Output: `{evaluacion, recomendacion, accion}` (accion solo en anuncios)
**`analyze_creative(image_url, ad_name)`** — Análisis visual de creatividad
- Modelo: `claude-sonnet-4-6`
- Descarga la imagen del anuncio, la analiza visualmente
- Output: `{score, analysis, recommendations}`
- Guarda resultado en tabla `creative_analyses` de Baserow
---
### `meta_ads_client.py` — Cliente Meta Marketing API
Métodos principales:
| Método | Descripción |
|--------|-------------|
| `get_campaign_metrics(from, to)` | Métricas por campaña en rango de fechas |
| `get_yesterday_metrics()` | Alias: rango = ayer |
| `get_period_campaign_metrics(days)` | Alias: rango = últimos N días |
| `get_daily_campaign_rows(from, to)` | Filas diarias por campaña (para tabla mensual) |
| `get_period_adset_metrics(cid, days)` | Top adsets por gasto, últimos N días |
| `get_period_ad_metrics(cid, days)` | Top anuncios ACTIVOS por gasto, últimos N días |
| `get_campaign_bid_config(cid)` | Estrategia de puja + presupuesto diario/vitalicio |
| `get_adset_bid_configs(cid)` | Cap de coste y presupuesto por adset |
| `get_ads_with_creatives(cid)` | Anuncios activos con thumbnail URL |
| `set_campaign_budget(cid, cents)` | Actualiza presupuesto diario de campaña |
| `pause_ad(ad_id)` | Pausa un anuncio concreto |
| `pause_campaign(cid)` | Pausa una campaña |
**`_count_conversions(actions)`** — Lógica de conteo de conversiones por tipo de campaña:
1. `lead` — campañas leadads estándar (prioridad máxima, evita doble conteo con `lead_grouped`)
2. `onsite_conversion.lead_grouped` — fallback leadads
3. `click_to_call_call_confirm` — campañas de llamadas (Vodafone, Lowi)
4. `call_confirm_grouped` — fallback llamadas
5. `call_confirm` / `contact` — otros
**Filtro de anuncios pausados:** `get_period_ad_metrics` excluye automáticamente anuncios con `effective_status != ACTIVE` para evitar análisis de anuncios ya pausados.
---
### `slack_notifier.py` — Notificaciones Slack
Envía el informe diario en múltiples mensajes (sin límite de bloques):
**Mensaje 1 — Resumen ejecutivo:**
- Tabla mensual de rentabilidad con columnas por vertical
- Resumen por vertical (datos de ayer)
- Top 10 mejores / Top 10 peores campañas
**Mensajes 2-N — Un mensaje por campaña:**
- Header: nombre, vertical, gasto/leads de ayer, margen, estrategia, presupuesto
- Decisión + justificación del agente
- Botones Aprobar/Rechazar (si la acción es INCREASE/REDUCE/PAUSE)
- Tabla de adsets (últimos 3 días): gasto, leads, CPL, CTR, cap de coste
- Tabla de anuncios (últimos 7 días): gasto, leads, CPL(3d), CPL(7d), CTR
- Botones de pausa por anuncio (si el agente recomienda PAUSE)
---
### `dashboard.py` — Dashboard web (Streamlit)
Panel interactivo para análisis y consulta. Lanzar con:
```bash
streamlit run dashboard.py
```
**4 pestañas:**
| Pestaña | Contenido |
|---------|-----------|
| 📅 Por día | Tabla diaria de gasto/leads/CPL/margen + desglose por campaña al seleccionar un día |
| 📊 Campañas | Todas las campañas del período con drill-down a adsets y anuncios (métricas en tiempo real desde Meta API) |
| 🏷️ Verticales | Resumen agregado por vertical con objetivo CPL y margen |
| 🗂️ Histórico | Snapshots diarios de Baserow — al hacer clic en una campaña muestra adsets y anuncios con evaluaciones del agente en expanders |
- Filtro de fecha y vertical en sidebar
- Caché de 5 minutos para las llamadas a Meta API
- Conecta directamente a Meta API (datos en tiempo real) + Baserow (histórico de análisis)
---
### `approval_server.py` — Servidor de aprobación (FastAPI)
Recibe callbacks de los botones Aprobar/Rechazar de Slack.
```bash
uvicorn approval_server:app --host 0.0.0.0 --port 3000
# Para desarrollo local: exponer con ngrok
```
Flujo:
1. Usuario hace clic en botón en Slack
2. Slack hace POST a `/slack/actions`
3. Servidor verifica firma HMAC (`SLACK_SIGNING_SECRET`)
4. Actualiza estado en Baserow: `pending``approved` o `rejected`
5. Actualiza el mensaje de Slack con el resultado y el nombre del usuario
---
### `send_slack_report.py` — Reenvío de informe
Re-envía el informe de Slack para una fecha concreta usando los snapshots guardados en Baserow (sin llamar a la API de Meta).
```bash
python send_slack_report.py # usa ayer
python send_slack_report.py 2026-06-13 # fecha específica
```
Útil cuando el informe de Slack falla o se quiere reenviar.
---
### `backfill.py` — Relleno histórico
Genera snapshots históricos con análisis Claude para un rango de fechas.
```bash
python backfill.py # mes en curso → ayer
python backfill.py --from 2026-06-01 --to 2026-06-04
python backfill.py --skip-existing # no reprocesa días ya guardados
```
Usa ventana de 1 día (no 3d/7d) porque los datos históricos ya están fijados.
---
## Base de datos (Baserow)
6 tablas configuradas vía `BASEROW_TABLE_*` en `.env`:
| Tabla | Contenido |
|-------|-----------|
| `campaigns` | Configuración por campaña (max_cpl manual, notas) — actualmente poco usada |
| `verticals` | Nombre de vertical + `target_cpl` → los CPL objetivo reales del sistema |
| `proposed_actions` | Acciones propuestas: campañas Y anuncios individuales. Estados: `pending → approved/rejected → executed` |
| `creative_analyses` | Análisis visual de creatividades (score 1-10, análisis, recomendaciones) |
| `daily_snapshots` | Snapshot diario por campaña: métricas + decisión + adsets_json + ads_json |
| `execution_logs` | Log de cada ejecución: campañas analizadas, acciones, errores, duración |
**Verticales configuradas (target_cpl en €):**
- `energia` → 6.5€
- `telefonia` → 1.0€
- `segurodecesos` → 7.0€
- `alarmas` → 8.0€
- (Vodafone/Lowi usan vertical `telefonia` con target 1€ — posiblemente demasiado bajo para llamadas)
---
## Automatización (GitHub Actions)
Archivo: `.github/workflows/daily.yml`
- **Estado actual:** `schedule` comentado (ejecución manual con `workflow_dispatch`)
- **Objetivo:** `0 5 * * *` (07:00 CEST) cuando se reactive
- Ejecuta `run.py` en Ubuntu con todos los secrets de GitHub
- Sube los logs como artefacto (retención 30 días)
- Variables de entorno: todas las `META_*`, `ANTHROPIC_*`, `BASEROW_*`, `SLACK_*`
Para reactivar el cron, descomentar las líneas `schedule` en el yml.
---
## Variables de entorno (.env)
```env
# Meta Ads
META_APP_ID=
META_APP_SECRET=
META_ACCESS_TOKEN=
META_AD_ACCOUNT_ID=act_XXXXXXXX
# Anthropic
ANTHROPIC_API_KEY=
# Baserow (self-hosted)
BASEROW_URL=https://baserow.roiserver.com
BASEROW_TOKEN=
BASEROW_TABLE_CAMPAIGNS=
BASEROW_TABLE_ACTIONS=
BASEROW_TABLE_CREATIVES=
BASEROW_TABLE_LOGS=
BASEROW_TABLE_VERTICALS=
BASEROW_TABLE_SNAPSHOTS=
# Slack (Bot Token, no webhook)
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=
SLACK_CHANNEL_ID=
# Configuración
META_CAMPAIGN_PREFIX=VIVIFUL
META_TARGET_CPL=0 # 0 = usar target por vertical desde Baserow
DRY_RUN=true # false para ejecutar acciones reales en Meta
```
---
## Flujo diario de operación
```
07:00 GitHub Actions ejecuta run.py
├─ Llama Meta API (métricas 1d/3d/7d)
├─ Claude analiza cada campaña, adset y anuncio
├─ Guarda snapshots + acciones en Baserow
└─ Envía informe a Slack
07:05 Equipo recibe informe en Slack
├─ Lee resumen por vertical y mensajes por campaña
└─ Aprueba o rechaza acciones con botones
07:05 approval_server.py (corriendo en roiserver.com)
├─ Recibe callback de botón Slack
└─ Actualiza estado en Baserow: pending → approved/rejected
(Siguiente ejecución de run.py)
└─ Si DRY_RUN=false: ejecuta acciones con estado "approved"
```
---
## Notas de implementación importantes
- **ABO vs CBO:** Si una campaña no tiene presupuesto a nivel campaña (`daily_budget_eur = None`), es ABO (presupuesto en adsets). En ese caso se omiten INCREASE_BUDGET y REDUCE_BUDGET y se mantiene MAINTAIN. Afecta a: energía y alarmas.
- **Doble conteo de leads:** La API de Meta devuelve tanto `lead` como `onsite_conversion.lead_grouped` para el mismo evento. `_count_conversions` prioriza `lead` para evitar duplicados.
- **Campañas de llamadas (Vodafone, Lowi):** Usan `click_to_call_call_confirm` como tipo de conversión, no `lead`. El sistema lo detecta automáticamente.
- **Límite de bloques Slack (50):** Superado cuando había muchas campañas. Solucionado enviando un mensaje por campaña en lugar de todo en un único mensaje.
- **Anuncios pausados:** Se excluyen del análisis consultando el `effective_status` actual antes de filtrar los resultados de insights.