# Usage Reference — Sales Prospecting Agent Loop All commands run from `sales-agent/`: `python3 run.py [flags]` --- ## Commands ### `loop` — run the harvest → reason → deliver cycle ```bash python3 run.py loop # all accounts (ordered, see below) python3 run.py loop --limit 30 # at most 30 accounts this run python3 run.py loop --account 300000172847316 python3 run.py loop --tier strategic # strategic | enterprise | growth python3 run.py loop --rep "Robert Parker" # one rep's accounts (name or id substring) python3 run.py loop --tier strategic --limit 10 python3 run.py loop --domain-filter # Exa: account's own domain only python3 run.py loop --no-domain-filter # Exa: broad-web news (default) ``` | Flag | Effect | |---|---| | `--account ID` | Run a single account by `accountId` (OEC number) | | `--tier T` | Only `strategic` / `enterprise` / `growth` accounts | | `--rep NAME` | Only accounts assigned to a rep (case-insensitive name or `rep_id` substring) | | `--limit N` | Process at most N accounts. Order: **strategic tier first, then least-recently-scanned** (never-scanned first). The run cursor (`output/state/run_cursor.json`) persists, so successive `--limit` runs walk the whole territory — e.g. a nightly `loop --limit 30` covers 342 accounts in ~12 days. | | `--domain-filter` | Scope Exa news to the account's own domain (newsroom/PR only) | | `--no-domain-filter` | Broad-web news about the account (default) | Per-account flow: harvest (Exa news + SEC EDGAR filings + stubs) → dedup → **quality gate** (drops awards/CSR/ticker chatter pre-LLM; log: `output/state/quality_gate_log.json`) → LLM relevance filter (with rolling **account narrative** as context) → LLM synthesis (briefs with **cited talking points** `[S#]` + updated narrative) → **citation verification** (uncited points dropped; brief skipped if none survive) → scoring → delivery (`output/briefs/*.md`) → memory + cursor update. On LLM rate-limit (429): waits and retries (no fabricated output). On LLM failure: account skipped, its signals retried next run. ### `digest` — per-rep intelligence digest ```bash python3 run.py digest # text digest, all reps (also -> output/digest.md) python3 run.py digest --rep rep_robert_parker # filter by rep id python3 run.py digest --rep "Misty Brew" --html # forwardable HTML -> output/digests/ python3 run.py digest --html # HTML for all reps ``` | Flag | Effect | |---|---| | `--rep X` | Filter to one rep (text mode: rep id; HTML mode: name substring or id) | | `--html` | Write a self-contained HTML file (`output/digests/digest__.html`) — clean enough to paste/forward as an email body | ### `renewals` — upcoming renewals by month (local entitlement data) ```bash python3 run.py renewals # next 6 months python3 run.py renewals --months 12 --limit 30 ``` | Flag | Effect | |---|---| | `--months N` | Horizon (default 6) | | `--limit N` | Max rows per month (default 15) | Renewal proximity also feeds scoring automatically: a brief whose capability (or account) has a renewal within `RENEWAL_WINDOW_DAYS` (default 365) gets a boost up to `RENEWAL_BOOST_MAX` (default +0.10; half for account-level renewals), scaled by closeness — computed locally, no LLM. Boosted briefs show a **💰 RENEWAL WINDOW** block in the brief/digest. ### `whitespace` — territory coverage report ```bash python3 run.py whitespace # prints + writes output/whitespace.md ``` Shows: scanned vs never-scanned vs stale (>14d) accounts, opportunities by capability, pipeline status counts, per-account coverage, and the backlog of never-scanned accounts. (Becomes a true owns-vs-whitespace matrix once entitlement data populates `currentProducts`.) ### `feedback` — record a rep's verdict on a brief ```bash python3 run.py feedback --brief --action act_on python3 run.py feedback --brief --action reject --note "already own this" ``` | Flag | Effect | |---|---| | `--brief ID` | Brief id (see `briefs` command or the digest) | | `--action A` | `act_on` \| `snooze` \| `reject` \| `won` \| `lost` | | `--note "..."` | Optional free-text context, stored with the feedback | ### `recalibrate` — recompute feedback-driven scoring weights ```bash python3 run.py recalibrate # weekly job; needs >=3 samples per combo ``` ### `accounts` / `briefs` / `exa-usage` — inspection ```bash python3 run.py accounts # list all target accounts (tier, rep, products) python3 run.py briefs # all briefs, ranked by score, with status python3 run.py exa-usage # Exa request budget (monthly cap / remaining) ``` ### Account import (repeatable) ```bash python3 scripts/import_accounts.py "/path/to/ISG Customer Sales Assignments.csv" ``` Rebuilds `data/accounts.json` from a territory CSV export (see script header for the field mapping). Backs up nothing — copy `data/accounts.json` first. ### Entitlement import (repeatable, 100% local) ```bash python3 scripts/import_entitlements.py data/SE_ACL_FY26_Q3.csv ``` Loads the confidential ACL export **entirely locally** (no LLM, no API): aggregates active contract lines per parent, joins to accounts by normalized parent name (the FY26 Q3 export's Parent Account ID column is Excel-corrupted), maps product families → capability ids via `data/product_capability_map.json`, and writes: - `data/accounts.json` — `currentProducts` (readable, fed to LLM prompts) + `ownedCapabilityIds` (used for local anti-signal checks) - `data/entitlements.json` (**gitignored**) — full per-account detail incl. next renewal dates - `output/state/entitlement_unmapped.json` — product families with no capability mapping yet; extend `product_capability_map.json` from this During the loop, a brief for an already-owned capability is tagged **⬆ EXPANSION** (local set lookup, no LLM call). The ACL CSV and entitlements.json are gitignored and never leave the machine. --- ## Configuration (`.env`) ### LLM provider | Var | Default | Meaning | |---|---|---| | `LLM_PROVIDER` | auto | `anthropic` \| `openai` \| `gemini` (auto-detects from keys if blank) | | `LLM_MODEL` | `claude-sonnet-5` | Anthropic model id | | `ANTHROPIC_EFFORT` | `low` | Reasoning effort: `low` \| `medium` \| `high` \| `max` | | `ANTHROPIC_USE_KEYCHAIN` | `1` | Read the Claude Code OAuth token from the macOS keychain (kept fresh by Claude Code; refresh with any `claude -p ...` call if expired) | | `ANTHROPIC_OAUTH_TOKEN` | — | Explicit OAuth token (overrides keychain) | | `ANTHROPIC_API_KEY` | — | Classic API key (used only if no OAuth token available) | | `OPENAI_BASE_URL` | — | Any OpenAI-compatible `/chat/completions` endpoint (NVIDIA, LM Studio, ... — commented examples in `.env`) | | `OPENAI_API_KEY` / `OPENAI_MODEL` | — | Credentials/model for that endpoint | | `OPENAI_JSON_MODE` | `1` | Send `response_format=json_object` (0 if endpoint rejects it) | | `OPENAI_TEMPERATURE` | `0.5` | Sampling (OpenAI-compatible only) | | `OPENAI_FREQUENCY_PENALTY` | `0.4` | Anti-repetition (OpenAI-compatible only) | | `GEMINI_API_KEY` / `GEMINI_MODEL` | — | Gemini credentials/model | | `LLM_RATELIMIT_WAIT_SECONDS` | `900` | Wait per 429 before retry (honors `Retry-After`) | | `LLM_RATELIMIT_MAX_RETRIES` | `24` | Max 429 waits before skipping the account | | `LLM_CONTENT_RETRIES` | `3` | Re-samples on degenerate/unparseable model output | **Switching providers** is a `.env` edit only: set `LLM_PROVIDER`, uncomment the relevant block (the NVIDIA endpoint is preserved, commented, for switch-back). ### Signal sources | Var | Default | Meaning | |---|---|---| | `EXA_API_KEY` | — | Exa.ai key (news search) | | `EXA_ENABLED` | `1` | `0` forces fixtures (no API calls) | | `EXA_MONTHLY_REQUEST_CAP` | `1000` | Hard monthly ceiling (persistent counter) | | `EXA_PER_RUN_REQUEST_CAP` | `25` | Hard per-run ceiling | | `EXA_RESULTS_PER_QUERY` | `5` | Articles per account per run | | `EXA_LOOKBACK_DAYS` | `45` | News/filings recency window | | `EXA_CATEGORY` | `news` | Exa category filter (`""` disables) — keeps out careers/marketing pages | | `EXA_DOMAIN_FILTER` | `0` | `1` = first-party newsroom only | | `EXA_EXCLUDE_DOMAINS` | — | Comma-separated hosts to always exclude | | `SEC_ENABLED` | `1` | SEC EDGAR filings harvester (free, public accounts auto-detected via CIK match) | | `SEC_MAX_FILINGS_PER_ACCOUNT` | `5` | Max recent filings per account per run | ### Scoring | Var | Default | Meaning | |---|---|---| | `PRIORITY_THRESHOLD` | `0.72` | Composite score for priority (immediate) delivery | | `STANDARD_THRESHOLD` | `0.45` | Composite score for standard (digest) delivery | | `CONFIDENCE_FLOOR` | `0.35` | Briefs the LLM itself scores below this are logged, never delivered | | `RENEWAL_WINDOW_DAYS` | `365` | Renewal proximity window for the score boost | | `RENEWAL_BOOST_MAX` | `0.10` | Max boost at renewal date (same-capability; half for account-level) | Composite formula: `signalStrength*0.30 + llmConfidence*0.20 + accountTier*0.20 + buyerSeniority*0.15 + timingUrgency*0.10 + repFeedback*0.05`. --- ## Output layout ``` output/ ├── briefs/ # delivered briefs (Slack-style markdown) ├── digests/ # HTML digests (digest__.html) ├── digest.md # latest text digest (convenience copy) ├── digest__