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