spatial-memory-mcp 1.9.1__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. spatial_memory/__init__.py +97 -0
  2. spatial_memory/__main__.py +271 -0
  3. spatial_memory/adapters/__init__.py +7 -0
  4. spatial_memory/adapters/lancedb_repository.py +880 -0
  5. spatial_memory/config.py +769 -0
  6. spatial_memory/core/__init__.py +118 -0
  7. spatial_memory/core/cache.py +317 -0
  8. spatial_memory/core/circuit_breaker.py +297 -0
  9. spatial_memory/core/connection_pool.py +220 -0
  10. spatial_memory/core/consolidation_strategies.py +401 -0
  11. spatial_memory/core/database.py +3072 -0
  12. spatial_memory/core/db_idempotency.py +242 -0
  13. spatial_memory/core/db_indexes.py +576 -0
  14. spatial_memory/core/db_migrations.py +588 -0
  15. spatial_memory/core/db_search.py +512 -0
  16. spatial_memory/core/db_versioning.py +178 -0
  17. spatial_memory/core/embeddings.py +558 -0
  18. spatial_memory/core/errors.py +317 -0
  19. spatial_memory/core/file_security.py +701 -0
  20. spatial_memory/core/filesystem.py +178 -0
  21. spatial_memory/core/health.py +289 -0
  22. spatial_memory/core/helpers.py +79 -0
  23. spatial_memory/core/import_security.py +433 -0
  24. spatial_memory/core/lifecycle_ops.py +1067 -0
  25. spatial_memory/core/logging.py +194 -0
  26. spatial_memory/core/metrics.py +192 -0
  27. spatial_memory/core/models.py +660 -0
  28. spatial_memory/core/rate_limiter.py +326 -0
  29. spatial_memory/core/response_types.py +500 -0
  30. spatial_memory/core/security.py +588 -0
  31. spatial_memory/core/spatial_ops.py +430 -0
  32. spatial_memory/core/tracing.py +300 -0
  33. spatial_memory/core/utils.py +110 -0
  34. spatial_memory/core/validation.py +406 -0
  35. spatial_memory/factory.py +444 -0
  36. spatial_memory/migrations/__init__.py +40 -0
  37. spatial_memory/ports/__init__.py +11 -0
  38. spatial_memory/ports/repositories.py +630 -0
  39. spatial_memory/py.typed +0 -0
  40. spatial_memory/server.py +1214 -0
  41. spatial_memory/services/__init__.py +70 -0
  42. spatial_memory/services/decay_manager.py +411 -0
  43. spatial_memory/services/export_import.py +1031 -0
  44. spatial_memory/services/lifecycle.py +1139 -0
  45. spatial_memory/services/memory.py +412 -0
  46. spatial_memory/services/spatial.py +1152 -0
  47. spatial_memory/services/utility.py +429 -0
  48. spatial_memory/tools/__init__.py +5 -0
  49. spatial_memory/tools/definitions.py +695 -0
  50. spatial_memory/verify.py +140 -0
  51. spatial_memory_mcp-1.9.1.dist-info/METADATA +509 -0
  52. spatial_memory_mcp-1.9.1.dist-info/RECORD +55 -0
  53. spatial_memory_mcp-1.9.1.dist-info/WHEEL +4 -0
  54. spatial_memory_mcp-1.9.1.dist-info/entry_points.txt +2 -0
  55. spatial_memory_mcp-1.9.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,630 @@
