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_IDGoogle 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_KEYSLACK_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:
- Sync:
airtable_client.py+google_ads_client.pypull campaign catalog and monthly metrics from Google Ads, write to Airtable tablesGoogle Ads Campaigns,GACampaignMes,Leads Lake,MetricasDiarias,GAMes. - Analyze:
analyzer.pycomputes per-campaign urgency (PAUSAR / SPRINT / ACELERAR / FRENAR / EN_RITMO). - Decide:
agent.pycalls Anthropic Claude (claude-sonnet-4-6) per campaign to get JSON decision with action, budget multiplier, justification, and advice. Also produces portfolio-level analysis. - Apply:
optimizer.pymutates Google Ads campaigns (budget changes, pause/unpause) — only ifDRY_RUN = False. - Report:
slack_reporter.pysends formatted summary to Slack via webhook.
Campaign naming conventions
Campaigns follow naming patterns that drive logic:
fco_search_<N>— Search campaigns for formation coursesfco_pmx_<N>— PMX (Performance Max) campaignsfco_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_diariasMetricasDiarias— 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— runspython run.pyat 00:00 UTC (2 AM CEST / 1 AM CET)weekly.yml— runspython weekly_report.pyMonday 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.pywrapssys.stdoutwith a customTeeclass that writes to both console andlogs/<timestamp>.log. Do not remove this.- The Anthropic model is hardcoded to
claude-sonnet-4-6inagent.py. If the model changes, update all three calls (decide,portfolio_daily_analysis,weekly_strategic_analysis). run.shcontains real API secrets — it should NOT be committed. Use.env+python run.pyinstead.