neural-memory 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.
- neural_memory/__init__.py +38 -0
- neural_memory/cli/__init__.py +15 -0
- neural_memory/cli/__main__.py +6 -0
- neural_memory/cli/config.py +176 -0
- neural_memory/cli/main.py +2702 -0
- neural_memory/cli/storage.py +169 -0
- neural_memory/cli/tui.py +471 -0
- neural_memory/core/__init__.py +52 -0
- neural_memory/core/brain.py +301 -0
- neural_memory/core/brain_mode.py +273 -0
- neural_memory/core/fiber.py +236 -0
- neural_memory/core/memory_types.py +331 -0
- neural_memory/core/neuron.py +168 -0
- neural_memory/core/project.py +257 -0
- neural_memory/core/synapse.py +215 -0
- neural_memory/engine/__init__.py +15 -0
- neural_memory/engine/activation.py +335 -0
- neural_memory/engine/encoder.py +391 -0
- neural_memory/engine/retrieval.py +440 -0
- neural_memory/extraction/__init__.py +42 -0
- neural_memory/extraction/entities.py +547 -0
- neural_memory/extraction/parser.py +337 -0
- neural_memory/extraction/router.py +396 -0
- neural_memory/extraction/temporal.py +428 -0
- neural_memory/mcp/__init__.py +9 -0
- neural_memory/mcp/__main__.py +6 -0
- neural_memory/mcp/server.py +621 -0
- neural_memory/py.typed +0 -0
- neural_memory/safety/__init__.py +31 -0
- neural_memory/safety/freshness.py +238 -0
- neural_memory/safety/sensitive.py +304 -0
- neural_memory/server/__init__.py +5 -0
- neural_memory/server/app.py +99 -0
- neural_memory/server/dependencies.py +33 -0
- neural_memory/server/models.py +138 -0
- neural_memory/server/routes/__init__.py +7 -0
- neural_memory/server/routes/brain.py +221 -0
- neural_memory/server/routes/memory.py +169 -0
- neural_memory/server/routes/sync.py +387 -0
- neural_memory/storage/__init__.py +17 -0
- neural_memory/storage/base.py +441 -0
- neural_memory/storage/factory.py +329 -0
- neural_memory/storage/memory_store.py +896 -0
- neural_memory/storage/shared_store.py +650 -0
- neural_memory/storage/sqlite_store.py +1613 -0
- neural_memory/sync/__init__.py +5 -0
- neural_memory/sync/client.py +435 -0
- neural_memory/unified_config.py +315 -0
- neural_memory/utils/__init__.py +5 -0
- neural_memory/utils/config.py +98 -0
- neural_memory-0.1.0.dist-info/METADATA +314 -0
- neural_memory-0.1.0.dist-info/RECORD +55 -0
- neural_memory-0.1.0.dist-info/WHEEL +4 -0
- neural_memory-0.1.0.dist-info/entry_points.txt +4 -0
- neural_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""WebSocket routes for real-time brain synchronization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/sync", tags=["sync"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SyncEventType(StrEnum):
|
|
18
|
+
"""Types of sync events."""
|
|
19
|
+
|
|
20
|
+
# Connection events
|
|
21
|
+
CONNECTED = "connected"
|
|
22
|
+
DISCONNECTED = "disconnected"
|
|
23
|
+
SUBSCRIBED = "subscribed"
|
|
24
|
+
UNSUBSCRIBED = "unsubscribed"
|
|
25
|
+
|
|
26
|
+
# Data events
|
|
27
|
+
NEURON_CREATED = "neuron_created"
|
|
28
|
+
NEURON_UPDATED = "neuron_updated"
|
|
29
|
+
NEURON_DELETED = "neuron_deleted"
|
|
30
|
+
|
|
31
|
+
SYNAPSE_CREATED = "synapse_created"
|
|
32
|
+
SYNAPSE_UPDATED = "synapse_updated"
|
|
33
|
+
SYNAPSE_DELETED = "synapse_deleted"
|
|
34
|
+
|
|
35
|
+
FIBER_CREATED = "fiber_created"
|
|
36
|
+
FIBER_UPDATED = "fiber_updated"
|
|
37
|
+
FIBER_DELETED = "fiber_deleted"
|
|
38
|
+
|
|
39
|
+
# Memory events
|
|
40
|
+
MEMORY_ENCODED = "memory_encoded"
|
|
41
|
+
MEMORY_QUERIED = "memory_queried"
|
|
42
|
+
|
|
43
|
+
# Sync events
|
|
44
|
+
FULL_SYNC = "full_sync"
|
|
45
|
+
PARTIAL_SYNC = "partial_sync"
|
|
46
|
+
|
|
47
|
+
# Error events
|
|
48
|
+
ERROR = "error"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class SyncEvent:
|
|
53
|
+
"""A synchronization event."""
|
|
54
|
+
|
|
55
|
+
type: SyncEventType
|
|
56
|
+
brain_id: str
|
|
57
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
58
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
source_client_id: str | None = None
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict[str, Any]:
|
|
62
|
+
"""Convert to dictionary for JSON serialization."""
|
|
63
|
+
return {
|
|
64
|
+
"type": self.type.value,
|
|
65
|
+
"brain_id": self.brain_id,
|
|
66
|
+
"timestamp": self.timestamp.isoformat(),
|
|
67
|
+
"data": self.data,
|
|
68
|
+
"source_client_id": self.source_client_id,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def to_json(self) -> str:
|
|
72
|
+
"""Convert to JSON string."""
|
|
73
|
+
return json.dumps(self.to_dict())
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict[str, Any]) -> SyncEvent:
|
|
77
|
+
"""Create from dictionary."""
|
|
78
|
+
return cls(
|
|
79
|
+
type=SyncEventType(data["type"]),
|
|
80
|
+
brain_id=data["brain_id"],
|
|
81
|
+
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
82
|
+
if data.get("timestamp")
|
|
83
|
+
else datetime.utcnow(),
|
|
84
|
+
data=data.get("data", {}),
|
|
85
|
+
source_client_id=data.get("source_client_id"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ConnectedClient:
|
|
91
|
+
"""A connected WebSocket client."""
|
|
92
|
+
|
|
93
|
+
client_id: str
|
|
94
|
+
websocket: WebSocket
|
|
95
|
+
brain_ids: set[str] = field(default_factory=set)
|
|
96
|
+
connected_at: datetime = field(default_factory=datetime.utcnow)
|
|
97
|
+
|
|
98
|
+
async def send_event(self, event: SyncEvent) -> bool:
|
|
99
|
+
"""Send event to client. Returns False if connection closed."""
|
|
100
|
+
try:
|
|
101
|
+
await self.websocket.send_text(event.to_json())
|
|
102
|
+
return True
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SyncManager:
|
|
108
|
+
"""
|
|
109
|
+
Manages WebSocket connections and event broadcasting.
|
|
110
|
+
|
|
111
|
+
Singleton pattern - use SyncManager.instance() to get the shared instance.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
_instance: SyncManager | None = None
|
|
115
|
+
|
|
116
|
+
def __init__(self) -> None:
|
|
117
|
+
self._clients: dict[str, ConnectedClient] = {}
|
|
118
|
+
self._brain_subscriptions: dict[str, set[str]] = {} # brain_id -> client_ids
|
|
119
|
+
self._event_history: dict[str, list[SyncEvent]] = {} # brain_id -> recent events
|
|
120
|
+
self._max_history = 100
|
|
121
|
+
self._lock = asyncio.Lock()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def instance(cls) -> SyncManager:
|
|
125
|
+
"""Get the singleton instance."""
|
|
126
|
+
if cls._instance is None:
|
|
127
|
+
cls._instance = cls()
|
|
128
|
+
return cls._instance
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def reset(cls) -> None:
|
|
132
|
+
"""Reset the singleton (for testing)."""
|
|
133
|
+
cls._instance = None
|
|
134
|
+
|
|
135
|
+
async def connect(self, client_id: str, websocket: WebSocket) -> ConnectedClient:
|
|
136
|
+
"""Register a new WebSocket client."""
|
|
137
|
+
async with self._lock:
|
|
138
|
+
client = ConnectedClient(client_id=client_id, websocket=websocket)
|
|
139
|
+
self._clients[client_id] = client
|
|
140
|
+
return client
|
|
141
|
+
|
|
142
|
+
async def disconnect(self, client_id: str) -> None:
|
|
143
|
+
"""Unregister a WebSocket client."""
|
|
144
|
+
async with self._lock:
|
|
145
|
+
if client_id in self._clients:
|
|
146
|
+
client = self._clients[client_id]
|
|
147
|
+
# Unsubscribe from all brains
|
|
148
|
+
for brain_id in client.brain_ids:
|
|
149
|
+
if brain_id in self._brain_subscriptions:
|
|
150
|
+
self._brain_subscriptions[brain_id].discard(client_id)
|
|
151
|
+
del self._clients[client_id]
|
|
152
|
+
|
|
153
|
+
async def subscribe(self, client_id: str, brain_id: str) -> bool:
|
|
154
|
+
"""Subscribe a client to a brain's events."""
|
|
155
|
+
async with self._lock:
|
|
156
|
+
if client_id not in self._clients:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
client = self._clients[client_id]
|
|
160
|
+
client.brain_ids.add(brain_id)
|
|
161
|
+
|
|
162
|
+
if brain_id not in self._brain_subscriptions:
|
|
163
|
+
self._brain_subscriptions[brain_id] = set()
|
|
164
|
+
self._brain_subscriptions[brain_id].add(client_id)
|
|
165
|
+
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
async def unsubscribe(self, client_id: str, brain_id: str) -> bool:
|
|
169
|
+
"""Unsubscribe a client from a brain's events."""
|
|
170
|
+
async with self._lock:
|
|
171
|
+
if client_id not in self._clients:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
client = self._clients[client_id]
|
|
175
|
+
client.brain_ids.discard(brain_id)
|
|
176
|
+
|
|
177
|
+
if brain_id in self._brain_subscriptions:
|
|
178
|
+
self._brain_subscriptions[brain_id].discard(client_id)
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
async def broadcast(
|
|
183
|
+
self,
|
|
184
|
+
event: SyncEvent,
|
|
185
|
+
exclude_client: str | None = None,
|
|
186
|
+
) -> int:
|
|
187
|
+
"""
|
|
188
|
+
Broadcast event to all clients subscribed to the brain.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
event: The event to broadcast
|
|
192
|
+
exclude_client: Don't send to this client (usually the source)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Number of clients that received the event
|
|
196
|
+
"""
|
|
197
|
+
# Store in history
|
|
198
|
+
async with self._lock:
|
|
199
|
+
if event.brain_id not in self._event_history:
|
|
200
|
+
self._event_history[event.brain_id] = []
|
|
201
|
+
|
|
202
|
+
history = self._event_history[event.brain_id]
|
|
203
|
+
history.append(event)
|
|
204
|
+
if len(history) > self._max_history:
|
|
205
|
+
self._event_history[event.brain_id] = history[-self._max_history :]
|
|
206
|
+
|
|
207
|
+
# Get subscribed clients
|
|
208
|
+
async with self._lock:
|
|
209
|
+
client_ids = self._brain_subscriptions.get(event.brain_id, set()).copy()
|
|
210
|
+
|
|
211
|
+
# Send to all subscribed clients
|
|
212
|
+
sent_count = 0
|
|
213
|
+
disconnected = []
|
|
214
|
+
|
|
215
|
+
for client_id in client_ids:
|
|
216
|
+
if client_id == exclude_client:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
async with self._lock:
|
|
220
|
+
client = self._clients.get(client_id)
|
|
221
|
+
|
|
222
|
+
if client:
|
|
223
|
+
success = await client.send_event(event)
|
|
224
|
+
if success:
|
|
225
|
+
sent_count += 1
|
|
226
|
+
else:
|
|
227
|
+
disconnected.append(client_id)
|
|
228
|
+
|
|
229
|
+
# Clean up disconnected clients
|
|
230
|
+
for client_id in disconnected:
|
|
231
|
+
await self.disconnect(client_id)
|
|
232
|
+
|
|
233
|
+
return sent_count
|
|
234
|
+
|
|
235
|
+
async def get_recent_events(
|
|
236
|
+
self,
|
|
237
|
+
brain_id: str,
|
|
238
|
+
since: datetime | None = None,
|
|
239
|
+
limit: int = 50,
|
|
240
|
+
) -> list[SyncEvent]:
|
|
241
|
+
"""Get recent events for a brain."""
|
|
242
|
+
async with self._lock:
|
|
243
|
+
history = self._event_history.get(brain_id, [])
|
|
244
|
+
|
|
245
|
+
if since:
|
|
246
|
+
history = [e for e in history if e.timestamp > since]
|
|
247
|
+
|
|
248
|
+
return history[-limit:]
|
|
249
|
+
|
|
250
|
+
def get_stats(self) -> dict[str, Any]:
|
|
251
|
+
"""Get sync manager statistics."""
|
|
252
|
+
return {
|
|
253
|
+
"connected_clients": len(self._clients),
|
|
254
|
+
"brain_subscriptions": {
|
|
255
|
+
brain_id: len(clients) for brain_id, clients in self._brain_subscriptions.items()
|
|
256
|
+
},
|
|
257
|
+
"event_history_size": sum(len(events) for events in self._event_history.values()),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Global sync manager instance
|
|
262
|
+
_sync_manager: SyncManager | None = None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_sync_manager() -> SyncManager:
|
|
266
|
+
"""Get the global sync manager instance."""
|
|
267
|
+
return SyncManager.instance()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@router.websocket("/ws")
|
|
271
|
+
async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
272
|
+
"""
|
|
273
|
+
WebSocket endpoint for real-time brain synchronization.
|
|
274
|
+
|
|
275
|
+
Protocol:
|
|
276
|
+
1. Client connects and sends: {"action": "connect", "client_id": "..."}
|
|
277
|
+
2. Server responds with: {"type": "connected", ...}
|
|
278
|
+
3. Client subscribes to brain: {"action": "subscribe", "brain_id": "..."}
|
|
279
|
+
4. Server sends events as they occur
|
|
280
|
+
5. Client can send changes: {"action": "event", "event": {...}}
|
|
281
|
+
"""
|
|
282
|
+
await websocket.accept()
|
|
283
|
+
sync_manager = get_sync_manager()
|
|
284
|
+
|
|
285
|
+
client_id: str | None = None
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
while True:
|
|
289
|
+
data = await websocket.receive_text()
|
|
290
|
+
message = json.loads(data)
|
|
291
|
+
action = message.get("action")
|
|
292
|
+
|
|
293
|
+
if action == "connect":
|
|
294
|
+
client_id = message.get("client_id", f"client-{id(websocket)}")
|
|
295
|
+
await sync_manager.connect(client_id, websocket)
|
|
296
|
+
await websocket.send_text(
|
|
297
|
+
SyncEvent(
|
|
298
|
+
type=SyncEventType.CONNECTED,
|
|
299
|
+
brain_id="*",
|
|
300
|
+
data={"client_id": client_id},
|
|
301
|
+
).to_json()
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
elif action == "subscribe" and client_id:
|
|
305
|
+
brain_id = message.get("brain_id")
|
|
306
|
+
if brain_id:
|
|
307
|
+
success = await sync_manager.subscribe(client_id, brain_id)
|
|
308
|
+
event_type = SyncEventType.SUBSCRIBED if success else SyncEventType.ERROR
|
|
309
|
+
await websocket.send_text(
|
|
310
|
+
SyncEvent(
|
|
311
|
+
type=event_type,
|
|
312
|
+
brain_id=brain_id,
|
|
313
|
+
data={
|
|
314
|
+
"success": success,
|
|
315
|
+
"client_id": client_id,
|
|
316
|
+
},
|
|
317
|
+
).to_json()
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
elif action == "unsubscribe" and client_id:
|
|
321
|
+
brain_id = message.get("brain_id")
|
|
322
|
+
if brain_id:
|
|
323
|
+
success = await sync_manager.unsubscribe(client_id, brain_id)
|
|
324
|
+
await websocket.send_text(
|
|
325
|
+
SyncEvent(
|
|
326
|
+
type=SyncEventType.UNSUBSCRIBED,
|
|
327
|
+
brain_id=brain_id,
|
|
328
|
+
data={"success": success},
|
|
329
|
+
).to_json()
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
elif action == "event" and client_id:
|
|
333
|
+
# Client is pushing an event
|
|
334
|
+
event_data = message.get("event", {})
|
|
335
|
+
event = SyncEvent.from_dict(event_data)
|
|
336
|
+
event = SyncEvent(
|
|
337
|
+
type=event.type,
|
|
338
|
+
brain_id=event.brain_id,
|
|
339
|
+
timestamp=event.timestamp,
|
|
340
|
+
data=event.data,
|
|
341
|
+
source_client_id=client_id,
|
|
342
|
+
)
|
|
343
|
+
# Broadcast to other clients
|
|
344
|
+
await sync_manager.broadcast(event, exclude_client=client_id)
|
|
345
|
+
|
|
346
|
+
elif action == "get_history" and client_id:
|
|
347
|
+
brain_id = message.get("brain_id")
|
|
348
|
+
since = message.get("since")
|
|
349
|
+
if brain_id:
|
|
350
|
+
since_dt = datetime.fromisoformat(since) if since else None
|
|
351
|
+
events = await sync_manager.get_recent_events(brain_id, since_dt)
|
|
352
|
+
await websocket.send_text(
|
|
353
|
+
json.dumps(
|
|
354
|
+
{
|
|
355
|
+
"type": "history",
|
|
356
|
+
"brain_id": brain_id,
|
|
357
|
+
"events": [e.to_dict() for e in events],
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
elif action == "ping":
|
|
363
|
+
await websocket.send_text(json.dumps({"type": "pong"}))
|
|
364
|
+
|
|
365
|
+
except WebSocketDisconnect:
|
|
366
|
+
pass
|
|
367
|
+
except Exception as e:
|
|
368
|
+
try:
|
|
369
|
+
await websocket.send_text(
|
|
370
|
+
SyncEvent(
|
|
371
|
+
type=SyncEventType.ERROR,
|
|
372
|
+
brain_id="*",
|
|
373
|
+
data={"error": str(e)},
|
|
374
|
+
).to_json()
|
|
375
|
+
)
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
finally:
|
|
379
|
+
if client_id:
|
|
380
|
+
await sync_manager.disconnect(client_id)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@router.get("/stats")
|
|
384
|
+
async def get_sync_stats() -> dict[str, Any]:
|
|
385
|
+
"""Get sync manager statistics."""
|
|
386
|
+
sync_manager = get_sync_manager()
|
|
387
|
+
return sync_manager.get_stats()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Storage backends for NeuralMemory."""
|
|
2
|
+
|
|
3
|
+
from neural_memory.storage.base import NeuralStorage
|
|
4
|
+
from neural_memory.storage.factory import HybridStorage, create_storage
|
|
5
|
+
from neural_memory.storage.memory_store import InMemoryStorage
|
|
6
|
+
from neural_memory.storage.shared_store import SharedStorage, SharedStorageError
|
|
7
|
+
from neural_memory.storage.sqlite_store import SQLiteStorage
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"HybridStorage",
|
|
11
|
+
"InMemoryStorage",
|
|
12
|
+
"NeuralStorage",
|
|
13
|
+
"SQLiteStorage",
|
|
14
|
+
"SharedStorage",
|
|
15
|
+
"SharedStorageError",
|
|
16
|
+
"create_storage",
|
|
17
|
+
]
|