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>
This commit is contained in:
165
sales-agent/README.md
Normal file
165
sales-agent/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 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.
|
||||
```
|
||||
Reference in New Issue
Block a user