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>
304 lines
12 KiB
Markdown
304 lines
12 KiB
Markdown
# 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.
|