leads-optimizer/docu.md
José Manuel Gómez d58448f698 Add technical documentation (docu.md)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:19:30 +02:00

6.7 KiB
Raw Blame History

Leads Optimizer — Documentación técnica

Qué hace

Agente automatizado que cada día:

  1. Sincroniza el catálogo de campañas de Google Ads → Airtable
  2. Lee las métricas del mes en curso (coste, conversiones, leads)
  3. Analiza el rendimiento de cada campaña
  4. Toma una decisión con IA (Claude) sobre qué hacer con el presupuesto
  5. Aplica la decisión en Google Ads (o la simula en modo DRY_RUN)
  6. Guarda el resultado en Airtable

Modelo de negocio

Ingreso  = leads_entregados × PPL
Margen   = (Ingreso - Gasto Google Ads) / Ingreso
CPA max  = PPL × 0.70  →  margen mínimo aceptable del 30%
  • PPL (Precio Por Lead): fijo por curso, ponderado por el % de invalidación del centro.
  • Capping mensual: máximo de leads a entregar por campaña ese mes. Si = 0, sin límite.

Flujo de ejecución (run.py)

1. Sincronizar campañas Google Ads → Google Ads Campaigns (Airtable)
2. Sincronizar GACampaignMes (estado mensual: coste, conversiones, PPL, cap)
3. Leer campañas activas del mes desde GACampaignMes
4. Detección anticipada:
     - courses_with_both:     cursos con Search Y PMX simultáneos
     - courses_with_leadform: cursos con campaña _leadform activa
5. PRIMERA PASADA: para cada campaña
     - Contar leads Airtable (get_leads_this_month_gads)
     - Obtener métricas Google Ads (get_campaign_metrics)
     - Calcular leads_grupo (para PMX con companion: usa conversiones Google)
     - Analizar (analyzer.py) → urgencia, CPA, margen, ritmo
     - Decidir (agent.py / Claude) → acción, presupuesto, consejo
6. Actualizar Status en ambas tablas Airtable
7. SEGUNDA PASADA (ordenada por criticidad):
     - Imprimir resultado por consola
     - Aplicar decisión en Google Ads
     - Construir log_text con notas de atribución PMX/leadform
8. Guardar en GACampaignMes: Consejo, Criticidad, Log
9. Guardar ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo

Módulos

analyzer.py

Calcula métricas derivadas y asigna la urgencia.

Entradas: campaign_config, leads_entregados, ads_metrics

Métrica Cálculo
ratio_leads leads / capping
ratio_mes día actual / días del mes
ritmo ratio_leads ratio_mes (>0 adelantado, <0 atrasado)
cpa_actual gasto / leads
margen (leads × PPL gasto) / (leads × PPL)

Urgencia:

Valor Condición
PAUSAR ratio_leads ≥ 1.0 (capping lleno)
SPRINT Atrasado >15% y quedan ≤5 días para fin de mes
ACELERAR ritmo < 0.15
FRENAR ritmo > +0.15
EN_RITMO diferencia ≤ 15%

agent.py

Llama a Claude (claude-sonnet-4-20250514) con el resultado del análisis y devuelve una decisión en JSON.

Reglas de decisión:

Urgencia Rentable Acción Parámetro
PAUSAR PAUSAR
SPRINT AUMENTAR_PRESUPUESTO ×1.31.5
ACELERAR AUMENTAR_PRESUPUESTO ×1.11.25
ACELERAR MANTENER
FRENAR REDUCIR_PRESUPUESTO ×0.750.9
EN_RITMO MANTENER
EN_RITMO REDUCIR_PRESUPUESTO ×0.85

Devuelve: accion, parametro, nuevo_budget_diario, justificacion, consejo, alerta, confianza.


optimizer.py

Aplica la decisión del agente sobre Google Ads. Si DRY_RUN=True, solo imprime lo que haría.

Acción Efecto
PAUSAR Pausa la campaña vía API
AUMENTAR_PRESUPUESTO / REDUCIR_PRESUPUESTO Ajusta el presupuesto diario
MANTENER No hace nada

airtable_client.py

Gestiona toda la comunicación con Airtable.

Tablas usadas:

Tabla Uso
Google Ads Campaigns Catálogo de campañas (nombre, CampaignID, PPL, Status)
GACampaignMes Estado mensual por campaña (coste, conversiones, leads, consejo, criticidad, log)
Leads Lake Leads recibidos (filtrado por campaña y mes)
Cursos Relación CursoID ↔ CentroCurso
CentroCurso PPL y % invalidación por centro
CursoMes Capping mensual por curso

Métodos principales:

  • build_campaign_lookups() — calcula PPL ponderado y capping mensual para todos los cursos
  • sync_campaigns_from_google_ads() — crea/actualiza el catálogo de campañas
  • sync_gacampaignmes() — crea/actualiza registros mensuales de campañas con actividad
  • get_active_gacampaignmes() — devuelve las campañas del mes en curso
  • get_leads_this_month_gads() — cuenta leads Airtable para una campaña Google Ads
  • batch_update_gacampaignmes_advice() — guarda Consejo, Criticidad y Log
  • batch_update_gacampaignmes_final_leads() — guarda ConvLeadsLakeMesFinal y ConvLeadsLakeMesGrupo

google_ads_client.py

Wrapper sobre la API de Google Ads.

  • get_all_campaigns() — lista todas las campañas no eliminadas
  • get_monthly_metrics_all() — conversiones y coste del mes para todas las campañas (1 query)
  • get_campaign_metrics() — métricas detalladas de una campaña concreta
  • set_campaign_budget() — cambia el presupuesto diario
  • pause_campaign() — pausa una campaña

Criticidad

Combina urgencia + acción para priorizar el output y el campo Criticidad en Airtable:

Criticidad Cuándo
Crítico Urgencia es PAUSAR o SPRINT
Peligro Acción cambia el presupuesto pero urgencia no es crítica
Mantener Acción es MANTENER

Casos especiales de atribución

PMX con companion Search (courses_with_both)

Cuando un curso tiene activos simultáneamente fco_search_NNN_* y fco_pmx_NNN_*, Google puede reatribuir conversiones del Search al PMX.

  • La campaña PMX usa como leads_grupo las conversiones de Google Ads (no los leads de Airtable).
  • Se añade una nota ⚠️ PMX ATTRIBUTION en el campo Log de la campaña Search.

Campaña Leadform (_leadform)

Las campañas con sufijo _leadform capturan leads directamente dentro de Google (sin que el usuario visite la web). Esos leads nunca llegan a Airtable.

  • Se añade LEADFORM en el Log de la propia campaña.
  • Las campañas hermanas del mismo curso reciben ⚠️ LEADFORM COMPANION en su Log.

Configuración (.env)

AIRTABLE_TOKEN=...
AIRTABLE_BASE_ID=...
GOOGLE_ADS_DEVELOPER_TOKEN=...
GOOGLE_ADS_CLIENT_ID=...
GOOGLE_ADS_CLIENT_SECRET=...
GOOGLE_ADS_REFRESH_TOKEN=...
GOOGLE_ADS_LOGIN_CUSTOMER_ID=...
ANTHROPIC_API_KEY=...

DRY_RUN = True en config.py — cambiar a False para aplicar cambios reales en Google Ads.


Logs

Cada ejecución genera un archivo en logs/YYYYMMDD_HHMMSS.log con la salida completa.


Ejecutar

python run.py