# Leads Optimizer — AGENTS.md ## Quick start ```bash 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_` — Search campaigns for formation courses - `fco_pmx_` — PMX (Performance Max) campaigns - `fco_leadform_` — Lead form campaigns (leads captured inside Google, never reach Airtable) When a course has both Search and PMX campaigns (`_search_` + `_pmx_` with same ``), 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/.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.