flywire-coding-cortex 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.
- flywire_coding_cortex/__init__.py +3 -0
- flywire_coding_cortex/assets/circuit.seed.json +1 -0
- flywire_coding_cortex/assets/manifest.json +54 -0
- flywire_coding_cortex/assets/seed-graph.json +18 -0
- flywire_coding_cortex/assets/skill/SKILL.md +55 -0
- flywire_coding_cortex/assets/skill/practices.md +14 -0
- flywire_coding_cortex/assets/skill/roles.md +17 -0
- flywire_coding_cortex/bridge.py +85 -0
- flywire_coding_cortex/cli.py +304 -0
- flywire_coding_cortex/etl/__init__.py +1 -0
- flywire_coding_cortex/etl/build_circuit.py +210 -0
- flywire_coding_cortex/etl/download.py +128 -0
- flywire_coding_cortex/install_skills.py +74 -0
- flywire_coding_cortex/lif.py +335 -0
- flywire_coding_cortex/mcp_server.py +208 -0
- flywire_coding_cortex/memory.py +106 -0
- flywire_coding_cortex/paths.py +112 -0
- flywire_coding_cortex/sense.py +62 -0
- flywire_coding_cortex-0.1.0.dist-info/METADATA +447 -0
- flywire_coding_cortex-0.1.0.dist-info/RECORD +24 -0
- flywire_coding_cortex-0.1.0.dist-info/WHEEL +5 -0
- flywire_coding_cortex-0.1.0.dist-info/entry_points.txt +2 -0
- flywire_coding_cortex-0.1.0.dist-info/licenses/LICENSE +21 -0
- flywire_coding_cortex-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Leaky integrate-and-fire on a FlyWire-derived circuit JSON."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Circuit:
|
|
14
|
+
neurons: list[dict[str, Any]]
|
|
15
|
+
edges: list[list[float]]
|
|
16
|
+
source: str = ""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def n(self) -> int:
|
|
20
|
+
return len(self.neurons)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def load(cls, path: Path) -> "Circuit":
|
|
24
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
return cls(
|
|
26
|
+
neurons=data["neurons"],
|
|
27
|
+
edges=data["edges"],
|
|
28
|
+
source=str(data.get("source", "")),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class LIFSim:
|
|
34
|
+
circuit: Circuit
|
|
35
|
+
weight_scale: float = 0.0008
|
|
36
|
+
decay: float = 0.9512 # ~20 ms tau at 1 ms step
|
|
37
|
+
threshold: float = 1.0
|
|
38
|
+
refractory_ms: float = 2.0
|
|
39
|
+
p_noise: float = 0.0022
|
|
40
|
+
noise_kick: float = 0.42
|
|
41
|
+
rate_alpha: float = 1.0 / 120.0
|
|
42
|
+
inh_delay_ms: int = 4
|
|
43
|
+
gap_junction_boost: float = 6.0
|
|
44
|
+
rng: np.random.Generator = field(default_factory=lambda: np.random.default_rng(0))
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
n = self.circuit.n
|
|
48
|
+
self.v = np.zeros(n, dtype=np.float64)
|
|
49
|
+
self.refr = np.zeros(n, dtype=np.float64)
|
|
50
|
+
self.baseline = np.zeros(n, dtype=np.float64)
|
|
51
|
+
self.roles = [str(nr.get("role", "other")) for nr in self.circuit.neurons]
|
|
52
|
+
self.types = [str(nr.get("type", "?")) for nr in self.circuit.neurons]
|
|
53
|
+
self.sides = [str(nr.get("side", "")) for nr in self.circuit.neurons]
|
|
54
|
+
self._build_groups()
|
|
55
|
+
self._build_baselines()
|
|
56
|
+
self._build_csr()
|
|
57
|
+
self.inh_queue = np.zeros((self.inh_delay_ms + 1, n), dtype=np.float64)
|
|
58
|
+
self.q_head = 0
|
|
59
|
+
self.sim_ms = 0
|
|
60
|
+
self.total_spikes = 0
|
|
61
|
+
self.gf_latch = False
|
|
62
|
+
self.rates = {
|
|
63
|
+
"loom": 0.0,
|
|
64
|
+
"dna_l": 0.0,
|
|
65
|
+
"dna_r": 0.0,
|
|
66
|
+
"mdn": 0.0,
|
|
67
|
+
"fwd": 0.0,
|
|
68
|
+
"groom": 0.0,
|
|
69
|
+
"escw": 0.0,
|
|
70
|
+
"pop": 0.0,
|
|
71
|
+
}
|
|
72
|
+
self.inputs = {
|
|
73
|
+
"loom_l": 0.0,
|
|
74
|
+
"loom_r": 0.0,
|
|
75
|
+
"risk": 0.0,
|
|
76
|
+
"ambiguity": 0.0,
|
|
77
|
+
"test_red": 0.0,
|
|
78
|
+
"urgency": 0.0,
|
|
79
|
+
"air_puff": 0.0,
|
|
80
|
+
}
|
|
81
|
+
self._active_stims: list[dict[str, Any]] = []
|
|
82
|
+
|
|
83
|
+
def _build_groups(self) -> None:
|
|
84
|
+
self.loom_left: list[int] = []
|
|
85
|
+
self.loom_right: list[int] = []
|
|
86
|
+
self.gf: list[int] = []
|
|
87
|
+
self.dna_l: list[int] = []
|
|
88
|
+
self.dna_r: list[int] = []
|
|
89
|
+
self.mdn: list[int] = []
|
|
90
|
+
self.fwd: list[int] = []
|
|
91
|
+
self.groom: list[int] = []
|
|
92
|
+
self.escw: list[int] = []
|
|
93
|
+
self.ascend: list[int] = []
|
|
94
|
+
self.sens: list[int] = []
|
|
95
|
+
for i, nr in enumerate(self.circuit.neurons):
|
|
96
|
+
role = self.roles[i]
|
|
97
|
+
side = self.sides[i]
|
|
98
|
+
if role in ("lc4", "lplc2"):
|
|
99
|
+
(self.loom_left if side == "left" else self.loom_right).append(i)
|
|
100
|
+
elif role == "gf":
|
|
101
|
+
self.gf.append(i)
|
|
102
|
+
elif role in ("dna01", "dna02"):
|
|
103
|
+
(self.dna_l if side == "left" else self.dna_r).append(i)
|
|
104
|
+
elif role == "mdn":
|
|
105
|
+
self.mdn.append(i)
|
|
106
|
+
elif role == "dnp09":
|
|
107
|
+
self.fwd.append(i)
|
|
108
|
+
elif role == "dng11":
|
|
109
|
+
self.groom.append(i)
|
|
110
|
+
elif role == "escw":
|
|
111
|
+
self.escw.append(i)
|
|
112
|
+
elif role == "other":
|
|
113
|
+
if self.types[i] == "ascending":
|
|
114
|
+
self.ascend.append(i)
|
|
115
|
+
elif self.types[i] == "sensory":
|
|
116
|
+
self.sens.append(i)
|
|
117
|
+
self.groups = {
|
|
118
|
+
"gf": self.gf,
|
|
119
|
+
"lc4": [i for i, r in enumerate(self.roles) if r == "lc4"],
|
|
120
|
+
"lplc2": [i for i, r in enumerate(self.roles) if r == "lplc2"],
|
|
121
|
+
"dnp09": self.fwd,
|
|
122
|
+
"mdn": self.mdn,
|
|
123
|
+
"dng11": self.groom,
|
|
124
|
+
"escw": self.escw,
|
|
125
|
+
"dna01": [i for i, r in enumerate(self.roles) if r == "dna01"],
|
|
126
|
+
"dna02": [i for i, r in enumerate(self.roles) if r == "dna02"],
|
|
127
|
+
"ascending": self.ascend,
|
|
128
|
+
"sensory": self.sens,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def _build_baselines(self) -> None:
|
|
132
|
+
for i, role in enumerate(self.roles):
|
|
133
|
+
if role == "other":
|
|
134
|
+
self.baseline[i] = float(self.rng.uniform(0.010, 0.070))
|
|
135
|
+
elif role in ("lc4", "lplc2"):
|
|
136
|
+
self.baseline[i] = 0.004
|
|
137
|
+
elif role in ("dna01", "dna02", "mdn", "dng11", "escw"):
|
|
138
|
+
self.baseline[i] = 0.036
|
|
139
|
+
elif role == "dnp09":
|
|
140
|
+
self.baseline[i] = 0.038
|
|
141
|
+
else:
|
|
142
|
+
self.baseline[i] = 0.002 # gf quiet unless driven
|
|
143
|
+
|
|
144
|
+
def _build_csr(self) -> None:
|
|
145
|
+
n = self.circuit.n
|
|
146
|
+
counts = np.zeros(n, dtype=np.int32)
|
|
147
|
+
for e in self.circuit.edges:
|
|
148
|
+
counts[int(e[0])] += 1
|
|
149
|
+
self.row_start = np.zeros(n + 1, dtype=np.int32)
|
|
150
|
+
for i in range(n):
|
|
151
|
+
self.row_start[i + 1] = self.row_start[i] + counts[i]
|
|
152
|
+
m = len(self.circuit.edges)
|
|
153
|
+
self.col_idx = np.zeros(m, dtype=np.int32)
|
|
154
|
+
self.w = np.zeros(m, dtype=np.float64)
|
|
155
|
+
fill = self.row_start.copy()
|
|
156
|
+
for e in self.circuit.edges:
|
|
157
|
+
pre, post = int(e[0]), int(e[1])
|
|
158
|
+
weight = float(e[2]) * self.weight_scale
|
|
159
|
+
electrical = self.roles[pre] in ("lc4", "lplc2") or (
|
|
160
|
+
self.roles[pre] == "other" and self.types[pre] == "sensory"
|
|
161
|
+
)
|
|
162
|
+
if electrical and self.roles[post] == "gf":
|
|
163
|
+
weight *= self.gap_junction_boost
|
|
164
|
+
slot = int(fill[pre])
|
|
165
|
+
self.col_idx[slot] = post
|
|
166
|
+
self.w[slot] = weight
|
|
167
|
+
fill[pre] += 1
|
|
168
|
+
|
|
169
|
+
def set_inputs(self, **kwargs: float) -> None:
|
|
170
|
+
for k, v in kwargs.items():
|
|
171
|
+
if k in self.inputs:
|
|
172
|
+
self.inputs[k] = float(v)
|
|
173
|
+
|
|
174
|
+
def stimulate(self, indices: list[int], strength: float = 0.25, duration_ms: int = 400) -> None:
|
|
175
|
+
if not indices:
|
|
176
|
+
return
|
|
177
|
+
self._active_stims.append(
|
|
178
|
+
{
|
|
179
|
+
"idx": [int(i) for i in indices if 0 <= int(i) < self.circuit.n],
|
|
180
|
+
"strength": float(strength),
|
|
181
|
+
"until": self.sim_ms + int(duration_ms),
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def stimulate_group(self, name: str, strength: float = 0.25, duration_ms: int = 400) -> int:
|
|
186
|
+
idx = self.groups.get(name, [])
|
|
187
|
+
self.stimulate(idx, strength=strength, duration_ms=duration_ms)
|
|
188
|
+
return len(idx)
|
|
189
|
+
|
|
190
|
+
def consume_gf(self) -> bool:
|
|
191
|
+
flag = self.gf_latch
|
|
192
|
+
self.gf_latch = False
|
|
193
|
+
return flag
|
|
194
|
+
|
|
195
|
+
def step(self, ms: int) -> dict[str, Any]:
|
|
196
|
+
ms = max(0, int(ms))
|
|
197
|
+
spiked_total = 0
|
|
198
|
+
for _ in range(ms):
|
|
199
|
+
self.sim_ms += 1
|
|
200
|
+
self._active_stims = [s for s in self._active_stims if self.sim_ms < s["until"]]
|
|
201
|
+
# leak + baseline + noise
|
|
202
|
+
active = self.refr <= 0
|
|
203
|
+
self.v[active] = self.v[active] * self.decay + self.baseline[active]
|
|
204
|
+
self.v[~active] *= self.decay
|
|
205
|
+
self.refr = np.maximum(0, self.refr - 1)
|
|
206
|
+
noise_mask = (self.rng.random(self.circuit.n) < self.p_noise) & active
|
|
207
|
+
self.v[noise_mask] += self.noise_kick
|
|
208
|
+
|
|
209
|
+
# task / sensory currents
|
|
210
|
+
loom = max(self.inputs["loom_l"], self.inputs["loom_r"], self.inputs["risk"])
|
|
211
|
+
if loom > 0.001:
|
|
212
|
+
gain = 0.30 * loom
|
|
213
|
+
for i in self.loom_left:
|
|
214
|
+
self.v[i] += gain * (1.0 if self.inputs["loom_l"] or self.inputs["risk"] else 0.0)
|
|
215
|
+
for i in self.loom_right:
|
|
216
|
+
self.v[i] += gain * (1.0 if self.inputs["loom_r"] or self.inputs["risk"] else 0.0)
|
|
217
|
+
if self.inputs["test_red"] > 0.001:
|
|
218
|
+
for i in self.sens:
|
|
219
|
+
self.v[i] += self.inputs["test_red"] * 0.10
|
|
220
|
+
if self.inputs["ambiguity"] > 0.001:
|
|
221
|
+
for i in self.ascend:
|
|
222
|
+
self.v[i] += self.inputs["ambiguity"] * 0.06
|
|
223
|
+
if self.inputs["urgency"] > 0.001:
|
|
224
|
+
for i in self.fwd:
|
|
225
|
+
self.v[i] += self.inputs["urgency"] * 0.05
|
|
226
|
+
if self.inputs["air_puff"] > 0.001:
|
|
227
|
+
for i in self.sens:
|
|
228
|
+
self.v[i] += self.inputs["air_puff"] * 0.12
|
|
229
|
+
|
|
230
|
+
for stim in self._active_stims:
|
|
231
|
+
for i in stim["idx"]:
|
|
232
|
+
self.v[i] += stim["strength"]
|
|
233
|
+
|
|
234
|
+
# delayed inhibition
|
|
235
|
+
slot = self.inh_queue[self.q_head]
|
|
236
|
+
nz = slot != 0
|
|
237
|
+
self.v[nz] = np.maximum(-2.0, self.v[nz] + slot[nz])
|
|
238
|
+
slot[:] = 0
|
|
239
|
+
|
|
240
|
+
fire = active & (self.v >= self.threshold)
|
|
241
|
+
spiked = np.flatnonzero(fire)
|
|
242
|
+
if spiked.size:
|
|
243
|
+
self.v[spiked] = 0.0
|
|
244
|
+
self.refr[spiked] = self.refractory_ms
|
|
245
|
+
spiked_total += int(spiked.size)
|
|
246
|
+
self.total_spikes += int(spiked.size)
|
|
247
|
+
inh_slot = (self.q_head + self.inh_delay_ms) % self.inh_queue.shape[0]
|
|
248
|
+
for i in spiked:
|
|
249
|
+
for k in range(self.row_start[i], self.row_start[i + 1]):
|
|
250
|
+
j = int(self.col_idx[k])
|
|
251
|
+
w = self.w[k]
|
|
252
|
+
if w >= 0:
|
|
253
|
+
self.v[j] = max(-2.0, self.v[j] + w)
|
|
254
|
+
else:
|
|
255
|
+
self.inh_queue[inh_slot, j] += w
|
|
256
|
+
if self.roles[int(i)] == "gf":
|
|
257
|
+
self.gf_latch = True
|
|
258
|
+
self.q_head = (self.q_head + 1) % self.inh_queue.shape[0]
|
|
259
|
+
self._update_rates(spiked)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"sim_ms": self.sim_ms,
|
|
263
|
+
"spikes": spiked_total,
|
|
264
|
+
"total_spikes": self.total_spikes,
|
|
265
|
+
"rates": dict(self.rates),
|
|
266
|
+
"gf_latched": self.gf_latch,
|
|
267
|
+
"n": self.circuit.n,
|
|
268
|
+
"edges": len(self.circuit.edges),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def _update_rates(self, spiked: np.ndarray) -> None:
|
|
272
|
+
c_loom = c_dl = c_dr = c_m = c_f = c_g = c_w = 0
|
|
273
|
+
for i in spiked:
|
|
274
|
+
role = self.roles[int(i)]
|
|
275
|
+
if role in ("lc4", "lplc2"):
|
|
276
|
+
c_loom += 1
|
|
277
|
+
elif role in ("dna01", "dna02"):
|
|
278
|
+
if int(i) in self.dna_l:
|
|
279
|
+
c_dl += 1
|
|
280
|
+
else:
|
|
281
|
+
c_dr += 1
|
|
282
|
+
elif role == "mdn":
|
|
283
|
+
c_m += 1
|
|
284
|
+
elif role == "dnp09":
|
|
285
|
+
c_f += 1
|
|
286
|
+
elif role == "dng11":
|
|
287
|
+
c_g += 1
|
|
288
|
+
elif role == "escw":
|
|
289
|
+
c_w += 1
|
|
290
|
+
a = self.rate_alpha
|
|
291
|
+
|
|
292
|
+
def ema(key: str, count: int, denom: int) -> None:
|
|
293
|
+
inst = count * 1000.0 / max(1, denom)
|
|
294
|
+
self.rates[key] += (inst - self.rates[key]) * a
|
|
295
|
+
|
|
296
|
+
ema("loom", c_loom, len(self.loom_left) + len(self.loom_right))
|
|
297
|
+
ema("dna_l", c_dl, len(self.dna_l))
|
|
298
|
+
ema("dna_r", c_dr, len(self.dna_r))
|
|
299
|
+
ema("mdn", c_m, len(self.mdn))
|
|
300
|
+
ema("fwd", c_f, len(self.fwd))
|
|
301
|
+
ema("groom", c_g, len(self.groom))
|
|
302
|
+
ema("escw", c_w, len(self.escw))
|
|
303
|
+
ema("pop", int(spiked.size), self.circuit.n)
|
|
304
|
+
|
|
305
|
+
def to_state(self) -> dict:
|
|
306
|
+
return {
|
|
307
|
+
"sim_ms": self.sim_ms,
|
|
308
|
+
"total_spikes": self.total_spikes,
|
|
309
|
+
"gf_latch": self.gf_latch,
|
|
310
|
+
"v": self.v.tolist(),
|
|
311
|
+
"refr": self.refr.tolist(),
|
|
312
|
+
"rates": dict(self.rates),
|
|
313
|
+
"inputs": dict(self.inputs),
|
|
314
|
+
"q_head": self.q_head,
|
|
315
|
+
"inh_queue": self.inh_queue.tolist(),
|
|
316
|
+
"stims": self._active_stims,
|
|
317
|
+
"dna_baseline": getattr(self, "dna_baseline", 0.0),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
def load_state(self, state: dict) -> None:
|
|
321
|
+
if not state or len(state.get("v", [])) != self.circuit.n:
|
|
322
|
+
return
|
|
323
|
+
self.sim_ms = int(state.get("sim_ms", 0))
|
|
324
|
+
self.total_spikes = int(state.get("total_spikes", 0))
|
|
325
|
+
self.gf_latch = bool(state.get("gf_latch", False))
|
|
326
|
+
self.v = np.asarray(state["v"], dtype=np.float64)
|
|
327
|
+
self.refr = np.asarray(state["refr"], dtype=np.float64)
|
|
328
|
+
self.rates.update(state.get("rates") or {})
|
|
329
|
+
self.inputs.update(state.get("inputs") or {})
|
|
330
|
+
self.q_head = int(state.get("q_head", 0))
|
|
331
|
+
iq = state.get("inh_queue")
|
|
332
|
+
if iq is not None:
|
|
333
|
+
self.inh_queue = np.asarray(iq, dtype=np.float64)
|
|
334
|
+
self._active_stims = list(state.get("stims") or [])
|
|
335
|
+
self.dna_baseline = float(state.get("dna_baseline", 0.0))
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Minimal MCP stdio server (JSON-RPC) for Cursor / agent connectors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import bridge, memory, sense
|
|
9
|
+
from .cli import _save_sim, get_sim
|
|
10
|
+
from .paths import active_profile, circuit_path, home
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TOOLS = [
|
|
14
|
+
{
|
|
15
|
+
"name": "cortex_status",
|
|
16
|
+
"description": "Show active FlyWire cortex profile and circuit stats",
|
|
17
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "cortex_sense",
|
|
21
|
+
"description": "Encode a coding task into sensory currents on the connectome",
|
|
22
|
+
"inputSchema": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {"text": {"type": "string"}},
|
|
25
|
+
"required": ["text"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "cortex_step",
|
|
30
|
+
"description": "Advance the FlyWire-derived LIF simulation",
|
|
31
|
+
"inputSchema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"ms": {"type": "integer", "default": 50},
|
|
35
|
+
"text": {"type": "string"},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "cortex_stimulate",
|
|
41
|
+
"description": "Stimulate a role population (gf, dnp09, mdn, …)",
|
|
42
|
+
"inputSchema": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"group": {"type": "string"},
|
|
46
|
+
"strength": {"type": "number", "default": 0.25},
|
|
47
|
+
"ms": {"type": "integer", "default": 400},
|
|
48
|
+
},
|
|
49
|
+
"required": ["group"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "cortex_signals",
|
|
54
|
+
"description": "Read clamped coding drives from population rates",
|
|
55
|
+
"inputSchema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {"ms": {"type": "integer", "default": 0}},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "cortex_remember",
|
|
62
|
+
"description": "Query or update Hebbian coding memory",
|
|
63
|
+
"inputSchema": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"action": {"type": "string", "enum": ["query", "strengthen", "weaken"]},
|
|
67
|
+
"text": {"type": "string"},
|
|
68
|
+
"pre": {"type": "string"},
|
|
69
|
+
"post": {"type": "string"},
|
|
70
|
+
"why": {"type": "string"},
|
|
71
|
+
},
|
|
72
|
+
"required": ["action"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _result(obj: Any) -> dict[str, Any]:
|
|
79
|
+
text = obj if isinstance(obj, str) else json.dumps(obj, indent=2)
|
|
80
|
+
return {"content": [{"type": "text", "text": text}]}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
84
|
+
if name == "cortex_status":
|
|
85
|
+
path = circuit_path()
|
|
86
|
+
info = {
|
|
87
|
+
"home": str(home()),
|
|
88
|
+
"active_profile": active_profile(),
|
|
89
|
+
"circuit": str(path),
|
|
90
|
+
"exists": path.exists(),
|
|
91
|
+
}
|
|
92
|
+
if path.exists():
|
|
93
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
94
|
+
info["neurons"] = len(data["neurons"])
|
|
95
|
+
info["edges"] = len(data["edges"])
|
|
96
|
+
return _result(info)
|
|
97
|
+
if name == "cortex_sense":
|
|
98
|
+
scores = sense.sense_text(arguments.get("text", ""))
|
|
99
|
+
sim = get_sim()
|
|
100
|
+
sim.set_inputs(**scores.as_inputs())
|
|
101
|
+
_save_sim(sim)
|
|
102
|
+
return _result(scores.as_inputs())
|
|
103
|
+
if name == "cortex_step":
|
|
104
|
+
sim = get_sim()
|
|
105
|
+
text = arguments.get("text") or ""
|
|
106
|
+
if text:
|
|
107
|
+
sim.set_inputs(**sense.sense_text(text).as_inputs())
|
|
108
|
+
out = sim.step(int(arguments.get("ms", 50)))
|
|
109
|
+
_save_sim(sim)
|
|
110
|
+
return _result(out)
|
|
111
|
+
if name == "cortex_stimulate":
|
|
112
|
+
sim = get_sim()
|
|
113
|
+
n = sim.stimulate_group(
|
|
114
|
+
arguments["group"],
|
|
115
|
+
strength=float(arguments.get("strength", 0.25)),
|
|
116
|
+
duration_ms=int(arguments.get("ms", 400)),
|
|
117
|
+
)
|
|
118
|
+
_save_sim(sim)
|
|
119
|
+
return _result({"group": arguments["group"], "neurons": n})
|
|
120
|
+
if name == "cortex_signals":
|
|
121
|
+
sim = get_sim()
|
|
122
|
+
ms = int(arguments.get("ms", 0))
|
|
123
|
+
if ms:
|
|
124
|
+
sim.step(ms)
|
|
125
|
+
gf = sim.consume_gf()
|
|
126
|
+
out = bridge.build_signals(sim.rates, gf_spike=gf).to_dict()
|
|
127
|
+
_save_sim(sim)
|
|
128
|
+
return _result(out)
|
|
129
|
+
if name == "cortex_remember":
|
|
130
|
+
action = arguments["action"]
|
|
131
|
+
if action == "query":
|
|
132
|
+
return _result(memory.query(arguments.get("text", "")))
|
|
133
|
+
if action == "strengthen":
|
|
134
|
+
return _result(
|
|
135
|
+
memory.strengthen(
|
|
136
|
+
arguments.get("pre", ""),
|
|
137
|
+
arguments.get("post", ""),
|
|
138
|
+
why=arguments.get("why", ""),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
if action == "weaken":
|
|
142
|
+
return _result(
|
|
143
|
+
memory.weaken(
|
|
144
|
+
arguments.get("pre", ""),
|
|
145
|
+
arguments.get("post", ""),
|
|
146
|
+
why=arguments.get("why", ""),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def run_stdio() -> None:
|
|
153
|
+
"""Very small MCP subset: initialize, tools/list, tools/call."""
|
|
154
|
+
|
|
155
|
+
def reply(msg_id: Any, result: Any) -> None:
|
|
156
|
+
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}) + "\n")
|
|
157
|
+
sys.stdout.flush()
|
|
158
|
+
|
|
159
|
+
def error(msg_id: Any, message: str) -> None:
|
|
160
|
+
sys.stdout.write(
|
|
161
|
+
json.dumps(
|
|
162
|
+
{
|
|
163
|
+
"jsonrpc": "2.0",
|
|
164
|
+
"id": msg_id,
|
|
165
|
+
"error": {"code": -32000, "message": message},
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
+ "\n"
|
|
169
|
+
)
|
|
170
|
+
sys.stdout.flush()
|
|
171
|
+
|
|
172
|
+
for line in sys.stdin:
|
|
173
|
+
line = line.strip()
|
|
174
|
+
if not line:
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
req = json.loads(line)
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
continue
|
|
180
|
+
method = req.get("method")
|
|
181
|
+
msg_id = req.get("id")
|
|
182
|
+
params = req.get("params") or {}
|
|
183
|
+
try:
|
|
184
|
+
if method == "initialize":
|
|
185
|
+
reply(
|
|
186
|
+
msg_id,
|
|
187
|
+
{
|
|
188
|
+
"protocolVersion": "2024-11-05",
|
|
189
|
+
"capabilities": {"tools": {}},
|
|
190
|
+
"serverInfo": {"name": "flywire-coding-cortex", "version": "0.1.0"},
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
elif method == "notifications/initialized":
|
|
194
|
+
continue
|
|
195
|
+
elif method == "tools/list":
|
|
196
|
+
reply(msg_id, {"tools": TOOLS})
|
|
197
|
+
elif method == "tools/call":
|
|
198
|
+
name = params.get("name")
|
|
199
|
+
arguments = params.get("arguments") or {}
|
|
200
|
+
reply(msg_id, call_tool(name, arguments))
|
|
201
|
+
elif method == "ping":
|
|
202
|
+
reply(msg_id, {})
|
|
203
|
+
else:
|
|
204
|
+
if msg_id is not None:
|
|
205
|
+
error(msg_id, f"Method not found: {method}")
|
|
206
|
+
except Exception as exc: # noqa: BLE001 — surface to MCP client
|
|
207
|
+
if msg_id is not None:
|
|
208
|
+
error(msg_id, str(exc))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Hebbian coding-memory graph (not fabricated FlyWire edges)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .paths import ensure_memory, memory_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load() -> dict[str, Any]:
|
|
14
|
+
path = ensure_memory()
|
|
15
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _save(graph: dict[str, Any]) -> None:
|
|
19
|
+
memory_path().write_text(json.dumps(graph, indent=2) + "\n", encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def query(text: str, limit: int = 8) -> list[dict[str, Any]]:
|
|
23
|
+
graph = _load()
|
|
24
|
+
tokens = set(re.findall(r"[a-z0-9_/-]+", text.lower()))
|
|
25
|
+
scored: list[tuple[float, dict[str, Any]]] = []
|
|
26
|
+
nodes = {n["id"]: n for n in graph.get("nodes", [])}
|
|
27
|
+
for e in graph.get("edges", []):
|
|
28
|
+
pre, post = e.get("pre"), e.get("post")
|
|
29
|
+
blob = f"{pre} {post} {e.get('why', '')} {nodes.get(pre, {}).get('label', '')} {nodes.get(post, {}).get('label', '')}".lower()
|
|
30
|
+
hit = sum(1 for t in tokens if t in blob)
|
|
31
|
+
if hit:
|
|
32
|
+
scored.append((hit * float(e.get("weight", 1)), e))
|
|
33
|
+
scored.sort(key=lambda x: -x[0])
|
|
34
|
+
out = []
|
|
35
|
+
for score, e in scored[:limit]:
|
|
36
|
+
out.append(
|
|
37
|
+
{
|
|
38
|
+
"score": score,
|
|
39
|
+
"pre": e.get("pre"),
|
|
40
|
+
"post": e.get("post"),
|
|
41
|
+
"weight": e.get("weight"),
|
|
42
|
+
"why": e.get("why", ""),
|
|
43
|
+
"pre_label": nodes.get(e.get("pre"), {}).get("label"),
|
|
44
|
+
"post_label": nodes.get(e.get("post"), {}).get("label"),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_node(node_id: str, label: str, kind: str = "concept") -> dict[str, Any]:
|
|
51
|
+
graph = _load()
|
|
52
|
+
nodes = graph.setdefault("nodes", [])
|
|
53
|
+
for n in nodes:
|
|
54
|
+
if n["id"] == node_id:
|
|
55
|
+
n["label"] = label
|
|
56
|
+
n["kind"] = kind
|
|
57
|
+
n["lastSeen"] = time.time()
|
|
58
|
+
_save(graph)
|
|
59
|
+
return n
|
|
60
|
+
node = {"id": node_id, "label": label, "kind": kind, "lastSeen": time.time()}
|
|
61
|
+
nodes.append(node)
|
|
62
|
+
_save(graph)
|
|
63
|
+
return node
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def add_edge(pre: str, post: str, weight: float = 1.0, why: str = "") -> dict[str, Any]:
|
|
67
|
+
graph = _load()
|
|
68
|
+
edges = graph.setdefault("edges", [])
|
|
69
|
+
for e in edges:
|
|
70
|
+
if e.get("pre") == pre and e.get("post") == post:
|
|
71
|
+
e["weight"] = float(weight)
|
|
72
|
+
if why:
|
|
73
|
+
e.setdefault("evidence", []).append(why)
|
|
74
|
+
e["why"] = why
|
|
75
|
+
_save(graph)
|
|
76
|
+
return e
|
|
77
|
+
edge = {"pre": pre, "post": post, "weight": float(weight), "why": why, "evidence": [why] if why else []}
|
|
78
|
+
edges.append(edge)
|
|
79
|
+
_save(graph)
|
|
80
|
+
return edge
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def strengthen(pre: str, post: str, delta: float = 0.25, why: str = "") -> dict[str, Any]:
|
|
84
|
+
graph = _load()
|
|
85
|
+
for e in graph.get("edges", []):
|
|
86
|
+
if e.get("pre") == pre and e.get("post") == post:
|
|
87
|
+
e["weight"] = float(e.get("weight", 1.0)) + float(delta)
|
|
88
|
+
if why:
|
|
89
|
+
e.setdefault("evidence", []).append(why)
|
|
90
|
+
e["why"] = why
|
|
91
|
+
_save(graph)
|
|
92
|
+
return e
|
|
93
|
+
return add_edge(pre, post, weight=1.0 + delta, why=why)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def weaken(pre: str, post: str, delta: float = 0.25, why: str = "") -> dict[str, Any]:
|
|
97
|
+
graph = _load()
|
|
98
|
+
for e in graph.get("edges", []):
|
|
99
|
+
if e.get("pre") == pre and e.get("post") == post:
|
|
100
|
+
e["weight"] = float(e.get("weight", 1.0)) - float(delta)
|
|
101
|
+
if why:
|
|
102
|
+
e.setdefault("evidence", []).append(f"weaken:{why}")
|
|
103
|
+
e["why"] = why
|
|
104
|
+
_save(graph)
|
|
105
|
+
return e
|
|
106
|
+
return add_edge(pre, post, weight=-abs(delta), why=why or "inhibitory")
|