#!/usr/bin/env python3 """Proposal signal watcher: detect customer configuration emails. When a customer clicks "Send Me This Configuration" in the interactive proposal, Sean receives an email with subject "Proposal configuration: ". This watcher (cron, every 30 min) parses those emails, updates the opportunity + dashboard, and reports hot leads for Sean to call. Output: JSON summary for the cron report. New signals -> chat alert. """ import os, json, re, subprocess, datetime BASE = os.path.dirname(os.path.abspath(__file__)) STATE = os.path.join(BASE, "signal_state.json") OPPS = "/home/hatch/workspace/goals/sales-weapon-ae-copilot/hidden_files/prospect-queues/opportunities.json" QUEUE = os.path.join(BASE, "dashboard", "queue.json") ACCOUNT = "d48c9e2643f643aeb3c4dacf347245b8" def cli(*args): r = subprocess.run(["hatch_gws_cli", "gmail", "--account", ACCOUNT] + list(args), capture_output=True, text=True, timeout=120) return r.stdout def load_state(): if os.path.exists(STATE): return json.load(open(STATE)) return {"processed": []} FIELD_PATS = { "model": re.compile(r"^Model:\s*(.+)$", re.M), "term": re.compile(r"^Term:\s*(.+)$", re.M), "options": re.compile(r"^Options:\s*(.+)$", re.M), "volumes": re.compile(r"^Volumes:\s*(.+)$", re.M), "monthly": re.compile(r"^Monthly:.*total\s*(\$[\d,]+\.\d\d)", re.M), "addons": re.compile(r"^Also interested in:\s*(.+)$", re.M), } def parse_config(text): out = {} for k, pat in FIELD_PATS.items(): m = pat.search(text or "") out[k] = m.group(1).strip() if m else "" return out def main(): state = load_state() processed = set(state["processed"]) out = cli("+triage", "--query", 'subject:"Proposal configuration:" newer_than:7d', "--max", "20", "--format", "json") try: msgs = json.loads(out) items = msgs.get("messages", msgs) if isinstance(msgs, dict) else msgs except Exception: items = [] if not isinstance(items, list): items = [] try: opps = json.load(open(OPPS)) except Exception: opps = [] try: queue = json.load(open(QUEUE)) if os.path.exists(QUEUE) else [] except Exception: queue = [] today = datetime.date.today().isoformat() signals = [] for m in items: mid = m.get("id") if not mid or mid in processed: continue body = cli("+read", "--id", mid, "--format", "json") try: msg = json.loads(body) text = msg.get("body_text") or msg.get("body") or "" subject = msg.get("subject", "") except Exception: text, subject = body, "" cfg = parse_config(text if isinstance(text, str) else str(text)) company = subject.split("Proposal configuration:")[-1].strip() or "Unknown" sender = "" try: sender = (json.loads(body).get("from") or "") except Exception: pass sig = {"message_id": mid, "company": company, "sender": sender, "date": today, **cfg} # match opportunity by company name opp = None for o in opps: if (o.get("account") or "").lower() in company.lower() or \ company.lower() in (o.get("account") or "").lower(): opp = o break if opp is not None: event = ("ENGAGED: customer configured via interactive proposal: %s, %s, " "total %s/mo%s." % ( cfg.get("model") or "model n/a", cfg.get("term") or "term n/a", cfg.get("monthly") or "n/a", "; also interested in %s" % cfg["addons"] if cfg.get("addons") else "")) opp.setdefault("history", []).append({"date": today, "event": event}) opp["next_action"] = "CALL %s: configured %s at %s/mo" % ( opp.get("contact") or company, cfg.get("model") or "a system", cfg.get("monthly") or "?") opp["next_action_date"] = today sig["opp_id"] = opp.get("id") for q in queue: if (q.get("customer", {}) or {}).get("company", "").lower() in company.lower(): q["status"] = "engaged" q["engaged_config"] = {k: cfg[k] for k in ("model", "term", "monthly", "addons")} signals.append(sig) processed.add(mid) state["processed"] = sorted(processed) json.dump(state, open(STATE, "w"), indent=1) json.dump(opps, open(OPPS, "w"), indent=1) if os.path.exists(QUEUE): json.dump(queue, open(QUEUE, "w"), indent=1) print(json.dumps({"new_signals": len(signals), "signals": signals}, indent=1)) if __name__ == "__main__": main()