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.
- atomicmemory/__init__.py +166 -0
- atomicmemory/_version.py +3 -0
- atomicmemory/client/__init__.py +22 -0
- atomicmemory/client/async_memory_client.py +202 -0
- atomicmemory/client/atomic_memory_client.py +181 -0
- atomicmemory/client/memory_client.py +292 -0
- atomicmemory/core/__init__.py +34 -0
- atomicmemory/core/errors.py +122 -0
- atomicmemory/core/events.py +65 -0
- atomicmemory/core/logging.py +37 -0
- atomicmemory/core/retry.py +124 -0
- atomicmemory/core/validation.py +22 -0
- atomicmemory/embeddings/__init__.py +16 -0
- atomicmemory/embeddings/base.py +39 -0
- atomicmemory/embeddings/sentence_transformers.py +104 -0
- atomicmemory/kv_cache/__init__.py +17 -0
- atomicmemory/kv_cache/adapter.py +50 -0
- atomicmemory/kv_cache/memory_storage.py +98 -0
- atomicmemory/kv_cache/sqlite_storage.py +122 -0
- atomicmemory/memory/__init__.py +82 -0
- atomicmemory/memory/filters.py +68 -0
- atomicmemory/memory/pipeline.py +42 -0
- atomicmemory/memory/provider.py +397 -0
- atomicmemory/memory/registry.py +95 -0
- atomicmemory/memory/service.py +199 -0
- atomicmemory/memory/types.py +398 -0
- atomicmemory/providers/__init__.py +5 -0
- atomicmemory/providers/atomicmemory/__init__.py +43 -0
- atomicmemory/providers/atomicmemory/agents.py +156 -0
- atomicmemory/providers/atomicmemory/async_handle_impl.py +198 -0
- atomicmemory/providers/atomicmemory/async_provider.py +245 -0
- atomicmemory/providers/atomicmemory/audit.py +74 -0
- atomicmemory/providers/atomicmemory/config.py +38 -0
- atomicmemory/providers/atomicmemory/config_handle.py +123 -0
- atomicmemory/providers/atomicmemory/handle.py +513 -0
- atomicmemory/providers/atomicmemory/handle_impl.py +325 -0
- atomicmemory/providers/atomicmemory/http.py +255 -0
- atomicmemory/providers/atomicmemory/lessons.py +133 -0
- atomicmemory/providers/atomicmemory/lifecycle.py +202 -0
- atomicmemory/providers/atomicmemory/mappers.py +125 -0
- atomicmemory/providers/atomicmemory/path.py +20 -0
- atomicmemory/providers/atomicmemory/provider.py +300 -0
- atomicmemory/providers/atomicmemory/scope_mapper.py +98 -0
- atomicmemory/providers/mem0/__init__.py +41 -0
- atomicmemory/providers/mem0/async_provider.py +191 -0
- atomicmemory/providers/mem0/config.py +51 -0
- atomicmemory/providers/mem0/http.py +195 -0
- atomicmemory/providers/mem0/mappers.py +145 -0
- atomicmemory/providers/mem0/provider.py +202 -0
- atomicmemory/py.typed +0 -0
- atomicmemory/search/__init__.py +47 -0
- atomicmemory/search/chunking.py +161 -0
- atomicmemory/search/ranking.py +94 -0
- atomicmemory/search/semantic_search.py +130 -0
- atomicmemory/search/similarity.py +110 -0
- atomicmemory/storage/__init__.py +63 -0
- atomicmemory/storage/_mapping.py +305 -0
- atomicmemory/storage/async_client.py +208 -0
- atomicmemory/storage/client.py +339 -0
- atomicmemory/storage/errors.py +115 -0
- atomicmemory/storage/types.py +305 -0
- atomicmemory/utils/__init__.py +5 -0
- atomicmemory/utils/environment.py +23 -0
- atomicmemory-1.0.0.dist-info/METADATA +146 -0
- atomicmemory-1.0.0.dist-info/RECORD +67 -0
- atomicmemory-1.0.0.dist-info/WHEEL +4 -0
- 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]
|