spatial-memory-mcp 1.0.3__py3-none-any.whl → 1.6.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.
Potentially problematic release.
This version of spatial-memory-mcp might be problematic. Click here for more details.
- spatial_memory/__init__.py +97 -97
- spatial_memory/__main__.py +241 -2
- spatial_memory/adapters/lancedb_repository.py +74 -5
- spatial_memory/config.py +115 -2
- spatial_memory/core/__init__.py +35 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +41 -3
- spatial_memory/core/consolidation_strategies.py +402 -0
- spatial_memory/core/database.py +791 -769
- 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 +156 -19
- spatial_memory/core/errors.py +75 -3
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/logging.py +194 -103
- spatial_memory/core/models.py +4 -0
- spatial_memory/core/rate_limiter.py +326 -105
- spatial_memory/core/response_types.py +497 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/validation.py +403 -319
- spatial_memory/factory.py +407 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/repositories.py +52 -2
- spatial_memory/server.py +329 -188
- spatial_memory/services/export_import.py +61 -43
- spatial_memory/services/lifecycle.py +397 -122
- spatial_memory/services/memory.py +81 -4
- spatial_memory/services/spatial.py +129 -46
- spatial_memory/tools/definitions.py +695 -671
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/METADATA +83 -3
- spatial_memory_mcp-1.6.0.dist-info/RECORD +54 -0
- spatial_memory_mcp-1.0.3.dist-info/RECORD +0 -41
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/WHEEL +0 -0
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/entry_points.txt +0 -0
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Request tracing and timing utilities for Spatial Memory MCP Server.
|
|
2
|
+
|
|
3
|
+
This module provides request context tracking and timing utilities to support
|
|
4
|
+
observability and debugging. It uses contextvars for safe async propagation.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from spatial_memory.core.tracing import (
|
|
8
|
+
RequestContext,
|
|
9
|
+
TimingContext,
|
|
10
|
+
get_current_context,
|
|
11
|
+
set_context,
|
|
12
|
+
clear_context,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Set request context
|
|
16
|
+
ctx = RequestContext(
|
|
17
|
+
request_id="abc123def456",
|
|
18
|
+
agent_id="agent-1",
|
|
19
|
+
tool_name="recall",
|
|
20
|
+
started_at=utc_now(),
|
|
21
|
+
namespace="default",
|
|
22
|
+
)
|
|
23
|
+
token = set_context(ctx)
|
|
24
|
+
try:
|
|
25
|
+
# ... do work
|
|
26
|
+
pass
|
|
27
|
+
finally:
|
|
28
|
+
clear_context(token)
|
|
29
|
+
|
|
30
|
+
# Measure operation timings
|
|
31
|
+
timing = TimingContext()
|
|
32
|
+
with timing.measure("embedding"):
|
|
33
|
+
# ... generate embedding
|
|
34
|
+
pass
|
|
35
|
+
with timing.measure("search"):
|
|
36
|
+
# ... perform search
|
|
37
|
+
pass
|
|
38
|
+
print(f"Total: {timing.total_ms():.2f}ms")
|
|
39
|
+
print(f"Timings: {timing.timings}")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import contextvars
|
|
45
|
+
import time
|
|
46
|
+
import uuid
|
|
47
|
+
from collections.abc import Generator
|
|
48
|
+
from contextlib import contextmanager
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from datetime import datetime
|
|
51
|
+
from typing import TYPE_CHECKING
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from contextvars import Token
|
|
55
|
+
|
|
56
|
+
from spatial_memory.core.utils import utc_now
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class RequestContext:
|
|
61
|
+
"""Context information for a request.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
request_id: Unique identifier for the request (first 12 chars of UUID).
|
|
65
|
+
agent_id: Optional identifier for the calling agent.
|
|
66
|
+
tool_name: Name of the MCP tool being called.
|
|
67
|
+
started_at: When the request started.
|
|
68
|
+
namespace: Optional namespace being operated on.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
request_id: str
|
|
72
|
+
agent_id: str | None
|
|
73
|
+
tool_name: str
|
|
74
|
+
started_at: datetime
|
|
75
|
+
namespace: str | None = None
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def create(
|
|
79
|
+
cls,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
agent_id: str | None = None,
|
|
82
|
+
namespace: str | None = None,
|
|
83
|
+
) -> RequestContext:
|
|
84
|
+
"""Create a new request context with auto-generated ID.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
tool_name: Name of the MCP tool being called.
|
|
88
|
+
agent_id: Optional identifier for the calling agent.
|
|
89
|
+
namespace: Optional namespace being operated on.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A new RequestContext with generated request_id and started_at.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
ctx = RequestContext.create("recall", agent_id="agent-1")
|
|
96
|
+
"""
|
|
97
|
+
return cls(
|
|
98
|
+
request_id=uuid.uuid4().hex[:12],
|
|
99
|
+
agent_id=agent_id,
|
|
100
|
+
tool_name=tool_name,
|
|
101
|
+
started_at=utc_now(),
|
|
102
|
+
namespace=namespace,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def elapsed_ms(self) -> float:
|
|
106
|
+
"""Calculate elapsed time since request started.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Elapsed time in milliseconds.
|
|
110
|
+
"""
|
|
111
|
+
return (utc_now() - self.started_at).total_seconds() * 1000
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Context variable for request tracking
|
|
115
|
+
_context: contextvars.ContextVar[RequestContext | None] = contextvars.ContextVar(
|
|
116
|
+
"request_context", default=None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_current_context() -> RequestContext | None:
|
|
121
|
+
"""Get the current request context.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The current RequestContext, or None if not in a request context.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
ctx = get_current_context()
|
|
128
|
+
if ctx:
|
|
129
|
+
print(f"Request {ctx.request_id} for tool {ctx.tool_name}")
|
|
130
|
+
"""
|
|
131
|
+
return _context.get()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def set_context(ctx: RequestContext) -> Token[RequestContext | None]:
|
|
135
|
+
"""Set the current request context.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
ctx: The request context to set.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A token that can be used to reset the context.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
ctx = RequestContext.create("recall")
|
|
145
|
+
token = set_context(ctx)
|
|
146
|
+
try:
|
|
147
|
+
# ... do work
|
|
148
|
+
pass
|
|
149
|
+
finally:
|
|
150
|
+
clear_context(token)
|
|
151
|
+
"""
|
|
152
|
+
return _context.set(ctx)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def clear_context(token: Token[RequestContext | None]) -> None:
|
|
156
|
+
"""Reset the context to its previous value.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
token: The token returned from set_context().
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
token = set_context(ctx)
|
|
163
|
+
try:
|
|
164
|
+
# ... do work
|
|
165
|
+
pass
|
|
166
|
+
finally:
|
|
167
|
+
clear_context(token)
|
|
168
|
+
"""
|
|
169
|
+
_context.reset(token)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@contextmanager
|
|
173
|
+
def request_context(
|
|
174
|
+
tool_name: str,
|
|
175
|
+
agent_id: str | None = None,
|
|
176
|
+
namespace: str | None = None,
|
|
177
|
+
) -> Generator[RequestContext, None, None]:
|
|
178
|
+
"""Context manager for request tracing.
|
|
179
|
+
|
|
180
|
+
Creates a RequestContext, sets it as current, and clears it on exit.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
tool_name: Name of the MCP tool being called.
|
|
184
|
+
agent_id: Optional identifier for the calling agent.
|
|
185
|
+
namespace: Optional namespace being operated on.
|
|
186
|
+
|
|
187
|
+
Yields:
|
|
188
|
+
The created RequestContext.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
with request_context("recall", agent_id="agent-1") as ctx:
|
|
192
|
+
print(f"Request {ctx.request_id}")
|
|
193
|
+
# ... do work
|
|
194
|
+
"""
|
|
195
|
+
ctx = RequestContext.create(tool_name, agent_id=agent_id, namespace=namespace)
|
|
196
|
+
token = set_context(ctx)
|
|
197
|
+
try:
|
|
198
|
+
yield ctx
|
|
199
|
+
finally:
|
|
200
|
+
clear_context(token)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class TimingContext:
|
|
205
|
+
"""Context for measuring operation timings.
|
|
206
|
+
|
|
207
|
+
Tracks timing of multiple named operations within a request.
|
|
208
|
+
Uses perf_counter for high-precision timing.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
timings: Dictionary mapping operation names to durations in milliseconds.
|
|
212
|
+
start: Start time of the context (perf_counter value).
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
timing = TimingContext()
|
|
216
|
+
with timing.measure("embedding"):
|
|
217
|
+
embed = generate_embedding(text)
|
|
218
|
+
with timing.measure("search"):
|
|
219
|
+
results = search(embed)
|
|
220
|
+
print(f"Total: {timing.total_ms():.2f}ms")
|
|
221
|
+
print(f"Embedding: {timing.timings['embedding']:.2f}ms")
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
timings: dict[str, float] = field(default_factory=dict)
|
|
225
|
+
start: float = field(default_factory=time.perf_counter)
|
|
226
|
+
|
|
227
|
+
@contextmanager
|
|
228
|
+
def measure(self, name: str) -> Generator[None, None, None]:
|
|
229
|
+
"""Measure the duration of a named operation.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
name: Name of the operation being measured.
|
|
233
|
+
|
|
234
|
+
Yields:
|
|
235
|
+
None
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
with timing.measure("database_query"):
|
|
239
|
+
results = db.query(...)
|
|
240
|
+
"""
|
|
241
|
+
t0 = time.perf_counter()
|
|
242
|
+
try:
|
|
243
|
+
yield
|
|
244
|
+
finally:
|
|
245
|
+
self.timings[name] = (time.perf_counter() - t0) * 1000
|
|
246
|
+
|
|
247
|
+
def total_ms(self) -> float:
|
|
248
|
+
"""Calculate total elapsed time since context creation.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Total elapsed time in milliseconds.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
timing = TimingContext()
|
|
255
|
+
# ... do some work
|
|
256
|
+
print(f"Total time: {timing.total_ms():.2f}ms")
|
|
257
|
+
"""
|
|
258
|
+
return (time.perf_counter() - self.start) * 1000
|
|
259
|
+
|
|
260
|
+
def summary(self) -> dict[str, float]:
|
|
261
|
+
"""Get a summary of all timings including total.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Dictionary with all named timings plus 'total_ms' and 'other_ms'.
|
|
265
|
+
'other_ms' is time not accounted for by named operations.
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
timing = TimingContext()
|
|
269
|
+
with timing.measure("op1"):
|
|
270
|
+
time.sleep(0.01)
|
|
271
|
+
summary = timing.summary()
|
|
272
|
+
# {'op1': 10.0, 'total_ms': 10.5, 'other_ms': 0.5}
|
|
273
|
+
"""
|
|
274
|
+
total = self.total_ms()
|
|
275
|
+
measured = sum(self.timings.values())
|
|
276
|
+
return {
|
|
277
|
+
**self.timings,
|
|
278
|
+
"total_ms": total,
|
|
279
|
+
"other_ms": max(0.0, total - measured),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def format_context_prefix() -> str:
|
|
284
|
+
"""Format the current context as a log prefix.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A string like "[req=abc123][agent=agent-1]" or "" if no context.
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
prefix = format_context_prefix()
|
|
291
|
+
logger.info(f"{prefix}Processing request...")
|
|
292
|
+
"""
|
|
293
|
+
ctx = get_current_context()
|
|
294
|
+
if ctx is None:
|
|
295
|
+
return ""
|
|
296
|
+
|
|
297
|
+
parts = [f"[req={ctx.request_id}]"]
|
|
298
|
+
if ctx.agent_id:
|
|
299
|
+
parts.append(f"[agent={ctx.agent_id}]")
|
|
300
|
+
return "".join(parts)
|