atomicmemory 1.0.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.
Files changed (67) hide show
  1. atomicmemory/__init__.py +166 -0
  2. atomicmemory/_version.py +3 -0
  3. atomicmemory/client/__init__.py +22 -0
  4. atomicmemory/client/async_memory_client.py +202 -0
  5. atomicmemory/client/atomic_memory_client.py +181 -0
  6. atomicmemory/client/memory_client.py +292 -0
  7. atomicmemory/core/__init__.py +34 -0
  8. atomicmemory/core/errors.py +122 -0
  9. atomicmemory/core/events.py +65 -0
  10. atomicmemory/core/logging.py +37 -0
  11. atomicmemory/core/retry.py +124 -0
  12. atomicmemory/core/validation.py +22 -0
  13. atomicmemory/embeddings/__init__.py +16 -0
  14. atomicmemory/embeddings/base.py +39 -0
  15. atomicmemory/embeddings/sentence_transformers.py +104 -0
  16. atomicmemory/kv_cache/__init__.py +17 -0
  17. atomicmemory/kv_cache/adapter.py +50 -0
  18. atomicmemory/kv_cache/memory_storage.py +98 -0
  19. atomicmemory/kv_cache/sqlite_storage.py +122 -0
  20. atomicmemory/memory/__init__.py +82 -0
  21. atomicmemory/memory/filters.py +68 -0
  22. atomicmemory/memory/pipeline.py +42 -0
  23. atomicmemory/memory/provider.py +397 -0
  24. atomicmemory/memory/registry.py +95 -0
  25. atomicmemory/memory/service.py +199 -0
  26. atomicmemory/memory/types.py +398 -0
  27. atomicmemory/providers/__init__.py +5 -0
  28. atomicmemory/providers/atomicmemory/__init__.py +43 -0
  29. atomicmemory/providers/atomicmemory/agents.py +156 -0
  30. atomicmemory/providers/atomicmemory/async_handle_impl.py +198 -0
  31. atomicmemory/providers/atomicmemory/async_provider.py +245 -0
  32. atomicmemory/providers/atomicmemory/audit.py +74 -0
  33. atomicmemory/providers/atomicmemory/config.py +38 -0
  34. atomicmemory/providers/atomicmemory/config_handle.py +123 -0
  35. atomicmemory/providers/atomicmemory/handle.py +513 -0
  36. atomicmemory/providers/atomicmemory/handle_impl.py +325 -0
  37. atomicmemory/providers/atomicmemory/http.py +255 -0
  38. atomicmemory/providers/atomicmemory/lessons.py +133 -0
  39. atomicmemory/providers/atomicmemory/lifecycle.py +202 -0
  40. atomicmemory/providers/atomicmemory/mappers.py +125 -0
  41. atomicmemory/providers/atomicmemory/path.py +20 -0
  42. atomicmemory/providers/atomicmemory/provider.py +300 -0
  43. atomicmemory/providers/atomicmemory/scope_mapper.py +98 -0
  44. atomicmemory/providers/mem0/__init__.py +41 -0
  45. atomicmemory/providers/mem0/async_provider.py +191 -0
  46. atomicmemory/providers/mem0/config.py +51 -0
  47. atomicmemory/providers/mem0/http.py +195 -0
  48. atomicmemory/providers/mem0/mappers.py +145 -0
  49. atomicmemory/providers/mem0/provider.py +202 -0
  50. atomicmemory/py.typed +0 -0
  51. atomicmemory/search/__init__.py +47 -0
  52. atomicmemory/search/chunking.py +161 -0
  53. atomicmemory/search/ranking.py +94 -0
  54. atomicmemory/search/semantic_search.py +130 -0
  55. atomicmemory/search/similarity.py +110 -0
  56. atomicmemory/storage/__init__.py +63 -0
  57. atomicmemory/storage/_mapping.py +305 -0
  58. atomicmemory/storage/async_client.py +208 -0
  59. atomicmemory/storage/client.py +339 -0
  60. atomicmemory/storage/errors.py +115 -0
  61. atomicmemory/storage/types.py +305 -0
  62. atomicmemory/utils/__init__.py +5 -0
  63. atomicmemory/utils/environment.py +23 -0
  64. atomicmemory-1.0.0.dist-info/METADATA +146 -0
  65. atomicmemory-1.0.0.dist-info/RECORD +67 -0
  66. atomicmemory-1.0.0.dist-info/WHEEL +4 -0
  67. atomicmemory-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,397 @@
