leads-optimizer/AGENTS.md

4.6 KiB

Leads Optimizer — AGENTS.md

Quick start

pip install -r requirements.txt
python run.py            # main optimizer run
python weekly_report.py  # weekly strategic report
streamlit run dashboard.py  # Streamlit dashboard (port 15002)

Python 3.12+. No build step, no linter, no test suite.

Execution entry points

File Trigger What it does
run.py daily.yml (00:00 UTC) or manual Syncs Google Ads → Airtable, analyses campaigns, applies budget decisions, reports to Slack
weekly_report.py weekly.yml (Mon 07:00 UTC) or manual Deeper week-over-week analysis via Anthropic, sends Slack
dashboard.py streamlit run dashboard.py UI on port 15002, reads from Airtable directly

DRY_RUN mode

config.py has DRY_RUN = True by default. When True, decisions are printed but no changes are applied to Google Ads. Must be set to False to apply budget/pause changes. This is a module-level constant, not an env var.

Required env vars (loaded via python-dotenv from .env)

All are mandatory except SLACK_WEBHOOK_URL (optional, has empty default):

  • AIRTABLE_TOKEN, AIRTABLE_BASE_ID
  • Google Ads: GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_LOGIN_CUSTOMER_ID (digits only, no dashes)
  • ANTHROPIC_API_KEY
  • SLACK_WEBHOOK_URL

The .github/workflows/ files pull these from GitHub Secrets. run.sh has hardcoded secrets — never commit changes to it.

Architecture

Flat structure, no packages. Flow:

  1. Sync: airtable_client.py + google_ads_client.py pull campaign catalog and monthly metrics from Google Ads, write to Airtable tables Google Ads Campaigns, GACampaignMes, Leads Lake, MetricasDiarias, GAMes.
  2. Analyze: analyzer.py computes per-campaign urgency (PAUSAR / SPRINT / ACELERAR / FRENAR / EN_RITMO).
  3. Decide: agent.py calls Anthropic Claude (claude-sonnet-4-6) per campaign to get JSON decision with action, budget multiplier, justification, and advice. Also produces portfolio-level analysis.
  4. Apply: optimizer.py mutates Google Ads campaigns (budget changes, pause/unpause) — only if DRY_RUN = False.
  5. Report: slack_reporter.py sends formatted summary to Slack via webhook.

Campaign naming conventions

Campaigns follow naming patterns that drive logic:

  • fco_search_<N> — Search campaigns for formation courses
  • fco_pmx_<N> — PMX (Performance Max) campaigns
  • fco_leadform_<N> — Lead form campaigns (leads captured inside Google, never reach Airtable)

When a course has both Search and PMX campaigns (_search_ + _pmx_ with same <N>), Search conversions reflow to PMX. When PMX has a _leadform companion, leadform conversions are summed into the PMX campaign's leads_grupo. If a course has multiple PMX campaigns (excluding leadform), paths 4/5 are disabled to avoid double counting.

Month-boundary metric handling

run.py has special logic (lines ~400-432) for when yesterday belongs to a different calendar month. It merges into the previous month's GACampaignMes record instead of overwriting the new month's (avoids a known bug that erased June history). Any edits to the metrics-writing section must preserve this redirect.

Airtable tables

  • Google Ads Campaigns — master campaign catalog (Curso, GoogleCampaignID, PPL, CapTotalMes, CPAMaximo, Activa)
  • Leads Lake — individual lead records (GoogleCampaignID, FechaEntrada)
  • GACampaignMes — per-campaign monthly snapshot; updated each run with leads, advice, criticidad, metricas_diarias
  • MetricasDiarias — JSON field with per-day {coste, ingreso, margen, leads, leads_lake}
  • GAMes — aggregated daily totals for all fco_ campaigns + monthly totals

Backfill and migration scripts

Scripts prefixed backfill_* and migrate_* are one-off data scripts. Do not call them from normal flow; they are only for historical data repairs.

CI/CD

  • daily.yml — runs python run.py at 00:00 UTC (2 AM CEST / 1 AM CET)
  • weekly.yml — runs python weekly_report.py Monday at 07:00 UTC (9 AM CEST)
  • Both use ubuntu-latest + Python 3.12 + secrets from GitHub
  • Logs uploaded as artifacts (30-day retention)

Gotchas

  • run.py wraps sys.stdout with a custom Tee class that writes to both console and logs/<timestamp>.log. Do not remove this.
  • The Anthropic model is hardcoded to claude-sonnet-4-6 in agent.py. If the model changes, update all three calls (decide, portfolio_daily_analysis, weekly_strategic_analysis).
  • run.sh contains real API secrets — it should NOT be committed. Use .env + python run.py instead.