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