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>
166 lines
7.5 KiB
Markdown
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.
|
|
```
|