personal-brain 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. personal_brain/__init__.py +3 -0
  2. personal_brain/cli.py +52 -0
  3. personal_brain/export_graph.py +95 -0
  4. personal_brain/export_shortlists.py +297 -0
  5. personal_brain/export_stories.py +155 -0
  6. personal_brain/layout_graph.py +60 -0
  7. personal_brain/load_graph.py +671 -0
  8. personal_brain/mcp_server.py +96 -0
  9. personal_brain/memory.py +330 -0
  10. personal_brain/paths.py +36 -0
  11. personal_brain/sample_data/enriched/batch_sample.json +14891 -0
  12. personal_brain/sample_data/enriched/companies_sample.json +511 -0
  13. personal_brain/sample_data/enriched/events_sample.json +160 -0
  14. personal_brain/sample_data/linkedin/Connections.csv +245 -0
  15. personal_brain/sample_data/linkedin/Education.csv +2 -0
  16. personal_brain/sample_data/linkedin/Positions.csv +3 -0
  17. personal_brain/sample_data/linkedin/Profile.csv +2 -0
  18. personal_brain/sample_data/linkedin/messages.csv +438 -0
  19. personal_brain/server.py +157 -0
  20. personal_brain/viewer/index.html +971 -0
  21. personal_brain/viewer/shortlists.html +157 -0
  22. personal_brain/viewer/vendor/three/LICENSE +21 -0
  23. personal_brain/viewer/vendor/three/build/three.module.js +54571 -0
  24. personal_brain/viewer/vendor/three/examples/jsm/controls/OrbitControls.js +1523 -0
  25. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/EffectComposer.js +231 -0
  26. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/MaskPass.js +104 -0
  27. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/OutputPass.js +97 -0
  28. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/Pass.js +95 -0
  29. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/RenderPass.js +99 -0
  30. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/ShaderPass.js +77 -0
  31. personal_brain/viewer/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +415 -0
  32. personal_brain/viewer/vendor/three/examples/jsm/shaders/CopyShader.js +45 -0
  33. personal_brain/viewer/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +64 -0
  34. personal_brain/viewer/vendor/three/examples/jsm/shaders/OutputShader.js +85 -0
  35. personal_brain-0.1.0.dist-info/METADATA +215 -0
  36. personal_brain-0.1.0.dist-info/RECORD +39 -0
  37. personal_brain-0.1.0.dist-info/WHEEL +4 -0
  38. personal_brain-0.1.0.dist-info/entry_points.txt +2 -0
  39. personal_brain-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """Personal Brain: your LinkedIn network as agent memory in FalkorDB."""
