José Manuel Gómez 9239e2f67f Initial scaffold: Meta Optimizer for RoiFormacion campaigns
Ports meta-optimizer's Meta Ads execution/approval/creative-analysis layer
(agent.py, meta_ads_client.py, baserow_client.py, slack_notifier.py,
approval_server.py) and replaces the per-vertical CPL model with the
PPL + monthly-capping-per-course model already used by leads-optimizer,
via a new airtable_client.py that shares Cursos/Familias/CentroCurso/
CursoMes/Leads Lake with that project and adds Meta Ads Campaigns /
MetaCampaignMes alongside its Google Ads Campaigns / GACampaignMes.
2026-07-07 16:53:03 +02:00

9.5 KiB
Raw Blame History

Meta Optimizer Formación — Documentación del Proyecto

Agente autónomo de optimización de campañas Meta Ads (Facebook/Instagram) para los cursos de formación (RoiFormacion_*), hermano de meta-optimizer (que gestiona las campañas VIVIFUL_* de la misma cuenta de Meta).

A diferencia de Viviful, aquí el modelo de negocio no es "CPL objetivo por vertical" sino PPL (precio por lead) + capping mensual por curso, el mismo modelo que usa leads-optimizer para gestionar los mismos cursos en Google Ads.


Por qué existe este proyecto (y no una copia más de Viviful)

  • El número que sigue a RoiFormacion_ en el nombre de la campaña de Meta (RoiFormacion_884_Curso_Desarrollador_leadads884) es el mismo CursoID que usa leads-optimizer en Airtable.
  • Los leads de Meta ya llegan hoy a la tabla Leads Lake de Airtable (attr_utm_source='Lead ads', attr_cursoid resuelto) — el capping mensual por curso (CursoMes.Caping Admitido) ya se consume con leads de Meta y de Google combinados, aunque hasta ahora nada lo controlaba desde el lado de Meta.
  • El campo Familia (desde Familias) en la tabla Cursos de Airtable ya da la segmentación temática que sustituye al concepto de "vertical" de Viviful.

Por eso la arquitectura se reparte en dos sistemas, no uno:

  • Airtable (compartido con leads-optimizer, misma base): la capa de negocio — Cursos, Familias, CentroCurso, CursoMes (capping, solo lectura), Leads Lake (solo lectura), y las tablas nuevas específicas de Meta: Meta Ads Campaigns y MetaCampaignMes (análogas a Google Ads Campaigns / GACampaignMes). El capping es un recurso compartido entre canales — vive en un solo sitio para que Meta y Google no se pisen al consumirlo.
  • Baserow (misma instancia self-hosted que Viviful, tablas nuevas): todo lo operativo específico de Meta que Google no tiene — desglose diario por adset/anuncio, propuestas de acción (incluida la pausa de anuncios individuales), análisis visual de creatividades, logs de ejecución.

Arquitectura general

Meta Ads API                          Airtable (compartido con leads-optimizer)
    │                                      │  Cursos / Familias / CentroCurso
    ▼                                      │  CursoMes (capping) / Leads Lake
run.py ──► agent.py (Claude Haiku) ◄───────┤  (solo lectura)
    │            │                         │
    │            ▼                         └─► Meta Ads Campaigns / MetaCampaignMes
    │      analyzer.py (PPL + capping,          (catálogo + estado mensual, r/w)
    │       urgencia/ritmo/margen)
    │
    └──────────────────────────────────► Baserow (snapshots, acciones, creatividades, logs)
                                                │
                                          Slack (informe + aprobación)
                                                │
                                    approval_server.py (FastAPI, puerto propio)
                                                │
                                           Baserow (ejecutar acción)
                                                │
                                         Meta Ads API (aplicar)

Componentes

run.py — Orquestador principal

  1. Carga PPL/capping/familia por curso desde Airtable (build_campaign_lookups).
  2. Sincroniza el catálogo Meta → Airtable (Meta Ads Campaigns, MetaCampaignMes).
  3. Para cada campaña RoiFormacion_* ACTIVA en Meta:
    • Cuenta leads del mes vía Leads Lake (get_leads_this_month_meta).
    • analyzer.analyze() calcula urgencia/ritmo/margen/rentabilidad.
    • agent.decide() propone acción (PAUSE / REDUCE_BUDGET / INCREASE_BUDGET / MAINTAIN).
    • Analiza top-5 adsets (3d) y top-5 anuncios activos (3d+7d) — igual que Viviful.
    • Guarda snapshot diario en Baserow, actualiza MetaCampaignMes (consejo/criticidad/leads).
  4. Envía informe a Slack agrupado por Familia.
  5. Si DRY_RUN=false: ejecuta acciones aprobadas del día anterior.

Modo: DRY_RUN=true por defecto.


analyzer.py — Motor de negocio (puerto de leads-optimizer)

Fórmulas de capping/PPL/ritmo, agnósticas de canal:

  • ratio_leads = leads_entregados / capping, ritmo = ratio_leads - ratio_mes.
  • margen = (leads_entregados × PPL gasto) / (leads_entregados × PPL).
  • Urgencia: PAUSAR (cap consumido) → SPRINT (atrasado, quedan pocos días) → ACELERAR / FRENAR (según ritmo) → EN_RITMO.

