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,5 @@
1
+ """Real-time synchronization for NeuralMemory."""
2
+
3
+ from neural_memory.sync.client import SyncClient, SyncClientState
4
+
5
+ __all__ = ["SyncClient", "SyncClientState"]
@@ -0,0 +1,435 @@
1
+ """WebSocket client for real-time brain synchronization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import uuid
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from datetime import datetime
11
+ from enum import StrEnum
12
+ from typing import Any
13
+
14
+ import aiohttp
15
+
16
+
17
+ class SyncClientState(StrEnum):
18
+ """Client connection state."""
19
+
20
+ DISCONNECTED = "disconnected"
21
+ CONNECTING = "connecting"
22
+ CONNECTED = "connected"
23
+ RECONNECTING = "reconnecting"
24
+
25
+
26
+ @dataclass
27
+ class SyncEvent:
28
+ """A synchronization event received from server."""
29
+
30
+ type: str
31
+ brain_id: str
32
+ timestamp: datetime
33
+ data: dict[str, Any]
34
+ source_client_id: str | None = None
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: dict[str, Any]) -> SyncEvent:
38
+ """Create from dictionary."""
39
+ return cls(
40
+ type=data["type"],
41
+ brain_id=data["brain_id"],
42
+ timestamp=datetime.fromisoformat(data["timestamp"])
43
+ if data.get("timestamp")
44
+ else datetime.utcnow(),
45
+ data=data.get("data", {}),
46
+ source_client_id=data.get("source_client_id"),
47
+ )
48
+
49
+
50
+ EventHandler = Callable[[SyncEvent], None]
51
+ AsyncEventHandler = Callable[[SyncEvent], Any]
52
+
53
+
54
+ class SyncClient:
55
+ """
56
+ WebSocket client for real-time brain synchronization.
57
+
58
+ Enables receiving real-time updates when other agents modify
59
+ shared brains, and pushing local changes to the server.
60
+
61
+ Usage:
62
+ async with SyncClient("ws://localhost:8000") as client:
63
+ # Subscribe to brain updates
64
+ await client.subscribe("brain-123")
65
+
66
+ # Register event handlers
67
+ client.on("neuron_created", handle_neuron_created)
68
+ client.on("memory_encoded", handle_memory_encoded)
69
+
70
+ # Push local changes
71
+ await client.push_event("neuron_created", "brain-123", {"id": "..."})
72
+
73
+ # Keep running
74
+ await client.run_forever()
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ server_url: str,
80
+ *,
81
+ client_id: str | None = None,
82
+ auto_reconnect: bool = True,
83
+ reconnect_delay: float = 1.0,
84
+ max_reconnect_attempts: int = 10,
85
+ ) -> None:
86
+ """
87
+ Initialize sync client.
88
+
89
+ Args:
90
+ server_url: WebSocket URL (e.g., "ws://localhost:8000/sync/ws")
91
+ client_id: Optional client identifier (auto-generated if not provided)
92
+ auto_reconnect: Whether to auto-reconnect on disconnect
93
+ reconnect_delay: Base delay between reconnect attempts (exponential backoff)
94
+ max_reconnect_attempts: Maximum reconnect attempts (0 = unlimited)
95
+ """
96
+ # Normalize WebSocket URL
97
+ if server_url.startswith("http://"):
98
+ server_url = server_url.replace("http://", "ws://")
99
+ elif server_url.startswith("https://"):
100
+ server_url = server_url.replace("https://", "wss://")
101
+
102
+ if not server_url.endswith("/sync/ws"):
103
+ server_url = server_url.rstrip("/") + "/sync/ws"
104
+
105
+ self._server_url = server_url
106
+ self._client_id = client_id or f"client-{uuid.uuid4().hex[:8]}"
107
+ self._auto_reconnect = auto_reconnect
108
+ self._reconnect_delay = reconnect_delay
109
+ self._max_reconnect_attempts = max_reconnect_attempts
110
+
111
+ self._session: aiohttp.ClientSession | None = None
112
+ self._ws: aiohttp.ClientWebSocketResponse | None = None
113
+ self._state = SyncClientState.DISCONNECTED
114
+ self._subscribed_brains: set[str] = set()
115
+ self._handlers: dict[str, list[AsyncEventHandler]] = {}
116
+ self._reconnect_attempts = 0
117
+ self._running = False
118
+ self._receive_task: asyncio.Task[None] | None = None
119
+
120
+ @property
121
+ def client_id(self) -> str:
122
+ """Get client ID."""
123
+ return self._client_id
124
+
125
+ @property
126
+ def state(self) -> SyncClientState:
127
+ """Get current connection state."""
128
+ return self._state
129
+
130
+ @property
131
+ def is_connected(self) -> bool:
132
+ """Check if connected to server."""
133
+ return self._state == SyncClientState.CONNECTED
134
+
135
+ @property
136
+ def subscribed_brains(self) -> frozenset[str]:
137
+ """Get set of subscribed brain IDs."""
138
+ return frozenset(self._subscribed_brains)
139
+
140
+ async def connect(self) -> None:
141
+ """Connect to the sync server."""
142
+ if self._state == SyncClientState.CONNECTED:
143
+ return
144
+
145
+ self._state = SyncClientState.CONNECTING
146
+
147
+ if self._session is None:
148
+ self._session = aiohttp.ClientSession()
149
+
150
+ try:
151
+ self._ws = await self._session.ws_connect(self._server_url)
152
+
153
+ # Send connect message
154
+ await self._send({"action": "connect", "client_id": self._client_id})
155
+
156
+ # Wait for connected confirmation
157
+ response = await self._ws.receive()
158
+ if response.type == aiohttp.WSMsgType.TEXT:
159
+ data = json.loads(response.data)
160
+ if data.get("type") == "connected":
161
+ self._state = SyncClientState.CONNECTED
162
+ self._reconnect_attempts = 0
163
+
164
+ # Re-subscribe to brains
165
+ for brain_id in self._subscribed_brains:
166
+ await self._send({"action": "subscribe", "brain_id": brain_id})
167
+
168
+ except Exception as e:
169
+ self._state = SyncClientState.DISCONNECTED
170
+ raise ConnectionError(f"Failed to connect to sync server: {e}") from e
171
+
172
+ async def disconnect(self) -> None:
173
+ """Disconnect from the sync server."""
174
+ self._running = False
175
+
176
+ if self._receive_task:
177
+ self._receive_task.cancel()
178
+ try:
179
+ await self._receive_task
180
+ except asyncio.CancelledError:
181
+ pass
182
+ self._receive_task = None
183
+
184
+ if self._ws:
185
+ await self._ws.close()
186
+ self._ws = None
187
+
188
+ if self._session:
189
+ await self._session.close()
190
+ self._session = None
191
+
192
+ self._state = SyncClientState.DISCONNECTED
193
+
194
+ async def __aenter__(self) -> SyncClient:
195
+ """Async context manager entry."""
196
+ await self.connect()
197
+ return self
198
+
199
+ async def __aexit__(
200
+ self,
201
+ exc_type: type[BaseException] | None,
202
+ exc_val: BaseException | None,
203
+ exc_tb: Any,
204
+ ) -> None:
205
+ """Async context manager exit."""
206
+ await self.disconnect()
207
+
208
+ async def subscribe(self, brain_id: str) -> bool:
209
+ """
210
+ Subscribe to updates for a brain.
211
+
212
+ Args:
213
+ brain_id: The brain ID to subscribe to
214
+
215
+ Returns:
216
+ True if subscription succeeded
217
+ """
218
+ self._subscribed_brains.add(brain_id)
219
+
220
+ if not self.is_connected:
221
+ return True # Will subscribe when connected
222
+
223
+ await self._send({"action": "subscribe", "brain_id": brain_id})
224
+
225
+ # Wait for confirmation
226
+ if self._ws:
227
+ response = await self._ws.receive()
228
+ if response.type == aiohttp.WSMsgType.TEXT:
229
+ data = json.loads(response.data)
230
+ return data.get("type") == "subscribed"
231
+
232
+ return False
233
+
234
+ async def unsubscribe(self, brain_id: str) -> bool:
235
+ """
236
+ Unsubscribe from updates for a brain.
237
+
238
+ Args:
239
+ brain_id: The brain ID to unsubscribe from
240
+
241
+ Returns:
242
+ True if unsubscription succeeded
243
+ """
244
+ self._subscribed_brains.discard(brain_id)
245
+
246
+ if not self.is_connected:
247
+ return True
248
+
249
+ await self._send({"action": "unsubscribe", "brain_id": brain_id})
250
+ return True
251
+
252
+ def on(self, event_type: str, handler: AsyncEventHandler) -> None:
253
+ """
254
+ Register an event handler.
255
+
256
+ Args:
257
+ event_type: The event type to handle (e.g., "neuron_created")
258
+ handler: Async function to call when event is received
259
+ """
260
+ if event_type not in self._handlers:
261
+ self._handlers[event_type] = []
262
+ self._handlers[event_type].append(handler)
263
+
264
+ def off(self, event_type: str, handler: AsyncEventHandler | None = None) -> None:
265
+ """
266
+ Unregister an event handler.
267
+
268
+ Args:
269
+ event_type: The event type
270
+ handler: Specific handler to remove (all if None)
271
+ """
272
+ if event_type not in self._handlers:
273
+ return
274
+
275
+ if handler is None:
276
+ del self._handlers[event_type]
277
+ else:
278
+ self._handlers[event_type] = [h for h in self._handlers[event_type] if h != handler]
279
+
280
+ async def push_event(
281
+ self,
282
+ event_type: str,
283
+ brain_id: str,
284
+ data: dict[str, Any] | None = None,
285
+ ) -> None:
286
+ """
287
+ Push an event to the server for broadcasting.
288
+
289
+ Args:
290
+ event_type: Type of event (e.g., "neuron_created")
291
+ brain_id: The brain this event relates to
292
+ data: Event data
293
+ """
294
+ if not self.is_connected:
295
+ raise ConnectionError("Not connected to sync server")
296
+
297
+ event = {
298
+ "type": event_type,
299
+ "brain_id": brain_id,
300
+ "timestamp": datetime.utcnow().isoformat(),
301
+ "data": data or {},
302
+ }
303
+
304
+ await self._send({"action": "event", "event": event})
305
+
306
+ async def get_history(
307
+ self,
308
+ brain_id: str,
309
+ since: datetime | None = None,
310
+ ) -> list[SyncEvent]:
311
+ """
312
+ Get recent event history for a brain.
313
+
314
+ Args:
315
+ brain_id: The brain ID
316
+ since: Only get events after this time
317
+
318
+ Returns:
319
+ List of recent events
320
+ """
321
+ if not self.is_connected:
322
+ raise ConnectionError("Not connected to sync server")
323
+
324
+ message: dict[str, Any] = {"action": "get_history", "brain_id": brain_id}
325
+ if since:
326
+ message["since"] = since.isoformat()
327
+
328
+ await self._send(message)
329
+
330
+ # Wait for response
331
+ if self._ws:
332
+ response = await self._ws.receive()
333
+ if response.type == aiohttp.WSMsgType.TEXT:
334
+ data = json.loads(response.data)
335
+ if data.get("type") == "history":
336
+ return [SyncEvent.from_dict(e) for e in data.get("events", [])]
337
+
338
+ return []
339
+
340
+ async def run_forever(self) -> None:
341
+ """
342
+ Run the client, receiving and dispatching events.
343
+
344
+ This method blocks until disconnect() is called or
345
+ max reconnect attempts is exceeded.
346
+ """
347
+ self._running = True
348
+
349
+ while self._running:
350
+ if not self.is_connected:
351
+ if self._auto_reconnect:
352
+ await self._try_reconnect()
353
+ else:
354
+ break
355
+
356
+ if not self._ws:
357
+ await asyncio.sleep(0.1)
358
+ continue
359
+
360
+ try:
361
+ message = await self._ws.receive()
362
+
363
+ if message.type == aiohttp.WSMsgType.TEXT:
364
+ data = json.loads(message.data)
365
+ await self._handle_message(data)
366
+
367
+ elif message.type in (
368
+ aiohttp.WSMsgType.CLOSED,
369
+ aiohttp.WSMsgType.ERROR,
370
+ ):
371
+ self._state = SyncClientState.DISCONNECTED
372
+ if self._auto_reconnect:
373
+ continue
374
+ else:
375
+ break
376
+
377
+ except asyncio.CancelledError:
378
+ break
379
+ except Exception:
380
+ self._state = SyncClientState.DISCONNECTED
381
+ if self._auto_reconnect:
382
+ continue
383
+ else:
384
+ raise
385
+
386
+ async def _send(self, data: dict[str, Any]) -> None:
387
+ """Send a message to the server."""
388
+ if self._ws:
389
+ await self._ws.send_str(json.dumps(data))
390
+
391
+ async def _handle_message(self, data: dict[str, Any]) -> None:
392
+ """Handle an incoming message."""
393
+ event_type = data.get("type")
394
+ if not event_type:
395
+ return
396
+
397
+ # Convert to SyncEvent
398
+ event = SyncEvent.from_dict(data)
399
+
400
+ # Skip events we originated
401
+ if event.source_client_id == self._client_id:
402
+ return
403
+
404
+ # Dispatch to handlers
405
+ handlers = self._handlers.get(event_type, [])
406
+ handlers.extend(self._handlers.get("*", [])) # Wildcard handlers
407
+
408
+ for handler in handlers:
409
+ try:
410
+ result = handler(event)
411
+ if asyncio.iscoroutine(result):
412
+ await result
413
+ except Exception:
414
+ pass # Don't let handler errors crash the client
415
+
416
+ async def _try_reconnect(self) -> None:
417
+ """Attempt to reconnect to the server."""
418
+ if self._max_reconnect_attempts > 0:
419
+ if self._reconnect_attempts >= self._max_reconnect_attempts:
420
+ self._running = False
421
+ return
422
+
423
+ self._state = SyncClientState.RECONNECTING
424
+ self._reconnect_attempts += 1
425
+
426
+ # Exponential backoff
427
+ delay = self._reconnect_delay * (2 ** (self._reconnect_attempts - 1))
428
+ delay = min(delay, 60.0) # Cap at 60 seconds
429
+
430
+ await asyncio.sleep(delay)
431
+
432
+ try:
433
+ await self.connect()
434
+ except Exception:
435
+ pass # Will retry on next loop iteration