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.
Files changed (55) hide show
  1. neural_memory/__init__.py +38 -0
  2. neural_memory/cli/__init__.py +15 -0
  3. neural_memory/cli/__main__.py +6 -0
  4. neural_memory/cli/config.py +176 -0
  5. neural_memory/cli/main.py +2702 -0
  6. neural_memory/cli/storage.py +169 -0
  7. neural_memory/cli/tui.py +471 -0
  8. neural_memory/core/__init__.py +52 -0
  9. neural_memory/core/brain.py +301 -0
  10. neural_memory/core/brain_mode.py +273 -0
  11. neural_memory/core/fiber.py +236 -0
  12. neural_memory/core/memory_types.py +331 -0
  13. neural_memory/core/neuron.py +168 -0
  14. neural_memory/core/project.py +257 -0
  15. neural_memory/core/synapse.py +215 -0
  16. neural_memory/engine/__init__.py +15 -0
  17. neural_memory/engine/activation.py +335 -0
  18. neural_memory/engine/encoder.py +391 -0
  19. neural_memory/engine/retrieval.py +440 -0
  20. neural_memory/extraction/__init__.py +42 -0
  21. neural_memory/extraction/entities.py +547 -0
  22. neural_memory/extraction/parser.py +337 -0
  23. neural_memory/extraction/router.py +396 -0
  24. neural_memory/extraction/temporal.py +428 -0
  25. neural_memory/mcp/__init__.py +9 -0
  26. neural_memory/mcp/__main__.py +6 -0
  27. neural_memory/mcp/server.py +621 -0
  28. neural_memory/py.typed +0 -0
  29. neural_memory/safety/__init__.py +31 -0
  30. neural_memory/safety/freshness.py +238 -0
  31. neural_memory/safety/sensitive.py +304 -0
  32. neural_memory/server/__init__.py +5 -0
  33. neural_memory/server/app.py +99 -0
  34. neural_memory/server/dependencies.py +33 -0
  35. neural_memory/server/models.py +138 -0
  36. neural_memory/server/routes/__init__.py +7 -0
  37. neural_memory/server/routes/brain.py +221 -0
  38. neural_memory/server/routes/memory.py +169 -0
  39. neural_memory/server/routes/sync.py +387 -0
  40. neural_memory/storage/__init__.py +17 -0
  41. neural_memory/storage/base.py +441 -0
  42. neural_memory/storage/factory.py +329 -0
  43. neural_memory/storage/memory_store.py +896 -0
  44. neural_memory/storage/shared_store.py +650 -0
  45. neural_memory/storage/sqlite_store.py +1613 -0
  46. neural_memory/sync/__init__.py +5 -0
  47. neural_memory/sync/client.py +435 -0
  48. neural_memory/unified_config.py +315 -0
  49. neural_memory/utils/__init__.py +5 -0
  50. neural_memory/utils/config.py +98 -0
  51. neural_memory-0.1.0.dist-info/METADATA +314 -0
  52. neural_memory-0.1.0.dist-info/RECORD +55 -0
  53. neural_memory-0.1.0.dist-info/WHEEL +4 -0
  54. neural_memory-0.1.0.dist-info/entry_points.txt +4 -0
  55. 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
+ ]