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,198 @@
|
|
|
1
|
+
"""Async AtomicMemoryHandle root implementation.
|
|
2
|
+
|
|
3
|
+
Async counterpart of :mod:`atomicmemory.providers.atomicmemory.handle_impl`.
|
|
4
|
+
Reuses the same scope mappers, body construction, and response shaping
|
|
5
|
+
helpers; the only difference is awaiting the HTTP transport.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from atomicmemory.core.errors import ProviderError
|
|
17
|
+
from atomicmemory.providers.atomicmemory.agents import AsyncAtomicMemoryAgents
|
|
18
|
+
from atomicmemory.providers.atomicmemory.audit import AsyncAtomicMemoryAudit
|
|
19
|
+
from atomicmemory.providers.atomicmemory.config_handle import AsyncAtomicMemoryConfig
|
|
20
|
+
from atomicmemory.providers.atomicmemory.handle import (
|
|
21
|
+
AtomicMemoryIngestInput,
|
|
22
|
+
AtomicMemoryIngestResult,
|
|
23
|
+
AtomicMemoryListOptions,
|
|
24
|
+
AtomicMemoryListResultPage,
|
|
25
|
+
AtomicMemoryMemory,
|
|
26
|
+
AtomicMemorySearchRequest,
|
|
27
|
+
AtomicMemorySearchResultPage,
|
|
28
|
+
MemoryScope,
|
|
29
|
+
WorkspaceScope,
|
|
30
|
+
)
|
|
31
|
+
from atomicmemory.providers.atomicmemory.handle_impl import (
|
|
32
|
+
_assert_list_options_scope_compat,
|
|
33
|
+
_coerce_list_options,
|
|
34
|
+
_map_search_response,
|
|
35
|
+
_to_atomic_memory,
|
|
36
|
+
)
|
|
37
|
+
from atomicmemory.providers.atomicmemory.http import (
|
|
38
|
+
HttpOptions,
|
|
39
|
+
afetch_json,
|
|
40
|
+
afetch_json_or_none,
|
|
41
|
+
afetch_void,
|
|
42
|
+
)
|
|
43
|
+
from atomicmemory.providers.atomicmemory.lessons import AsyncAtomicMemoryLessons
|
|
44
|
+
from atomicmemory.providers.atomicmemory.lifecycle import AsyncAtomicMemoryLifecycle
|
|
45
|
+
from atomicmemory.providers.atomicmemory.scope_mapper import (
|
|
46
|
+
assert_scope_allows_visibility,
|
|
47
|
+
scope_to_fields,
|
|
48
|
+
scope_to_query_pairs,
|
|
49
|
+
strip_agent_scope,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
Route = Callable[[str], str]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AsyncAtomicMemoryHandle:
|
|
56
|
+
"""Async typed handle for AtomicMemory-specific routes."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, client: httpx.AsyncClient, http: HttpOptions, route: Route) -> None:
|
|
59
|
+
self._client = client
|
|
60
|
+
self._http = http
|
|
61
|
+
self._route = route
|
|
62
|
+
self.lifecycle = AsyncAtomicMemoryLifecycle(client, http, route)
|
|
63
|
+
self.audit = AsyncAtomicMemoryAudit(client, http, route)
|
|
64
|
+
self.lessons = AsyncAtomicMemoryLessons(client, http, route)
|
|
65
|
+
self.config = AsyncAtomicMemoryConfig(client, http, route)
|
|
66
|
+
self.agents = AsyncAtomicMemoryAgents(client, http, route)
|
|
67
|
+
|
|
68
|
+
async def ingest_full(self, input: AtomicMemoryIngestInput, scope: MemoryScope) -> AtomicMemoryIngestResult:
|
|
69
|
+
return await self._post_ingest(self._route("/memories/ingest"), input, scope)
|
|
70
|
+
|
|
71
|
+
async def ingest_quick(
|
|
72
|
+
self,
|
|
73
|
+
input: AtomicMemoryIngestInput,
|
|
74
|
+
scope: MemoryScope,
|
|
75
|
+
skip_extraction: bool = False,
|
|
76
|
+
) -> AtomicMemoryIngestResult:
|
|
77
|
+
return await self._post_ingest(
|
|
78
|
+
self._route("/memories/ingest/quick"), input, scope, skip_extraction=skip_extraction
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def search(self, request: AtomicMemorySearchRequest, scope: MemoryScope) -> AtomicMemorySearchResultPage:
|
|
82
|
+
return await self._post_search(self._route("/memories/search"), request, scope)
|
|
83
|
+
|
|
84
|
+
async def search_fast(self, request: AtomicMemorySearchRequest, scope: MemoryScope) -> AtomicMemorySearchResultPage:
|
|
85
|
+
return await self._post_search(self._route("/memories/search/fast"), request, scope)
|
|
86
|
+
|
|
87
|
+
async def expand(self, refs: list[str], scope: MemoryScope) -> list[AtomicMemoryMemory]:
|
|
88
|
+
body: dict[str, Any] = {**scope_to_fields(scope), "memory_ids": refs}
|
|
89
|
+
raw = await afetch_json(
|
|
90
|
+
self._client,
|
|
91
|
+
self._http,
|
|
92
|
+
self._route("/memories/expand"),
|
|
93
|
+
method="POST",
|
|
94
|
+
json=body,
|
|
95
|
+
)
|
|
96
|
+
echoed = strip_agent_scope(scope)
|
|
97
|
+
return [_to_atomic_memory(m, echoed) for m in raw.get("memories", [])]
|
|
98
|
+
|
|
99
|
+
async def list(
|
|
100
|
+
self,
|
|
101
|
+
scope: MemoryScope,
|
|
102
|
+
options: AtomicMemoryListOptions | dict[str, Any] | None = None,
|
|
103
|
+
) -> AtomicMemoryListResultPage:
|
|
104
|
+
opts = _coerce_list_options(options)
|
|
105
|
+
_assert_list_options_scope_compat(scope, opts)
|
|
106
|
+
pairs: list[tuple[str, str]] = scope_to_query_pairs(scope)
|
|
107
|
+
if opts.limit is not None:
|
|
108
|
+
pairs.append(("limit", str(opts.limit)))
|
|
109
|
+
if opts.offset is not None:
|
|
110
|
+
pairs.append(("offset", str(opts.offset)))
|
|
111
|
+
if opts.source_site:
|
|
112
|
+
pairs.append(("source_site", opts.source_site))
|
|
113
|
+
if opts.episode_id:
|
|
114
|
+
pairs.append(("episode_id", opts.episode_id))
|
|
115
|
+
path = self._route(f"/memories/list?{urlencode(pairs)}")
|
|
116
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
117
|
+
limit = opts.limit if opts.limit is not None else 20
|
|
118
|
+
offset = opts.offset if opts.offset is not None else 0
|
|
119
|
+
memories_raw = raw.get("memories", [])
|
|
120
|
+
next_offset = offset + len(memories_raw)
|
|
121
|
+
has_more = len(memories_raw) == limit
|
|
122
|
+
echoed = strip_agent_scope(scope)
|
|
123
|
+
return AtomicMemoryListResultPage(
|
|
124
|
+
memories=[_to_atomic_memory(m, echoed) for m in memories_raw],
|
|
125
|
+
count=raw.get("count", len(memories_raw)),
|
|
126
|
+
cursor=str(next_offset) if has_more else None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def get(self, id: str, scope: MemoryScope) -> AtomicMemoryMemory | None:
|
|
130
|
+
path = self._route(f"/memories/{quote(id, safe='')}?{urlencode(scope_to_query_pairs(scope))}")
|
|
131
|
+
raw = await afetch_json_or_none(self._client, self._http, path)
|
|
132
|
+
if raw is None:
|
|
133
|
+
return None
|
|
134
|
+
return _to_atomic_memory(raw, strip_agent_scope(scope))
|
|
135
|
+
|
|
136
|
+
async def delete(self, id: str, scope: MemoryScope) -> None:
|
|
137
|
+
path = self._route(f"/memories/{quote(id, safe='')}?{urlencode(scope_to_query_pairs(scope))}")
|
|
138
|
+
try:
|
|
139
|
+
await afetch_void(self._client, self._http, path, method="DELETE")
|
|
140
|
+
except ProviderError as exc:
|
|
141
|
+
if exc.status_code == 404:
|
|
142
|
+
return
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
async def _post_ingest(
|
|
146
|
+
self,
|
|
147
|
+
path: str,
|
|
148
|
+
input: AtomicMemoryIngestInput,
|
|
149
|
+
scope: MemoryScope,
|
|
150
|
+
*,
|
|
151
|
+
skip_extraction: bool = False,
|
|
152
|
+
) -> AtomicMemoryIngestResult:
|
|
153
|
+
assert_scope_allows_visibility(scope, input.visibility)
|
|
154
|
+
body: dict[str, Any] = {
|
|
155
|
+
**scope_to_fields(scope),
|
|
156
|
+
"conversation": input.conversation,
|
|
157
|
+
"source_site": input.source_site,
|
|
158
|
+
"source_url": input.source_url or "",
|
|
159
|
+
}
|
|
160
|
+
if isinstance(scope, WorkspaceScope) and input.visibility:
|
|
161
|
+
body["visibility"] = input.visibility
|
|
162
|
+
if input.config_override is not None:
|
|
163
|
+
body["config_override"] = input.config_override
|
|
164
|
+
if skip_extraction:
|
|
165
|
+
body["skip_extraction"] = True
|
|
166
|
+
raw = await afetch_json(self._client, self._http, path, method="POST", json=body)
|
|
167
|
+
return AtomicMemoryIngestResult.model_validate(raw)
|
|
168
|
+
|
|
169
|
+
async def _post_search(
|
|
170
|
+
self,
|
|
171
|
+
path: str,
|
|
172
|
+
request: AtomicMemorySearchRequest,
|
|
173
|
+
scope: MemoryScope,
|
|
174
|
+
) -> AtomicMemorySearchResultPage:
|
|
175
|
+
body: dict[str, Any] = {
|
|
176
|
+
**scope_to_fields(scope, include_agent_scope=True),
|
|
177
|
+
"query": request.query,
|
|
178
|
+
}
|
|
179
|
+
if request.limit is not None:
|
|
180
|
+
body["limit"] = request.limit
|
|
181
|
+
if request.threshold is not None:
|
|
182
|
+
body["threshold"] = request.threshold
|
|
183
|
+
if request.as_of is not None:
|
|
184
|
+
body["as_of"] = request.as_of.isoformat()
|
|
185
|
+
if request.retrieval_mode is not None:
|
|
186
|
+
body["retrieval_mode"] = request.retrieval_mode
|
|
187
|
+
if request.token_budget is not None:
|
|
188
|
+
body["token_budget"] = request.token_budget
|
|
189
|
+
if request.namespace_scope is not None:
|
|
190
|
+
body["namespace_scope"] = request.namespace_scope
|
|
191
|
+
if request.source_site is not None:
|
|
192
|
+
body["source_site"] = request.source_site
|
|
193
|
+
if request.skip_repair:
|
|
194
|
+
body["skip_repair"] = True
|
|
195
|
+
if request.config_override is not None:
|
|
196
|
+
body["config_override"] = request.config_override
|
|
197
|
+
raw = await afetch_json(self._client, self._http, path, method="POST", json=body)
|
|
198
|
+
return _map_search_response(raw, scope)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Async AtomicMemoryProvider — V3 core + Packager + TemporalSearch + Versioner + Health.
|
|
2
|
+
|
|
3
|
+
Async counterpart of `provider.py`. Uses :class:`httpx.AsyncClient` and
|
|
4
|
+
the ``a*`` HTTP helpers; body construction is delegated to the shared
|
|
5
|
+
private builders defined alongside the sync provider.
|
|
6
|
+
|
|
7
|
+
The ``atomicmemory.*`` handle namespace is wired in
|
|
8
|
+
``async_handle_impl.py``; this module owns provider lifecycle + the
|
|
9
|
+
``do_*`` overrides on :class:`BaseAsyncMemoryProvider`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from atomicmemory.core.errors import ProviderError
|
|
20
|
+
from atomicmemory.memory.provider import BaseAsyncMemoryProvider
|
|
21
|
+
from atomicmemory.memory.types import (
|
|
22
|
+
Capabilities,
|
|
23
|
+
CapabilitiesExtensions,
|
|
24
|
+
CapabilitiesRequiredScope,
|
|
25
|
+
ContextPackage,
|
|
26
|
+
CustomExtensionMeta,
|
|
27
|
+
HealthStatus,
|
|
28
|
+
IngestInput,
|
|
29
|
+
IngestResult,
|
|
30
|
+
ListRequest,
|
|
31
|
+
ListResultPage,
|
|
32
|
+
Memory,
|
|
33
|
+
MemoryRef,
|
|
34
|
+
MemoryVersion,
|
|
35
|
+
PackageRequest,
|
|
36
|
+
SearchRequest,
|
|
37
|
+
SearchResult,
|
|
38
|
+
SearchResultPage,
|
|
39
|
+
)
|
|
40
|
+
from atomicmemory.providers.atomicmemory.async_handle_impl import AsyncAtomicMemoryHandle
|
|
41
|
+
from atomicmemory.providers.atomicmemory.config import AtomicMemoryProviderConfig
|
|
42
|
+
from atomicmemory.providers.atomicmemory.handle import ATOMICMEMORY_EXTENSION_NAMES
|
|
43
|
+
from atomicmemory.providers.atomicmemory.http import (
|
|
44
|
+
HttpOptions,
|
|
45
|
+
adelete_ignore_404,
|
|
46
|
+
afetch_json,
|
|
47
|
+
afetch_json_or_none,
|
|
48
|
+
)
|
|
49
|
+
from atomicmemory.providers.atomicmemory.mappers import (
|
|
50
|
+
to_ingest_result,
|
|
51
|
+
to_memory,
|
|
52
|
+
to_memory_version,
|
|
53
|
+
to_search_result,
|
|
54
|
+
)
|
|
55
|
+
from atomicmemory.providers.atomicmemory.path import normalize_api_version
|
|
56
|
+
from atomicmemory.providers.atomicmemory.provider import (
|
|
57
|
+
_build_ingest_body,
|
|
58
|
+
_build_package_body,
|
|
59
|
+
_build_search_body,
|
|
60
|
+
_qs,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_ATOMICMEMORY_CUSTOM_EXTENSIONS: dict[str, CustomExtensionMeta] = {
|
|
64
|
+
name: CustomExtensionMeta(version="1.0.0") for name in ATOMICMEMORY_EXTENSION_NAMES
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AsyncAtomicMemoryProvider(BaseAsyncMemoryProvider):
|
|
69
|
+
"""Async HTTP-backed V3 provider for atomicmemory-core."""
|
|
70
|
+
|
|
71
|
+
name = "atomicmemory"
|
|
72
|
+
|
|
73
|
+
def __init__(self, config: AtomicMemoryProviderConfig) -> None:
|
|
74
|
+
self._config = config
|
|
75
|
+
self._http_options = HttpOptions(
|
|
76
|
+
api_url=config.api_url.rstrip("/"),
|
|
77
|
+
api_key=config.api_key,
|
|
78
|
+
timeout_seconds=config.timeout_seconds,
|
|
79
|
+
)
|
|
80
|
+
self._api_prefix = normalize_api_version(config.api_version)
|
|
81
|
+
self._client: httpx.AsyncClient | None = None
|
|
82
|
+
self._handle: AsyncAtomicMemoryHandle | None = None
|
|
83
|
+
self._initialized = False
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Lifecycle
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
async def initialize(self) -> None:
|
|
90
|
+
if self._client is None:
|
|
91
|
+
self._client = httpx.AsyncClient()
|
|
92
|
+
if self._handle is None:
|
|
93
|
+
self._handle = AsyncAtomicMemoryHandle(self._client, self._http_options, self._route)
|
|
94
|
+
self._initialized = True
|
|
95
|
+
|
|
96
|
+
async def close(self) -> None:
|
|
97
|
+
self._handle = None
|
|
98
|
+
if self._client is not None:
|
|
99
|
+
await self._client.aclose()
|
|
100
|
+
self._client = None
|
|
101
|
+
self._initialized = False
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# V3 core methods
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
async def do_ingest(self, input: IngestInput) -> IngestResult:
|
|
108
|
+
body = _build_ingest_body(input)
|
|
109
|
+
path = self._route("/memories/ingest/quick" if input.mode == "verbatim" else "/memories/ingest")
|
|
110
|
+
raw = await afetch_json(self._require_client(), self._http_options, path, method="POST", json=body)
|
|
111
|
+
return to_ingest_result(raw)
|
|
112
|
+
|
|
113
|
+
async def do_search(self, request: SearchRequest) -> SearchResultPage:
|
|
114
|
+
body = _build_search_body(request)
|
|
115
|
+
raw = await afetch_json(
|
|
116
|
+
self._require_client(),
|
|
117
|
+
self._http_options,
|
|
118
|
+
self._route("/memories/search/fast"),
|
|
119
|
+
method="POST",
|
|
120
|
+
json=body,
|
|
121
|
+
)
|
|
122
|
+
return SearchResultPage(
|
|
123
|
+
results=[to_search_result(m, request.scope) for m in raw.get("memories", [])],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def do_get(self, ref: MemoryRef) -> Memory | None:
|
|
127
|
+
path = self._route(f"/memories/{ref.id}?user_id={_qs(ref.scope.user)}")
|
|
128
|
+
raw = await afetch_json_or_none(self._require_client(), self._http_options, path)
|
|
129
|
+
if raw is None:
|
|
130
|
+
return None
|
|
131
|
+
return to_memory(raw, ref.scope)
|
|
132
|
+
|
|
133
|
+
async def do_delete(self, ref: MemoryRef) -> None:
|
|
134
|
+
path = self._route(f"/memories/{ref.id}?user_id={_qs(ref.scope.user)}")
|
|
135
|
+
await adelete_ignore_404(self._require_client(), self._http_options, path)
|
|
136
|
+
|
|
137
|
+
async def do_list(self, request: ListRequest) -> ListResultPage:
|
|
138
|
+
offset = int(request.cursor) if request.cursor else 0
|
|
139
|
+
limit = request.limit if request.limit is not None else 20
|
|
140
|
+
path = self._route(f"/memories/list?user_id={_qs(request.scope.user)}&limit={limit}&offset={offset}")
|
|
141
|
+
raw = await afetch_json(self._require_client(), self._http_options, path)
|
|
142
|
+
memories = [to_memory(m, request.scope) for m in raw.get("memories", [])]
|
|
143
|
+
next_offset = offset + len(memories)
|
|
144
|
+
cursor = str(next_offset) if len(memories) == limit else None
|
|
145
|
+
return ListResultPage(memories=memories, cursor=cursor)
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# Capabilities + extension dispatch
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def capabilities(self) -> Capabilities:
|
|
152
|
+
return Capabilities(
|
|
153
|
+
ingest_modes=["text", "messages", "verbatim"],
|
|
154
|
+
required_scope=CapabilitiesRequiredScope(default=["user"]),
|
|
155
|
+
extensions=CapabilitiesExtensions(
|
|
156
|
+
package=True,
|
|
157
|
+
temporal=True,
|
|
158
|
+
versioning=True,
|
|
159
|
+
health=True,
|
|
160
|
+
),
|
|
161
|
+
custom_extensions=_ATOMICMEMORY_CUSTOM_EXTENSIONS,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def get_extension(self, name: str) -> Any | None:
|
|
165
|
+
if name in {"package", "temporal", "versioning", "health"}:
|
|
166
|
+
return self
|
|
167
|
+
if name == "atomicmemory.base":
|
|
168
|
+
return self._handle
|
|
169
|
+
if name == "atomicmemory.lifecycle" and self._handle is not None:
|
|
170
|
+
return self._handle.lifecycle
|
|
171
|
+
if name == "atomicmemory.audit" and self._handle is not None:
|
|
172
|
+
return self._handle.audit
|
|
173
|
+
if name == "atomicmemory.lessons" and self._handle is not None:
|
|
174
|
+
return self._handle.lessons
|
|
175
|
+
if name == "atomicmemory.config" and self._handle is not None:
|
|
176
|
+
return self._handle.config
|
|
177
|
+
if name == "atomicmemory.agents" and self._handle is not None:
|
|
178
|
+
return self._handle.agents
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
# V3 extensions implemented inline
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
async def package(self, request: PackageRequest) -> ContextPackage:
|
|
186
|
+
body = _build_package_body(request)
|
|
187
|
+
raw = await afetch_json(
|
|
188
|
+
self._require_client(),
|
|
189
|
+
self._http_options,
|
|
190
|
+
self._route("/memories/search"),
|
|
191
|
+
method="POST",
|
|
192
|
+
json=body,
|
|
193
|
+
)
|
|
194
|
+
results: list[SearchResult] = [to_search_result(m, request.scope) for m in raw.get("memories", [])]
|
|
195
|
+
budget_constrained = raw.get("budget_constrained")
|
|
196
|
+
if not isinstance(budget_constrained, bool):
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"atomicmemory async provider.package: backend response missing required boolean "
|
|
199
|
+
"field `budget_constrained`"
|
|
200
|
+
)
|
|
201
|
+
return ContextPackage(
|
|
202
|
+
text=raw.get("injection_text") or "",
|
|
203
|
+
results=results,
|
|
204
|
+
tokens=raw.get("estimated_context_tokens") or 0,
|
|
205
|
+
budget_constrained=budget_constrained,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
async def search_as_of(self, request: SearchRequest, as_of: datetime) -> SearchResultPage:
|
|
209
|
+
body = _build_search_body(request)
|
|
210
|
+
body["as_of"] = as_of.isoformat()
|
|
211
|
+
raw = await afetch_json(
|
|
212
|
+
self._require_client(),
|
|
213
|
+
self._http_options,
|
|
214
|
+
self._route("/memories/search"),
|
|
215
|
+
method="POST",
|
|
216
|
+
json=body,
|
|
217
|
+
)
|
|
218
|
+
return SearchResultPage(
|
|
219
|
+
results=[to_search_result(m, request.scope) for m in raw.get("memories", [])],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def history(self, ref: MemoryRef) -> list[MemoryVersion]:
|
|
223
|
+
path = self._route(f"/memories/{ref.id}/audit?user_id={_qs(ref.scope.user)}")
|
|
224
|
+
raw = await afetch_json(self._require_client(), self._http_options, path)
|
|
225
|
+
return [to_memory_version(entry) for entry in raw.get("trail", [])]
|
|
226
|
+
|
|
227
|
+
async def health(self) -> HealthStatus:
|
|
228
|
+
path = self._route("/memories/health")
|
|
229
|
+
raw = await afetch_json(self._require_client(), self._http_options, path)
|
|
230
|
+
return HealthStatus(ok=raw.get("status") == "ok")
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# Internals
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def _route(self, path: str) -> str:
|
|
237
|
+
return f"{self._api_prefix}{path}"
|
|
238
|
+
|
|
239
|
+
def _require_client(self) -> httpx.AsyncClient:
|
|
240
|
+
if self._client is None:
|
|
241
|
+
raise ProviderError(
|
|
242
|
+
"AsyncAtomicMemoryProvider is not initialized. Call initialize() first.",
|
|
243
|
+
provider=self.name,
|
|
244
|
+
)
|
|
245
|
+
return self._client
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""AtomicMemoryAudit — mutation audit category.
|
|
2
|
+
|
|
3
|
+
Port of the audit section of
|
|
4
|
+
`atomicmemory-sdk/src/memory/atomicmemory-provider/handle-impl.ts:720-880`.
|
|
5
|
+
All routes are user-scoped.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from atomicmemory.providers.atomicmemory.handle import (
|
|
16
|
+
AuditTrailResult,
|
|
17
|
+
MutationSummary,
|
|
18
|
+
RecentMutationsResult,
|
|
19
|
+
)
|
|
20
|
+
from atomicmemory.providers.atomicmemory.http import HttpOptions, afetch_json, fetch_json
|
|
21
|
+
|
|
22
|
+
Route = Callable[[str], str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AtomicMemoryAudit:
|
|
26
|
+
"""Audit-trail accessors for a user's claim-version history."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: httpx.Client, http: HttpOptions, route: Route) -> None:
|
|
29
|
+
self._client = client
|
|
30
|
+
self._http = http
|
|
31
|
+
self._route = route
|
|
32
|
+
|
|
33
|
+
def summary(self, user_id: str) -> MutationSummary:
|
|
34
|
+
path = self._route(f"/memories/audit/summary?user_id={quote(user_id, safe='')}")
|
|
35
|
+
raw = fetch_json(self._client, self._http, path)
|
|
36
|
+
return MutationSummary.model_validate(raw)
|
|
37
|
+
|
|
38
|
+
def recent(self, user_id: str, limit: int | None = None) -> RecentMutationsResult:
|
|
39
|
+
url = f"/memories/audit/recent?user_id={quote(user_id, safe='')}"
|
|
40
|
+
if limit is not None:
|
|
41
|
+
url += f"&limit={limit}"
|
|
42
|
+
raw = fetch_json(self._client, self._http, self._route(url))
|
|
43
|
+
return RecentMutationsResult.model_validate(raw)
|
|
44
|
+
|
|
45
|
+
def trail(self, memory_id: str, user_id: str) -> AuditTrailResult:
|
|
46
|
+
path = self._route(f"/memories/{quote(memory_id, safe='')}/audit?user_id={quote(user_id, safe='')}")
|
|
47
|
+
raw = fetch_json(self._client, self._http, path)
|
|
48
|
+
return AuditTrailResult.model_validate(raw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AsyncAtomicMemoryAudit:
|
|
52
|
+
"""Async counterpart of :class:`AtomicMemoryAudit`."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client: httpx.AsyncClient, http: HttpOptions, route: Route) -> None:
|
|
55
|
+
self._client = client
|
|
56
|
+
self._http = http
|
|
57
|
+
self._route = route
|
|
58
|
+
|
|
59
|
+
async def summary(self, user_id: str) -> MutationSummary:
|
|
60
|
+
path = self._route(f"/memories/audit/summary?user_id={quote(user_id, safe='')}")
|
|
61
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
62
|
+
return MutationSummary.model_validate(raw)
|
|
63
|
+
|
|
64
|
+
async def recent(self, user_id: str, limit: int | None = None) -> RecentMutationsResult:
|
|
65
|
+
url = f"/memories/audit/recent?user_id={quote(user_id, safe='')}"
|
|
66
|
+
if limit is not None:
|
|
67
|
+
url += f"&limit={limit}"
|
|
68
|
+
raw = await afetch_json(self._client, self._http, self._route(url))
|
|
69
|
+
return RecentMutationsResult.model_validate(raw)
|
|
70
|
+
|
|
71
|
+
async def trail(self, memory_id: str, user_id: str) -> AuditTrailResult:
|
|
72
|
+
path = self._route(f"/memories/{quote(memory_id, safe='')}/audit?user_id={quote(user_id, safe='')}")
|
|
73
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
74
|
+
return AuditTrailResult.model_validate(raw)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AtomicMemory provider configuration.
|
|
2
|
+
|
|
3
|
+
Port of `atomicmemory-sdk/src/memory/atomicmemory-provider/types.ts:1-36`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
ATOMICMEMORY_DEFAULT_TIMEOUT_SECONDS: float = 30.0
|
|
11
|
+
"""Default request timeout (seconds). Mirrors TS ``ATOMICMEMORY_DEFAULT_TIMEOUT`` (ms)."""
|
|
12
|
+
|
|
13
|
+
ATOMICMEMORY_DEFAULT_API_VERSION: str = "v1"
|
|
14
|
+
"""Matches core's mount at `atomicmemory-core/src/app/create-app.ts:31-32`."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AtomicMemoryProviderConfig(BaseModel):
|
|
18
|
+
"""Inputs to construct an AtomicMemoryProvider."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
21
|
+
|
|
22
|
+
api_url: str = Field(alias="apiUrl")
|
|
23
|
+
"""Base URL of the atomicmemory-core instance, e.g. ``http://localhost:3050``."""
|
|
24
|
+
|
|
25
|
+
api_key: str | None = Field(default=None, alias="apiKey")
|
|
26
|
+
"""Optional bearer token forwarded as ``Authorization: Bearer <api_key>``."""
|
|
27
|
+
|
|
28
|
+
timeout_seconds: float = Field(
|
|
29
|
+
default=ATOMICMEMORY_DEFAULT_TIMEOUT_SECONDS,
|
|
30
|
+
alias="timeoutSeconds",
|
|
31
|
+
)
|
|
32
|
+
"""Per-request timeout (seconds). Default 30s. Pre-port note: TS uses ms."""
|
|
33
|
+
|
|
34
|
+
api_version: str = Field(
|
|
35
|
+
default=ATOMICMEMORY_DEFAULT_API_VERSION,
|
|
36
|
+
alias="apiVersion",
|
|
37
|
+
)
|
|
38
|
+
"""API-version segment prepended to every route path (e.g. ``v1`` → ``/v1/...``)."""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""AtomicMemoryConfig — runtime config category.
|
|
2
|
+
|
|
3
|
+
Port of the config section of
|
|
4
|
+
`atomicmemory-sdk/src/memory/atomicmemory-provider/handle-impl.ts:1008-1098`.
|
|
5
|
+
``health`` is global; ``update_config`` is gated on
|
|
6
|
+
``CORE_RUNTIME_CONFIG_MUTATION_ENABLED`` server-side.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from atomicmemory.providers.atomicmemory.handle import (
|
|
18
|
+
AtomicMemoryHealthStatus,
|
|
19
|
+
ConfigUpdateResult,
|
|
20
|
+
ConfigUpdates,
|
|
21
|
+
HealthConfig,
|
|
22
|
+
)
|
|
23
|
+
from atomicmemory.providers.atomicmemory.http import HttpOptions, afetch_json, fetch_json
|
|
24
|
+
|
|
25
|
+
Route = Callable[[str], str]
|
|
26
|
+
|
|
27
|
+
_SNAKE_TO_CAMEL_RE = re.compile(r"_([a-z])")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _snake_to_camel(value: str) -> str:
|
|
31
|
+
"""Convert a snake_case identifier to camelCase.
|
|
32
|
+
|
|
33
|
+
Mirrors TS handle-impl.ts: applied field names are echoed back to
|
|
34
|
+
Python in the same camelCase the TS SDK uses. Pure cosmetic; the
|
|
35
|
+
actual config payload is already mapped via Pydantic.
|
|
36
|
+
"""
|
|
37
|
+
return _SNAKE_TO_CAMEL_RE.sub(lambda m: m.group(1).upper(), value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AtomicMemoryConfig:
|
|
41
|
+
"""Runtime configuration accessors."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, client: httpx.Client, http: HttpOptions, route: Route) -> None:
|
|
44
|
+
self._client = client
|
|
45
|
+
self._http = http
|
|
46
|
+
self._route = route
|
|
47
|
+
|
|
48
|
+
def health(self) -> AtomicMemoryHealthStatus:
|
|
49
|
+
raw = fetch_json(self._client, self._http, self._route("/memories/health"))
|
|
50
|
+
return AtomicMemoryHealthStatus.model_validate(
|
|
51
|
+
{"status": raw.get("status", "ok"), "config": HealthConfig.model_validate(raw["config"])}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def update_config(self, updates: ConfigUpdates | dict[str, Any]) -> ConfigUpdateResult:
|
|
55
|
+
if isinstance(updates, dict):
|
|
56
|
+
updates = ConfigUpdates.model_validate(updates)
|
|
57
|
+
body: dict[str, Any] = {}
|
|
58
|
+
if updates.similarity_threshold is not None:
|
|
59
|
+
body["similarity_threshold"] = updates.similarity_threshold
|
|
60
|
+
if updates.audn_candidate_threshold is not None:
|
|
61
|
+
body["audn_candidate_threshold"] = updates.audn_candidate_threshold
|
|
62
|
+
if updates.clarification_conflict_threshold is not None:
|
|
63
|
+
body["clarification_conflict_threshold"] = updates.clarification_conflict_threshold
|
|
64
|
+
if updates.max_search_results is not None:
|
|
65
|
+
body["max_search_results"] = updates.max_search_results
|
|
66
|
+
raw = fetch_json(
|
|
67
|
+
self._client,
|
|
68
|
+
self._http,
|
|
69
|
+
self._route("/memories/config"),
|
|
70
|
+
method="PUT",
|
|
71
|
+
json=body,
|
|
72
|
+
)
|
|
73
|
+
applied = [_snake_to_camel(name) for name in raw.get("applied", [])]
|
|
74
|
+
return ConfigUpdateResult.model_validate(
|
|
75
|
+
{
|
|
76
|
+
"applied": applied,
|
|
77
|
+
"config": HealthConfig.model_validate(raw["config"]),
|
|
78
|
+
"note": raw.get("note", ""),
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AsyncAtomicMemoryConfig:
|
|
84
|
+
"""Async counterpart of :class:`AtomicMemoryConfig`."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, client: httpx.AsyncClient, http: HttpOptions, route: Route) -> None:
|
|
87
|
+
self._client = client
|
|
88
|
+
self._http = http
|
|
89
|
+
self._route = route
|
|
90
|
+
|
|
91
|
+
async def health(self) -> AtomicMemoryHealthStatus:
|
|
92
|
+
raw = await afetch_json(self._client, self._http, self._route("/memories/health"))
|
|
93
|
+
return AtomicMemoryHealthStatus.model_validate(
|
|
94
|
+
{"status": raw.get("status", "ok"), "config": HealthConfig.model_validate(raw["config"])}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def update_config(self, updates: ConfigUpdates | dict[str, Any]) -> ConfigUpdateResult:
|
|
98
|
+
if isinstance(updates, dict):
|
|
99
|
+
updates = ConfigUpdates.model_validate(updates)
|
|
100
|
+
body: dict[str, Any] = {}
|
|
101
|
+
if updates.similarity_threshold is not None:
|
|
102
|
+
body["similarity_threshold"] = updates.similarity_threshold
|
|
103
|
+
if updates.audn_candidate_threshold is not None:
|
|
104
|
+
body["audn_candidate_threshold"] = updates.audn_candidate_threshold
|
|
105
|
+
if updates.clarification_conflict_threshold is not None:
|
|
106
|
+
body["clarification_conflict_threshold"] = updates.clarification_conflict_threshold
|
|
107
|
+
if updates.max_search_results is not None:
|
|
108
|
+
body["max_search_results"] = updates.max_search_results
|
|
109
|
+
raw = await afetch_json(
|
|
110
|
+
self._client,
|
|
111
|
+
self._http,
|
|
112
|
+
self._route("/memories/config"),
|
|
113
|
+
method="PUT",
|
|
114
|
+
json=body,
|
|
115
|
+
)
|
|
116
|
+
applied = [_snake_to_camel(name) for name in raw.get("applied", [])]
|
|
117
|
+
return ConfigUpdateResult.model_validate(
|
|
118
|
+
{
|
|
119
|
+
"applied": applied,
|
|
120
|
+
"config": HealthConfig.model_validate(raw["config"]),
|
|
121
|
+
"note": raw.get("note", ""),
|
|
122
|
+
}
|
|
123
|
+
)
|