Files
ProspectingAgentLoop/sales-agent/USAGE.md
Chris Olson c2854ba163 Anthropic OAuth LLM, entitlements (feature 1), renewals, + 7 enhancements
- LLM: Anthropic Sonnet 5 (effort=low) via OAuth Bearer (Claude Code keychain
  token, expiry-aware); NVIDIA/LM Studio endpoints kept commented for switch-back.
- Entitlements: scripts/import_entitlements.py joins ACL export to accounts by
  normalized parent name (324/342 matched; ID column Excel-corrupted), maps
  products->capabilities via data/product_capability_map.json. Raw ACL csv and
  entitlements.json are gitignored (confidential); accounts.json carries derived
  currentProducts + ownedCapabilityIds. Owned capabilities tag briefs EXPANSION.
- Renewals: local renewal-proximity score boost (window 365d, max +0.10) +
  'renewals' report; briefs/digests show RENEWAL WINDOW context.
- Batch runs: loop --limit/--rep with persistent cursor (strategic + stale-first).
- Digests: per-rep HTML export; timestamped digest_<date>_<time>.md kept forever.
- Account narrative memory fed into relevance/synthesis; synthesis maintains it.
- Signal quality gate drops PR fluff pre-LLM; SEC EDGAR filings connector.
- Citation verification: talking points must cite [S#] signals or are dropped.
- Brief filenames use account-name slug; whitespace report + installed-base view.
- USAGE.md: full command/flag/env reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:12:36 -04:00

10 KiB

Usage Reference — Sales Prospecting Agent Loop

All commands run from sales-agent/: python3 run.py <command> [flags]


Commands

loop — run the harvest → reason → deliver cycle

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

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_<rep>_<date>.html) — clean enough to paste/forward as an email body

renewals — upcoming renewals by month (local entitlement data)

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

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

python3 run.py feedback --brief <BRIEF_ID> --action act_on
python3 run.py feedback --brief <BRIEF_ID> --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

python3 run.py recalibrate    # weekly job; needs >=3 samples per combo

accounts / briefs / exa-usage — inspection

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)

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)

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.jsoncurrentProducts (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_<rep>_<date>.html)
├── digest.md                     # latest text digest (convenience copy)
├── digest_<date>_<time>.md       # timestamped digests (every run kept)
├── whitespace.md                 # latest coverage report
└── state/
    ├── run_cursor.json           # per-account last-scanned timestamps (--limit batching)
    ├── memory_<accountId>.json   # per-account memory incl. accountNarrative
    ├── briefs_index.json         # all briefs + status
    ├── feedback.json             # rep feedback records
    ├── scoring_weights.json      # recalibrated weights
    ├── quality_gate_log.json     # last 500 signals dropped pre-LLM (tune the gate here)
    ├── exa_usage.json            # Exa monthly request counter
    ├── sec_company_tickers.json  # SEC company->CIK table (cached 7 days)
    └── sec_cik_map.json          # accountId->CIK resolution cache (incl. non-public misses)

Typical cadences

# Nightly (cron): walk the territory 30 accounts at a time
python3 run.py loop --limit 30

# Monday morning: coverage review + per-rep digests
python3 run.py whitespace
python3 run.py digest --rep "Misty Brew" --html

# Weekly: fold rep feedback into scoring
python3 run.py recalibrate