Files
ProspectingAgentLoop/sales-agent/README.md
Chris Olson 39a338990f Initial commit: prospecting agent loop MVP
Closed-loop account-intelligence system: harvest -> relevance filter vs.
Capability Library -> synthesize -> score -> deliver -> feedback.

- 40-capability Broadcom + VMware library
- Live Exa.ai news connector with hard free-plan request ceilings
- Provider-agnostic LLM layer (Gemini/Anthropic) with offline mock fallback
- LLM confidence wired into composite scoring + low-confidence floor
- 7-account pilot list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:39:59 -04:00

166 lines
7.5 KiB
Markdown

# Sales Prospecting Agent Loop — MVP (v1)
A runnable first cut of the closed-loop account-intelligence system described in
[`../sales-prospecting-agent-loop.md`](../sales-prospecting-agent-loop.md). It
harvests signals per account, evaluates them against a capability library, scores
and synthesizes opportunity briefs, delivers them, and feeds rep feedback back into
scoring — the full loop, end to end.
## ⚠️ What's real vs. placeholder in this version
This MVP is built to **run today with zero setup** so we can review output quality
and iterate. Deliberate v1 simplifications (we'll revisit each tomorrow):
| Design calls for | v1 MVP uses | Why |
|---|---|---|
| TypeScript / Node + BullMQ | **Python 3, stdlib only** | Node isn't installed on this machine; concepts map 1:1. Easy to port. |
| Proxycurl/Apify, JSearch, SEC EDGAR | **Fixture file** (`data/fixtures.json`) | No API keys needed; swap each connector for the real API. |
| Exa.ai news/web search | **LIVE** when `EXA_API_KEY` set (else fixtures) | Real connector with hard request ceilings — see below. |
| Salesforce/HubSpot | **`data/accounts.json`** + console write-back | Read context + log-activity stub. |
| Postgres + Redis + pgvector | **JSON files** under `output/state/` | Hash dedup works; semantic dedup is a documented v2 hook. |
| Vector ANN capability pre-filter | **Keyword-overlap pre-filter** | Same interface, no embedding service. |
| Slack Bolt + SendGrid | **Console + markdown files** | Briefs render Slack-style; digest writes to `output/digest.md`. |
| LLM relevance + synthesis | **Gemini or Anthropic when a key is set, else a deterministic mock reasoner** | Provider-agnostic (`LLM_PROVIDER`); loop runs offline without a key. |
## Run it
```bash
cd sales-agent
# 1. Run the loop over all target accounts (harvest -> deliver)
python3 run.py loop
# Single account or one tier:
python3 run.py loop --account acct_meridian
python3 run.py loop --tier strategic
# 2. See the per-rep daily digest
python3 run.py digest
python3 run.py digest --rep rep_sarah
# 3. Record rep feedback (closes the loop)
python3 run.py feedback --brief <BRIEF_ID> --action act_on
# actions: act_on | snooze | reject | won | lost (reject also suppresses 14d)
# 4. Recompute scoring weights from feedback (the weekly recalibration job)
python3 run.py recalibrate
# Helpers
python3 run.py accounts # list target accounts
python3 run.py briefs # list all generated briefs + status
```
### LLM reasoning (provider-agnostic)
The relevance filter and synthesizer call whichever provider is configured. Set in
`.env`:
```bash
# Gemini (current default)
LLM_PROVIDER=gemini
GEMINI_API_KEY=...
GEMINI_MODEL=gemini-2.5-flash
# or Anthropic
LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=...
LLM_MODEL=claude-sonnet-4-6
```
If `LLM_PROVIDER` is blank it auto-detects from whichever key is present (Gemini
preferred). With no key, a deterministic mock reasoner produces grounded briefs so
the full pipeline still runs offline. Both providers use plain `urllib` — no SDKs.
## Exa.ai news connector & free-plan protection
The news harvester ([`src/connectors/exa.py`](src/connectors/exa.py)) calls the real
Exa search API (`POST https://api.exa.ai/search`) when `EXA_API_KEY` is set, and
falls back to fixtures otherwise. **Two hard ceilings keep you inside the free plan**
(Exa's free tier is ~20,000 requests/month; we default far below that):
- `EXA_MONTHLY_REQUEST_CAP` (default **1000**) — a persistent counter in
`output/state/exa_usage.json` that resets at the start of each calendar month.
Once hit, the connector refuses to call Exa and falls back to fixtures.
- `EXA_PER_RUN_REQUEST_CAP` (default **25**) — a per-process cap so a single loop
can't burn through the budget.
A request is only counted when a call actually reaches Exa (a billable call);
network failures that never hit Exa are not counted. Each account uses **one** Exa
request per loop run (so the default 3 sample accounts = 3 requests/run).
```bash
python3 run.py exa-usage # show remaining monthly budget
```
### Domain scoping (default on)
By default, news search is **scoped to each account's own `domain`** (via Exa
`includeDomains`) — newsroom, press releases, investor-relations — which cuts out
stock-ticker and analyst noise from a bare company-name search. Toggle it:
```bash
python3 run.py loop --no-domain-filter # widen to broad third-party web coverage
# or set EXA_DOMAIN_FILTER=0 in .env to default to broad
```
Note: domain scoping only returns results when the account's real domain has indexed
content. The fictional sample accounts return 0 domain-scoped articles by design —
real target-account domains will return their first-party news.
Tune all limits in `.env` (`EXA_MONTHLY_REQUEST_CAP`, `EXA_PER_RUN_REQUEST_CAP`,
`EXA_RESULTS_PER_QUERY`, `EXA_LOOKBACK_DAYS`). Set `EXA_ENABLED=0` to force fixtures.
## How the loop maps to the design
```
run.py loop
└─ src/pipeline.run_account_cycle (design §7.3)
1. agents/harvester Phase 1 signal harvest (connectors/*)
2. agents/normalizer Phase 2 normalize + hash dedup
3. agents/relevance Phase 3 LLM vs. capability library
4. agents/synthesizer Phase 4 opportunity briefs
5. agents/scorer Phase 5 composite score + priority
6. delivery/render Phase 7 deliver (console/md) + CRM log
7. memory/store Phase 6 account memory + suppression
feedback ─ memory/feedback §6 feedback store + recalibration
```
## Project layout
```
sales-agent/
├── run.py CLI entrypoint
├── data/
│ ├── accounts.json target accounts (stand-in for CRM)
│ ├── capabilities.json the Capability Library (the "brain")
│ └── fixtures.json placeholder harvested signals
├── src/
│ ├── config.py paths, thresholds, .env loader
│ ├── types.py RawSignal / NormalizedSignal / Brief dataclasses
│ ├── llm.py Anthropic call (urllib) + mock fallback
│ ├── capabilities.py library loader + keyword pre-filter
│ ├── connectors/ linkedin, news, jobs, crm (fixture-backed)
│ ├── agents/ harvester, normalizer, relevance, synthesizer, scorer
│ ├── memory/ store (dedup/memory/briefs) + feedback (recalibration)
│ ├── delivery/ render (briefs) + digest
│ └── pipeline.py the per-account orchestration
└── output/ generated briefs, digest, and JSON state (created on run)
```
## Suggested review focus for tomorrow
1. **Capability Library** (`data/capabilities.json`) — now seeded with 40 real
Broadcom/VMware capabilities across Mainframe (13), Enterprise Software (9),
Identity & Access Management (6), and VMware by Broadcom (12). Review wording of
buying-signals/anti-signals and add any missing SKUs. The prior placeholder library
is kept at `data/capabilities.placeholder.json`.
2. **Accounts** (`data/accounts.json`) — sample BSG target accounts; swap for a real
target-account list + CRM pull.
3. **Scoring weights** — the composite formula in `agents/scorer.py` uses the design's
weights; tune the tier/seniority/timing tables.
4. **Connectors** — Exa.ai news is now **live** (with request caps). Improve query
precision (a bare company name pulls in stock-ticker noise — add domain filtering
via the account `domain`), then wire the next source (jobs/LinkedIn).
5. **Reasoning quality** — set an API key and compare Claude briefs vs. the mock.
```