← back to Rentv Sheet Enrich Refine

team_vp.py

82 lines

#!/usr/bin/env python3
"""
team_vp.py — $0 LOCAL Marketing/VP finder. All local: headless urllib fetch of a company's
own Team/Leadership/About pages + Muse (glimmer, local Ollama) to EXTRACT the marketing
lead from the REAL page text. Anti-fabrication: the extracted name must literally appear
in the fetched text, else dropped. No Browserbase, no Exa, no cloud.

Usage: team_vp.py [limit]   (processes companies that have a Website but no Marketing/VP Name)
"""
import lib, json, re, os, ssl, sys, urllib.request
from concurrent.futures import ThreadPoolExecutor
GID=3823360
MODEL=os.environ.get("VP_MODEL","qwen3:14b")  # was muse-glimmer:30b-mlx; repointed 2026-08-31 (TK-10980, muse removed to reclaim 19GB). Override via VP_MODEL.
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
UA={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/537.36"}
MKT=re.compile(r'(chief marketing|vp[, ]+marketing|vice president[, ]+marketing|marketing (director|manager|officer)|head of marketing|director[, ]+marketing|marketing & communications|communications director)',re.I)
TAG=re.compile(r'<[^>]+>')
def fetch(u):
    try: return urllib.request.urlopen(urllib.request.Request(u,headers=UA),timeout=8,context=ctx).read(400000).decode("utf-8","ignore")
    except Exception: return ""
def team_text(domain):
    txt=""
    for p in ("/team","/leadership","/our-team","/about","/about-us","/people","/management","/company","/who-we-are"):
        for sch in ("https://","http://"):
            h=fetch(sch+domain+p)
            if h and MKT.search(h): txt+=" "+TAG.sub(" ",h); break
    return re.sub(r'\s+',' ',txt)[:6000]
def ask(prompt):
    body=json.dumps({"model":MODEL,"prompt":prompt,"stream":False,"options":{"temperature":0}}).encode()
    try:
        r=urllib.request.urlopen(urllib.request.Request("http://localhost:11434/api/generate",data=body,headers={"Content-Type":"application/json"}),timeout=90)
        return json.load(r).get("response","")
    except Exception: return ""
def extract_vp(domain, text):
    if not text: return None
    p=(f"From this company web-page text, find the ONE person whose title is a marketing role "
       f"(Chief Marketing Officer / VP Marketing / Marketing Director / Marketing Manager / Head of Marketing / Communications Director). "
       f'Reply ONLY compact JSON: {{"name":"Full Name","title":"Their Title"}} — or {{}} if no marketing person is named. '
       f"Use ONLY a name that appears verbatim in the text. Do not invent.\n\nTEXT: {text}")
    out=ask(p)
    m=re.search(r'\{[^{}]*\}',out)
    if not m: return None
    try: d=json.loads(m.group(0))
    except Exception: return None
    name=(d.get("name") or "").strip()
    if not name or name.lower() not in text.lower(): return None   # verify-in-fetch guard
    return {"name":name,"title":(d.get("title") or "").strip()}

def main():
    limit=int(sys.argv[1]) if len(sys.argv)>1 else 9999
    tok=lib.access_token()
    title={s["properties"]["sheetId"]:s["properties"]["title"] for s in lib.get_meta(tok)["sheets"]}[GID]
    rows=lib.read_tab(tok,title);hdr=rows[0]
    c={v.strip():j for j,v in enumerate(hdr) if v.strip()}
    def g(r,i): return (r[i] if i<len(r) else "").strip()
    wi=c['Website']; ni=c['Marketing/VP Name']; cvi=c['Company/Venue']
    # unique companies with website but no marketing name yet
    seen=set(); todo=[]
    for r in range(1,len(rows)):
        comp=g(rows[r],cvi); dom=g(rows[r],wi)
        k=comp.lower()
        if not comp or not dom or k in seen or g(rows[r],ni): continue
        seen.add(k); todo.append((comp,dom))
    todo=todo[:limit]
    print(f"{len(todo)} companies to try (model={MODEL})",flush=True)
    # fetch team text concurrently, then extract sequentially (LLM is the bottleneck)
    with ThreadPoolExecutor(max_workers=24) as ex:
        texts=list(ex.map(lambda cd: team_text(cd[1]), todo))
    res=[]; done=0
    for (comp,dom),text in zip(todo,texts):
        vp=extract_vp(dom,text); done+=1
        if vp:
            res.append({"company":comp,"mvp":f"{vp['name']} — {vp['title']} (from {dom})",
                        "why":f"Marketing lead named on {dom} team page (local glimmer extract, verified)"})
            print(f"  {comp[:30]:30s} -> {vp['name']} ({vp['title']})",flush=True)
        if done%25==0: print(f"  ...{done}/{len(todo)}",flush=True)
    out="/tmp/team_vp.json"; json.dump(res,open(out,"w"))
    print(f"{len(res)} marketing leads found",flush=True)
    if res:
        import subprocess; subprocess.run(["python3",os.path.join(os.path.dirname(__file__),"write_by_name.py"),str(GID),out])
if __name__=="__main__": main()