agent.py — Agente de decisión (Claude Haiku/Sonnet)

  • decide(analysis): traduce la urgencia/rentabilidad de analyzer.py a las mismas acciones que ya usa Viviful (PAUSE/REDUCE_BUDGET/INCREASE_BUDGET/MAINTAIN), para no tener que tocar baserow_client.py, slack_notifier.py ni approval_server.py.
  • analyze_unit(metrics, level): igual que Viviful — granularidad táctica de adset/anuncio a 3d/7d, comparando contra cpa_maximo (= PPL × 0.70) en vez de max_cpl.
  • analyze_creative / analyze_creative_deep / compare_adset_creatives: idénticas a Viviful, no dependen del modelo de negocio.

meta_ads_client.py

Copia casi literal de Viviful. Único añadido: get_all_campaigns() (lista completa de campañas RoiFormacion_* independientemente del gasto, necesaria para sincronizar el catálogo de Airtable).

airtable_client.py

Cliente de la base compartida con leads-optimizer. Solo lee Cursos / CentroCurso / CursoMes (el catálogo de cursos se mantiene externamente, igual que en leads-optimizer). Lee y escribe Meta Ads Campaigns y MetaCampaignMes.

extract_cursoid(campaign_name) — regex roiformaci[oó]n_?(\d+), tolerante a variantes sin guion bajo tras el número (RoiFormacion_1281Instaladores_...).

baserow_client.py

Igual que Viviful salvo que no existen las tablas campaigns ni verticals (sustituidas por Airtable). daily_snapshots usa el campo familia en vez de vertical.

slack_notifier.py

Mismo patrón multi-mensaje que Viviful, agrupado por Familia en vez de vertical. No hay un "CPL objetivo" único por familia (cada curso tiene su propio PPL), así que las tarjetas de campaña muestran urgencia, ritmo y leads-consumidos/capping en vez de "CPL vs objetivo".

approval_server.py

Copia literal de Viviful. Se despliega por separado, en otro puerto de roiserver.com, apuntando a las tablas Baserow de este proyecto.


Base de datos

Airtable (compartida con leads-optimizer)

Tabla Uso Acceso
Cursos Catálogo de cursos, CursoID, Familia solo lectura
CentroCurso PPL y % invalidación por centro solo lectura
CursoMes Capping mensual por curso (compartido Meta+Google) solo lectura
Leads Lake Leads de todos los canales (attr_utm_source='Lead ads' = Meta) solo lectura
Meta Ads Campaigns Catálogo de campañas de Meta lectura/escritura
MetaCampaignMes Estado mensual: PPL, cap, coste, leads, consejo, criticidad lectura/escritura

Aprovisionar con python setup_airtable_meta_tables.py (una sola vez; requiere token con scope schema.bases:write; ⚠️ modifica una base ya en producción).

Baserow (misma instancia self-hosted que Viviful, tablas nuevas)

Tabla Contenido
proposed_actions Acciones propuestas (campañas y anuncios). Estados: pending → approved/rejected → executed
creative_analyses Análisis visual de creatividades
daily_snapshots Snapshot diario por campaña: métricas + decisión + familia + adsets_json + ads_json
execution_logs Log de cada ejecución

Aprovisionar con python setup_baserow.py (una sola vez).


Variables de entorno (.env)

Ver .env.example. Reutiliza las credenciales de Meta/Anthropic/Slack de meta-optimizer (misma cuenta de Meta, mismo bot de Slack con canal distinto) y las de Airtable de leads-optimizer (misma base).


Automatización (GitHub Actions)

.github/workflows/daily.yml — cron propuesto unas horas antes que el de Viviful para no competir por rate limit de la misma cuenta de Meta. schedule comentado por defecto (ejecución manual con workflow_dispatch).


Notas de implementación importantes

  • Capping compartido entre canales: CursoMes.Caping Admitido lo consumen Meta y Google a la vez. Si un curso corre en ambos canales simultáneamente, cualquier cambio en cómo se cuenta el consumo debe revisarse en los dos proyectos (meta-optimizer-formacion y leads-optimizer).
  • margin vs margen_pct: en run.py, margin (€) es un proxy diario tipo Viviful (leads × PPL gasto) para las tablas de Slack/Baserow; margen_pct es la rentabilidad acumulada del mes que calcula analyzer.py ((ingreso gasto) / ingreso). No son la misma magnitud, no sumar una con otra.
  • send_slack_report.py reconstruye desde snapshots de Baserow, que no guardan urgencia/leads_mes/capping — al reenviar un informe esos campos salen con su valor por defecto (no es un bug, es una limitación conocida, igual que bid_config={} en la versión de Viviful).
  • Regex de CursoID: roiformaci[oó]n_?(\d+) — tolera nombres sin guion bajo tras el número. Si aparecen nuevas variantes de nomenclatura, ajustar extract_cursoid() en airtable_client.py (usado también por run.py, backfill.py, send_slack_report.py, dashboard.py, analyze_creatives.py).