1
+ """V3 memory provider interface and base classes (sync + async).
2
+
3
+ Port of `atomicmemory-sdk/src/memory/provider.ts`. Defines the
4
+ ``MemoryProvider`` interface, every standard V3 extension Protocol, and
5
+ two abstract base classes — one sync, one async — that share scope
6
+ validation, ready-state enforcement, and error wrapping.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Awaitable, Callable
13
+ from datetime import datetime
14
+ from typing import Any, Protocol, TypeVar, runtime_checkable
15
+
16
+ from atomicmemory.core.errors import NotInitializedError, ProviderError, ValidationError
17
+ from atomicmemory.memory.types import (
18
+ Capabilities,
19
+ ContextPackage,
20
+ GraphResult,
21
+ GraphSearchRequest,
22
+ HealthStatus,
23
+ IngestInput,
24
+ IngestResult,
25
+ Insight,
26
+ ListRequest,
27
+ ListResultPage,
28
+ Memory,
29
+ MemoryRef,
30
+ MemoryVersion,
31
+ PackageRequest,
32
+ Profile,
33
+ Scope,
34
+ SearchRequest,
35
+ SearchResultPage,
36
+ )
37
+
38
+ T = TypeVar("T")
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Standard extension Protocols (sync + async)
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ @runtime_checkable
46
+ class Updater(Protocol):
47
+ def update(self, ref: MemoryRef, content: str) -> Memory: ...
48
+
49
+
50
+ @runtime_checkable
51
+ class AsyncUpdater(Protocol):
52
+ async def update(self, ref: MemoryRef, content: str) -> Memory: ...
53
+
54
+
55
+ @runtime_checkable
56
+ class Packager(Protocol):
57
+ def package(self, request: PackageRequest) -> ContextPackage: ...
58
+
59
+
60
+ @runtime_checkable
61
+ class AsyncPackager(Protocol):
62
+ async def package(self, request: PackageRequest) -> ContextPackage: ...
63
+
64
+
65
+ @runtime_checkable
66
+ class TemporalSearch(Protocol):
67
+ def search_as_of(self, request: SearchRequest, as_of: datetime) -> SearchResultPage: ...
68
+
69
+
70
+ @runtime_checkable
71
+ class AsyncTemporalSearch(Protocol):
72
+ async def search_as_of(self, request: SearchRequest, as_of: datetime) -> SearchResultPage: ...
73
+
74
+
75
+ @runtime_checkable
76
+ class GraphSearch(Protocol):
77
+ def search_graph(self, request: GraphSearchRequest) -> GraphResult: ...
78
+
79
+
80
+ @runtime_checkable
81
+ class AsyncGraphSearch(Protocol):
82
+ async def search_graph(self, request: GraphSearchRequest) -> GraphResult: ...
83
+
84
+
85
+ @runtime_checkable
86
+ class Forgetter(Protocol):
87
+ def forget(self, ref: MemoryRef, reason: str | None = None) -> None: ...
88
+
89
+
90
+ @runtime_checkable
91
+ class AsyncForgetter(Protocol):
92
+ async def forget(self, ref: MemoryRef, reason: str | None = None) -> None: ...
93
+
94
+
95
+ @runtime_checkable
96
+ class Profiler(Protocol):
97
+ def profile(self, scope: Scope, instructions: list[str] | None = None) -> Profile: ...
98
+
99
+
100
+ @runtime_checkable
101
+ class AsyncProfiler(Protocol):
102
+ async def profile(self, scope: Scope, instructions: list[str] | None = None) -> Profile: ...
103
+
104
+
105
+ @runtime_checkable
106
+ class Reflector(Protocol):
107
+ def reflect(self, query: str, scope: Scope) -> list[Insight]: ...
108
+
109
+
110
+ @runtime_checkable
111
+ class AsyncReflector(Protocol):
112
+ async def reflect(self, query: str, scope: Scope) -> list[Insight]: ...
113
+
114
+
115
+ @runtime_checkable
116
+ class Versioner(Protocol):
117
+ def history(self, ref: MemoryRef) -> list[MemoryVersion]: ...
118
+
119
+
120
+ @runtime_checkable
121
+ class AsyncVersioner(Protocol):
122
+ async def history(self, ref: MemoryRef) -> list[MemoryVersion]: ...
123
+
124
+
125
+ @runtime_checkable
126
+ class BatchOps(Protocol):
127
+ def batch_ingest(self, inputs: list[IngestInput]) -> list[IngestResult]: ...
128
+ def batch_delete(self, refs: list[MemoryRef]) -> None: ...
129
+
130
+
131
+ @runtime_checkable
132
+ class AsyncBatchOps(Protocol):
133
+ async def batch_ingest(self, inputs: list[IngestInput]) -> list[IngestResult]: ...
134
+ async def batch_delete(self, refs: list[MemoryRef]) -> None: ...
135
+
136
+
137
+ @runtime_checkable
138
+ class Health(Protocol):
139
+ def health(self) -> HealthStatus: ...
140
+
141
+
142
+ @runtime_checkable
143
+ class AsyncHealth(Protocol):
144
+ async def health(self) -> HealthStatus: ...
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Shared scope validation
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def _missing_scope_fields(scope: Scope, required: list[str]) -> list[str]:
153
+ """Return required fields that are missing/empty on ``scope``."""
154
+ missing: list[str] = []
155
+ for field in required:
156
+ value = getattr(scope, field, None)
157
+ if not value:
158
+ missing.append(field)
159
+ return missing
160
+
161
+
162
+ # Operations whose name conflicts with a Python builtin/keyword and is
163
+ # stored under a trailing-underscore field name on the model.
164
+ _OPERATION_FIELD_OVERRIDES: dict[str, str] = {"list": "list_"}
165
+
166
+
167
+ def _required_for(capabilities: Capabilities, operation: str) -> list[str]:
168
+ """Look up the required-scope list for ``operation``.
169
+
170
+ Falls back to ``capabilities.required_scope.default`` when the
171
+ operation has no override.
172
+ """
173
+ field = _OPERATION_FIELD_OVERRIDES.get(operation, operation)
174
+ op_specific: list[str] | None = getattr(capabilities.required_scope, field, None)
175
+ if op_specific is not None:
176
+ return op_specific
177
+ return capabilities.required_scope.default
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Sync base class
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ class BaseMemoryProvider(ABC):
186
+ """Sync abstract base for synchronous V3 memory providers.
187
+
188
+ Subclasses implement the ``do_*`` hooks; this base wires
189
+ ready-state, scope validation, and error normalization through
190
+ ``_run_operation``.
191
+ """
192
+
193
+ name: str = ""
194
+ _initialized: bool = True
195
+
196
+ @abstractmethod
197
+ def do_ingest(self, input: IngestInput) -> IngestResult: ...
198
+
199
+ @abstractmethod
200
+ def do_search(self, request: SearchRequest) -> SearchResultPage: ...
201
+
202
+ @abstractmethod
203
+ def do_get(self, ref: MemoryRef) -> Memory | None: ...
204
+
205
+ @abstractmethod
206
+ def do_delete(self, ref: MemoryRef) -> None: ...
207
+
208
+ @abstractmethod
209
+ def do_list(self, request: ListRequest) -> ListResultPage: ...
210
+
211
+ @abstractmethod
212
+ def capabilities(self) -> Capabilities: ...
213
+
214
+ def initialize(self) -> None: # noqa: B027 — override-or-pass is intentional
215
+ """Optional async setup hook. Default is a no-op."""
216
+
217
+ def close(self) -> None: # noqa: B027 — override-or-pass is intentional
218
+ """Optional teardown hook. Default is a no-op."""
219
+
220
+ def ingest(self, input: IngestInput) -> IngestResult:
221
+ return self._run_operation("ingest", input.scope, lambda: self.do_ingest(input))
222
+
223
+ def search(self, request: SearchRequest) -> SearchResultPage:
224
+ return self._run_operation("search", request.scope, lambda: self.do_search(request))
225
+
226
+ def get(self, ref: MemoryRef) -> Memory | None:
227
+ return self._run_operation("get", ref.scope, lambda: self.do_get(ref))
228
+
229
+ def delete(self, ref: MemoryRef) -> None:
230
+ self._run_operation("delete", ref.scope, lambda: self.do_delete(ref))
231
+
232
+ def list(self, request: ListRequest) -> ListResultPage:
233
+ return self._run_operation("list", request.scope, lambda: self.do_list(request))
234
+
235
+ def get_extension(self, name: str) -> Any | None:
236
+ return self._resolve_extension(name)
237
+
238
+ def _resolve_extension(self, name: str) -> Any | None:
239
+ caps = self.capabilities()
240
+ if getattr(caps.extensions, name, False):
241
+ return self
242
+ return None
243
+
244
+ def _run_operation(
245
+ self,
246
+ operation: str,
247
+ scope: Scope | None,
248
+ fn: Callable[[], T],
249
+ ) -> T:
250
+ self._assert_ready()
251
+ if scope is not None:
252
+ self._validate_scope(scope, operation)
253
+ try:
254
+ return fn()
255
+ except (ProviderError, ValidationError, NotInitializedError):
256
+ raise
257
+ except Exception as exc:
258
+ raise ProviderError(str(exc), provider=self.name, context={"operation": operation}) from exc
259
+
260
+ def _assert_ready(self) -> None:
261
+ if not self._initialized:
262
+ raise NotInitializedError(f"{self.name} is not initialized. Call initialize() first.")
263
+
264
+ def _validate_scope(self, scope: Scope, operation: str) -> None:
265
+ required = _required_for(self.capabilities(), operation)
266
+ if not required:
267
+ return
268
+ missing = _missing_scope_fields(scope, required)
269
+ if missing:
270
+ raise ValidationError(
271
+ f"{self.name}: scope is missing required fields for '{operation}': {missing}",
272
+ context={"operation": operation, "missing": missing, "provider": self.name},
273
+ )
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Async base class
278
+ # ---------------------------------------------------------------------------
279
+
280
+
281
+ class BaseAsyncMemoryProvider(ABC):
282
+ """Async abstract base for asynchronous V3 memory providers."""
283
+
284
+ name: str = ""
285
+ _initialized: bool = True
286
+
287
+ @abstractmethod
288
+ async def do_ingest(self, input: IngestInput) -> IngestResult: ...
289
+
290
+ @abstractmethod
291
+ async def do_search(self, request: SearchRequest) -> SearchResultPage: ...
292
+
293
+ @abstractmethod
294
+ async def do_get(self, ref: MemoryRef) -> Memory | None: ...
295
+
296
+ @abstractmethod
297
+ async def do_delete(self, ref: MemoryRef) -> None: ...
298
+
299
+ @abstractmethod
300
+ async def do_list(self, request: ListRequest) -> ListResultPage: ...
301
+
302
+ @abstractmethod
303
+ def capabilities(self) -> Capabilities: ...
304
+
305
+ async def initialize(self) -> None: # noqa: B027 — override-or-pass is intentional
306
+ """Optional async setup hook. Default is a no-op."""
307
+
308
+ async def close(self) -> None: # noqa: B027 — override-or-pass is intentional
309
+ """Optional async teardown hook. Default is a no-op."""
310
+
311
+ async def ingest(self, input: IngestInput) -> IngestResult:
312
+ return await self._run_operation("ingest", input.scope, lambda: self.do_ingest(input))
313
+
314
+ async def search(self, request: SearchRequest) -> SearchResultPage:
315
+ return await self._run_operation("search", request.scope, lambda: self.do_search(request))
316
+
317
+ async def get(self, ref: MemoryRef) -> Memory | None:
318
+ return await self._run_operation("get", ref.scope, lambda: self.do_get(ref))
319
+
320
+ async def delete(self, ref: MemoryRef) -> None:
321
+ await self._run_operation("delete", ref.scope, lambda: self.do_delete(ref))
322
+
323
+ async def list(self, request: ListRequest) -> ListResultPage:
324
+ return await self._run_operation("list", request.scope, lambda: self.do_list(request))
325
+
326
+ def get_extension(self, name: str) -> Any | None:
327
+ return self._resolve_extension(name)
328
+
329
+ def _resolve_extension(self, name: str) -> Any | None:
330
+ caps = self.capabilities()
331
+ if getattr(caps.extensions, name, False):
332
+ return self
333
+ return None
334
+
335
+ async def _run_operation(
336
+ self,
337
+ operation: str,
338
+ scope: Scope | None,
339
+ fn: Callable[[], Awaitable[T]],
340
+ ) -> T:
341
+ self._assert_ready()
342
+ if scope is not None:
343
+ self._validate_scope(scope, operation)
344
+ try:
345
+ return await fn()
346
+ except (ProviderError, ValidationError, NotInitializedError):
347
+ raise
348
+ except Exception as exc:
349
+ raise ProviderError(str(exc), provider=self.name, context={"operation": operation}) from exc
350
+
351
+ def _assert_ready(self) -> None:
352
+ if not self._initialized:
353
+ raise NotInitializedError(f"{self.name} is not initialized. Call initialize() first.")
354
+
355
+ def _validate_scope(self, scope: Scope, operation: str) -> None:
356
+ required = _required_for(self.capabilities(), operation)
357
+ if not required:
358
+ return
359
+ missing = _missing_scope_fields(scope, required)
360
+ if missing:
361
+ raise ValidationError(
362
+ f"{self.name}: scope is missing required fields for '{operation}': {missing}",
363
+ context={"operation": operation, "missing": missing, "provider": self.name},
364
+ )
365
+
366
+
367
+ # Type alias for any provider — useful for client/service signatures that
368
+ # don't care which flavor.
369
+ MemoryProvider = BaseMemoryProvider
370
+ AsyncMemoryProvider = BaseAsyncMemoryProvider
371
+
372
+ __all__ = [
373
+ "AsyncBatchOps",
374
+ "AsyncForgetter",
375
+ "AsyncGraphSearch",
376
+ "AsyncHealth",
377
+ "AsyncMemoryProvider",
378
+ "AsyncPackager",
379
+ "AsyncProfiler",
380
+ "AsyncReflector",
381
+ "AsyncTemporalSearch",
382
+ "AsyncUpdater",
383
+ "AsyncVersioner",
384
+ "BaseAsyncMemoryProvider",
385
+ "BaseMemoryProvider",
386
+ "BatchOps",
387
+ "Forgetter",
388
+ "GraphSearch",
389
+ "Health",
390
+ "MemoryProvider",
391
+ "Packager",
392
+ "Profiler",
393
+ "Reflector",
394
+ "TemporalSearch",
395
+ "Updater",
396
+ "Versioner",
397
+ ]
@@ -0,0 +1,95 @@
1
+ """Provider registry — name → factory mapping.
2
+
3
+ Port of `atomicmemory-sdk/src/memory/providers/registry.ts`. Concrete
4
+ provider modules register themselves here so the client can wire them
5
+ up by name from a config dict.
6
+
7
+ Two registries live side-by-side: one for sync providers and one for
8
+ async providers. Each maps a provider name (e.g. ``"atomicmemory"``,
9
+ ``"mem0"``) to a factory that takes the provider's config object and
10
+ returns a `ProviderRegistration`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass
17
+ from typing import Any
18
+
19
+ from atomicmemory.memory.pipeline import NOOP_PIPELINE, MemoryProcessingPipeline
20
+ from atomicmemory.memory.provider import (
21
+ BaseAsyncMemoryProvider,
22
+ BaseMemoryProvider,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ProviderRegistration:
28
+ """Result of a provider factory call."""
29
+
30
+ provider: BaseMemoryProvider
31
+ pipeline: MemoryProcessingPipeline = NOOP_PIPELINE
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AsyncProviderRegistration:
36
+ """Async counterpart of `ProviderRegistration`."""
37
+
38
+ provider: BaseAsyncMemoryProvider
39
+ pipeline: MemoryProcessingPipeline = NOOP_PIPELINE
40
+
41
+
42
+ SyncProviderFactory = Callable[[Any], ProviderRegistration]
43
+ AsyncProviderFactory = Callable[[Any], AsyncProviderRegistration]
44
+
45
+
46
+ class ProviderRegistry:
47
+ """Mutable registry of sync provider factories.
48
+
49
+ Each provider package (e.g. ``atomicmemory.providers.atomicmemory``)
50
+ calls :meth:`register` on import to add itself to the default
51
+ registry; callers can also create their own registry instances for
52
+ test isolation.
53
+ """
54
+
55
+ def __init__(self) -> None:
56
+ self._factories: dict[str, SyncProviderFactory] = {}
57
+
58
+ def register(self, name: str, factory: SyncProviderFactory) -> None:
59
+ if name in self._factories:
60
+ raise ValueError(f"Provider '{name}' is already registered")
61
+ self._factories[name] = factory
62
+
63
+ def get(self, name: str) -> SyncProviderFactory | None:
64
+ return self._factories.get(name)
65
+
66
+ def names(self) -> list[str]:
67
+ return sorted(self._factories.keys())
68
+
69
+ def __contains__(self, name: str) -> bool:
70
+ return name in self._factories
71
+
72
+
73
+ class AsyncProviderRegistry:
74
+ """Async counterpart of `ProviderRegistry`."""
75
+
76
+ def __init__(self) -> None:
77
+ self._factories: dict[str, AsyncProviderFactory] = {}
78
+
79
+ def register(self, name: str, factory: AsyncProviderFactory) -> None:
80
+ if name in self._factories:
81
+ raise ValueError(f"Async provider '{name}' is already registered")
82
+ self._factories[name] = factory
83
+
84
+ def get(self, name: str) -> AsyncProviderFactory | None:
85
+ return self._factories.get(name)
86
+
87
+ def names(self) -> list[str]:
88
+ return sorted(self._factories.keys())
89
+
90
+ def __contains__(self, name: str) -> bool:
91
+ return name in self._factories
92
+
93
+
94
+ default_registry = ProviderRegistry()
95
+ default_async_registry = AsyncProviderRegistry()
@@ -0,0 +1,199 @@
1
+ """Provider routing service.
2
+
3
+ Port of `atomicmemory-sdk/src/memory/memory-service.ts`. Provides one
4
+ sync `MemoryService` and one async `AsyncMemoryService`. Each owns the
5
+ provider→pipeline map, dispatches calls to the named (or default)
6
+ provider, and synthesizes the V3 ``package`` extension from
7
+ provider.get_extension.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from atomicmemory.core.errors import ConfigError, ProviderError
16
+ from atomicmemory.memory.pipeline import NOOP_PIPELINE, MemoryProcessingPipeline
17
+ from atomicmemory.memory.provider import (
18
+ BaseAsyncMemoryProvider,
19
+ BaseMemoryProvider,
20
+ )
21
+ from atomicmemory.memory.registry import (
22
+ AsyncProviderRegistry,
23
+ ProviderRegistry,
24
+ default_async_registry,
25
+ default_registry,
26
+ )
27
+ from atomicmemory.memory.types import (
28
+ ContextPackage,
29
+ IngestInput,
30
+ IngestResult,
31
+ ListRequest,
32
+ ListResultPage,
33
+ Memory,
34
+ MemoryRef,
35
+ PackageRequest,
36
+ SearchRequest,
37
+ SearchResultPage,
38
+ )
39
+
40
+
41
+ @dataclass
42
+ class MemoryServiceConfig:
43
+ """Inputs to construct a service."""
44
+
45
+ default_provider: str
46
+ provider_configs: dict[str, Any]
47
+
48
+
49
+ class _ServiceBase:
50
+ """Shared state + lookup helpers for sync and async services."""
51
+
52
+ def __init__(self, config: MemoryServiceConfig) -> None:
53
+ if not config.provider_configs:
54
+ raise ConfigError("MemoryService requires at least one provider config")
55
+ if config.default_provider not in config.provider_configs:
56
+ raise ConfigError(f"default_provider '{config.default_provider}' is not in provider_configs")
57
+ self._config = config
58
+ self._default_provider_name = config.default_provider
59
+
60
+ @property
61
+ def default_provider_name(self) -> str:
62
+ return self._default_provider_name
63
+
64
+ def get_configured_providers(self) -> list[str]:
65
+ return list(self._config.provider_configs.keys())
66
+
67
+
68
+ class MemoryService(_ServiceBase):
69
+ """Sync provider router."""
70
+
71
+ def __init__(self, config: MemoryServiceConfig) -> None:
72
+ super().__init__(config)
73
+ self._providers: dict[str, BaseMemoryProvider] = {}
74
+ self._pipelines: dict[str, MemoryProcessingPipeline] = {}
75
+
76
+ def initialize(self, registry: ProviderRegistry | None = None) -> None:
77
+ reg = registry if registry is not None else default_registry
78
+ for name, provider_config in self._config.provider_configs.items():
79
+ factory = reg.get(name)
80
+ if factory is None:
81
+ continue
82
+ registration = factory(provider_config)
83
+ self._providers[name] = registration.provider
84
+ self._pipelines[name] = registration.pipeline
85
+ registration.provider.initialize()
86
+
87
+ def close(self) -> None:
88
+ for provider in self._providers.values():
89
+ provider.close()
90
+
91
+ def get_provider(self, name: str | None = None) -> BaseMemoryProvider:
92
+ provider_name = name or self._default_provider_name
93
+ provider = self._providers.get(provider_name)
94
+ if provider is None:
95
+ raise ConfigError(f"Provider '{provider_name}' is not registered")
96
+ return provider
97
+
98
+ def get_available_providers(self) -> list[str]:
99
+ return sorted(self._providers.keys())
100
+
101
+ def ingest(self, input: IngestInput, provider_name: str | None = None) -> IngestResult:
102
+ provider = self.get_provider(provider_name)
103
+ return provider.ingest(input)
104
+
105
+ def search(self, request: SearchRequest, provider_name: str | None = None) -> SearchResultPage:
106
+ provider = self.get_provider(provider_name)
107
+ return provider.search(request)
108
+
109
+ def get(self, ref: MemoryRef, provider_name: str | None = None) -> Memory | None:
110
+ provider = self.get_provider(provider_name)
111
+ return provider.get(ref)
112
+
113
+ def delete(self, ref: MemoryRef, provider_name: str | None = None) -> None:
114
+ provider = self.get_provider(provider_name)
115
+ provider.delete(ref)
116
+
117
+ def list(self, request: ListRequest, provider_name: str | None = None) -> ListResultPage:
118
+ provider = self.get_provider(provider_name)
119
+ return provider.list(request)
120
+
121
+ def package(self, request: PackageRequest, provider_name: str | None = None) -> ContextPackage:
122
+ provider = self.get_provider(provider_name)
123
+ packager = provider.get_extension("package")
124
+ if packager is None or not hasattr(packager, "package"):
125
+ raise ProviderError(
126
+ f"Provider '{provider.name}' does not support the 'package' extension",
127
+ provider=provider.name,
128
+ context={"operation": "package"},
129
+ )
130
+ return packager.package(request) # type: ignore[no-any-return]
131
+
132
+ def _pipeline(self, name: str | None) -> MemoryProcessingPipeline:
133
+ provider_name = name or self._default_provider_name
134
+ return self._pipelines.get(provider_name, NOOP_PIPELINE)
135
+
136
+
137
+ class AsyncMemoryService(_ServiceBase):
138
+ """Async provider router."""
139
+
140
+ def __init__(self, config: MemoryServiceConfig) -> None:
141
+ super().__init__(config)
142
+ self._providers: dict[str, BaseAsyncMemoryProvider] = {}
143
+ self._pipelines: dict[str, MemoryProcessingPipeline] = {}
144
+
145
+ async def initialize(self, registry: AsyncProviderRegistry | None = None) -> None:
146
+ reg = registry if registry is not None else default_async_registry
147
+ for name, provider_config in self._config.provider_configs.items():
148
+ factory = reg.get(name)
149
+ if factory is None:
150
+ continue
151
+ registration = factory(provider_config)
152
+ self._providers[name] = registration.provider
153
+ self._pipelines[name] = registration.pipeline
154
+ await registration.provider.initialize()
155
+
156
+ async def close(self) -> None:
157
+ for provider in self._providers.values():
158
+ await provider.close()
159
+
160
+ def get_provider(self, name: str | None = None) -> BaseAsyncMemoryProvider:
161
+ provider_name = name or self._default_provider_name
162
+ provider = self._providers.get(provider_name)
163
+ if provider is None:
164
+ raise ConfigError(f"Provider '{provider_name}' is not registered")
165
+ return provider
166
+
167
+ def get_available_providers(self) -> list[str]:
168
+ return sorted(self._providers.keys())
169
+
170
+ async def ingest(self, input: IngestInput, provider_name: str | None = None) -> IngestResult:
171
+ provider = self.get_provider(provider_name)
172
+ return await provider.ingest(input)
173
+
174
+ async def search(self, request: SearchRequest, provider_name: str | None = None) -> SearchResultPage:
175
+ provider = self.get_provider(provider_name)
176
+ return await provider.search(request)
177
+
178
+ async def get(self, ref: MemoryRef, provider_name: str | None = None) -> Memory | None:
179
+ provider = self.get_provider(provider_name)
180
+ return await provider.get(ref)
181
+
182
+ async def delete(self, ref: MemoryRef, provider_name: str | None = None) -> None:
183
+ provider = self.get_provider(provider_name)
184
+ await provider.delete(ref)
185
+
186
+ async def list(self, request: ListRequest, provider_name: str | None = None) -> ListResultPage:
187
+ provider = self.get_provider(provider_name)
188
+ return await provider.list(request)
189
+
190
+ async def package(self, request: PackageRequest, provider_name: str | None = None) -> ContextPackage:
191
+ provider = self.get_provider(provider_name)
192
+ packager = provider.get_extension("package")
193
+ if packager is None or not hasattr(packager, "package"):
194
+ raise ProviderError(
195
+ f"Provider '{provider.name}' does not support the 'package' extension",
196
+ provider=provider.name,
197
+ context={"operation": "package"},
198
+ )
199
+ return await packager.package(request) # type: ignore[no-any-return]