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