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,412 @@
1
+ """Memory service for core operations.
2
+
3
+ This service provides the application layer for memory operations:
4
+ - remember: Store new memories
5
+ - recall: Search for similar memories
6
+ - nearby: Find neighbors of a memory
7
+ - forget: Delete memories
8
+
9
+ The service uses dependency injection for repository and embedding services.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING, Any, Protocol
17
+
18
+ from spatial_memory.core.errors import MemoryNotFoundError, ValidationError
19
+ from spatial_memory.core.models import Memory, MemorySource
20
+ from spatial_memory.core.validation import validate_content, validate_importance
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ if TYPE_CHECKING:
25
+ from spatial_memory.core.db_idempotency import IdempotencyRecord
26
+ from spatial_memory.core.models import MemoryResult
27
+ from spatial_memory.ports.repositories import (
28
+ EmbeddingServiceProtocol,
29
+ MemoryRepositoryProtocol,
30
+ )
31
+
32
+
33
+ class IdempotencyProviderProtocol(Protocol):
34
+ """Protocol for idempotency key storage and lookup.
35
+
36
+ Implementations should handle key-to-memory-id mappings with TTL support.
37
+ """
38
+
39
+ def get_by_idempotency_key(self, key: str) -> IdempotencyRecord | None:
40
+ """Look up an idempotency record by key.
41
+
42
+ Args:
43
+ key: The idempotency key to look up.
44
+
45
+ Returns:
46
+ IdempotencyRecord if found and not expired, None otherwise.
47
+ """
48
+ ...
49
+
50
+ def store_idempotency_key(
51
+ self,
52
+ key: str,
53
+ memory_id: str,
54
+ ttl_hours: float = 24.0,
55
+ ) -> None:
56
+ """Store an idempotency key mapping.
57
+
58
+ Args:
59
+ key: The idempotency key.
60
+ memory_id: The memory ID that was created.
61
+ ttl_hours: Time-to-live in hours (default: 24 hours).
62
+ """
63
+ ...
64
+
65
+
66
+ @dataclass
67
+ class RememberResult:
68
+ """Result of storing a memory."""
69
+
70
+ id: str
71
+ content: str
72
+ namespace: str
73
+ deduplicated: bool = False
74
+
75
+
76
+ @dataclass
77
+ class RememberBatchResult:
78
+ """Result of storing multiple memories."""
79
+
80
+ ids: list[str]
81
+ count: int
82
+
83
+
84
+ @dataclass
85
+ class RecallResult:
86
+ """Result of recalling memories."""
87
+
88
+ memories: list[MemoryResult]
89
+ total: int
90
+
91
+
92
+ @dataclass
93
+ class NearbyResult:
94
+ """Result of finding nearby memories."""
95
+
96
+ reference: Memory
97
+ neighbors: list[MemoryResult]
98
+
99
+
100
+ @dataclass
101
+ class ForgetResult:
102
+ """Result of forgetting memories."""
103
+
104
+ deleted: int
105
+ ids: list[str] = field(default_factory=list)
106
+
107
+
108
+ class MemoryService:
109
+ """Service for memory operations.
110
+
111
+ Uses Clean Architecture - depends on protocol interfaces, not implementations.
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ repository: MemoryRepositoryProtocol,
117
+ embeddings: EmbeddingServiceProtocol,
118
+ idempotency_provider: IdempotencyProviderProtocol | None = None,
119
+ ) -> None:
120
+ """Initialize the memory service.
121
+
122
+ Args:
123
+ repository: Repository for memory storage.
124
+ embeddings: Service for generating embeddings.
125
+ idempotency_provider: Optional provider for idempotency key support.
126
+ """
127
+ self._repo = repository
128
+ self._embeddings = embeddings
129
+ self._idempotency = idempotency_provider
130
+
131
+ # Use centralized validation functions
132
+ _validate_content = staticmethod(validate_content)
133
+ _validate_importance = staticmethod(validate_importance)
134
+
135
+ def remember(
136
+ self,
137
+ content: str,
138
+ namespace: str = "default",
139
+ tags: list[str] | None = None,
140
+ importance: float = 0.5,
141
+ metadata: dict[str, Any] | None = None,
142
+ idempotency_key: str | None = None,
143
+ ) -> RememberResult:
144
+ """Store a new memory.
145
+
146
+ Args:
147
+ content: Text content of the memory.
148
+ namespace: Namespace for organization.
149
+ tags: Optional list of tags.
150
+ importance: Importance score (0-1).
151
+ metadata: Optional metadata dict.
152
+ idempotency_key: Optional key for idempotent requests. If provided
153
+ and a memory was already created with this key, returns the
154
+ existing memory ID with deduplicated=True.
155
+
156
+ Returns:
157
+ RememberResult with the new memory's ID. If idempotency_key was
158
+ provided and matched an existing request, deduplicated=True.
159
+
160
+ Raises:
161
+ ValidationError: If input validation fails.
162
+ """
163
+ # Check idempotency key first (before any expensive operations)
164
+ if idempotency_key and self._idempotency:
165
+ existing = self._idempotency.get_by_idempotency_key(idempotency_key)
166
+ if existing:
167
+ logger.debug(
168
+ f"Idempotency key '{idempotency_key}' matched existing "
169
+ f"memory '{existing.memory_id}'"
170
+ )
171
+ # Return cached result - fetch the memory to get content
172
+ cached_memory = self._repo.get(existing.memory_id)
173
+ if cached_memory:
174
+ return RememberResult(
175
+ id=existing.memory_id,
176
+ content=cached_memory.content,
177
+ namespace=cached_memory.namespace,
178
+ deduplicated=True,
179
+ )
180
+ # Memory was deleted but key exists - proceed with new insert
181
+ logger.warning(
182
+ f"Idempotency key '{idempotency_key}' references deleted "
183
+ f"memory '{existing.memory_id}', creating new memory"
184
+ )
185
+
186
+ # Validate inputs
187
+ self._validate_content(content)
188
+ self._validate_importance(importance)
189
+
190
+ # Generate embedding
191
+ vector = self._embeddings.embed(content)
192
+
193
+ # Create memory object (ID will be assigned by repository)
194
+ memory = Memory(
195
+ id="", # Will be replaced by repository
196
+ content=content,
197
+ namespace=namespace,
198
+ tags=tags or [],
199
+ importance=importance,
200
+ source=MemorySource.MANUAL,
201
+ metadata=metadata or {},
202
+ )
203
+
204
+ # Store in repository
205
+ memory_id = self._repo.add(memory, vector)
206
+
207
+ # Store idempotency key mapping if provided
208
+ if idempotency_key and self._idempotency:
209
+ try:
210
+ self._idempotency.store_idempotency_key(idempotency_key, memory_id)
211
+ except Exception as e:
212
+ # Log but don't fail the memory creation
213
+ logger.warning(
214
+ f"Failed to store idempotency key '{idempotency_key}': {e}"
215
+ )
216
+
217
+ return RememberResult(
218
+ id=memory_id,
219
+ content=content,
220
+ namespace=namespace,
221
+ deduplicated=False,
222
+ )
223
+
224
+ def remember_batch(
225
+ self,
226
+ memories: list[dict[str, Any]],
227
+ ) -> RememberBatchResult:
228
+ """Store multiple memories efficiently.
229
+
230
+ Args:
231
+ memories: List of dicts with content and optional fields.
232
+ Each dict can have: content, namespace, tags, importance, metadata.
233
+
234
+ Returns:
235
+ RememberBatchResult with IDs and count.
236
+
237
+ Raises:
238
+ ValidationError: If input validation fails.
239
+ """
240
+ if not memories:
241
+ raise ValidationError("Memory list cannot be empty")
242
+
243
+ # Validate all memories first
244
+ for mem_dict in memories:
245
+ content = mem_dict.get("content", "")
246
+ self._validate_content(content)
247
+ importance = mem_dict.get("importance", 0.5)
248
+ self._validate_importance(importance)
249
+
250
+ # Extract content for batch embedding
251
+ contents = [m["content"] for m in memories]
252
+ vectors = self._embeddings.embed_batch(contents)
253
+
254
+ # Create Memory objects
255
+ memory_objects: list[Memory] = []
256
+ for mem_dict in memories:
257
+ memory = Memory(
258
+ id="", # Will be replaced by repository
259
+ content=mem_dict["content"],
260
+ namespace=mem_dict.get("namespace", "default"),
261
+ tags=mem_dict.get("tags", []),
262
+ importance=mem_dict.get("importance", 0.5),
263
+ source=MemorySource.MANUAL,
264
+ metadata=mem_dict.get("metadata", {}),
265
+ )
266
+ memory_objects.append(memory)
267
+
268
+ # Store in repository
269
+ ids = self._repo.add_batch(memory_objects, vectors)
270
+
271
+ return RememberBatchResult(
272
+ ids=ids,
273
+ count=len(ids),
274
+ )
275
+
276
+ def recall(
277
+ self,
278
+ query: str,
279
+ limit: int = 5,
280
+ namespace: str | None = None,
281
+ min_similarity: float = 0.0,
282
+ ) -> RecallResult:
283
+ """Search for similar memories.
284
+
285
+ Args:
286
+ query: Query text to search for.
287
+ limit: Maximum number of results.
288
+ namespace: Optional namespace filter.
289
+ min_similarity: Minimum similarity threshold.
290
+
291
+ Returns:
292
+ RecallResult with matching memories.
293
+
294
+ Raises:
295
+ ValidationError: If input validation fails.
296
+ """
297
+ # Validate inputs
298
+ if not query or not query.strip():
299
+ raise ValidationError("Query cannot be empty")
300
+ if limit < 1:
301
+ raise ValidationError("Limit must be at least 1")
302
+
303
+ # Generate query embedding
304
+ vector = self._embeddings.embed(query)
305
+
306
+ # Search repository
307
+ results = self._repo.search(vector, limit=limit, namespace=namespace)
308
+
309
+ # Filter by minimum similarity
310
+ filtered_results = [r for r in results if r.similarity >= min_similarity]
311
+
312
+ # Update access stats for returned memories (batch for efficiency)
313
+ if filtered_results:
314
+ memory_ids = [r.id for r in filtered_results]
315
+ try:
316
+ self._repo.update_access_batch(memory_ids)
317
+ except Exception as e:
318
+ # Log but don't fail the search if access update fails
319
+ logger.warning(f"Failed to update access stats: {e}")
320
+
321
+ return RecallResult(
322
+ memories=filtered_results,
323
+ total=len(filtered_results),
324
+ )
325
+
326
+ def nearby(
327
+ self,
328
+ memory_id: str,
329
+ limit: int = 5,
330
+ namespace: str | None = None,
331
+ ) -> NearbyResult:
332
+ """Find memories similar to a reference memory.
333
+
334
+ Args:
335
+ memory_id: ID of the reference memory.
336
+ limit: Maximum number of neighbors.
337
+ namespace: Optional namespace filter.
338
+
339
+ Returns:
340
+ NearbyResult with reference and neighbors.
341
+
342
+ Raises:
343
+ MemoryNotFoundError: If reference memory not found.
344
+ """
345
+ # Get reference memory with its vector
346
+ result = self._repo.get_with_vector(memory_id)
347
+ if result is None:
348
+ raise MemoryNotFoundError(memory_id)
349
+
350
+ reference_memory, reference_vector = result
351
+
352
+ # Search for similar memories (request limit+1 to account for self)
353
+ search_results = self._repo.search(
354
+ reference_vector,
355
+ limit=limit + 1,
356
+ namespace=namespace,
357
+ )
358
+
359
+ # Filter out the reference memory itself
360
+ neighbors = [r for r in search_results if r.id != memory_id]
361
+
362
+ # Limit to requested count
363
+ neighbors = neighbors[:limit]
364
+
365
+ return NearbyResult(
366
+ reference=reference_memory,
367
+ neighbors=neighbors,
368
+ )
369
+
370
+ def forget(
371
+ self,
372
+ memory_id: str,
373
+ ) -> ForgetResult:
374
+ """Delete a memory.
375
+
376
+ Args:
377
+ memory_id: ID of memory to delete.
378
+
379
+ Returns:
380
+ ForgetResult with deletion count.
381
+ """
382
+ deleted = self._repo.delete(memory_id)
383
+
384
+ return ForgetResult(
385
+ deleted=1 if deleted else 0,
386
+ ids=[memory_id] if deleted else [],
387
+ )
388
+
389
+ def forget_batch(
390
+ self,
391
+ memory_ids: list[str],
392
+ ) -> ForgetResult:
393
+ """Delete multiple memories.
394
+
395
+ Args:
396
+ memory_ids: List of memory IDs to delete.
397
+
398
+ Returns:
399
+ ForgetResult with deletion count.
400
+
401
+ Raises:
402
+ ValidationError: If input validation fails.
403
+ """
404
+ if not memory_ids:
405
+ raise ValidationError("Memory ID list cannot be empty")
406
+
407
+ deleted_count, deleted_ids = self._repo.delete_batch(memory_ids)
408
+
409
+ return ForgetResult(
410
+ deleted=deleted_count,
411
+ ids=deleted_ids,
412
+ )