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,401 @@
1
+ """Strategy pattern for consolidation operations.
2
+
3
+ This module implements the Strategy design pattern for memory consolidation,
4
+ allowing different approaches to be used when merging duplicate memories.
5
+
6
+ Each strategy determines:
7
+ 1. Which memory becomes the representative (for non-merge strategies)
8
+ 2. How to handle the group of duplicates
9
+ 3. What action to record in the result
10
+
11
+ Usage in lifecycle.py:
12
+ strategy_impl = get_strategy(strategy_name)
13
+ action = strategy_impl.apply(group_member_dicts, ...)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from abc import ABC, abstractmethod
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Any, Literal
21
+
22
+ from spatial_memory.core.lifecycle_ops import (
23
+ merge_memory_content,
24
+ merge_memory_metadata,
25
+ )
26
+ from spatial_memory.core.models import Memory, MemorySource
27
+
28
+ if TYPE_CHECKING:
29
+
30
+ from spatial_memory.ports.repositories import (
31
+ EmbeddingServiceProtocol,
32
+ MemoryRepositoryProtocol,
33
+ )
34
+
35
+
36
+ # =============================================================================
37
+ # Strategy Result
38
+ # =============================================================================
39
+
40
+
41
+ @dataclass
42
+ class ConsolidationAction:
43
+ """Result of applying a consolidation strategy to a group.
44
+
45
+ Attributes:
46
+ representative_id: ID of the memory kept/created as representative.
47
+ deleted_ids: IDs of memories that were deleted.
48
+ action: Description of what was done.
49
+ memories_merged: Count of memories merged into one.
50
+ memories_deleted: Count of memories deleted.
51
+ """
52
+
53
+ representative_id: str
54
+ deleted_ids: list[str]
55
+ action: str
56
+ memories_merged: int = 0
57
+ memories_deleted: int = 0
58
+
59
+
60
+ # =============================================================================
61
+ # Strategy Protocol/Base Class
62
+ # =============================================================================
63
+
64
+
65
+ class ConsolidationStrategy(ABC):
66
+ """Abstract base class for consolidation strategies.
67
+
68
+ Each strategy defines how to select a representative memory
69
+ and how to process a group of duplicate memories.
70
+ """
71
+
72
+ @property
73
+ @abstractmethod
74
+ def name(self) -> str:
75
+ """Return the strategy name."""
76
+ ...
77
+
78
+ @abstractmethod
79
+ def select_representative(self, members: list[dict[str, Any]]) -> int:
80
+ """Select the index of the representative memory.
81
+
82
+ Args:
83
+ members: List of memory dictionaries with 'created_at',
84
+ 'importance', and 'content' keys.
85
+
86
+ Returns:
87
+ Index of the representative memory within the list.
88
+ """
89
+ ...
90
+
91
+ @abstractmethod
92
+ def apply(
93
+ self,
94
+ members: list[dict[str, Any]],
95
+ member_ids: list[str],
96
+ namespace: str,
97
+ repository: MemoryRepositoryProtocol,
98
+ embeddings: EmbeddingServiceProtocol,
99
+ dry_run: bool = True,
100
+ ) -> ConsolidationAction:
101
+ """Apply the consolidation strategy to a group of memories.
102
+
103
+ Args:
104
+ members: List of memory dictionaries.
105
+ member_ids: List of memory IDs.
106
+ namespace: Namespace of the memories.
107
+ repository: Repository for database operations.
108
+ embeddings: Embedding service for generating vectors.
109
+ dry_run: If True, preview without making changes.
110
+
111
+ Returns:
112
+ ConsolidationAction describing what was done.
113
+ """
114
+ ...
115
+
116
+
117
+ # =============================================================================
118
+ # Keep Representative Strategies
119
+ # =============================================================================
120
+
121
+
122
+ class KeepRepresentativeStrategy(ConsolidationStrategy):
123
+ """Base class for strategies that keep one memory and delete others."""
124
+
125
+ def apply(
126
+ self,
127
+ members: list[dict[str, Any]],
128
+ member_ids: list[str],
129
+ namespace: str,
130
+ repository: MemoryRepositoryProtocol,
131
+ embeddings: EmbeddingServiceProtocol,
132
+ dry_run: bool = True,
133
+ ) -> ConsolidationAction:
134
+ """Keep the representative and delete other members."""
135
+ rep_idx = self.select_representative(members)
136
+ rep_id = member_ids[rep_idx]
137
+
138
+ if dry_run:
139
+ return ConsolidationAction(
140
+ representative_id=rep_id,
141
+ deleted_ids=[],
142
+ action="preview",
143
+ memories_merged=0,
144
+ memories_deleted=0,
145
+ )
146
+
147
+ # Delete non-representative members
148
+ deleted_ids = []
149
+ for mid in member_ids:
150
+ if mid != rep_id:
151
+ try:
152
+ repository.delete(mid)
153
+ deleted_ids.append(mid)
154
+ except Exception as e:
155
+ # Log but continue with other deletions
156
+ import logging
157
+
158
+ logging.getLogger(__name__).warning(
159
+ f"Failed to delete memory {mid}: {e}"
160
+ )
161
+
162
+ return ConsolidationAction(
163
+ representative_id=rep_id,
164
+ deleted_ids=deleted_ids,
165
+ action="kept_representative",
166
+ memories_merged=1,
167
+ memories_deleted=len(deleted_ids),
168
+ )
169
+
170
+
171
+ class KeepNewestStrategy(KeepRepresentativeStrategy):
172
+ """Keep the most recently created memory."""
173
+
174
+ @property
175
+ def name(self) -> str:
176
+ return "keep_newest"
177
+
178
+ def select_representative(self, members: list[dict[str, Any]]) -> int:
179
+ """Select the newest memory by created_at."""
180
+ if not members:
181
+ return 0
182
+ return max(range(len(members)), key=lambda i: members[i].get("created_at", 0))
183
+
184
+
185
+ class KeepOldestStrategy(KeepRepresentativeStrategy):
186
+ """Keep the oldest (original) memory."""
187
+
188
+ @property
189
+ def name(self) -> str:
190
+ return "keep_oldest"
191
+
192
+ def select_representative(self, members: list[dict[str, Any]]) -> int:
193
+ """Select the oldest memory by created_at."""
194
+ if not members:
195
+ return 0
196
+ return min(
197
+ range(len(members)),
198
+ key=lambda i: members[i].get("created_at", float("inf")),
199
+ )
200
+
201
+
202
+ class KeepHighestImportanceStrategy(KeepRepresentativeStrategy):
203
+ """Keep the memory with the highest importance score."""
204
+
205
+ @property
206
+ def name(self) -> str:
207
+ return "keep_highest_importance"
208
+
209
+ def select_representative(self, members: list[dict[str, Any]]) -> int:
210
+ """Select the memory with highest importance."""
211
+ if not members:
212
+ return 0
213
+ return max(range(len(members)), key=lambda i: members[i].get("importance", 0.5))
214
+
215
+
216
+ # =============================================================================
217
+ # Merge Content Strategy
218
+ # =============================================================================
219
+
220
+
221
+ class MergeContentStrategy(ConsolidationStrategy):
222
+ """Merge all memories into a new combined memory."""
223
+
224
+ @property
225
+ def name(self) -> str:
226
+ return "merge_content"
227
+
228
+ def select_representative(self, members: list[dict[str, Any]]) -> int:
229
+ """Select the memory with longest content as base.
230
+
231
+ For merge strategy, this determines which memory's structure
232
+ is used as the base for the merged content.
233
+ """
234
+ if not members:
235
+ return 0
236
+ return max(
237
+ range(len(members)),
238
+ key=lambda i: len(members[i].get("content", "")),
239
+ )
240
+
241
+ def apply(
242
+ self,
243
+ members: list[dict[str, Any]],
244
+ member_ids: list[str],
245
+ namespace: str,
246
+ repository: MemoryRepositoryProtocol,
247
+ embeddings: EmbeddingServiceProtocol,
248
+ dry_run: bool = True,
249
+ ) -> ConsolidationAction:
250
+ """Merge all memories into a new combined memory."""
251
+ rep_idx = self.select_representative(members)
252
+ rep_id = member_ids[rep_idx]
253
+
254
+ if dry_run:
255
+ return ConsolidationAction(
256
+ representative_id=rep_id,
257
+ deleted_ids=[],
258
+ action="preview",
259
+ memories_merged=0,
260
+ memories_deleted=0,
261
+ )
262
+
263
+ import logging
264
+
265
+ logger = logging.getLogger(__name__)
266
+
267
+ # Create merged content
268
+ group_contents = [str(m["content"]) for m in members]
269
+ merged_content = merge_memory_content(group_contents)
270
+ merged_meta = merge_memory_metadata(members)
271
+
272
+ # Generate new embedding
273
+ new_vector = embeddings.embed(merged_content)
274
+
275
+ # Prepare merged memory with pending status marker
276
+ pending_metadata = merged_meta.get("metadata", {}).copy()
277
+ pending_metadata["_consolidation_status"] = "pending"
278
+ pending_metadata["_consolidation_source_ids"] = member_ids
279
+
280
+ merged_memory = Memory(
281
+ id="", # Will be assigned
282
+ content=merged_content,
283
+ namespace=namespace,
284
+ tags=merged_meta.get("tags", []),
285
+ importance=merged_meta.get("importance", 0.5),
286
+ source=MemorySource.CONSOLIDATED,
287
+ metadata=pending_metadata,
288
+ )
289
+
290
+ # ADD FIRST pattern: add merged memory before deleting originals
291
+ try:
292
+ new_id = repository.add(merged_memory, new_vector)
293
+ except Exception as add_err:
294
+ logger.error(
295
+ f"Consolidation add failed, originals preserved. "
296
+ f"Group IDs: {member_ids}. Error: {add_err}"
297
+ )
298
+ return ConsolidationAction(
299
+ representative_id=rep_id,
300
+ deleted_ids=[],
301
+ action="failed",
302
+ memories_merged=0,
303
+ memories_deleted=0,
304
+ )
305
+
306
+ # Delete originals using batch operation
307
+ deleted_ids: list[str] = []
308
+ try:
309
+ deleted_count, deleted_ids = repository.delete_batch(member_ids)
310
+ except Exception as del_err:
311
+ logger.warning(
312
+ f"Consolidation delete failed after add. "
313
+ f"Merged memory {new_id} has pending status. "
314
+ f"Original IDs: {member_ids}. Error: {del_err}"
315
+ )
316
+ return ConsolidationAction(
317
+ representative_id=new_id,
318
+ deleted_ids=[],
319
+ action="failed",
320
+ memories_merged=0,
321
+ memories_deleted=0,
322
+ )
323
+
324
+ # Activate merged memory by removing pending status
325
+ try:
326
+ final_metadata = merged_meta.get("metadata", {}).copy()
327
+ repository.update(new_id, {"metadata": final_metadata})
328
+ except Exception as update_err:
329
+ # Minor issue - memory works, just has pending marker
330
+ logger.warning(
331
+ f"Failed to remove pending status from {new_id}: {update_err}"
332
+ )
333
+ # Don't fail - consolidation succeeded
334
+
335
+ return ConsolidationAction(
336
+ representative_id=new_id,
337
+ deleted_ids=deleted_ids,
338
+ action="merged",
339
+ memories_merged=1,
340
+ memories_deleted=len(deleted_ids),
341
+ )
342
+
343
+
344
+ # =============================================================================
345
+ # Strategy Registry
346
+ # =============================================================================
347
+
348
+ # Type for valid strategy names
349
+ StrategyName = Literal[
350
+ "keep_newest", "keep_oldest", "keep_highest_importance", "merge_content"
351
+ ]
352
+
353
+ # Registry of available strategies
354
+ _STRATEGIES: dict[str, ConsolidationStrategy] = {
355
+ "keep_newest": KeepNewestStrategy(),
356
+ "keep_oldest": KeepOldestStrategy(),
357
+ "keep_highest_importance": KeepHighestImportanceStrategy(),
358
+ "merge_content": MergeContentStrategy(),
359
+ }
360
+
361
+
362
+ def get_strategy(name: str) -> ConsolidationStrategy:
363
+ """Get a consolidation strategy by name.
364
+
365
+ Args:
366
+ name: Strategy name (keep_newest, keep_oldest,
367
+ keep_highest_importance, merge_content).
368
+
369
+ Returns:
370
+ The corresponding ConsolidationStrategy instance.
371
+
372
+ Raises:
373
+ ValueError: If the strategy name is unknown.
374
+ """
375
+ strategy = _STRATEGIES.get(name)
376
+ if strategy is None:
377
+ valid_names = ", ".join(_STRATEGIES.keys())
378
+ raise ValueError(f"Unknown strategy: {name}. Valid strategies: {valid_names}")
379
+ return strategy
380
+
381
+
382
+ def register_strategy(name: str, strategy: ConsolidationStrategy) -> None:
383
+ """Register a custom consolidation strategy.
384
+
385
+ This allows extending the consolidation system with custom strategies
386
+ without modifying the core code.
387
+
388
+ Args:
389
+ name: Unique name for the strategy.
390
+ strategy: ConsolidationStrategy instance.
391
+ """
392
+ _STRATEGIES[name] = strategy
393
+
394
+
395
+ def list_strategies() -> list[str]:
396
+ """List all registered strategy names.
397
+
398
+ Returns:
399
+ List of registered strategy names.
400
+ """
401
+ return list(_STRATEGIES.keys())