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>
This commit is contained in:
Jose Manuel 2026-06-16 21:29:13 +02:00
parent c46baad502
commit 17f80842be

303
CLAUDE.md Normal file
View File

@ -0,0 +1,303 @@
# 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.