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,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}"