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:
61
sales-agent/src/capabilities.py
Normal file
61
sales-agent/src/capabilities.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Capability Library loader + lightweight keyword pre-filter.
|
||||
|
||||
The design uses a vector ANN pre-filter to cut LLM calls ~60%. For the MVP we
|
||||
approximate that with keyword overlap scoring (no embedding service required).
|
||||
The interface is the same: given a signal, return candidate capabilities.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_library() -> dict:
|
||||
return json.loads((config.DATA_DIR / "capabilities.json").read_text())
|
||||
|
||||
|
||||
def capability_by_id(cap_id: str) -> dict | None:
|
||||
for cap in load_library()["capabilities"]:
|
||||
if cap["id"] == cap_id:
|
||||
return cap
|
||||
return None
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
return set(re.findall(r"[a-z]{3,}", text.lower()))
|
||||
|
||||
|
||||
def prefilter(signal_content: str, top_k: int = 3) -> list[dict]:
|
||||
"""Return capabilities whose keywords overlap the signal, best first.
|
||||
|
||||
Stand-in for the vector ANN pre-filter in the design (section 5.2).
|
||||
"""
|
||||
sig_tokens = _tokens(signal_content)
|
||||
scored: list[tuple[float, dict]] = []
|
||||
for cap in load_library()["capabilities"]:
|
||||
kw = set(k.lower() for k in cap.get("keywords", []))
|
||||
kw_tokens: set[str] = set()
|
||||
for k in kw:
|
||||
kw_tokens |= _tokens(k)
|
||||
overlap = len(sig_tokens & kw_tokens)
|
||||
if overlap:
|
||||
scored.append((overlap, cap))
|
||||
scored.sort(key=lambda x: x[0], reverse=True)
|
||||
return [cap for _, cap in scored[:top_k]]
|
||||
|
||||
|
||||
def library_summary_for_prompt(candidates: list[dict]) -> str:
|
||||
"""Compact capability descriptions for the relevance-filter prompt."""
|
||||
lines = []
|
||||
for c in candidates:
|
||||
lines.append(
|
||||
f"- {c['id']} | {c['name']}: {c['description']}\n"
|
||||
f" Buying signals: {'; '.join(c['buyingSignals'])}\n"
|
||||
f" Anti-signals: {'; '.join(c['antiSignals'])}\n"
|
||||
f" Target buyer: {', '.join(c['targetBuyer']['titles'])}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user