From 17f80842be97c6e7cd1ffceed866e96c4a8e3811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20G=C3=B3mez?= Date: Tue, 16 Jun 2026 21:29:13 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..901bf00 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,303 @@ +# 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.