2
+
3
+ __version__ = "0.1.0"
personal_brain/cli.py ADDED
@@ -0,0 +1,52 @@
1
+ """The personal-brain command."""
2
+
3
+ import importlib
4
+ import sys
5
+
6
+ from personal_brain import __version__
7
+ from personal_brain.paths import HOME
8
+
9
+ USAGE = f"""personal-brain {__version__}: your LinkedIn network as agent memory in FalkorDB.
10
+
11
+ Commands:
12
+ mcp run the MCP server for your agent (stdio)
13
+ load [--sample] [--cloud] [--force]
14
+ build the graph from your LinkedIn export, or from the made-up sample
15
+ export [--cloud] write the viewer's data from the graph
16
+ view open the viewer's server on http://127.0.0.1:8765
17
+
18
+ Your data folder: {HOME}
19
+ """
20
+
21
+
22
+ def run(module, *args):
23
+ """Each module reads its options from sys.argv, some of them on import."""
24
+ sys.argv = [f"personal-brain {module}", *args]
25
+ importlib.import_module(f"personal_brain.{module}").main()
26
+
27
+
28
+ def main():
29
+ command, args = (sys.argv[1], sys.argv[2:]) if len(sys.argv) > 1 else ("", [])
30
+ if command == "--version":
31
+ print(f"personal-brain {__version__}")
32
+ elif command in ("mcp", "load", "export", "view"):
33
+ HOME.mkdir(parents=True, exist_ok=True, mode=0o700)
34
+ if command == "mcp":
35
+ run("mcp_server", *args)
36
+ elif command == "load":
37
+ run("load_graph", *args)
38
+ elif command == "view":
39
+ run("server", *args)
40
+ else:
41
+ run("export_graph", *args)
42
+ run("layout_graph", "--dims=2")
43
+ run("export_stories", *args)
44
+ run("export_shortlists", *args)
45
+ elif command in ("", "-h", "--help", "help"):
46
+ print(USAGE)
47
+ else:
48
+ sys.exit(f"Unknown command: {command}\n\n{USAGE}")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,95 @@
1
+ """THROWAWAY SPIKE: export the whole FalkorDB graph to JSON for the WebGL viewer.
2
+
3
+ Reads every node and edge, in pages (FalkorDB Cloud caps results at 10k rows),
4
+ and writes data/viewer/graph.json (gitignored: it has real names).
5
+
6
+ personal-brain export # local Docker FalkorDB
7
+ personal-brain export --cloud # FALKORDB_URL from the environment or .env
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from falkordb import FalkorDB
17
+
18
+ from personal_brain.paths import HOME, cloud_url
19
+ OUT = Path(os.environ.get("PB_VIEWER_DIR", HOME / "data" / "viewer")) / "graph.json"
20
+ PAGE = 500 # node ids per page
21
+ CAP = 10000 # FalkorDB Cloud result-set cap
22
+
23
+
24
+ def connect():
25
+ if "--cloud" in sys.argv:
26
+ return FalkorDB.from_url(cloud_url()), "FalkorDB Cloud"
27
+ return FalkorDB(host="localhost", port=6379), "local FalkorDB"
28
+
29
+
30
+ def main():
31
+ db, where = connect()
32
+ g = db.select_graph(os.environ.get("PB_GRAPH", "network"))
33
+ t0 = time.time()
34
+ max_id = g.query("MATCH (n) RETURN max(id(n))").result_set[0][0]
35
+
36
+ def edge_page(lo, hi):
37
+ rows = g.query("MATCH (a)-[r]->(b) WHERE id(a) >= $lo AND id(a) < $hi"
38
+ " RETURN id(a), id(b), type(r) ORDER BY id(r) LIMIT $cap",
39
+ {"lo": lo, "hi": hi, "cap": CAP}).result_set
40
+ if len(rows) < CAP:
41
+ return rows
42
+ if hi - lo == 1:
43
+ # One node with more edges than a result can hold (you, with 10k+ connections).
44
+ skip = 0
45
+ while len(rows) % CAP == 0 and len(rows) == skip + CAP:
46
+ skip += CAP
47
+ rows += g.query("MATCH (a)-[r]->(b) WHERE id(a) = $lo RETURN id(a), id(b), type(r)"
48
+ " ORDER BY id(r) SKIP $skip LIMIT $cap", {"lo": lo, "skip": skip, "cap": CAP}).result_set
49
+ return rows
50
+ mid = (lo + hi) // 2
51
+ return edge_page(lo, mid) + edge_page(mid, hi)
52
+
53
+ nodes, edges = {}, []
54
+ for lo in range(0, max_id + 1, PAGE):
55
+ params = {"lo": lo, "hi": lo + PAGE}
56
+ for nid, label, name, sub, when in g.query(
57
+ "MATCH (n) WHERE id(n) >= $lo AND id(n) < $hi"
58
+ " RETURN id(n), labels(n)[0], n.name, coalesce(n.headline, n.title, n.funding, n.summary, ''),"
59
+ " coalesce(n.connected_on, n.date, '')",
60
+ params).result_set:
61
+ nodes[nid] = [label, name or "", sub or "", when or ""]
62
+ edges += [tuple(row) for row in edge_page(lo, lo + PAGE)]
63
+
64
+ total = g.query("MATCH ()-[r]->() RETURN count(r)").result_set[0][0]
65
+ if total != len(edges):
66
+ sys.exit(f"exported {len(edges)} edges but graph has {total}")
67
+
68
+ # Re-index nodes 0..n-1 for the viewer's typed arrays.
69
+ ids = sorted(nodes)
70
+ index = {nid: i for i, nid in enumerate(ids)}
71
+ labels = sorted({n[0] for n in nodes.values()})
72
+ types = sorted({e[2] for e in edges})
73
+ degree = [0] * len(ids)
74
+ for a, b, _ in edges:
75
+ degree[index[a]] += 1
76
+ degree[index[b]] += 1
77
+
78
+ data = {
79
+ "source": where,
80
+ "labels": labels,
81
+ "types": types,
82
+ # node: [labelIndex, name, subtitle, degree, when (connected_on / event date)]
83
+ "nodes": [[labels.index(nodes[n][0]), nodes[n][1], nodes[n][2][:120], degree[index[n]],
84
+ nodes[n][3]] for n in ids],
85
+ # edge: [sourceIndex, targetIndex, typeIndex]
86
+ "edges": [[index[a], index[b], types.index(t)] for a, b, t in edges],
87
+ }
88
+ OUT.parent.mkdir(parents=True, exist_ok=True)
89
+ OUT.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
90
+ print(f"exported from {where}: {len(ids)} nodes, {len(edges)} edges "
91
+ f"in {time.time() - t0:.1f}s -> {OUT} ({OUT.stat().st_size / 1e6:.1f} MB)")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -0,0 +1,297 @@
1
+ """Ranked shortlists from the FalkorDB graph: co-hosts & sponsors, podcast guests, places to speak.
2
+
3
+ Each list is a few Cypher queries plus a transparent score, so every row says
4
+ why it ranks where it does. Writes data/viewer/shortlists.json (for
5
+ shortlists.html) and one CSV per list in data/viewer/shortlists/.
6
+
7
+ personal-brain export --cloud
8
+ """
9
+
10
+ import csv
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ from collections import defaultdict
16
+ from datetime import date
17
+ from pathlib import Path
18
+
19
+ from falkordb import FalkorDB
20
+
21
+ from personal_brain.load_graph import norm
22
+
23
+ from personal_brain.paths import HOME, cloud_url
24
+ OUT = Path(os.environ.get("PB_VIEWER_DIR", HOME / "data" / "viewer"))
25
+ SETTINGS = HOME / "my_brain.json"
26
+ TOP = 60
27
+
28
+ SENIOR_RE = re.compile(r"\b(founder|co-?founder|ceo|cto|chief|head of|vp|vice president|director|principal"
29
+ r"|staff|distinguished|partner|professor|research scientist|lead)\b", re.I)
30
+ ORG_RE = re.compile(r"\b(summit|conference|conf|meetup|meetups|club|community|hackathon|hackathons|ted"
31
+ r"|developer groups?|gdg|circle|society|festival|collective|guild|events|forum"
32
+ r"|week|hacks)\b", re.I)
33
+ NOT_ORG_RE = re.compile(r"\b(software|university|college|student|students|career|design|school)\b", re.I)
34
+ ORGANIZER_RE = re.compile(r"\b(organi[sz]er|co-?organi[sz]er|curator|chapter lead|host|community lead"
35
+ r"|head of community|community manager|events? (lead|manager|director))\b", re.I)
36
+ TALK_RE = re.compile(r"\b(talks?|panel|meetup|lightning|demo|tech talk|summit|conference|conf|workshop"
37
+ r"|forum|unconference|showcase|fireside|seminar|lecture|keynote|speakers?)\b", re.I)
38
+ SOCIAL_RE = re.compile(r"\b(dinner|happy hour|party|drinks|brunch|mixer|golf|run club|yoga|pop-up"
39
+ r"|retreat|picnic|karaoke)\b", re.I)
40
+ EVENT_TOPIC_POINTS = {"Agent Memory": 3, "Knowledge Graphs": 3, "RAG & Search": 2, "AI Agents": 1.5,
41
+ "Developer Relations": 1, "LLMs & GenAI": 0.5}
42
+ # Big tech hosts many events but rarely co-hosts with a startup: listed, but ranked lower.
43
+ BIG_TECH = {"google", "meta", "facebook", "microsoft", "apple", "nvidia", "amazon", "amazon web services aws",
44
+ "ibm", "salesforce", "oracle", "github", "intel", "linkedin"}
45
+ GUEST_TOPIC_POINTS = {"Agent Memory": 3, "Knowledge Graphs": 2, "AI Agents": 1, "RAG & Search": 1,
46
+ "LLMs & GenAI": 0.5}
47
+
48
+
49
+ def connect():
50
+ if "--cloud" in sys.argv:
51
+ return FalkorDB.from_url(cloud_url()), "FalkorDB Cloud"
52
+ return FalkorDB(host="localhost", port=6379), "local FalkorDB"
53
+
54
+
55
+ def competitors():
56
+ """Companies you would not co-host with (my_brain.json "competitors"), normalized."""
57
+ s = json.loads(SETTINGS.read_text()) if SETTINGS.exists() else {}
58
+ return {norm(c) for c in s.get("competitors", [])}
59
+
60
+
61
+ def main():
62
+ db, where = connect()
63
+ g = db.select_graph(os.environ.get("PB_GRAPH", "network"))
64
+ q = lambda cypher, **params: g.query(cypher, params).result_set
65
+ today = date.today()
66
+ since = f"{today.year - 1}-01-01"
67
+ rivals = competitors()
68
+
69
+ # ---------- facts about people ----------
70
+ people = {}
71
+ for pid, name, headline, title, company, url, messages, lead in q(
72
+ "MATCH (:Me)-[:CONNECTED]->(p:Person) RETURN p.id, p.name, p.headline, p.title, p.company,"
73
+ " p.url, p.messages, p.event_contact"):
74
+ people[pid] = {"id": pid, "name": name, "headline": headline or "", "title": title or "",
75
+ "company": company or "", "url": url or "", "messages": messages or 0,
76
+ "lead": bool(lead), "topics": set(), "city": "", "employers": [], "hosts": []}
77
+ for pid, topic in q("MATCH (p:Person)-[:INTO]->(t:Topic) RETURN p.id, t.name"):
78
+ if pid in people:
79
+ people[pid]["topics"].add(topic)
80
+ for pid, city in q("MATCH (p:Person)-[:LIVES_IN]->(c:City) RETURN p.id, c.name"):
81
+ if pid in people:
82
+ people[pid]["city"] = city
83
+ for pid, cid, cname, wf, title in q(
84
+ "MATCH (p:Person)-[w:WORKED_AT {current:true}]->(c:Company)"
85
+ " RETURN p.id, c.id, c.name, c.well_funded, w.title"):
86
+ if pid in people:
87
+ people[pid]["employers"].append({"id": cid, "name": cname, "well_funded": bool(wf),
88
+ "title": title or ""})
89
+ graph_years = dict(q("MATCH (p:Person)-[x:HAS_EXPERIENCE]->(:Skill {id:'graph databases'})"
90
+ " RETURN p.id, x.years"))
91
+ met_recently = defaultdict(int)
92
+ for pid, in q("MATCH (p:Person)-[:MET_AT]->(e:Event) WHERE e.date >= $since RETURN p.id", since=since):
93
+ met_recently[pid] += 1
94
+ mine = {cid for cid, in q("MATCH (:Me)-[:WORKED_AT]->(c:Company) RETURN c.id")}
95
+
96
+ # ---------- upcoming events and how they link to you ----------
97
+ events = {}
98
+ for eid, name, day, time, url, guests, hosts, venue, blurb in q(
99
+ "MATCH (e:Event {upcoming:true}) RETURN e.id, e.name, e.date, e.time, e.url, e.guests,"
100
+ " e.hosts, e.venue, e.blurb"):
101
+ events[eid] = {"id": eid, "name": name, "date": day, "time": time, "url": url, "guests": guests or 0,
102
+ "hosts": hosts or "", "venue": venue or "", "blurb": blurb or "", "topics": [],
103
+ "host_people": [], "host_companies": []}
104
+ for eid, topic in q("MATCH (e:Event {upcoming:true})-[:ABOUT]->(t:Topic) RETURN e.id, t.name"):
105
+ events[eid]["topics"].append(topic)
106
+ for eid, pid in q("MATCH (e:Event {upcoming:true})-[:HOSTED_BY]->(p:Person) RETURN e.id, p.id"):
107
+ events[eid]["host_people"].append(pid)
108
+ if pid in people:
109
+ people[pid]["hosts"].append(eid)
110
+ hosted_by_company = defaultdict(list)
111
+ for eid, cid, cname, via in q("MATCH (e:Event {upcoming:true})-[h:HOSTED_BY]->(c:Company)"
112
+ " RETURN e.id, c.id, c.name, h.via"):
113
+ events[eid]["host_companies"].append({"id": cid, "name": cname, "via": via})
114
+ hosted_by_company[cid].append(eid)
115
+
116
+ staff = defaultdict(list) # company id -> your connections working there now
117
+ for p in people.values():
118
+ for emp in p["employers"]:
119
+ staff[emp["id"]].append(p)
120
+
121
+ def best_contact(company_id):
122
+ """The event/community lead you message most there, else anyone you message most."""
123
+ ps = sorted(staff[company_id], key=lambda p: (not p["lead"], -p["messages"]))
124
+ return ps[0] if ps else None
125
+
126
+ def short_date(day):
127
+ return date.fromisoformat(day).strftime("%b %d")
128
+
129
+ # ---------- 1. co-hosts & sponsors ----------
130
+ companies = {}
131
+ for cid, name, wf, funding, employees, industries in q(
132
+ "MATCH (c:Company) RETURN c.id, c.name, c.well_funded, c.funding, c.employees, c.industries"):
133
+ if cid in staff and cid not in mine and cid not in rivals:
134
+ companies[cid] = (name, bool(wf), funding or "", employees or "", industries or "")
135
+ cohosts = []
136
+ for cid, (name, wf, funding, employees, industries) in companies.items():
137
+ ps = staff[cid]
138
+ leads = [p for p in ps if p["lead"]]
139
+ hosted = sorted(set(hosted_by_company[cid]), key=lambda e: events[e]["date"])
140
+ met = sum(1 for p in ps if met_recently[p["id"]])
141
+ ai = sum(1 for p in ps if p["topics"] & {"AI Agents", "Agent Memory", "Knowledge Graphs", "RAG & Search"})
142
+ big = cid in BIG_TECH
143
+ score = (3 * min(len(hosted), 3) + 3 * wf + 2 * min(len(leads), 2) + 0.5 * min(len(ps), 5)
144
+ + min(met, 3) + (1 if ai >= 2 else 0) - 4 * big)
145
+ if not (hosted or wf or leads or met):
146
+ continue
147
+ why = ["big tech"] if big else []
148
+ if hosted:
149
+ why.append(f"hosts {len(hosted)} upcoming event{'s' * (len(hosted) > 1)}")
150
+ if wf:
151
+ why.append(f"well-funded ({funding})" if funding else "well-funded")
152
+ if leads:
153
+ why.append(f"{len(leads)} event/community lead{'s' * (len(leads) > 1)} you know")
154
+ why.append(f"{len(ps)} connection{'s' * (len(ps) > 1)} there")
155
+ if met:
156
+ why.append(f"met {met} of them at events since {today.year - 1}")
157
+ contact = best_contact(cid)
158
+ cohosts.append({
159
+ "score": round(score, 1), "company": name, "why": " · ".join(why),
160
+ "contact": contact["name"], "contact_title": contact["headline"][:90],
161
+ "contact_url": contact["url"], "contact_messages": contact["messages"],
162
+ "events": "; ".join(f"{short_date(events[e]['date'])} {events[e]['name']}" for e in hosted[:3]),
163
+ "funding": funding, "people": employees})
164
+ cohosts.sort(key=lambda r: -r["score"])
165
+
166
+ # ---------- 2. podcast guests ----------
167
+ guests = []
168
+ for p in people.values():
169
+ topic_pts = sum(GUEST_TOPIC_POINTS.get(t, 0) for t in p["topics"])
170
+ if not p["topics"] & {"Agent Memory", "Knowledge Graphs", "AI Agents", "RAG & Search"}:
171
+ continue
172
+ if any(e["id"] in mine for e in p["employers"]): # your own coworkers
173
+ continue
174
+ role = f"{p['title']} {p['headline']}"
175
+ senior = bool(SENIOR_RE.search(role))
176
+ funded = next((e["name"] for e in p["employers"] if e["well_funded"]), "")
177
+ yrs = graph_years.get(p["id"], 0)
178
+ score = (topic_pts + 2 * senior + ("Podcast & Media" in p["topics"]) + (p["city"] == "SF Bay Area")
179
+ + bool(p["hosts"]) + (p["messages"] > 0) + bool(funded) + (yrs >= 1))
180
+ why = [", ".join(t for t in GUEST_TOPIC_POINTS if t in p["topics"])]
181
+ if senior:
182
+ why.append("senior")
183
+ if yrs >= 1:
184
+ why.append(f"{yrs:g} yrs graph databases")
185
+ if "Podcast & Media" in p["topics"]:
186
+ why.append("already does podcasts/media")
187
+ if p["hosts"]:
188
+ why.append(f"hosts {events[p['hosts'][0]]['name'][:40]}")
189
+ if funded:
190
+ why.append(f"at {funded} (well-funded)")
191
+ if p["city"] == "SF Bay Area":
192
+ why.append("in SF")
193
+ guests.append({"score": round(score, 1), "name": p["name"], "headline": p["headline"][:110],
194
+ "company": p["employers"][0]["name"] if p["employers"] else p["company"],
195
+ "city": p["city"], "why": " · ".join(why),
196
+ "warmth": f"{p['messages']} messages" if p["messages"] else "connected, no messages yet",
197
+ "url": p["url"]})
198
+ guests.sort(key=lambda r: -r["score"])
199
+
200
+ # ---------- 3a. upcoming events to speak at ----------
201
+ speak = []
202
+ for e in events.values():
203
+ topic_pts = sum(EVENT_TOPIC_POINTS.get(t, 0) for t in e["topics"])
204
+ host_people = [people[pid] for pid in e["host_people"] if pid in people]
205
+ host_cos = [c for c in e["host_companies"] if c["id"] in staff and c["id"] not in mine]
206
+ rival = any(c["id"] in rivals for c in e["host_companies"])
207
+ days = (date.fromisoformat(e["date"]) - today).days
208
+ talk, social = bool(TALK_RE.search(e["name"])), bool(SOCIAL_RE.search(e["name"]))
209
+ score = (topic_pts + 3 * bool(host_people) + 2 * bool(host_cos)
210
+ + any(best_contact(c["id"])["lead"] for c in host_cos)
211
+ + talk - social + (e["guests"] >= 150) - (days < 10) - 4 * rival)
212
+ if not (topic_pts or host_people or host_cos) or rival or days < 3: # under 3 days: too late
213
+ continue
214
+ if host_people:
215
+ ask = f"ask {host_people[0]['name']} (host, your connection)"
216
+ elif host_cos:
217
+ c = max(host_cos, key=lambda c: best_contact(c["id"])["lead"])
218
+ ask = f"ask {best_contact(c['id'])['name']} at {c['name']} ({c['via']})"
219
+ else:
220
+ ask = "cold: message the host on Luma"
221
+ why = [", ".join(t for t in EVENT_TOPIC_POINTS if t in e["topics"]) or "AI"]
222
+ if talk:
223
+ why.append("has talks/demos")
224
+ if social:
225
+ why.append("social, few talk slots")
226
+ if e["guests"] >= 150:
227
+ why.append(f"{e['guests']} going")
228
+ if days < 10:
229
+ why.append("soon: ask now or skip")
230
+ speak.append({"score": round(score, 1), "date": e["date"], "event": e["name"], "why": " · ".join(why),
231
+ "ask": ask, "hosts": e["hosts"][:120], "guests": e["guests"], "url": e["url"]})
232
+ speak.sort(key=lambda r: (-r["score"], r["date"]))
233
+
234
+ # ---------- 3b. organizers you know (recurring stages) ----------
235
+ organizers = []
236
+ for p in people.values():
237
+ orgs = [e for e in p["employers"] if ORG_RE.search(e["name"]) and not NOT_ORG_RE.search(e["name"])]
238
+ role = " ".join([p["title"], p["headline"]] + [e["title"] for e in p["employers"]])
239
+ organizing = bool(ORGANIZER_RE.search(role))
240
+ if not (orgs or (organizing and p["lead"])):
241
+ continue
242
+ ai = bool(p["topics"] & {"AI Agents", "Agent Memory", "Knowledge Graphs", "RAG & Search", "LLMs & GenAI"})
243
+ score = (2 * bool(orgs) + 2 * organizing + 2 * bool(p["hosts"]) + ai
244
+ + (p["city"] == "SF Bay Area") + (p["messages"] > 0))
245
+ why = []
246
+ if orgs:
247
+ why.append(f"runs/works at {orgs[0]['name']}")
248
+ if organizing:
249
+ why.append("organizer role")
250
+ if p["hosts"]:
251
+ why.append(f"hosts {events[p['hosts'][0]]['name'][:40]} ({short_date(events[p['hosts'][0]]['date'])})")
252
+ if ai:
253
+ why.append("into AI")
254
+ if p["city"] == "SF Bay Area":
255
+ why.append("in SF")
256
+ if p["messages"]:
257
+ why.append(f"{p['messages']} messages")
258
+ organizers.append({"score": score, "name": p["name"], "headline": p["headline"][:110],
259
+ "org": (orgs[0]["name"] if orgs else p["employers"][0]["name"] if p["employers"]
260
+ else p["company"]),
261
+ "city": p["city"], "why": " · ".join(why), "url": p["url"]})
262
+ organizers.sort(key=lambda r: -r["score"])
263
+
264
+ lists = {
265
+ "cohosts": {"title": "Co-hosts & sponsors", "rows": cohosts[:TOP], "total": len(cohosts),
266
+ "note": "Companies where you know people, ranked by: hosts upcoming AI events (3 each, max 3) "
267
+ "+ well-funded (3) + event/community leads you know (2 each, max 2) + connections "
268
+ "(0.5 each, max 5) + people met at events (1 each, max 3) + AI team (1)."},
269
+ "guests": {"title": "Podcast guests", "rows": guests[:TOP], "total": len(guests),
270
+ "note": "Connections into agents, memory, graphs or RAG, ranked by topic (Agent Memory 3, "
271
+ "Knowledge Graphs 2, AI Agents 1, RAG 1) + senior (2) + 1 each for: graph DB years, "
272
+ "does podcasts, hosts events, at a well-funded startup, in SF, you have messaged."},
273
+ "speak": {"title": "Upcoming events to speak at", "rows": speak[:TOP], "total": len(speak),
274
+ "note": "Upcoming SF Luma events, ranked by topic (Agent Memory 3, Knowledge Graphs 3, RAG 2, "
275
+ "AI Agents 1.5) + host is your connection (3) + you know people at the host company (2, "
276
+ "+1 if an event lead) + talks (1) − social only (1) + 150+ going (1) − under 10 days (1)."},
277
+ "organizers": {"title": "Organizers you know", "rows": organizers[:TOP], "total": len(organizers),
278
+ "note": "People at conferences, meetups and communities, or with organizer roles. Recurring "
279
+ "stages: ask them for a slot at the next one."},
280
+ }
281
+ payload = {"generated": today.isoformat(), "source": where, "lists": lists}
282
+ (OUT / "shortlists.json").write_text(json.dumps(payload, ensure_ascii=False, indent=1))
283
+ (OUT / "shortlists").mkdir(exist_ok=True)
284
+ for key, lst in lists.items():
285
+ rows = lst["rows"]
286
+ if rows:
287
+ with open(OUT / "shortlists" / f"{key}.csv", "w", newline="", encoding="utf-8") as f:
288
+ w = csv.DictWriter(f, fieldnames=["rank"] + list(rows[0]))
289
+ w.writeheader()
290
+ w.writerows({"rank": i + 1, **r} for i, r in enumerate(rows))
291
+ print(f"{key:10s} {lst['total']:4d} candidates, top: "
292
+ + " | ".join(r.get("name") or r.get("company") or r.get("event") for r in rows[:5]))
293
+ print(f"wrote {OUT / 'shortlists.json'}")
294
+
295
+
296
+ if __name__ == "__main__":
297
+ main()
@@ -0,0 +1,155 @@
1
+ """THROWAWAY SPIKE: run the demo "story" queries on FalkorDB and save results for the viewer.
2
+
3
+ Each story is a Cypher query. We store the query text, FalkorDB's run time, and
4
+ the node ids it touched, so the viewer can light them up.
5
+
6
+ personal-brain export --cloud
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ from datetime import date
13
+ from pathlib import Path
14
+
15
+ from falkordb import FalkorDB
16
+
17
+ from personal_brain.paths import HOME, cloud_url
18
+ OUT = Path(os.environ.get("PB_VIEWER_DIR", HOME / "data" / "viewer")) / "stories.json"
19
+
20
+ STORIES = [
21
+ {
22
+ "id": "founders",
23
+ "title": "Which founders did I meet at events this year?",
24
+ "focus": "MATCH (m:Me) RETURN id(m)",
25
+ "cypher": f"""MATCH (me:Me)-[:ATTENDED]->(e:Event)<-[:MET_AT]-(p:Person)
26
+ -[:INTO]->(:Topic {{name:'Founder'}})
27
+ WHERE e.date >= '{date.today().year}-01-01'
28
+ RETURN id(me), id(e), id(p)""",
29
+ "pairs": [(0, 1), (1, 2)],
30
+ },
31
+ {
32
+ "id": "bigtech",
33
+ "title": "Who in my network works at big tech?",
34
+ "focus": "MATCH (m:Me) RETURN id(m)",
35
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(p:Person)
36
+ -[:WORKED_AT {current:true}]->(c:Company)
37
+ WHERE c.name IN ['Google', 'Meta', 'Microsoft', 'Apple', 'NVIDIA',
38
+ 'Amazon', 'Amazon Web Services (AWS)', 'IBM', 'Salesforce', 'Oracle']
39
+ RETURN id(me), id(p), id(c)""",
40
+ "pairs": [(0, 1), (1, 2)],
41
+ },
42
+ {
43
+ "id": "funded-intro",
44
+ "title": "Who can warm-intro me to people at well-funded startups?",
45
+ "focus": "MATCH (m:Me) RETURN id(m)",
46
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(friend:Person)-[:KNOWS]-(p:Person)
47
+ -[:WORKED_AT {current:true}]->(c:Company {well_funded:true})
48
+ OPTIONAL MATCH (c)-[:FUNDED_BY]->(i:Investor)
49
+ RETURN id(me), id(friend), id(p), id(c), collect(id(i)) AS investors""",
50
+ "pairs": [(0, 1), (1, 2), (2, 3), (3, 4)],
51
+ },
52
+ {
53
+ "id": "cohosts",
54
+ "title": "Which startups host AI events, and who do I know there?",
55
+ "focus": "MATCH (m:Me) RETURN id(m)",
56
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(p:Person)-[:WORKED_AT {current:true}]->(c:Company)
57
+ <-[:HOSTED_BY]-(e:Event {upcoming:true})
58
+ WHERE NOT c.name IN ['Google', 'Meta', 'Microsoft', 'Apple', 'NVIDIA', 'Amazon',
59
+ 'Amazon Web Services (AWS)', 'IBM', 'Salesforce', 'Oracle', 'GitHub']
60
+ RETURN id(me), id(p), id(c), id(e)""",
61
+ "pairs": [(0, 1), (1, 2), (2, 3)],
62
+ },
63
+ {
64
+ "id": "podcast",
65
+ "title": "Who should I invite to my podcast on agents and memory?",
66
+ "focus": "MATCH (m:Me) RETURN id(m)",
67
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(p:Person)-[:INTO]->(t:Topic)
68
+ WHERE t.name IN ['Agent Memory', 'Knowledge Graphs', 'AI Agents', 'RAG & Search']
69
+ AND ANY(w IN ['founder', 'cto', 'ceo', 'chief', 'head of', 'vp', 'director',
70
+ 'principal', 'staff', 'research'] WHERE toLower(p.headline) CONTAINS w)
71
+ RETURN id(me), id(p), id(t)""",
72
+ "pairs": [(0, 1), (1, 2)],
73
+ },
74
+ {
75
+ "id": "speak",
76
+ "title": "Which upcoming agent events can I speak at through people I know?",
77
+ "focus": "MATCH (m:Me) RETURN id(m)",
78
+ # Two ways in: you know the host, or you know someone at the host company.
79
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(p:Person)<-[:HOSTED_BY]-(e:Event {upcoming:true})
80
+ -[:ABOUT]->(t:Topic)
81
+ WHERE t.name IN ['AI Agents', 'Agent Memory', 'Knowledge Graphs', 'RAG & Search']
82
+ RETURN id(me) AS me, id(p) AS friend, id(p) AS host, id(e) AS event, id(t) AS topic
83
+ UNION
84
+ MATCH (me:Me)-[:CONNECTED]->(p:Person)-[:WORKED_AT {current:true}]->(c:Company)
85
+ <-[:HOSTED_BY]-(e:Event {upcoming:true})-[:ABOUT]->(t:Topic)
86
+ WHERE t.name IN ['AI Agents', 'Agent Memory', 'Knowledge Graphs', 'RAG & Search']
87
+ RETURN id(me) AS me, id(p) AS friend, id(c) AS host, id(e) AS event, id(t) AS topic""",
88
+ "pairs": [(0, 1), (1, 2), (2, 3), (3, 4)],
89
+ },
90
+ {
91
+ "id": "investors",
92
+ "title": "Who can warm-intro me to the investors behind OpenAI?",
93
+ "focus": "MATCH (m:Me) RETURN id(m)",
94
+ # The investor firm is a Company (its people work there) and an Investor (it funds companies), same id.
95
+ "cypher": """MATCH (t:Company {name:'OpenAI'})-[:FUNDED_BY]->(i:Investor)
96
+ MATCH (firm:Company {id: i.id})<-[:WORKED_AT {current:true}]-(vc:Person)-[:INTO]->(:Topic {name:'Investing'})
97
+ MATCH (me:Me)-[:CONNECTED]->(friend:Person)-[:KNOWS]-(vc)
98
+ WHERE friend.messages > 0
99
+ RETURN id(me), id(friend), id(vc), id(firm), id(i), id(t)""",
100
+ "pairs": [(0, 1), (1, 2), (2, 3), (4, 5)],
101
+ },
102
+ {
103
+ "id": "executives",
104
+ "title": "Which founders and CEOs can I reach through people I talk to?",
105
+ "focus": "MATCH (m:Me) RETURN id(m)",
106
+ "cypher": """MATCH (me:Me)-[:CONNECTED]->(friend:Person)-[:KNOWS]-(p:Person)
107
+ WHERE friend.messages > 0 AND p.messages = 0
108
+ AND ANY(w IN ['founder', 'ceo', 'chief', 'cto', 'coo', 'cfo'] WHERE toLower(p.title) CONTAINS w)
109
+ RETURN id(me), id(friend), id(p)""",
110
+ "pairs": [(0, 1), (1, 2)],
111
+ },
112
+ ]
113
+
114
+
115
+ def connect():
116
+ if "--cloud" in sys.argv:
117
+ return FalkorDB.from_url(cloud_url()), "FalkorDB Cloud"
118
+ return FalkorDB(host="localhost", port=6379), "local FalkorDB"
119
+
120
+
121
+ def main():
122
+ db, where = connect()
123
+ g = db.select_graph(os.environ.get("PB_GRAPH", "network"))
124
+ count, max_id = g.query("MATCH (n) RETURN count(n), max(id(n))").result_set[0]
125
+ if count != max_id + 1:
126
+ sys.exit("node ids are not contiguous; viewer indices would not match FalkorDB ids")
127
+
128
+ out = []
129
+ for story in STORIES:
130
+ result = g.query(story["cypher"])
131
+ # A column can be a list (collect); flatten it for the lit-node set.
132
+ flat = lambda v: v if isinstance(v, list) else [v]
133
+ ids = sorted({x for row in result.result_set for v in row for x in flat(v) if x is not None})
134
+ # Edges to light: only the path steps, not every edge between lit nodes.
135
+ # Keep direction (me -> friend -> expert) so the viewer can send light pulses along it.
136
+ seen, pairs = set(), []
137
+ for row in result.result_set:
138
+ for a, b in story["pairs"]:
139
+ for x in flat(row[a]):
140
+ for y in flat(row[b]):
141
+ if x is not None and y is not None and x != y and (x, y) not in seen:
142
+ seen.add((x, y))
143
+ pairs.append([x, y])
144
+ focus = g.query(story["focus"]).result_set[0][0]
145
+ out.append({**{k: story[k] for k in ("id", "title", "cypher")},
146
+ "focus": focus, "nodes": ids, "pairs": pairs, "rows": len(result.result_set),
147
+ "ms": round(result.run_time_ms, 2), "source": where})
148
+ print(f"{story['id']:9s} rows={len(result.result_set):4d} nodes={len(ids):4d} pairs={len(pairs):4d} "
149
+ f"{result.run_time_ms:.2f} ms")
150
+ OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1))
151
+ print(f"wrote {OUT}")
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
@@ -0,0 +1,60 @@
1
+ """THROWAWAY SPIKE: precompute 2D and 3D layouts for the WebGL viewer.
2
+
3
+ Reads data/viewer/graph.json (from export_graph.py), runs igraph's DrL layout
4
+ in 2D and 3D, and writes the positions back into the same file.
5
+
6
+ personal-brain export # runs this after export_graph
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ import igraph as ig
16
+
17
+ from personal_brain.paths import HOME
18
+ GRAPH_JSON = Path(os.environ.get("PB_VIEWER_DIR", HOME / "data" / "viewer")) / "graph.json"
19
+ RADIUS = 1000.0
20
+ # "Me" links to everyone, which would crush the map into one ball: leave those
21
+ # links out of the layout and put Me at the center afterwards. Skill links are
22
+ # many and weak, so they pull less than jobs, schools and "knows" links.
23
+ SKIP = {"CONNECTED", "ATTENDED"}
24
+ WEIGHTS = {"HAS_SKILL": 0.2, "LIVES_IN": 0.15, "HELD_IN": 0.15}
25
+
26
+
27
+ def normalize(coords, dims):
28
+ n = len(coords)
29
+ center = [sum(c[d] for c in coords) / n for d in range(dims)]
30
+ shifted = [[c[d] - center[d] for d in range(dims)] for c in coords]
31
+ radii = sorted(sum(v * v for v in c) ** 0.5 for c in shifted)
32
+ scale = RADIUS / (radii[int(n * 0.98)] or 1.0)
33
+ return [round(v * scale, 1) for c in shifted for v in c]
34
+
35
+
36
+ def main():
37
+ data = json.loads(GRAPH_JSON.read_text())
38
+ types = data["types"]
39
+ n = len(data["nodes"])
40
+ kept = [(a, b, t) for a, b, t in data["edges"] if a != b and types[t] not in SKIP]
41
+ g = ig.Graph(n=n, edges=[(a, b) for a, b, _ in kept])
42
+ weights = [WEIGHTS.get(types[t], 1.0) for _, _, t in kept]
43
+ me = next(i for i, node in enumerate(data["nodes"]) if data["labels"][node[0]] == "Me")
44
+ dims_list = [int(a.split("=")[1]) for a in sys.argv if a.startswith("--dims=")] or [2, 3]
45
+ for dims in dims_list:
46
+ t0 = time.time()
47
+ coords = g.layout_drl(weights=weights, dim=dims).coords
48
+ coords[me] = [0.0] * dims # normalize() re-centers on the mean, so set it after
49
+ flat = normalize(coords, dims)
50
+ flat[me * dims:(me + 1) * dims] = [0.0] * dims
51
+ data[f"pos{dims}"] = flat
52
+ radii = sorted(sum(v * v for v in flat[i * dims:(i + 1) * dims]) ** 0.5 for i in range(n))
53
+ print(f"{dims}D layout: {time.time() - t0:.1f}s, radius p10/p50/p90 = "
54
+ f"{radii[n // 10]:.0f}/{radii[n // 2]:.0f}/{radii[n * 9 // 10]:.0f}")
55
+ GRAPH_JSON.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
56
+ print(f"wrote {GRAPH_JSON} ({GRAPH_JSON.stat().st_size / 1e6:.1f} MB)")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()