- 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>
236 lines
10 KiB
Markdown
236 lines
10 KiB
Markdown
# 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
|
|
|
|
```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_<rep>_<date>.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 <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
|
|
|
|
```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_<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
|
|
|
|
```bash
|
|
# 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
|
|
```
|