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:
2026-06-23 11:39:59 -04:00
commit 39a338990f
35 changed files with 4020 additions and 0 deletions

103
sales-agent/src/config.py Normal file
View File

@@ -0,0 +1,103 @@
"""Central config & paths. Loads a .env file if present (no external deps)."""
from __future__ import annotations
import os
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = ROOT / "data"
OUTPUT_DIR = ROOT / "output"
STATE_DIR = OUTPUT_DIR / "state"
BRIEFS_DIR = OUTPUT_DIR / "briefs"
def _load_dotenv() -> None:
"""Minimal .env loader so the user can keep keys in sales-agent/.env."""
env_path = ROOT / ".env"
if not env_path.exists():
return
for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
key, val = key.strip(), val.strip().strip('"').strip("'")
os.environ.setdefault(key, val)
_load_dotenv()
# --- LLM ---
# Provider abstraction: gemini | anthropic | mock. If LLM_PROVIDER is unset we
# auto-detect from whichever key is present (Gemini preferred), falling back to
# a deterministic mock reasoner so the loop still runs offline.
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "").strip()
LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-4-6")
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "").strip()
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
_provider = os.environ.get("LLM_PROVIDER", "").strip().lower()
if not _provider:
if GEMINI_API_KEY:
_provider = "gemini"
elif ANTHROPIC_API_KEY:
_provider = "anthropic"
else:
_provider = "mock"
# A forced USE_MOCK_LLM=1, or selecting a provider whose key is missing, => mock.
if os.environ.get("USE_MOCK_LLM", "").strip() == "1":
_provider = "mock"
elif _provider == "gemini" and not GEMINI_API_KEY:
_provider = "mock"
elif _provider == "anthropic" and not ANTHROPIC_API_KEY:
_provider = "mock"
LLM_PROVIDER = _provider
USE_MOCK_LLM = LLM_PROVIDER == "mock"
def llm_label() -> str:
if LLM_PROVIDER == "mock":
return "MOCK reasoner (no LLM key)"
if LLM_PROVIDER == "gemini":
return f"Gemini ({GEMINI_MODEL})"
return f"Anthropic ({LLM_MODEL})"
# --- Scoring thresholds (Phase 5 of the design) ---
PRIORITY_THRESHOLD = float(os.environ.get("PRIORITY_THRESHOLD", "0.72"))
STANDARD_THRESHOLD = float(os.environ.get("STANDARD_THRESHOLD", "0.45"))
# Briefs whose LLM confidence falls below this floor are demoted to "low"
# (logged, not delivered) regardless of composite score — keeps weak matches
# the model itself doubts out of the rep's queue.
CONFIDENCE_FLOOR = float(os.environ.get("CONFIDENCE_FLOOR", "0.35"))
# --- Exa.ai news/web harvester ---
EXA_API_KEY = os.environ.get("EXA_API_KEY", "").strip()
# Enabled only when a key is present AND not explicitly turned off.
EXA_ENABLED = (os.environ.get("EXA_ENABLED", "1").strip() == "1") and bool(EXA_API_KEY)
# Hard request ceilings to protect the free plan. The connector will NOT call
# Exa once either cap is reached; the monthly counter persists across runs.
EXA_MONTHLY_REQUEST_CAP = int(os.environ.get("EXA_MONTHLY_REQUEST_CAP", "1000"))
EXA_PER_RUN_REQUEST_CAP = int(os.environ.get("EXA_PER_RUN_REQUEST_CAP", "25"))
EXA_RESULTS_PER_QUERY = int(os.environ.get("EXA_RESULTS_PER_QUERY", "5"))
EXA_LOOKBACK_DAYS = int(os.environ.get("EXA_LOOKBACK_DAYS", "45"))
# Scope news search to each account's own domain (newsroom/press/IR) to cut
# third-party ticker/analyst noise. Toggle off in .env or per-run with
# `loop --no-domain-filter` to include broad third-party coverage.
EXA_DOMAIN_FILTER = os.environ.get("EXA_DOMAIN_FILTER", "1").strip() == "1"
# --- Dedup / suppression windows ---
SEMANTIC_DEDUP_DAYS = 90
SAME_TYPE_SUPPRESS_DAYS = 14
# Per-tier harvest cadence (informational for the scheduler; the MVP runs on demand)
TIER_CADENCE_HOURS = {"strategic": 24, "enterprise": 60, "growth": 168}
# Brief expiry (opportunities decay if not acted on)
BRIEF_TTL_DAYS = 21
def ensure_dirs() -> None:
for d in (OUTPUT_DIR, STATE_DIR, BRIEFS_DIR):
d.mkdir(parents=True, exist_ok=True)