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,339 @@
1
+ """Synchronous client for backend artifact storage.
2
+
3
+ This module calls core's `/v1/storage/artifacts/*` API and mirrors the
4
+ TypeScript SDK's `ConcreteStorageClient`. It sends bearer auth plus
5
+ ``X-AtomicMemory-User-Id`` on every request and never serializes the
6
+ legacy ``?user_id=`` URL parameter.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import json
13
+ from collections.abc import Iterator
14
+ from contextlib import contextmanager
15
+ from types import TracebackType
16
+ from typing import Any
17
+ from urllib.parse import quote, urlencode
18
+
19
+ import httpx
20
+ from pydantic import ValidationError as PydanticValidationError
21
+
22
+ from atomicmemory.core.validation import sanitized_pydantic_errors
23
+ from atomicmemory.storage._mapping import (
24
+ map_delete_result,
25
+ map_head_headers,
26
+ map_stored_artifact,
27
+ map_verify_result,
28
+ raise_for_storage_response,
29
+ validate_response_model,
30
+ )
31
+ from atomicmemory.storage.errors import StorageClientError
32
+ from atomicmemory.storage.types import (
33
+ ArtifactHead,
34
+ ArtifactMetadata,
35
+ ArtifactRef,
36
+ DeleteArtifactOptions,
37
+ DeleteArtifactResult,
38
+ ManagedBody,
39
+ PutArtifactInput,
40
+ PutManagedInput,
41
+ PutPointerInput,
42
+ StorageCapabilities,
43
+ StorageClientConfig,
44
+ StoredArtifact,
45
+ VerificationResult,
46
+ VerifyArtifactOptions,
47
+ )
48
+
49
+ METADATA_HEADER = "X-AtomicMemory-Metadata"
50
+
51
+
52
+ class StorageClient:
53
+ """Sync entry point for the direct artifact-storage API."""
54
+
55
+ def __init__(self, config: StorageClientConfig | dict[str, Any]) -> None:
56
+ self._config = _coerce_config(config)
57
+ self._api_url = self._config.api_url.rstrip("/")
58
+ self._client = httpx.Client(timeout=self._config.timeout_seconds)
59
+
60
+ def capabilities(self) -> StorageCapabilities:
61
+ response = self._request("GET", "/v1/storage/capabilities")
62
+ return validate_response_model(StorageCapabilities, _json_response(response), "StorageCapabilities")
63
+
64
+ def put(self, input: PutArtifactInput | dict[str, Any]) -> StoredArtifact:
65
+ value = _coerce_put_input(input)
66
+ if isinstance(value, PutPointerInput):
67
+ return self._put_pointer(value)
68
+ return self._put_managed(value)
69
+
70
+ def get(self, ref: ArtifactRef | dict[str, Any]) -> StoredArtifact:
71
+ artifact_id = _require_artifact_id(ref)
72
+ response = self._request("GET", f"/v1/storage/artifacts/{_quote_id(artifact_id)}", artifact_id=artifact_id)
73
+ return map_stored_artifact(_json_response(response))
74
+
75
+ def get_content(self, ref: ArtifactRef | dict[str, Any]) -> httpx.Response:
76
+ """Return a fully buffered content response for small artifacts.
77
+
78
+ For large artifacts, use :meth:`stream_content` so the response
79
+ body is consumed incrementally inside a context manager.
80
+ """
81
+ artifact_id = _require_artifact_id(ref)
82
+ return self._request("GET", f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content", artifact_id=artifact_id)
83
+
84
+ @contextmanager
85
+ def stream_content(self, ref: ArtifactRef | dict[str, Any]) -> Iterator[httpx.Response]:
86
+ """Stream artifact bytes without loading the whole response into memory."""
87
+ artifact_id = _require_artifact_id(ref)
88
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content"
89
+ try:
90
+ with self._client.stream("GET", f"{self._api_url}{path}", headers=self._headers(None)) as response:
91
+ if response.is_success:
92
+ yield response
93
+ return
94
+ response.read()
95
+ raise_for_storage_response(response, artifact_id)
96
+ except httpx.RequestError as exc:
97
+ raise _network_error("GET", path, exc) from exc
98
+ raise AssertionError("unreachable")
99
+
100
+ def head(self, ref: ArtifactRef | dict[str, Any]) -> ArtifactHead:
101
+ artifact_id = _require_artifact_id(ref)
102
+ response = self._request("HEAD", f"/v1/storage/artifacts/{_quote_id(artifact_id)}", artifact_id=artifact_id)
103
+ return map_head_headers(response.headers, artifact_id)
104
+
105
+ def delete(
106
+ self,
107
+ ref: ArtifactRef | dict[str, Any],
108
+ options: DeleteArtifactOptions | dict[str, Any] | None = None,
109
+ ) -> DeleteArtifactResult:
110
+ artifact_id = _require_artifact_id(ref)
111
+ path = _delete_path(artifact_id, options)
112
+ response = self._request("DELETE", path, artifact_id=artifact_id)
113
+ return map_delete_result(_json_response(response))
114
+
115
+ def verify(
116
+ self,
117
+ ref: ArtifactRef | dict[str, Any],
118
+ options: VerifyArtifactOptions | dict[str, Any] | None = None,
119
+ ) -> VerificationResult:
120
+ _coerce_verify_options(options)
121
+ artifact_id = _require_artifact_id(ref)
122
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/verify"
123
+ response = self._request("POST", path, artifact_id=artifact_id)
124
+ return map_verify_result(_json_response(response))
125
+
126
+ def close(self) -> None:
127
+ self._client.close()
128
+
129
+ def __enter__(self) -> StorageClient:
130
+ return self
131
+
132
+ def __exit__(
133
+ self,
134
+ exc_type: type[BaseException] | None,
135
+ exc: BaseException | None,
136
+ tb: TracebackType | None,
137
+ ) -> None:
138
+ self.close()
139
+
140
+ def _put_pointer(self, input: PutPointerInput) -> StoredArtifact:
141
+ payload = _pointer_payload(input)
142
+ response = self._request(
143
+ "POST",
144
+ "/v1/storage/artifacts",
145
+ headers={"Content-Type": "application/json"},
146
+ content=json.dumps(payload, separators=(",", ":")).encode(),
147
+ )
148
+ return map_stored_artifact(_json_response(response))
149
+
150
+ def _put_managed(self, input: PutManagedInput) -> StoredArtifact:
151
+ body = _coerce_managed_body(input.body)
152
+ path = _managed_path(input.disclose_content_hash)
153
+ headers = {"Content-Type": input.content_type, "Content-Length": str(len(body))}
154
+ if input.metadata is not None:
155
+ headers[METADATA_HEADER] = _encode_metadata_header(input.metadata)
156
+ response = self._request("POST", path, headers=headers, content=body)
157
+ return map_stored_artifact(_json_response(response))
158
+
159
+ def _request(
160
+ self,
161
+ method: str,
162
+ path: str,
163
+ *,
164
+ headers: dict[str, str] | None = None,
165
+ content: bytes | None = None,
166
+ artifact_id: str | None = None,
167
+ ) -> httpx.Response:
168
+ try:
169
+ response = self._client.request(
170
+ method,
171
+ f"{self._api_url}{path}",
172
+ headers=self._headers(headers),
173
+ content=content,
174
+ )
175
+ except httpx.RequestError as exc:
176
+ raise _network_error(method, path, exc) from exc
177
+ if response.is_success:
178
+ return response
179
+ raise_for_storage_response(response, artifact_id)
180
+ raise AssertionError("unreachable")
181
+
182
+ def _headers(self, extra: dict[str, str] | None) -> dict[str, str]:
183
+ headers = {
184
+ "Authorization": f"Bearer {self._config.api_key}",
185
+ "X-AtomicMemory-User-Id": self._config.user_id,
186
+ }
187
+ if extra:
188
+ headers.update(extra)
189
+ return headers
190
+
191
+
192
+ def _coerce_config(value: StorageClientConfig | dict[str, Any]) -> StorageClientConfig:
193
+ if isinstance(value, StorageClientConfig):
194
+ return value
195
+ try:
196
+ return StorageClientConfig.model_validate(value)
197
+ except PydanticValidationError as exc:
198
+ raise _validation_error("StorageClientConfig", exc) from exc
199
+
200
+
201
+ def _coerce_put_input(value: PutArtifactInput | dict[str, Any]) -> PutArtifactInput:
202
+ if isinstance(value, PutPointerInput | PutManagedInput):
203
+ return value
204
+ if not isinstance(value, dict):
205
+ raise _input_error("PutArtifactInput must be a model or dict")
206
+ try:
207
+ if value.get("mode") == "pointer":
208
+ return PutPointerInput.model_validate(value)
209
+ if value.get("mode") == "managed":
210
+ return PutManagedInput.model_validate(value)
211
+ except PydanticValidationError as exc:
212
+ raise _validation_error("PutArtifactInput", exc) from exc
213
+ raise _input_error("PutArtifactInput.mode must be 'pointer' or 'managed'")
214
+
215
+
216
+ def _coerce_ref(value: ArtifactRef | dict[str, Any]) -> ArtifactRef:
217
+ if isinstance(value, ArtifactRef):
218
+ return value
219
+ try:
220
+ return ArtifactRef.model_validate(value)
221
+ except PydanticValidationError as exc:
222
+ raise _validation_error("ArtifactRef", exc) from exc
223
+
224
+
225
+ def _coerce_delete_options(value: DeleteArtifactOptions | dict[str, Any] | None) -> DeleteArtifactOptions:
226
+ if value is None:
227
+ return DeleteArtifactOptions()
228
+ if isinstance(value, DeleteArtifactOptions):
229
+ return value
230
+ try:
231
+ return DeleteArtifactOptions.model_validate(value)
232
+ except PydanticValidationError as exc:
233
+ raise _validation_error("DeleteArtifactOptions", exc) from exc
234
+
235
+
236
+ def _coerce_verify_options(value: VerifyArtifactOptions | dict[str, Any] | None) -> VerifyArtifactOptions:
237
+ if value is None:
238
+ return VerifyArtifactOptions()
239
+ if isinstance(value, VerifyArtifactOptions):
240
+ return value
241
+ try:
242
+ return VerifyArtifactOptions.model_validate(value)
243
+ except PydanticValidationError as exc:
244
+ raise _validation_error("VerifyArtifactOptions", exc) from exc
245
+
246
+
247
+ def _require_artifact_id(ref: ArtifactRef | dict[str, Any]) -> str:
248
+ artifact_id = _coerce_ref(ref).artifact_id
249
+ if artifact_id is None:
250
+ raise StorageClientError(
251
+ "ArtifactRef.artifact_id is required for this operation in v1",
252
+ error_code="missing_artifact_id",
253
+ status=0,
254
+ body_text="",
255
+ )
256
+ return artifact_id
257
+
258
+
259
+ def _pointer_payload(input: PutPointerInput) -> dict[str, Any]:
260
+ payload: dict[str, Any] = {"mode": "pointer", "uri": input.uri, "content_type": input.content_type}
261
+ if input.size_bytes is not None:
262
+ payload["size_bytes"] = input.size_bytes
263
+ if input.content_hash is not None:
264
+ payload["content_hash"] = input.content_hash
265
+ if input.metadata is not None:
266
+ payload["metadata"] = input.metadata
267
+ return payload
268
+
269
+
270
+ def _managed_path(disclose_content_hash: bool) -> str:
271
+ query = {"mode": "managed"}
272
+ if disclose_content_hash:
273
+ query["disclose_content_hash"] = "true"
274
+ return f"/v1/storage/artifacts?{urlencode(query)}"
275
+
276
+
277
+ def _delete_path(artifact_id: str, options: DeleteArtifactOptions | dict[str, Any] | None) -> str:
278
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}"
279
+ policy = _coerce_delete_options(options).policy
280
+ return f"{path}?{urlencode({'policy': policy})}" if policy else path
281
+
282
+
283
+ def _coerce_managed_body(body: ManagedBody) -> bytes:
284
+ if isinstance(body, bytes):
285
+ return body
286
+ if isinstance(body, bytearray):
287
+ return bytes(body)
288
+ if isinstance(body, memoryview):
289
+ return body.tobytes()
290
+ raise StorageClientError(
291
+ "StorageClient.put: only bytes, bytearray, or memoryview are accepted in v1",
292
+ error_code="streaming_body_not_supported",
293
+ status=0,
294
+ body_text="",
295
+ )
296
+
297
+
298
+ def _encode_metadata_header(metadata: ArtifactMetadata) -> str:
299
+ encoded = json.dumps(metadata, separators=(",", ":")).encode()
300
+ return base64.b64encode(encoded).decode()
301
+
302
+
303
+ def _json_response(response: httpx.Response) -> Any:
304
+ try:
305
+ return response.json()
306
+ except json.JSONDecodeError as exc:
307
+ raise StorageClientError(
308
+ "storage API response is not valid JSON",
309
+ error_code="invalid_storage_response",
310
+ status=response.status_code,
311
+ body_text=response.text,
312
+ ) from exc
313
+
314
+
315
+ def _quote_id(artifact_id: str) -> str:
316
+ return quote(artifact_id, safe="")
317
+
318
+
319
+ def _validation_error(type_name: str, exc: PydanticValidationError) -> StorageClientError:
320
+ return StorageClientError(
321
+ f"Invalid {type_name}: {exc}",
322
+ error_code="invalid_storage_input",
323
+ status=0,
324
+ body_text="",
325
+ context={"type": type_name, "errors": sanitized_pydantic_errors(exc)},
326
+ )
327
+
328
+
329
+ def _input_error(message: str) -> StorageClientError:
330
+ return StorageClientError(message, error_code="invalid_storage_input", status=0, body_text="")
331
+
332
+
333
+ def _network_error(method: str, path: str, exc: httpx.RequestError) -> StorageClientError:
334
+ return StorageClientError(
335
+ f"Network error while calling {method} {path}: {exc}",
336
+ error_code="network_error",
337
+ status=0,
338
+ body_text="",
339
+ )
@@ -0,0 +1,115 @@
1
+ """Typed errors for the backend artifact-storage API.
2
+
3
+ These classes mirror `atomicmemory-sdk/src/storage/errors.ts` while
4
+ following the Python SDK error contract: every SDK-raised exception
5
+ inherits from :class:`atomicmemory.core.errors.AtomicMemoryError`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from atomicmemory.core.errors import AtomicMemoryError
13
+
14
+
15
+ class StorageClientError(AtomicMemoryError):
16
+ """Base error for ``client.storage.*`` failures."""
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ error_code: str,
23
+ status: int,
24
+ body_text: str,
25
+ context: dict[str, Any] | None = None,
26
+ ) -> None:
27
+ merged = {"error_code": error_code, "status": status}
28
+ if context:
29
+ merged.update(context)
30
+ super().__init__(message, context=merged)
31
+ self.error_code = error_code
32
+ self.status = status
33
+ self.body_text = body_text
34
+
35
+
36
+ class UnsupportedCapabilityError(StorageClientError):
37
+ """A requested storage capability is not supported by the backend."""
38
+
39
+ def __init__(self, *, capability: str, message: str, body_text: str) -> None:
40
+ super().__init__(
41
+ message,
42
+ error_code="unsupported_capability",
43
+ status=400,
44
+ body_text=body_text,
45
+ context={"capability": capability},
46
+ )
47
+ self.capability = capability
48
+
49
+
50
+ class ArtifactNotFoundError(StorageClientError):
51
+ """The requested artifact does not exist for the caller."""
52
+
53
+ def __init__(self, *, artifact_id: str, body_text: str) -> None:
54
+ super().__init__(
55
+ f"Storage artifact {artifact_id} not found",
56
+ error_code="artifact_not_found",
57
+ status=404,
58
+ body_text=body_text,
59
+ context={"artifact_id": artifact_id},
60
+ )
61
+ self.artifact_id = artifact_id
62
+
63
+
64
+ class ArtifactInUseError(StorageClientError):
65
+ """The artifact is still referenced by one or more documents."""
66
+
67
+ def __init__(
68
+ self,
69
+ *,
70
+ artifact_id: str,
71
+ referenced_by_document_count: int,
72
+ body_text: str,
73
+ ) -> None:
74
+ super().__init__(
75
+ "Storage artifact "
76
+ f"{artifact_id} is referenced by {referenced_by_document_count} document(s); "
77
+ "pass policy='with_documents' to cascade",
78
+ error_code="artifact_in_use",
79
+ status=409,
80
+ body_text=body_text,
81
+ context={
82
+ "artifact_id": artifact_id,
83
+ "referenced_by_document_count": referenced_by_document_count,
84
+ },
85
+ )
86
+ self.artifact_id = artifact_id
87
+ self.referenced_by_document_count = referenced_by_document_count
88
+
89
+
90
+ class PointerContentNotManagedError(StorageClientError):
91
+ """Raised when ``get_content`` targets a pointer-mode artifact."""
92
+
93
+ def __init__(self, *, artifact_id: str, uri: str, body_text: str) -> None:
94
+ super().__init__(
95
+ f"Artifact {artifact_id} is pointer-mode; fetch the URI directly",
96
+ error_code="pointer_content_not_managed",
97
+ status=409,
98
+ body_text=body_text,
99
+ context={"artifact_id": artifact_id, "uri": uri},
100
+ )
101
+ self.artifact_id = artifact_id
102
+ self.uri = uri
103
+
104
+
105
+ class FilecoinDirectStorageNotSupportedError(StorageClientError):
106
+ """Direct managed Filecoin uploads are not supported by this API version."""
107
+
108
+ def __init__(self, *, body_text: str) -> None:
109
+ super().__init__(
110
+ "Direct Filecoin artifact uploads are not supported in this version. "
111
+ "Use document ingestion or pointer mode.",
112
+ error_code="filecoin_direct_storage_not_yet_supported",
113
+ status=501,
114
+ body_text=body_text,
115
+ )