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

7.5 KiB

Sales Prospecting Agent Loop — MVP (v1)

A runnable first cut of the closed-loop account-intelligence system described in ../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

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:

# 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) 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).

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:

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.