spatial-memory-mcp 1.0.2__py3-none-any.whl → 1.5.3__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.

@@ -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)