BinSaqban commited on
Commit
f4143ce
Β·
verified Β·
1 Parent(s): 5647699

Upload connectome.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. connectome.py +401 -0
connectome.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Connectome Builder β€” Flood-Filling Agent Mesh (FFAM)
3
+
4
+ Applies Google Neural Mapping concepts to multi-agent systems:
5
+ - Build a complete map of agent communications (like brain connectomics)
6
+ - Track information flow through agent networks (like Flood-Filling Networks)
7
+ - Detect bottlenecks, hubs, orphans (like SegCLR cell type discovery)
8
+ - Generate synthetic agent graphs for training (like MoGen)
9
+
10
+ Author: HayulaLab β€” July 2026
11
+ Based on: Google Neural Mapping research (FFN, SegCLR, MoGen, LICONN)
12
+ """
13
+ import json, time, os, sys, threading
14
+ from pathlib import Path
15
+ from collections import defaultdict, deque
16
+ from datetime import datetime
17
+ import hashlib
18
+
19
+ try:
20
+ import networkx as nx
21
+ except ImportError:
22
+ nx = None
23
+ print("[WARN] networkx not installed β€” graph analysis disabled")
24
+
25
+ # ─── Configuration ───────────────────────────────────────
26
+ LOG_FILE = Path(os.environ.get("CONNECTOME_LOG", "/tmp/agent-connectome.jsonl"))
27
+ SNAPSHOT_DIR = Path(os.environ.get("CONNECTOME_DIR", "/tmp/agent-connectome-snapshots"))
28
+ GRAPH_EXPORT = Path(os.environ.get("CONNECTOME_GRAPH", "/tmp/agent-connectome-graph.json"))
29
+ FLUSH_INTERVAL = int(os.environ.get("CONNECTOME_FLUSH_MS", "5000")) # ms
30
+
31
+ # ─── Event Types (like synapse types) ────────────────────
32
+ EVENT_TYPES = {
33
+ "task:dispatch": "excitatory", # task assigned
34
+ "task:complete": "signal", # task finished
35
+ "agent:query": "request", # one agent asks another
36
+ "agent:response": "response", # reply
37
+ "skill:invoke": "activation", # skill used
38
+ "memory:read": "read", # memory access
39
+ "memory:write": "write", # memory update
40
+ "router:decision": "route", # routing choice
41
+ "error:timeout": "failure", # timeout
42
+ "error:refusal": "refusal", # refusal
43
+ }
44
+
45
+ # ─── Core: Connectome Builder ─────────────────────────────
46
+ class AgentConnectome:
47
+ """The complete connectome of a multi-agent system."""
48
+
49
+ def __init__(self):
50
+ self.agents: dict[str, dict] = {} # agent_id β†’ metadata
51
+ self.skills: dict[str, dict] = {} # skill_id β†’ metadata
52
+ self.edges: list[dict] = [] # communication events
53
+ self.metrics: dict = defaultdict(int) # aggregate counts
54
+ self.communities: dict = {} # detected communities
55
+ self.bottlenecks: list = [] # detected bottlenecks
56
+ self._lock = threading.Lock()
57
+ self._start_time = time.time()
58
+
59
+ # ─── Event ingestion (like FFN's voxel classifier) ────
60
+ def ingest(self, event: dict):
61
+ """Record one agent communication event."""
62
+ with self._lock:
63
+ event["_ts"] = time.time()
64
+ event["_idx"] = len(self.edges)
65
+
66
+ # Register agents
67
+ for field in ["from_agent", "to_agent", "agent"]:
68
+ a = event.get(field)
69
+ if a and a not in self.agents:
70
+ self.agents[a] = {
71
+ "id": a,
72
+ "first_seen": event["_ts"],
73
+ "events_sent": 0,
74
+ "events_received": 0,
75
+ "skills_used": set(),
76
+ "type": "unknown"
77
+ }
78
+
79
+ sender = event.get("from_agent")
80
+ receiver = event.get("to_agent")
81
+ etype = event.get("type", "unknown")
82
+
83
+ if sender:
84
+ if sender in self.agents:
85
+ self.agents[sender]["events_sent"] += 1
86
+ if receiver:
87
+ if receiver in self.agents:
88
+ self.agents[receiver]["events_received"] += 1
89
+
90
+ skill = event.get("skill")
91
+ if skill:
92
+ if skill not in self.skills:
93
+ self.skills[skill] = {"id": skill, "invocations": 0, "agents": set()}
94
+ self.skills[skill]["invocations"] += 1
95
+ if sender:
96
+ self.skills[skill]["agents"].add(sender)
97
+ if sender in self.agents:
98
+ self.agents[sender]["skills_used"].add(skill)
99
+
100
+ self.metrics[f"events:{etype}"] += 1
101
+ self.metrics["total_events"] += 1
102
+ self.edges.append(event)
103
+
104
+ # ─── Build graph (like connectome reconstruction) ─────
105
+ def build_graph(self) -> dict:
106
+ """Build full agent connectome."""
107
+ G = nx.DiGraph()
108
+
109
+ for aid, adata in self.agents.items():
110
+ G.add_node(aid, **adata)
111
+
112
+ edge_weights = defaultdict(int)
113
+ for e in self.edges:
114
+ u, v = e.get("from_agent"), e.get("to_agent")
115
+ if u and v:
116
+ edge_weights[(u, v)] += 1
117
+ edge_weights[(v, u)] += 0 # track reverse
118
+
119
+ for (u, v), w in edge_weights.items():
120
+ if w > 0:
121
+ etype = "bidirectional" if edge_weights.get((v, u), 0) > 0 else "unidirectional"
122
+ G.add_edge(u, v, weight=w, type=etype)
123
+
124
+ return {
125
+ "nodes": len(G.nodes),
126
+ "edges": len(G.edges),
127
+ "density": nx.density(G) if len(G) > 1 else 0,
128
+ "is_connected": nx.is_weakly_connected(G) if len(G) > 1 else False,
129
+ "diameter": nx.diameter(G.to_undirected()) if len(G) > 1 and nx.is_connected(G.to_undirected()) else -1,
130
+ "avg_path_length": nx.average_shortest_path_length(G.to_undirected()) if len(G) > 1 and nx.is_connected(G.to_undirected()) else -1,
131
+ }
132
+
133
+ # ─── Hub detection (like SegCLR cell type discovery) ──
134
+ def find_hubs(self, min_connections: int = 3) -> list[dict]:
135
+ """Find hub agents (most connected) β€” like hub neurons."""
136
+ G = nx.DiGraph()
137
+ for aid in self.agents:
138
+ G.add_node(aid)
139
+ for e in self.edges:
140
+ u, v = e.get("from_agent"), e.get("to_agent")
141
+ if u and v:
142
+ G.add_edge(u, v)
143
+
144
+ hubs = []
145
+ for node in G.nodes():
146
+ degree = G.degree(node)
147
+ in_deg = G.in_degree(node)
148
+ out_deg = G.out_degree(node)
149
+ if degree >= min_connections:
150
+ hubs.append({
151
+ "agent": node,
152
+ "degree": degree,
153
+ "in_degree": in_deg,
154
+ "out_degree": out_deg,
155
+ "betweenness": nx.betweenness_centrality(G).get(node, 0),
156
+ "type": "router" if out_deg > in_deg * 2 else
157
+ "aggregator" if in_deg > out_deg * 2 else
158
+ "peer"
159
+ })
160
+ hubs.sort(key=lambda x: x["degree"], reverse=True)
161
+ return hubs
162
+
163
+ # ─── Bottleneck detection ──────────────────────────────
164
+ def find_bottlenecks(self, threshold: float = 0.3) -> list[dict]:
165
+ """Find bottlenecks β€” agents that are single points of failure."""
166
+ G = nx.DiGraph()
167
+ for aid in self.agents:
168
+ G.add_node(aid)
169
+ for e in self.edges:
170
+ u, v = e.get("from_agent"), e.get("to_agent")
171
+ if u and v:
172
+ G.add_edge(u, v)
173
+
174
+ if len(G) < 3:
175
+ return []
176
+
177
+ try:
178
+ bc = nx.betweenness_centrality(G)
179
+ avg_bc = sum(bc.values()) / len(bc) if bc else 0
180
+ bottlenecks = []
181
+ for node, score in bc.items():
182
+ if score > avg_bc * (1 + threshold):
183
+ bottlenecks.append({
184
+ "agent": node,
185
+ "betweenness": score,
186
+ "severity": "critical" if score > avg_bc * 3 else "high" if score > avg_bc * 2 else "moderate",
187
+ "recommendation": "Add redundant agent" if score > avg_bc * 3 else
188
+ "Consider load balancing" if score > avg_bc * 2 else
189
+ "Monitor"
190
+ })
191
+ return sorted(bottlenecks, key=lambda x: x["betweenness"], reverse=True)
192
+ except:
193
+ return []
194
+
195
+ # ─── Critical path analysis (like neural pathway tracing) ──
196
+ def critical_paths(self, top_k: int = 5) -> list[dict]:
197
+ """Find the most common agent chains (critical paths)."""
198
+ paths = defaultdict(int)
199
+
200
+ # Build agent sequences from events
201
+ sequences = []
202
+ current_seq = []
203
+ for e in self.edges:
204
+ sender = e.get("from_agent")
205
+ receiver = e.get("to_agent")
206
+ if sender:
207
+ if not current_seq or current_seq[-1] != sender:
208
+ current_seq.append(sender)
209
+ if receiver:
210
+ current_seq.append(receiver)
211
+
212
+ # Find common subsequences
213
+ for i in range(len(current_seq)):
214
+ for j in range(i+2, min(i+8, len(current_seq))):
215
+ seq = tuple(current_seq[i:j])
216
+ paths[seq] += 1
217
+
218
+ top = sorted(paths.items(), key=lambda x: x[1], reverse=True)[:top_k]
219
+ return [{"path": list(p), "frequency": f} for p, f in top]
220
+
221
+ # ─── Synthetic graph generation (MoGen-inspired) ──────
222
+ def generate_synthetic(self, num_agents: int = 10, density: float = 0.3) -> list[dict]:
223
+ """Generate synthetic agent graphs for training β€” like MoGen's synthetic neurons."""
224
+ if not nx:
225
+ return []
226
+
227
+ G = nx.gnp_random_graph(num_agents, density, directed=True)
228
+ agents = []
229
+ for i in range(num_agents):
230
+ agent_type = nx.random.choice(["router", "worker", "verifier", "memory", "observer"],
231
+ p=[0.15, 0.5, 0.1, 0.15, 0.1])
232
+ agents.append({
233
+ "id": f"synth-agent-{i:03d}",
234
+ "type": agent_type,
235
+ "connections": list(G.neighbors(i)),
236
+ "degree": G.degree(i),
237
+ })
238
+ return agents
239
+
240
+ # ─── Snapshot (like Neuroglancer scene capture) ───────
241
+ def snapshot(self) -> dict:
242
+ """Take a complete snapshot of the connectome."""
243
+ return {
244
+ "timestamp": datetime.now().isoformat(),
245
+ "uptime_seconds": time.time() - self._start_time,
246
+ "stats": {
247
+ "agents": len(self.agents),
248
+ "skills": len(self.skills),
249
+ "events": len(self.edges),
250
+ "metrics": dict(self.metrics),
251
+ },
252
+ "graph": self.build_graph(),
253
+ "hubs": self.find_hubs(),
254
+ "bottlenecks": self.find_bottlenecks(),
255
+ "critical_paths": self.critical_paths(),
256
+ "agent_list": list(self.agents.keys()),
257
+ "skill_list": list(self.skills.keys()),
258
+ }
259
+
260
+ def save(self, path: str = None):
261
+ """Save snapshot to JSON."""
262
+ path = path or str(SNAPSHOT_DIR / f"connectome-{int(time.time())}.json")
263
+ snapshot = self.snapshot()
264
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
265
+ with open(path, "w") as f:
266
+ json.dump(snapshot, f, indent=2, ensure_ascii=False)
267
+ return path
268
+
269
+
270
+ # ─── Integration: Hook into existing Hayula infrastructure ──
271
+ class ConnectomeIntegrator:
272
+ """Hooks the connectome into DragonMesh, EventBus, Observability."""
273
+
274
+ def __init__(self):
275
+ self.connectome = AgentConnectome()
276
+ self._running = False
277
+ self._thread = None
278
+
279
+ def hook_eventbus(self, eventbus):
280
+ """Wrap EventBus.publish to record all events."""
281
+ original_publish = eventbus.publish
282
+
283
+ def traced_publish(event):
284
+ self.connectome.ingest(event)
285
+ return original_publish(event)
286
+
287
+ eventbus.publish = traced_publish
288
+ return eventbus
289
+
290
+ def hook_dragonmesh(self, mesh):
291
+ """Wrap DragonMesh route to trace routing decisions."""
292
+ if hasattr(mesh, 'route'):
293
+ original_route = mesh.route
294
+ def traced_route(task):
295
+ result = original_route(task)
296
+ self.connectome.ingest({
297
+ "type": "router:decision",
298
+ "from_agent": "dragon_mesh",
299
+ "to_agent": result.get("agent", "unknown"),
300
+ "task": str(task)[:100]
301
+ })
302
+ return result
303
+ mesh.route = traced_route
304
+ return mesh
305
+
306
+ def hook_a2a(self, bridge):
307
+ """Wrap A2A bridge to trace agent-to-agent communication."""
308
+ if hasattr(bridge, 'send'):
309
+ original_send = bridge.send
310
+ def traced_send(agent, message):
311
+ self.connectome.ingest({
312
+ "type": "agent:query",
313
+ "from_agent": "bridge",
314
+ "to_agent": agent,
315
+ "message": str(message)[:200]
316
+ })
317
+ result = original_send(agent, message)
318
+ self.connectome.ingest({
319
+ "type": "agent:response",
320
+ "from_agent": agent,
321
+ "to_agent": "bridge",
322
+ "result": str(result)[:200]
323
+ })
324
+ return result
325
+ bridge.send = traced_send
326
+ return bridge
327
+
328
+
329
+ # ─── CLI ──────────────────────────────────────────────────
330
+ if __name__ == "__main__":
331
+ import argparse
332
+ p = argparse.ArgumentParser(description="Agent Connectome β€” Flood-Filling Agent Mesh")
333
+ sp = p.add_subparsers(dest="cmd")
334
+
335
+ # Demo: simulate agent traffic
336
+ sp.add_parser("demo", help="Run demo with simulated agent traffic")
337
+
338
+ # Analyze existing log
339
+ analyze = sp.add_parser("analyze", help="Analyze agent connectome from log")
340
+ analyze.add_argument("--log", default=str(LOG_FILE))
341
+
342
+ # Generate synthetic graph
343
+ synth = sp.add_parser("synth", help="Generate synthetic agent graph")
344
+ synth.add_argument("-n", type=int, default=10, help="Number of synthetic agents")
345
+ synth.add_argument("-d", type=float, default=0.3, help="Graph density")
346
+
347
+ # Snapshot
348
+ sp.add_parser("snapshot", help="Take connectome snapshot")
349
+
350
+ args = p.parse_args()
351
+
352
+ if args.cmd == "demo":
353
+ connectome = AgentConnectome()
354
+ agents = ["rushd", "wafa", "awf", "dragon", "hermes", "musa", "zeus", "haytham"]
355
+ skills = ["code_review", "text_gen", "trade_signal", "memory_search", "task_route"]
356
+
357
+ print(f"[FFAM] Starting demo with {len(agents)} agents, {len(skills)} skills")
358
+
359
+ for i in range(100):
360
+ import random
361
+ sender = random.choice(agents)
362
+ receiver = random.choice([a for a in agents if a != sender])
363
+ event = {
364
+ "type": random.choice(list(EVENT_TYPES.keys())),
365
+ "from_agent": sender,
366
+ "to_agent": receiver,
367
+ "skill": random.choice(skills) if random.random() > 0.5 else None,
368
+ "task_id": f"task-{i:04d}",
369
+ }
370
+ connectome.ingest(event)
371
+ time.sleep(0.01)
372
+
373
+ snap = connectome.snapshot()
374
+ print(json.dumps(snap["stats"], indent=2))
375
+ print(f"\nπŸ” Hubs:")
376
+ for h in snap["hubs"][:5]:
377
+ print(f" {h['agent']:12} degree={h['degree']:3d} type={h['type']}")
378
+ print(f"\n⚠️ Bottlenecks:")
379
+ for b in snap["bottlenecks"][:3]:
380
+ print(f" {b['agent']:12} severity={b['severity']:10} β†’ {b['recommendation']}")
381
+ print(f"\nπŸ›€οΈ Critical paths:")
382
+ for cp in snap["critical_paths"]:
383
+ print(f" {' β†’ '.join(cp['path'])} (Γ—{cp['frequency']})")
384
+
385
+ connectome.save()
386
+ print(f"\nβœ… Snapshot saved to {SNAPSHOT_DIR}")
387
+
388
+ elif args.cmd == "synth":
389
+ connectome = AgentConnectome()
390
+ g = connectome.generate_synthetic(args.n, args.d)
391
+ print(json.dumps(g, indent=2))
392
+
393
+ elif args.cmd == "snapshot":
394
+ connectome = AgentConnectome()
395
+ if LOG_FILE.exists():
396
+ with open(LOG_FILE) as f:
397
+ for line in f:
398
+ connectome.ingest(json.loads(line.strip()))
399
+ path = connectome.save()
400
+ print(f"Snap: {path}")
401
+ print(json.dumps(connectome.snapshot()["stats"], indent=2))