1
+ """Protocol interfaces for repository and embedding services.
2
+
3
+ These protocols define the contracts between the service layer and infrastructure.
4
+ Using typing.Protocol enables structural subtyping (duck typing with type checking).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterator
10
+ from pathlib import Path
11
+ from typing import Any, Protocol
12
+
13
+ import numpy as np
14
+
15
+ from spatial_memory.core.models import Memory, MemoryResult
16
+
17
+
18
+ class MemoryRepositoryProtocol(Protocol):
19
+ """Protocol for memory storage and retrieval operations.
20
+
21
+ Implementations must provide all methods defined here.
22
+ The LanceDBMemoryRepository is the primary implementation.
23
+ """
24
+
25
+ def add(self, memory: Memory, vector: np.ndarray) -> str:
26
+ """Add a memory with its embedding vector.
27
+
28
+ Args:
29
+ memory: The Memory object to store.
30
+ vector: The embedding vector for the memory.
31
+
32
+ Returns:
33
+ The generated memory ID (UUID string).
34
+
35
+ Raises:
36
+ ValidationError: If input validation fails.
37
+ StorageError: If database operation fails.
38
+ """
39
+ ...
40
+
41
+ def add_batch(
42
+ self,
43
+ memories: list[Memory],
44
+ vectors: list[np.ndarray],
45
+ ) -> list[str]:
46
+ """Add multiple memories efficiently.
47
+
48
+ Args:
49
+ memories: List of Memory objects to store.
50
+ vectors: List of embedding vectors (same order as memories).
51
+
52
+ Returns:
53
+ List of generated memory IDs.
54
+
55
+ Raises:
56
+ ValidationError: If input validation fails.
57
+ StorageError: If database operation fails.
58
+ """
59
+ ...
60
+
61
+ def get(self, memory_id: str) -> Memory | None:
62
+ """Get a memory by ID.
63
+
64
+ Args:
65
+ memory_id: The memory UUID.
66
+
67
+ Returns:
68
+ The Memory object, or None if not found.
69
+
70
+ Raises:
71
+ ValidationError: If memory_id is invalid.
72
+ StorageError: If database operation fails.
73
+ """
74
+ ...
75
+
76
+ def get_with_vector(self, memory_id: str) -> tuple[Memory, np.ndarray] | None:
77
+ """Get a memory and its vector by ID.
78
+
79
+ Args:
80
+ memory_id: The memory UUID.
81
+
82
+ Returns:
83
+ Tuple of (Memory, vector), or None if not found.
84
+
85
+ Raises:
86
+ ValidationError: If memory_id is invalid.
87
+ StorageError: If database operation fails.
88
+ """
89
+ ...
90
+
91
+ def delete(self, memory_id: str) -> bool:
92
+ """Delete a memory by ID.
93
+
94
+ Args:
95
+ memory_id: The memory UUID.
96
+
97
+ Returns:
98
+ True if deleted, False if not found.
99
+
100
+ Raises:
101
+ ValidationError: If memory_id is invalid.
102
+ StorageError: If database operation fails.
103
+ """
104
+ ...
105
+
106
+ def delete_batch(self, memory_ids: list[str]) -> tuple[int, list[str]]:
107
+ """Delete multiple memories.
108
+
109
+ Args:
110
+ memory_ids: List of memory UUIDs to delete.
111
+
112
+ Returns:
113
+ Tuple of (count_deleted, list_of_deleted_ids) where:
114
+ - count_deleted: Number of memories actually deleted
115
+ - list_of_deleted_ids: IDs that were actually deleted
116
+
117
+ Raises:
118
+ ValidationError: If any memory_id is invalid.
119
+ StorageError: If database operation fails.
120
+ """
121
+ ...
122
+
123
+ def search(
124
+ self,
125
+ query_vector: np.ndarray,
126
+ limit: int = 5,
127
+ namespace: str | None = None,
128
+ include_vector: bool = False,
129
+ ) -> list[MemoryResult]:
130
+ """Search for similar memories by vector.
131
+
132
+ Args:
133
+ query_vector: Query embedding vector.
134
+ limit: Maximum number of results.
135
+ namespace: Filter to specific namespace.
136
+ include_vector: Whether to include embedding vectors in results.
137
+ Defaults to False to reduce response size.
138
+
139
+ Returns:
140
+ List of MemoryResult objects with similarity scores.
141
+ If include_vector=True, each result includes its embedding vector.
142
+
143
+ Raises:
144
+ ValidationError: If input validation fails.
145
+ StorageError: If database operation fails.
146
+ """
147
+ ...
148
+
149
+ def update_access(self, memory_id: str) -> None:
150
+ """Update access timestamp and count for a memory.
151
+
152
+ Args:
153
+ memory_id: The memory UUID.
154
+
155
+ Raises:
156
+ ValidationError: If memory_id is invalid.
157
+ MemoryNotFoundError: If memory doesn't exist.
158
+ StorageError: If database operation fails.
159
+ """
160
+ ...
161
+
162
+ def update_access_batch(self, memory_ids: list[str]) -> int:
163
+ """Update access timestamp and count for multiple memories.
164
+
165
+ Args:
166
+ memory_ids: List of memory UUIDs.
167
+
168
+ Returns:
169
+ Number of memories successfully updated.
170
+
171
+ Raises:
172
+ ValidationError: If any memory_id is invalid.
173
+ StorageError: If database operation fails.
174
+ """
175
+ ...
176
+
177
+ def update(self, memory_id: str, updates: dict[str, Any]) -> None:
178
+ """Update a memory's fields.
179
+
180
+ Args:
181
+ memory_id: The memory UUID.
182
+ updates: Fields to update.
183
+
184
+ Raises:
185
+ ValidationError: If input validation fails.
186
+ MemoryNotFoundError: If memory doesn't exist.
187
+ StorageError: If database operation fails.
188
+ """
189
+ ...
190
+
191
+ def get_batch(self, memory_ids: list[str]) -> dict[str, Memory]:
192
+ """Get multiple memories by ID in a single query.
193
+
194
+ Args:
195
+ memory_ids: List of memory UUIDs to retrieve.
196
+
197
+ Returns:
198
+ Dict mapping memory_id to Memory object. Missing IDs are not included.
199
+
200
+ Raises:
201
+ ValidationError: If any memory_id format is invalid.
202
+ StorageError: If database operation fails.
203
+ """
204
+ ...
205
+
206
+ def update_batch(
207
+ self, updates: list[tuple[str, dict[str, Any]]]
208
+ ) -> tuple[int, list[str]]:
209
+ """Update multiple memories in a single batch operation.
210
+
211
+ Args:
212
+ updates: List of (memory_id, updates_dict) tuples.
213
+
214
+ Returns:
215
+ Tuple of (success_count, list of failed memory_ids).
216
+
217
+ Raises:
218
+ StorageError: If database operation fails completely.
219
+ """
220
+ ...
221
+
222
+ def count(self, namespace: str | None = None) -> int:
223
+ """Count memories.
224
+
225
+ Args:
226
+ namespace: Filter to specific namespace.
227
+
228
+ Returns:
229
+ Number of memories.
230
+
231
+ Raises:
232
+ ValidationError: If namespace is invalid.
233
+ StorageError: If database operation fails.
234
+ """
235
+ ...
236
+
237
+ def get_namespaces(self) -> list[str]:
238
+ """Get all unique namespaces.
239
+
240
+ Returns:
241
+ List of namespace names.
242
+
243
+ Raises:
244
+ StorageError: If database operation fails.
245
+ """
246
+ ...
247
+
248
+ def get_all(
249
+ self,
250
+ namespace: str | None = None,
251
+ limit: int | None = None,
252
+ ) -> list[tuple[Memory, np.ndarray]]:
253
+ """Get all memories with their vectors.
254
+
255
+ Args:
256
+ namespace: Filter to specific namespace.
257
+ limit: Maximum number of results.
258
+
259
+ Returns:
260
+ List of (Memory, vector) tuples.
261
+
262
+ Raises:
263
+ ValidationError: If namespace is invalid.
264
+ StorageError: If database operation fails.
265
+ """
266
+ ...
267
+
268
+ def hybrid_search(
269
+ self,
270
+ query_vector: np.ndarray,
271
+ query_text: str,
272
+ limit: int = 5,
273
+ namespace: str | None = None,
274
+ alpha: float = 0.5,
275
+ ) -> list[MemoryResult]:
276
+ """Search using both vector similarity and full-text search.
277
+
278
+ Args:
279
+ query_vector: Query embedding vector.
280
+ query_text: Query text for FTS.
281
+ limit: Maximum results.
282
+ namespace: Optional namespace filter.
283
+ alpha: Balance between vector (1.0) and FTS (0.0).
284
+
285
+ Returns:
286
+ List of matching memories ranked by combined score.
287
+
288
+ Raises:
289
+ ValidationError: If input validation fails.
290
+ StorageError: If database operation fails.
291
+ """
292
+ ...
293
+
294
+ def get_health_metrics(self) -> dict[str, Any]:
295
+ """Get database health metrics.
296
+
297
+ Returns:
298
+ Dictionary with health metrics.
299
+
300
+ Raises:
301
+ StorageError: If database operation fails.
302
+ """
303
+ ...
304
+
305
+ def optimize(self) -> dict[str, Any]:
306
+ """Run optimization and compaction.
307
+
308
+ Returns:
309
+ Dictionary with optimization results.
310
+
311
+ Raises:
312
+ StorageError: If database operation fails.
313
+ """
314
+ ...
315
+
316
+ def export_to_parquet(self, path: Path) -> int:
317
+ """Export memories to Parquet file.
318
+
319
+ Args:
320
+ path: Output file path.
321
+
322
+ Returns:
323
+ Number of records exported.
324
+
325
+ Raises:
326
+ StorageError: If export fails.
327
+ """
328
+ ...
329
+
330
+ def import_from_parquet(
331
+ self,
332
+ path: Path,
333
+ namespace_override: str | None = None,
334
+ ) -> int:
335
+ """Import memories from Parquet file.
336
+
337
+ Args:
338
+ path: Input file path.
339
+ namespace_override: Override namespace for imported memories.
340
+
341
+ Returns:
342
+ Number of records imported.
343
+
344
+ Raises:
345
+ ValidationError: If input validation fails.
346
+ StorageError: If import fails.
347
+ """
348
+ ...
349
+
350
+ def get_vectors_for_clustering(
351
+ self,
352
+ namespace: str | None = None,
353
+ max_memories: int = 10_000,
354
+ ) -> tuple[list[str], np.ndarray]:
355
+ """Extract memory IDs and vectors efficiently for clustering.
356
+
357
+ Optimized for memory efficiency with large datasets. Used by
358
+ spatial operations like HDBSCAN clustering for region detection.
359
+
360
+ Args:
361
+ namespace: Filter to specific namespace.
362
+ max_memories: Maximum memories to fetch.
363
+
364
+ Returns:
365
+ Tuple of (memory_ids, vectors_array) where vectors_array
366
+ is a 2D numpy array of shape (n_memories, embedding_dim).
367
+
368
+ Raises:
369
+ ValidationError: If input validation fails.
370
+ StorageError: If database operation fails.
371
+ """
372
+ ...
373
+
374
+ def batch_vector_search(
375
+ self,
376
+ query_vectors: list[np.ndarray],
377
+ limit_per_query: int = 3,
378
+ namespace: str | None = None,
379
+ include_vector: bool = False,
380
+ ) -> list[list[dict[str, Any]]]:
381
+ """Search for memories near multiple query points.
382
+
383
+ Efficient for operations like journey interpolation where multiple
384
+ points need to find nearby memories. Supports parallel execution.
385
+
386
+ Args:
387
+ query_vectors: List of query embedding vectors.
388
+ limit_per_query: Maximum results per query vector.
389
+ namespace: Filter to specific namespace.
390
+ include_vector: Whether to include embedding vectors in results.
391
+ Defaults to False to reduce response size.
392
+
393
+ Returns:
394
+ List of result lists (one per query vector). Each result
395
+ is a dict containing memory fields and similarity score.
396
+ If include_vector=True, each dict includes the 'vector' field.
397
+
398
+ Raises:
399
+ ValidationError: If input validation fails.
400
+ StorageError: If database operation fails.
401
+ """
402
+ ...
403
+
404
+ def vector_search(
405
+ self,
406
+ query_vector: np.ndarray,
407
+ limit: int = 5,
408
+ namespace: str | None = None,
409
+ ) -> list[dict[str, Any]]:
410
+ """Search for similar memories by vector (returns raw dict).
411
+
412
+ Lower-level search that returns raw dictionary results instead
413
+ of MemoryResult objects. Useful for spatial operations that need
414
+ direct access to all fields including vectors.
415
+
416
+ Args:
417
+ query_vector: Query embedding vector.
418
+ limit: Maximum number of results.
419
+ namespace: Filter to specific namespace.
420
+
421
+ Returns:
422
+ List of memory records as dictionaries with similarity scores.
423
+
424
+ Raises:
425
+ ValidationError: If input validation fails.
426
+ StorageError: If database operation fails.
427
+ """
428
+ ...
429
+
430
+ # -------------------------------------------------------------------------
431
+ # Phase 5 Protocol Extensions: Utility & Export/Import Operations
432
+ # -------------------------------------------------------------------------
433
+
434
+ def delete_by_namespace(self, namespace: str) -> int:
435
+ """Delete all memories in a namespace.
436
+
437
+ Removes all memories belonging to the specified namespace from storage.
438
+ This is a destructive operation that cannot be undone.
439
+
440
+ Args:
441
+ namespace: The namespace whose memories should be deleted.
442
+
443
+ Returns:
444
+ Number of memories deleted.
445
+
446
+ Raises:
447
+ ValidationError: If namespace is invalid.
448
+ StorageError: If database operation fails.
449
+ """
450
+ ...
451
+
452
+ def rename_namespace(self, old_namespace: str, new_namespace: str) -> int:
453
+ """Rename all memories from one namespace to another.
454
+
455
+ Atomically updates the namespace field for all memories belonging
456
+ to the source namespace. Uses batch updates for data integrity.
457
+
458
+ Args:
459
+ old_namespace: The current namespace name (source).
460
+ new_namespace: The new namespace name (target).
461
+
462
+ Returns:
463
+ Number of memories renamed.
464
+
465
+ Raises:
466
+ ValidationError: If namespace names are invalid.
467
+ NamespaceNotFoundError: If old_namespace doesn't exist.
468
+ StorageError: If database operation fails.
469
+ """
470
+ ...
471
+
472
+ def get_stats(self, namespace: str | None = None) -> dict[str, Any]:
473
+ """Get comprehensive database statistics.
474
+
475
+ Retrieves statistics about the memory database including total counts,
476
+ storage size, index information, and health metrics. Uses efficient
477
+ database-level aggregation queries.
478
+
479
+ Args:
480
+ namespace: Filter statistics to a specific namespace.
481
+ If None, returns statistics for all namespaces.
482
+
483
+ Returns:
484
+ Dictionary containing:
485
+ - total_memories: Total count of memories
486
+ - memories_by_namespace: Dict mapping namespace to count
487
+ - storage_bytes: Total storage size in bytes
488
+ - storage_mb: Total storage size in megabytes
489
+ - has_vector_index: Whether vector index exists
490
+ - has_fts_index: Whether full-text search index exists
491
+ - num_fragments: Number of storage fragments
492
+ - needs_compaction: Whether compaction is recommended
493
+ - table_version: Current table version number
494
+ - indices: List of index information dicts
495
+
496
+ Raises:
497
+ ValidationError: If namespace is invalid.
498
+ StorageError: If database operation fails.
499
+ """
500
+ ...
501
+
502
+ def get_namespace_stats(self, namespace: str) -> dict[str, Any]:
503
+ """Get statistics for a specific namespace.
504
+
505
+ Retrieves detailed statistics for a single namespace including
506
+ memory count, date ranges, and storage estimates.
507
+
508
+ Args:
509
+ namespace: The namespace to get statistics for.
510
+
511
+ Returns:
512
+ Dictionary containing:
513
+ - namespace: The namespace name
514
+ - memory_count: Number of memories in namespace
515
+ - oldest_memory: Datetime of oldest memory (or None)
516
+ - newest_memory: Datetime of newest memory (or None)
517
+ - avg_content_length: Average content length (optional)
518
+
519
+ Raises:
520
+ ValidationError: If namespace is invalid.
521
+ NamespaceNotFoundError: If namespace doesn't exist.
522
+ StorageError: If database operation fails.
523
+ """
524
+ ...
525
+
526
+ def get_all_for_export(
527
+ self,
528
+ namespace: str | None = None,
529
+ batch_size: int = 1000,
530
+ ) -> Iterator[list[dict[str, Any]]]:
531
+ """Stream all memories for export in batches.
532
+
533
+ Memory-efficient export using generator pattern. Yields batches
534
+ of memory records suitable for writing to export files. This method
535
+ handles only data retrieval; file I/O is the service layer's concern.
536
+
537
+ Args:
538
+ namespace: Filter to a specific namespace.
539
+ If None, exports all namespaces.
540
+ batch_size: Number of records per yielded batch.
541
+
542
+ Yields:
543
+ Batches of memory dictionaries. Each dictionary contains all
544
+ memory fields including the embedding vector.
545
+
546
+ Raises:
547
+ ValidationError: If namespace is invalid.
548
+ StorageError: If database operation fails.
549
+ """
550
+ ...
551
+
552
+ def bulk_import(
553
+ self,
554
+ records: Iterator[dict[str, Any]],
555
+ batch_size: int = 1000,
556
+ namespace_override: str | None = None,
557
+ ) -> tuple[int, list[str]]:
558
+ """Import memories from an iterator of records.
559
+
560
+ Supports streaming import for large datasets. Accepts pre-parsed
561
+ and pre-validated records; file parsing and validation are handled
562
+ by the service layer.
563
+
564
+ Args:
565
+ records: Iterator of memory dictionaries. Each dictionary
566
+ should contain required fields (content, vector, etc.).
567
+ batch_size: Number of records per database insert batch.
568
+ namespace_override: If provided, overrides the namespace
569
+ field for all imported records.
570
+
571
+ Returns:
572
+ Tuple of (records_imported, list_of_new_ids) where:
573
+ - records_imported: Total number of records successfully imported
574
+ - list_of_new_ids: List of generated memory IDs
575
+
576
+ Raises:
577
+ ValidationError: If records contain invalid data.
578
+ StorageError: If database operation fails.
579
+ """
580
+ ...
581
+
582
+
583
+ class EmbeddingServiceProtocol(Protocol):
584
+ """Protocol for text embedding generation.
585
+
586
+ Implementations can use local models (sentence-transformers)
587
+ or API-based services (OpenAI).
588
+ """
589
+
590
+ @property
591
+ def dimensions(self) -> int:
592
+ """Get the embedding dimensions."""
593
+ ...
594
+
595
+ @property
596
+ def backend(self) -> str:
597
+ """Get the active embedding backend.
598
+
599
+ Returns:
600
+ 'openai' for OpenAI API, 'onnx' or 'pytorch' for local models.
601
+ """
602
+ ...
603
+
604
+ def embed(self, text: str) -> np.ndarray:
605
+ """Generate embedding for a single text.
606
+
607
+ Args:
608
+ text: Text to embed.
609
+
610
+ Returns:
611
+ Embedding vector as numpy array.
612
+
613
+ Raises:
614
+ EmbeddingError: If embedding generation fails.
615
+ """
616
+ ...
617
+
618
+ def embed_batch(self, texts: list[str]) -> list[np.ndarray]:
619
+ """Generate embeddings for multiple texts.
620
+
621
+ Args:
622
+ texts: List of texts to embed.
623
+
624
+ Returns:
625
+ List of embedding vectors.
626
+
627
+ Raises:
628
+ EmbeddingError: If embedding generation fails.
629
+ """
630
+ ...
File without changes