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

12 KiB
Raw Permalink Blame History

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:

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.

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: pendingapproved 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).

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.

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)

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