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>
12 KiB
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:
- Carga objetivos CPL por vertical desde Baserow
- Obtiene métricas de ayer + ventanas 3d y 7d desde Meta API
- 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
- Llama a
- Envía informe consolidado a Slack
- Guarda log de ejecución en Baserow
- 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_analysesde 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:
lead— campañas leadads estándar (prioridad máxima, evita doble conteo conlead_grouped)onsite_conversion.lead_grouped— fallback leadadsclick_to_call_call_confirm— campañas de llamadas (Vodafone, Lowi)call_confirm_grouped— fallback llamadascall_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:
- Usuario hace clic en botón en Slack
- Slack hace POST a
/slack/actions - Servidor verifica firma HMAC (
SLACK_SIGNING_SECRET) - Actualiza estado en Baserow:
pending→approvedorejected - 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
telefoniacon target 1€ — posiblemente demasiado bajo para llamadas)
Automatización (GitHub Actions)
Archivo: .github/workflows/daily.yml
- Estado actual:
schedulecomentado (ejecución manual conworkflow_dispatch) - Objetivo:
0 5 * * *(07:00 CEST) cuando se reactive - Ejecuta
run.pyen 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
leadcomoonsite_conversion.lead_groupedpara el mismo evento._count_conversionspriorizaleadpara evitar duplicados. - Campañas de llamadas (Vodafone, Lowi): Usan
click_to_call_call_confirmcomo tipo de conversión, nolead. 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_statusactual antes de filtrar los resultados de insights.