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,133 @@
|
|
|
1
|
+
"""AtomicMemoryLessons — lessons category.
|
|
2
|
+
|
|
3
|
+
Port of the lessons section of
|
|
4
|
+
`atomicmemory-sdk/src/memory/atomicmemory-provider/handle-impl.ts:895-993`.
|
|
5
|
+
All routes are user-scoped.
|
|
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
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from atomicmemory.providers.atomicmemory.handle import (
|
|
17
|
+
Lesson,
|
|
18
|
+
LessonSeverity,
|
|
19
|
+
LessonsListResult,
|
|
20
|
+
LessonStats,
|
|
21
|
+
ReportLessonResult,
|
|
22
|
+
)
|
|
23
|
+
from atomicmemory.providers.atomicmemory.http import (
|
|
24
|
+
HttpOptions,
|
|
25
|
+
afetch_json,
|
|
26
|
+
afetch_void,
|
|
27
|
+
fetch_json,
|
|
28
|
+
fetch_void,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Route = Callable[[str], str]
|
|
32
|
+
|
|
33
|
+
# `Lessons.list` shadows the `list` builtin inside the class body, so
|
|
34
|
+
# `list[str]` annotations there refer to the method. Module-level alias.
|
|
35
|
+
_StrList = list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AtomicMemoryLessons:
|
|
39
|
+
"""Lesson list / report / delete operations for a user."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: httpx.Client, http: HttpOptions, route: Route) -> None:
|
|
42
|
+
self._client = client
|
|
43
|
+
self._http = http
|
|
44
|
+
self._route = route
|
|
45
|
+
|
|
46
|
+
def list(self, user_id: str) -> LessonsListResult:
|
|
47
|
+
path = self._route(f"/memories/lessons?user_id={quote(user_id, safe='')}")
|
|
48
|
+
raw = fetch_json(self._client, self._http, path)
|
|
49
|
+
return LessonsListResult.model_validate(
|
|
50
|
+
{
|
|
51
|
+
"lessons": [Lesson.model_validate(row) for row in raw.get("lessons", [])],
|
|
52
|
+
"count": raw.get("count", 0),
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def stats(self, user_id: str) -> LessonStats:
|
|
57
|
+
path = self._route(f"/memories/lessons/stats?user_id={quote(user_id, safe='')}")
|
|
58
|
+
raw = fetch_json(self._client, self._http, path)
|
|
59
|
+
return LessonStats.model_validate(raw)
|
|
60
|
+
|
|
61
|
+
def report(
|
|
62
|
+
self,
|
|
63
|
+
user_id: str,
|
|
64
|
+
pattern: str,
|
|
65
|
+
sources: _StrList | None = None,
|
|
66
|
+
severity: LessonSeverity | None = None,
|
|
67
|
+
) -> ReportLessonResult:
|
|
68
|
+
body: dict[str, Any] = {"user_id": user_id, "pattern": pattern}
|
|
69
|
+
if sources:
|
|
70
|
+
body["source_memory_ids"] = sources
|
|
71
|
+
if severity is not None:
|
|
72
|
+
body["severity"] = severity
|
|
73
|
+
raw = fetch_json(
|
|
74
|
+
self._client,
|
|
75
|
+
self._http,
|
|
76
|
+
self._route("/memories/lessons/report"),
|
|
77
|
+
method="POST",
|
|
78
|
+
json=body,
|
|
79
|
+
)
|
|
80
|
+
return ReportLessonResult.model_validate(raw)
|
|
81
|
+
|
|
82
|
+
def delete(self, lesson_id: str, user_id: str) -> None:
|
|
83
|
+
path = self._route(f"/memories/lessons/{quote(lesson_id, safe='')}?user_id={quote(user_id, safe='')}")
|
|
84
|
+
fetch_void(self._client, self._http, path, method="DELETE")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AsyncAtomicMemoryLessons:
|
|
88
|
+
"""Async counterpart of :class:`AtomicMemoryLessons`."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, client: httpx.AsyncClient, http: HttpOptions, route: Route) -> None:
|
|
91
|
+
self._client = client
|
|
92
|
+
self._http = http
|
|
93
|
+
self._route = route
|
|
94
|
+
|
|
95
|
+
async def list(self, user_id: str) -> LessonsListResult:
|
|
96
|
+
path = self._route(f"/memories/lessons?user_id={quote(user_id, safe='')}")
|
|
97
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
98
|
+
return LessonsListResult.model_validate(
|
|
99
|
+
{
|
|
100
|
+
"lessons": [Lesson.model_validate(row) for row in raw.get("lessons", [])],
|
|
101
|
+
"count": raw.get("count", 0),
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def stats(self, user_id: str) -> LessonStats:
|
|
106
|
+
path = self._route(f"/memories/lessons/stats?user_id={quote(user_id, safe='')}")
|
|
107
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
108
|
+
return LessonStats.model_validate(raw)
|
|
109
|
+
|
|
110
|
+
async def report(
|
|
111
|
+
self,
|
|
112
|
+
user_id: str,
|
|
113
|
+
pattern: str,
|
|
114
|
+
sources: _StrList | None = None,
|
|
115
|
+
severity: LessonSeverity | None = None,
|
|
116
|
+
) -> ReportLessonResult:
|
|
117
|
+
body: dict[str, Any] = {"user_id": user_id, "pattern": pattern}
|
|
118
|
+
if sources:
|
|
119
|
+
body["source_memory_ids"] = sources
|
|
120
|
+
if severity is not None:
|
|
121
|
+
body["severity"] = severity
|
|
122
|
+
raw = await afetch_json(
|
|
123
|
+
self._client,
|
|
124
|
+
self._http,
|
|
125
|
+
self._route("/memories/lessons/report"),
|
|
126
|
+
method="POST",
|
|
127
|
+
json=body,
|
|
128
|
+
)
|
|
129
|
+
return ReportLessonResult.model_validate(raw)
|
|
130
|
+
|
|
131
|
+
async def delete(self, lesson_id: str, user_id: str) -> None:
|
|
132
|
+
path = self._route(f"/memories/lessons/{quote(lesson_id, safe='')}?user_id={quote(user_id, safe='')}")
|
|
133
|
+
await afetch_void(self._client, self._http, path, method="DELETE")
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""AtomicMemoryLifecycle — admin lifecycle category.
|
|
2
|
+
|
|
3
|
+
Port of the lifecycle section of
|
|
4
|
+
`atomicmemory-sdk/src/memory/atomicmemory-provider/handle-impl.ts:474-564`.
|
|
5
|
+
All routes are user-scoped per core (no workspace_id / agent_id
|
|
6
|
+
accepted); cross-workspace admin lives elsewhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from atomicmemory.providers.atomicmemory.handle import (
|
|
17
|
+
CapCheckResult,
|
|
18
|
+
ConsolidationExecutionResult,
|
|
19
|
+
ConsolidationResult,
|
|
20
|
+
ConsolidationScanResult,
|
|
21
|
+
DecayResult,
|
|
22
|
+
ReconcileStatus,
|
|
23
|
+
ReconciliationResult,
|
|
24
|
+
ResetSourceResult,
|
|
25
|
+
StatsResult,
|
|
26
|
+
)
|
|
27
|
+
from atomicmemory.providers.atomicmemory.http import HttpOptions, afetch_json, fetch_json
|
|
28
|
+
|
|
29
|
+
Route = Callable[[str], str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _to_consolidation_result(raw: dict[str, Any]) -> ConsolidationResult:
|
|
33
|
+
if "consolidated_memory_ids" in raw:
|
|
34
|
+
return ConsolidationExecutionResult.model_validate(raw)
|
|
35
|
+
return ConsolidationScanResult.model_validate(raw)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AtomicMemoryLifecycle:
|
|
39
|
+
"""Lifecycle admin operations for a user."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: httpx.Client, http: HttpOptions, route: Route) -> None:
|
|
42
|
+
self._client = client
|
|
43
|
+
self._http = http
|
|
44
|
+
self._route = route
|
|
45
|
+
|
|
46
|
+
def consolidate(self, user_id: str, execute: bool = False) -> ConsolidationResult:
|
|
47
|
+
body: dict[str, Any] = {"user_id": user_id}
|
|
48
|
+
if execute:
|
|
49
|
+
body["execute"] = True
|
|
50
|
+
raw = fetch_json(
|
|
51
|
+
self._client,
|
|
52
|
+
self._http,
|
|
53
|
+
self._route("/memories/consolidate"),
|
|
54
|
+
method="POST",
|
|
55
|
+
json=body,
|
|
56
|
+
)
|
|
57
|
+
return _to_consolidation_result(raw)
|
|
58
|
+
|
|
59
|
+
def decay(self, user_id: str, dry_run: bool = True) -> DecayResult:
|
|
60
|
+
body: dict[str, Any] = {"user_id": user_id}
|
|
61
|
+
# Core treats dry_run as true unless explicitly false; only forward
|
|
62
|
+
# the flag when the caller opted into a non-dry pass.
|
|
63
|
+
if dry_run is False:
|
|
64
|
+
body["dry_run"] = False
|
|
65
|
+
raw = fetch_json(
|
|
66
|
+
self._client,
|
|
67
|
+
self._http,
|
|
68
|
+
self._route("/memories/decay"),
|
|
69
|
+
method="POST",
|
|
70
|
+
json=body,
|
|
71
|
+
)
|
|
72
|
+
return DecayResult.model_validate(raw)
|
|
73
|
+
|
|
74
|
+
def cap(self, user_id: str) -> CapCheckResult:
|
|
75
|
+
path = self._route(f"/memories/cap?user_id={user_id}")
|
|
76
|
+
raw = fetch_json(self._client, self._http, path)
|
|
77
|
+
return CapCheckResult.model_validate(raw)
|
|
78
|
+
|
|
79
|
+
def stats(self, user_id: str) -> StatsResult:
|
|
80
|
+
path = self._route(f"/memories/stats?user_id={user_id}")
|
|
81
|
+
raw = fetch_json(self._client, self._http, path)
|
|
82
|
+
return StatsResult.model_validate(raw)
|
|
83
|
+
|
|
84
|
+
def reset_source(self, user_id: str, source_site: str) -> ResetSourceResult:
|
|
85
|
+
raw = fetch_json(
|
|
86
|
+
self._client,
|
|
87
|
+
self._http,
|
|
88
|
+
self._route("/memories/reset-source"),
|
|
89
|
+
method="POST",
|
|
90
|
+
json={"user_id": user_id, "source_site": source_site},
|
|
91
|
+
)
|
|
92
|
+
return ResetSourceResult.model_validate(raw)
|
|
93
|
+
|
|
94
|
+
def reconcile(self, user_id: str) -> ReconciliationResult:
|
|
95
|
+
raw = fetch_json(
|
|
96
|
+
self._client,
|
|
97
|
+
self._http,
|
|
98
|
+
self._route("/memories/reconcile"),
|
|
99
|
+
method="POST",
|
|
100
|
+
json={"user_id": user_id},
|
|
101
|
+
)
|
|
102
|
+
return ReconciliationResult.model_validate(raw)
|
|
103
|
+
|
|
104
|
+
def reconcile_all(self) -> ReconciliationResult:
|
|
105
|
+
"""Run reconciliation across every user (privileged batch pass).
|
|
106
|
+
|
|
107
|
+
Maps to the no-``user_id`` branch of core's ``POST
|
|
108
|
+
/memories/reconcile`` route.
|
|
109
|
+
"""
|
|
110
|
+
raw = fetch_json(
|
|
111
|
+
self._client,
|
|
112
|
+
self._http,
|
|
113
|
+
self._route("/memories/reconcile"),
|
|
114
|
+
method="POST",
|
|
115
|
+
json={},
|
|
116
|
+
)
|
|
117
|
+
return ReconciliationResult.model_validate(raw)
|
|
118
|
+
|
|
119
|
+
def reconcile_status(self, user_id: str) -> ReconcileStatus:
|
|
120
|
+
path = self._route(f"/memories/reconcile/status?user_id={user_id}")
|
|
121
|
+
raw = fetch_json(self._client, self._http, path)
|
|
122
|
+
return ReconcileStatus.model_validate(raw)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AsyncAtomicMemoryLifecycle:
|
|
126
|
+
"""Async counterpart of :class:`AtomicMemoryLifecycle`."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, client: httpx.AsyncClient, http: HttpOptions, route: Route) -> None:
|
|
129
|
+
self._client = client
|
|
130
|
+
self._http = http
|
|
131
|
+
self._route = route
|
|
132
|
+
|
|
133
|
+
async def consolidate(self, user_id: str, execute: bool = False) -> ConsolidationResult:
|
|
134
|
+
body: dict[str, Any] = {"user_id": user_id}
|
|
135
|
+
if execute:
|
|
136
|
+
body["execute"] = True
|
|
137
|
+
raw = await afetch_json(
|
|
138
|
+
self._client,
|
|
139
|
+
self._http,
|
|
140
|
+
self._route("/memories/consolidate"),
|
|
141
|
+
method="POST",
|
|
142
|
+
json=body,
|
|
143
|
+
)
|
|
144
|
+
return _to_consolidation_result(raw)
|
|
145
|
+
|
|
146
|
+
async def decay(self, user_id: str, dry_run: bool = True) -> DecayResult:
|
|
147
|
+
body: dict[str, Any] = {"user_id": user_id}
|
|
148
|
+
if dry_run is False:
|
|
149
|
+
body["dry_run"] = False
|
|
150
|
+
raw = await afetch_json(
|
|
151
|
+
self._client,
|
|
152
|
+
self._http,
|
|
153
|
+
self._route("/memories/decay"),
|
|
154
|
+
method="POST",
|
|
155
|
+
json=body,
|
|
156
|
+
)
|
|
157
|
+
return DecayResult.model_validate(raw)
|
|
158
|
+
|
|
159
|
+
async def cap(self, user_id: str) -> CapCheckResult:
|
|
160
|
+
path = self._route(f"/memories/cap?user_id={user_id}")
|
|
161
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
162
|
+
return CapCheckResult.model_validate(raw)
|
|
163
|
+
|
|
164
|
+
async def stats(self, user_id: str) -> StatsResult:
|
|
165
|
+
path = self._route(f"/memories/stats?user_id={user_id}")
|
|
166
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
167
|
+
return StatsResult.model_validate(raw)
|
|
168
|
+
|
|
169
|
+
async def reset_source(self, user_id: str, source_site: str) -> ResetSourceResult:
|
|
170
|
+
raw = await afetch_json(
|
|
171
|
+
self._client,
|
|
172
|
+
self._http,
|
|
173
|
+
self._route("/memories/reset-source"),
|
|
174
|
+
method="POST",
|
|
175
|
+
json={"user_id": user_id, "source_site": source_site},
|
|
176
|
+
)
|
|
177
|
+
return ResetSourceResult.model_validate(raw)
|
|
178
|
+
|
|
179
|
+
async def reconcile(self, user_id: str) -> ReconciliationResult:
|
|
180
|
+
raw = await afetch_json(
|
|
181
|
+
self._client,
|
|
182
|
+
self._http,
|
|
183
|
+
self._route("/memories/reconcile"),
|
|
184
|
+
method="POST",
|
|
185
|
+
json={"user_id": user_id},
|
|
186
|
+
)
|
|
187
|
+
return ReconciliationResult.model_validate(raw)
|
|
188
|
+
|
|
189
|
+
async def reconcile_all(self) -> ReconciliationResult:
|
|
190
|
+
raw = await afetch_json(
|
|
191
|
+
self._client,
|
|
192
|
+
self._http,
|
|
193
|
+
self._route("/memories/reconcile"),
|
|
194
|
+
method="POST",
|
|
195
|
+
json={},
|
|
196
|
+
)
|
|
197
|
+
return ReconciliationResult.model_validate(raw)
|
|
198
|
+
|
|
199
|
+
async def reconcile_status(self, user_id: str) -> ReconcileStatus:
|
|
200
|
+
path = self._route(f"/memories/reconcile/status?user_id={user_id}")
|
|
201
|
+
raw = await afetch_json(self._client, self._http, path)
|
|
202
|
+
return ReconcileStatus.model_validate(raw)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Wire-format ↔ V3 type mappers for the AtomicMemory provider.
|
|
2
|
+
|
|
3
|
+
Port of `atomicmemory-sdk/src/memory/atomicmemory-provider/mappers.ts`.
|
|
4
|
+
Field-for-field equivalent. Date strings are parsed with
|
|
5
|
+
``datetime.fromisoformat`` (handles both ``+00:00`` and ``Z`` in 3.11+
|
|
6
|
+
via the small normalization helper below).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from atomicmemory.memory.types import (
|
|
15
|
+
IngestResult,
|
|
16
|
+
Memory,
|
|
17
|
+
MemoryVersion,
|
|
18
|
+
MemoryVersionEvent,
|
|
19
|
+
Provenance,
|
|
20
|
+
Scope,
|
|
21
|
+
SearchResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_AUDIT_EVENTS: set[MemoryVersionEvent] = {"created", "updated", "superseded", "invalidated"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _coalesce(*values: Any) -> Any:
|
|
28
|
+
"""Return the first value that is not None.
|
|
29
|
+
|
|
30
|
+
Python equivalent of TS's ``??`` (nullish coalescing). Crucial for
|
|
31
|
+
score fields where ``0.0`` is a legitimate value: ``a or b`` would
|
|
32
|
+
incorrectly treat ``0.0`` as missing.
|
|
33
|
+
"""
|
|
34
|
+
for value in values:
|
|
35
|
+
if value is not None:
|
|
36
|
+
return value
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_iso(value: str | None) -> datetime | None:
|
|
41
|
+
"""Parse an ISO 8601 datetime, normalizing trailing 'Z' to UTC."""
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
text = value.replace("Z", "+00:00") if value.endswith("Z") else value
|
|
45
|
+
return datetime.fromisoformat(text)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_memory(raw: dict[str, Any], scope: Scope) -> Memory:
|
|
49
|
+
"""Map a single core memory record to a V3 ``Memory``."""
|
|
50
|
+
created_at = _parse_iso(raw.get("created_at")) or datetime.now(tz=timezone.utc)
|
|
51
|
+
return Memory(
|
|
52
|
+
id=raw["id"],
|
|
53
|
+
content=raw["content"],
|
|
54
|
+
scope=scope,
|
|
55
|
+
created_at=created_at,
|
|
56
|
+
provenance=_build_provenance(raw),
|
|
57
|
+
metadata=_build_metadata(raw),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_provenance(raw: dict[str, Any]) -> Provenance | None:
|
|
62
|
+
fields: dict[str, Any] = {}
|
|
63
|
+
if "source_site" in raw and raw["source_site"] is not None:
|
|
64
|
+
fields["source"] = raw["source_site"]
|
|
65
|
+
if "source_url" in raw and raw["source_url"] is not None:
|
|
66
|
+
fields["source_url"] = raw["source_url"]
|
|
67
|
+
if not fields:
|
|
68
|
+
return None
|
|
69
|
+
return Provenance(**fields)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_metadata(raw: dict[str, Any]) -> dict[str, Any] | None:
|
|
73
|
+
metadata: dict[str, Any] = {}
|
|
74
|
+
if "importance" in raw and raw["importance"] is not None:
|
|
75
|
+
metadata["importance"] = raw["importance"]
|
|
76
|
+
if "episode_id" in raw and raw["episode_id"] is not None:
|
|
77
|
+
metadata["episode_id"] = raw["episode_id"]
|
|
78
|
+
return metadata or None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def to_search_result(raw: dict[str, Any], scope: Scope) -> SearchResult:
|
|
82
|
+
"""Map a single search hit, preserving every score variant core emits.
|
|
83
|
+
|
|
84
|
+
Mirrors TS's ``??`` semantics so that legitimate ``0.0`` scores are
|
|
85
|
+
not silently replaced by a fallback field.
|
|
86
|
+
"""
|
|
87
|
+
similarity = _coalesce(raw.get("semantic_similarity"), raw.get("similarity"))
|
|
88
|
+
ranking_score = _coalesce(raw.get("ranking_score"), raw.get("score"))
|
|
89
|
+
relevance = raw.get("relevance")
|
|
90
|
+
score = _coalesce(ranking_score, similarity, 0.0)
|
|
91
|
+
return SearchResult(
|
|
92
|
+
memory=to_memory(raw, scope),
|
|
93
|
+
score=score,
|
|
94
|
+
similarity=similarity,
|
|
95
|
+
ranking_score=ranking_score,
|
|
96
|
+
relevance=relevance,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def to_ingest_result(raw: dict[str, Any]) -> IngestResult:
|
|
101
|
+
"""Map ``POST /memories/ingest[/quick]`` response to V3 IngestResult."""
|
|
102
|
+
return IngestResult(
|
|
103
|
+
created=list(raw.get("stored_memory_ids") or []),
|
|
104
|
+
updated=list(raw.get("updated_memory_ids") or []),
|
|
105
|
+
unchanged=[],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def to_memory_version(raw: dict[str, Any]) -> MemoryVersion:
|
|
110
|
+
"""Map an audit-trail entry to a V3 ``MemoryVersion``.
|
|
111
|
+
|
|
112
|
+
Unknown ``event`` values are normalized to ``"created"`` (matches TS).
|
|
113
|
+
"""
|
|
114
|
+
raw_event = raw.get("event")
|
|
115
|
+
event: MemoryVersionEvent = raw_event if raw_event in _AUDIT_EVENTS else "created"
|
|
116
|
+
created_at = _parse_iso(raw.get("created_at"))
|
|
117
|
+
if created_at is None:
|
|
118
|
+
raise ValueError("audit entry missing created_at")
|
|
119
|
+
return MemoryVersion(
|
|
120
|
+
id=raw["id"],
|
|
121
|
+
content=raw["content"],
|
|
122
|
+
created_at=created_at,
|
|
123
|
+
parent_id=raw.get("parent_id"),
|
|
124
|
+
event=event,
|
|
125
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Route-path helpers for the AtomicMemory provider.
|
|
2
|
+
|
|
3
|
+
Port of `atomicmemory-sdk/src/memory/atomicmemory-provider/path.ts`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_api_version(api_version: str) -> str:
|
|
10
|
+
"""Normalize a config value to a leading-slash prefix, no trailing slash.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
``"v1"`` → ``"/v1"``
|
|
14
|
+
``"/v1/"`` → ``"/v1"``
|
|
15
|
+
``""`` → ``""`` (empty disables prefixing)
|
|
16
|
+
"""
|
|
17
|
+
trimmed = api_version.strip("/")
|
|
18
|
+
if trimmed == "":
|
|
19
|
+
return ""
|
|
20
|
+
return f"/{trimmed}"
|