#!/usr/bin/env python3 """CLI entrypoint for the Sales Prospecting Agent Loop (MVP). Commands: python run.py loop [--account ID] [--tier strategic|enterprise|growth] python run.py digest [--rep rep_sarah] python run.py feedback --brief ID --action act_on|snooze|reject|won|lost [--note "..."] python run.py recalibrate python run.py accounts python run.py briefs """ from __future__ import annotations import argparse import sys from src import config, pipeline from src.connectors import crm, exa from src.delivery import digest as digest_mod from src.delivery import whitespace as whitespace_mod from src.memory import store, feedback def cmd_loop(args): from src import capabilities capabilities.set_active_set(args.capability) if args.no_domain_filter: config.EXA_DOMAIN_FILTER = False # per-run override if args.domain_filter: config.EXA_DOMAIN_FILTER = True pipeline.run_all(tier=args.tier, account_id=args.account, limit=args.limit, rep=args.rep) print("\n--- digest preview ---") print(digest_mod.write_digest()) def cmd_digest(args): if args.html: path = digest_mod.write_digest_html(rep=args.rep) print(f"HTML digest written: {path}") else: print(digest_mod.write_digest(rep_id=args.rep)) def cmd_whitespace(args): print(whitespace_mod.write_report()) print(f"(also written to {config.OUTPUT_DIR / 'whitespace.md'})") def cmd_renewals(args): """Upcoming renewals across the territory (local entitlement data).""" import json as _json from collections import defaultdict from datetime import datetime, timezone p = config.DATA_DIR / "entitlements.json" if not p.exists(): print("No entitlement data. Run: python3 scripts/import_entitlements.py ") return ent = _json.loads(p.read_text()) now = datetime.now(timezone.utc) horizon = args.months * 31 by_month = defaultdict(list) for aid, e in ent.items(): for prod in e["products"]: d = prod.get("nextRenewal") if not d: continue days = (datetime.strptime(d, "%Y-%m-%d").replace(tzinfo=timezone.utc) - now).days if 0 <= days <= horizon: by_month[d[:7]].append((d, e["accountName"], prod["product"], prod["lines"])) print(f"💰 UPCOMING RENEWALS — next {args.months} month(s)\n") if not by_month: print(" none in window") for month in sorted(by_month): items = sorted(by_month[month]) print(f"── {month} ({len(items)} renewal line-group(s)) " + "─" * 20) for d, name, prod, lines in items[:args.limit]: print(f" {d} {name[:32]:32} {prod[:58]} ({lines} lines)") if len(items) > args.limit: print(f" ... and {len(items) - args.limit} more (use --limit to widen)") print() def cmd_feedback(args): entry = feedback.record_feedback(args.brief, args.action, args.note or "") print(f"Recorded '{args.action}' on brief {args.brief}. " f"New status: {store.get_brief(args.brief)['status']}") def cmd_recalibrate(args): weights = feedback.recalibrate() if not weights: print("Not enough feedback yet (need >=3 samples per capability/tier/signal combo).") return print("Updated scoring weights:") for k, v in weights.items(): print(f" {k}: weight={v['weight']} (n={v['samples']})") def cmd_capability_sets(args): from src import capabilities import json as _json for name in capabilities.available_sets(): if name == "ALL": n = len(_json.loads((config.DATA_DIR / "capabilities.json").read_text())["capabilities"]) print(f" ALL {n} capabilities (full portfolio, default)") else: doc = _json.loads((config.DATA_DIR / "capability_sets" / f"{name}.json").read_text()) caps = ", ".join(c["name"].split(" — ")[0] for c in doc["capabilities"]) print(f" {name:12} {len(doc['capabilities'])} capability(ies): {caps}") def cmd_accounts(args): for a in crm.load_accounts(): prods = a.get("currentProducts") or [] has = f"{len(prods)} products, {len(a.get('ownedCapabilityIds') or [])} caps" if prods else "-" print(f" {a['accountId']:18} {a['accountTier']:10} {a['accountName']:28} " f"rep={a['assignedRep']['name']} owns: {has}") def cmd_briefs(args): briefs = sorted(store.load_briefs(), key=lambda b: b["composite_score"], reverse=True) if not briefs: print("No briefs yet. Run: python run.py loop") return for b in briefs: print(f" {b['id']} [{b['priority']:8}] score={b['composite_score']:<5} " f"status={b['status']:9} {b['account_name']} — {b['capability_name']}") def cmd_exa_usage(args): st = exa.usage_status() print("Exa.ai request budget") print(f" enabled: {st['enabled']}") print(f" month: {st['month']}") print(f" used this month: {st['used_this_month']}") print(f" monthly cap: {st['monthly_cap']}") print(f" monthly remaining: {st['monthly_remaining']}") print(f" per-run cap: {st['per_run_cap']}") if not st["enabled"]: print(" (Exa disabled — set EXA_API_KEY in .env to enable; using fixtures)") def main(argv=None): parser = argparse.ArgumentParser(description="Sales Prospecting Agent Loop (MVP)") sub = parser.add_subparsers(dest="command", required=True) p_loop = sub.add_parser("loop", help="Run the harvest->deliver loop") p_loop.add_argument("--account", help="single account id") p_loop.add_argument("--tier", choices=["strategic", "enterprise", "growth"]) p_loop.add_argument("--rep", help="only accounts assigned to this rep (name or id substring)") p_loop.add_argument("--limit", type=int, help="process at most N accounts (strategic + least-recently-run first); " "successive runs resume where the last left off") p_loop.add_argument("--capability", default="ALL", metavar="SET", help="capability set to reason against: ALL (full portfolio, default) " "or a name from data/capability_sets/ (e.g. agentminder)") p_loop.add_argument("--no-domain-filter", action="store_true", help="broad-web news about the account (default behavior)") p_loop.add_argument("--domain-filter", action="store_true", help="scope Exa to the account's own domain (newsroom/press only)") p_loop.set_defaults(func=cmd_loop) p_dig = sub.add_parser("digest", help="Print the daily intelligence digest") p_dig.add_argument("--rep", help="filter to a rep (name substring or rep id)") p_dig.add_argument("--html", action="store_true", help="write a forwardable HTML digest to output/digests/") p_dig.set_defaults(func=cmd_digest) sub.add_parser("whitespace", help="Territory coverage report: opportunities, gaps, never-scanned accounts" ).set_defaults(func=cmd_whitespace) p_ren = sub.add_parser("renewals", help="Upcoming renewals by month (local entitlement data)") p_ren.add_argument("--months", type=int, default=6, help="horizon in months (default 6)") p_ren.add_argument("--limit", type=int, default=15, help="max rows shown per month (default 15)") p_ren.set_defaults(func=cmd_renewals) p_fb = sub.add_parser("feedback", help="Record rep feedback on a brief") p_fb.add_argument("--brief", required=True) p_fb.add_argument("--action", required=True, choices=["act_on", "snooze", "reject", "won", "lost"]) p_fb.add_argument("--note", default="") p_fb.set_defaults(func=cmd_feedback) p_re = sub.add_parser("recalibrate", help="Recompute feedback weights (weekly job)") p_re.set_defaults(func=cmd_recalibrate) sub.add_parser("capability-sets", help="List available capability sets" ).set_defaults(func=cmd_capability_sets) sub.add_parser("accounts", help="List target accounts").set_defaults(func=cmd_accounts) sub.add_parser("briefs", help="List all generated briefs").set_defaults(func=cmd_briefs) sub.add_parser("exa-usage", help="Show Exa.ai request budget").set_defaults(func=cmd_exa_usage) args = parser.parse_args(argv) config.ensure_dirs() try: args.func(args) except KeyboardInterrupt: print("\nInterrupted — no partial briefs were written; unprocessed " "signals will be retried on the next run.") return 130 if __name__ == "__main__": sys.exit(main())