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,16 @@
1
+ """Optional local embeddings — gated behind the ``embeddings`` extra.
2
+
3
+ Install with ``pip install 'atomicmemory[embeddings]'`` to pull in
4
+ ``sentence-transformers``. Without the extra, importing
5
+ :class:`SentenceTransformersAdapter` raises a clear actionable error;
6
+ the protocol :class:`EmbeddingGenerator` itself is always available.
7
+ """
8
+
9
+ from atomicmemory.embeddings.base import EmbeddingGenerator, EmbeddingResult
10
+ from atomicmemory.embeddings.sentence_transformers import SentenceTransformersAdapter
11
+
12
+ __all__ = [
13
+ "EmbeddingGenerator",
14
+ "EmbeddingResult",
15
+ "SentenceTransformersAdapter",
16
+ ]
@@ -0,0 +1,39 @@
1
+ """Embedding adapter protocol + result type.
2
+
3
+ Port of `atomicmemory-sdk/src/embedding/embedding-generator.ts`'s
4
+ public surface. Implementations live in sibling modules
5
+ (``sentence_transformers.py``, future others).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Sequence
11
+ from dataclasses import dataclass
12
+ from typing import Protocol, runtime_checkable
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class EmbeddingResult:
17
+ """A single embedding with provenance metadata."""
18
+
19
+ embedding: list[float]
20
+ dimensions: int
21
+ model: str
22
+ processing_time_seconds: float
23
+ provider: str
24
+ cache_hit: bool = False
25
+
26
+
27
+ @runtime_checkable
28
+ class EmbeddingGenerator(Protocol):
29
+ """Common contract for any embedding backend."""
30
+
31
+ @property
32
+ def dimensions(self) -> int: ...
33
+
34
+ @property
35
+ def model_name(self) -> str: ...
36
+
37
+ def embed(self, text: str) -> EmbeddingResult: ...
38
+
39
+ def embed_batch(self, texts: Sequence[str]) -> list[EmbeddingResult]: ...
@@ -0,0 +1,104 @@
1
+ """Sentence-Transformers adapter — local embeddings via the ``embeddings`` extra.
2
+
3
+ Port of the WASM/transformers.js path in
4
+ `atomicmemory-sdk/src/embedding/transformers-adapter.ts`. Default
5
+ model is ``sentence-transformers/all-MiniLM-L6-v2`` (384 dims), matching
6
+ the TS SDK's ``Xenova/all-MiniLM-L6-v2``.
7
+
8
+ Lazy-imports ``sentence_transformers`` at construction time. If the
9
+ extra was not installed, a clear actionable error is raised.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from collections.abc import Sequence
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from atomicmemory.core.errors import ConfigError
19
+ from atomicmemory.embeddings.base import EmbeddingResult
20
+
21
+ if TYPE_CHECKING:
22
+ from sentence_transformers import SentenceTransformer
23
+
24
+ DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
25
+ DEFAULT_DIMENSIONS = 384
26
+
27
+
28
+ def _import_sentence_transformers() -> Any:
29
+ try:
30
+ import sentence_transformers
31
+ except ImportError as exc:
32
+ raise ConfigError(
33
+ "atomicmemory[embeddings] is not installed. "
34
+ "Install the optional extra: pip install 'atomicmemory[embeddings]'.",
35
+ context={"missing_dependency": "sentence_transformers"},
36
+ ) from exc
37
+ return sentence_transformers
38
+
39
+
40
+ class SentenceTransformersAdapter:
41
+ """Local embedding adapter backed by ``sentence-transformers``.
42
+
43
+ Loads the model lazily on first use; subsequent calls reuse the
44
+ same in-memory instance.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ model_name: str = DEFAULT_MODEL,
51
+ dimensions: int = DEFAULT_DIMENSIONS,
52
+ normalize: bool = True,
53
+ device: str | None = None,
54
+ ) -> None:
55
+ self._model_name = model_name
56
+ self._dimensions = dimensions
57
+ self._normalize = normalize
58
+ self._device = device
59
+ self._model: SentenceTransformer | None = None
60
+
61
+ @property
62
+ def model_name(self) -> str:
63
+ return self._model_name
64
+
65
+ @property
66
+ def dimensions(self) -> int:
67
+ return self._dimensions
68
+
69
+ def _ensure_model(self) -> Any:
70
+ if self._model is None:
71
+ module = _import_sentence_transformers()
72
+ self._model = module.SentenceTransformer(self._model_name, device=self._device)
73
+ return self._model
74
+
75
+ def embed(self, text: str) -> EmbeddingResult:
76
+ model = self._ensure_model()
77
+ start = time.monotonic()
78
+ vector = model.encode(text, normalize_embeddings=self._normalize)
79
+ return EmbeddingResult(
80
+ embedding=[float(x) for x in vector.tolist()],
81
+ dimensions=self._dimensions,
82
+ model=self._model_name,
83
+ processing_time_seconds=time.monotonic() - start,
84
+ provider="sentence-transformers",
85
+ )
86
+
87
+ def embed_batch(self, texts: Sequence[str]) -> list[EmbeddingResult]:
88
+ if not texts:
89
+ return []
90
+ model = self._ensure_model()
91
+ start = time.monotonic()
92
+ vectors = model.encode(list(texts), normalize_embeddings=self._normalize)
93
+ elapsed = time.monotonic() - start
94
+ per_item = elapsed / len(texts) if texts else 0.0
95
+ return [
96
+ EmbeddingResult(
97
+ embedding=[float(x) for x in v.tolist()],
98
+ dimensions=self._dimensions,
99
+ model=self._model_name,
100
+ processing_time_seconds=per_item,
101
+ provider="sentence-transformers",
102
+ )
103
+ for v in vectors
104
+ ]
@@ -0,0 +1,17 @@
1
+ """Local key/value cache adapters — in-memory + SQLite.
2
+
3
+ Port of the kv-cache layer in `atomicmemory-sdk/src/kv-cache/`. The
4
+ ``StorageAdapter`` Protocol is the contract every adapter satisfies;
5
+ two concrete implementations ship with the SDK:
6
+
7
+ - :class:`MemoryStorageAdapter` — pure-Python dict-backed store, useful
8
+ for tests and short-lived agent runs.
9
+ - :class:`SQLiteStorageAdapter` — stdlib ``sqlite3``-backed store with
10
+ TTL support, suitable for local persistence in CLIs and notebooks.
11
+ """
12
+
13
+ from atomicmemory.kv_cache.adapter import StorageAdapter
14
+ from atomicmemory.kv_cache.memory_storage import MemoryStorageAdapter
15
+ from atomicmemory.kv_cache.sqlite_storage import SQLiteStorageAdapter
16
+
17
+ __all__ = ["MemoryStorageAdapter", "SQLiteStorageAdapter", "StorageAdapter"]
@@ -0,0 +1,50 @@
1
+ """StorageAdapter protocol — common contract for local key/value caches.
2
+
3
+ Port of `atomicmemory-sdk/src/kv-cache/storage-adapter.ts`. The methods
4
+ mirror the TS surface, with snake_case naming.
5
+
6
+ Encryption is **not** implemented in any shipped adapter. The
7
+ ``encrypt`` keyword on :meth:`set` exists for forward compatibility
8
+ with future ciphered backends, but concrete adapters MUST fail closed:
9
+ when a caller passes ``encrypt=True``, a conformant implementation
10
+ raises :class:`atomicmemory.core.errors.ConfigError` rather than
11
+ silently storing plaintext under a flag that signals confidentiality.
12
+ :class:`MemoryStorageAdapter` and :class:`SQLiteStorageAdapter` follow
13
+ that contract today; new adapters must too until real encryption lands.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Protocol, TypeVar, runtime_checkable
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ @runtime_checkable
24
+ class StorageAdapter(Protocol):
25
+ """Common storage contract — get/set/delete/keys + TTL + size."""
26
+
27
+ def initialize(self) -> None: ...
28
+
29
+ def close(self) -> None: ...
30
+
31
+ def get(self, key: str) -> Any | None: ...
32
+
33
+ def set(
34
+ self,
35
+ key: str,
36
+ value: Any,
37
+ *,
38
+ ttl_seconds: float | None = None,
39
+ encrypt: bool = False,
40
+ ) -> None: ...
41
+
42
+ def delete(self, key: str) -> bool: ...
43
+
44
+ def has(self, key: str) -> bool: ...
45
+
46
+ def keys(self) -> list[str]: ...
47
+
48
+ def size(self) -> int: ...
49
+
50
+ def clear(self) -> None: ...
@@ -0,0 +1,98 @@
1
+ """In-memory key/value cache adapter.
2
+
3
+ Port of `atomicmemory-sdk/src/kv-cache/memory-storage.ts`. Threaded
4
+ access is **not** safe; if you need concurrent writers, wrap your own
5
+ lock or use the SQLite adapter.
6
+
7
+ The TTL clock is injectable: pass ``clock=`` to use a deterministic
8
+ callable in tests. Defaults to :func:`time.monotonic` so wall-clock
9
+ changes don't affect in-memory expiry.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass
17
+ from typing import Any
18
+
19
+ from atomicmemory.core.errors import ConfigError
20
+
21
+
22
+ @dataclass
23
+ class _MemoryItem:
24
+ value: Any
25
+ created_at: float
26
+ expires_at: float | None
27
+
28
+
29
+ class MemoryStorageAdapter:
30
+ """Pure-Python in-memory store with TTL support."""
31
+
32
+ def __init__(self, *, clock: Callable[[], float] | None = None) -> None:
33
+ self._data: dict[str, _MemoryItem] = {}
34
+ self._clock: Callable[[], float] = clock or time.monotonic
35
+
36
+ def initialize(self) -> None:
37
+ """No-op; the dict is ready to use as soon as the adapter is constructed."""
38
+
39
+ def close(self) -> None:
40
+ self._data.clear()
41
+
42
+ def get(self, key: str) -> Any | None:
43
+ item = self._data.get(key)
44
+ if item is None:
45
+ return None
46
+ if item.expires_at is not None and self._clock() >= item.expires_at:
47
+ del self._data[key]
48
+ return None
49
+ return item.value
50
+
51
+ def set(
52
+ self,
53
+ key: str,
54
+ value: Any,
55
+ *,
56
+ ttl_seconds: float | None = None,
57
+ encrypt: bool = False,
58
+ ) -> None:
59
+ if encrypt:
60
+ raise ConfigError(
61
+ "MemoryStorageAdapter does not implement encryption; "
62
+ "passing encrypt=True would silently store plaintext. "
63
+ "Omit the flag or use a backend that implements ciphered storage.",
64
+ context={"adapter": "MemoryStorageAdapter"},
65
+ )
66
+ now = self._clock()
67
+ self._data[key] = _MemoryItem(
68
+ value=value,
69
+ created_at=now,
70
+ expires_at=now + ttl_seconds if ttl_seconds is not None else None,
71
+ )
72
+
73
+ def delete(self, key: str) -> bool:
74
+ return self._data.pop(key, None) is not None
75
+
76
+ def has(self, key: str) -> bool:
77
+ return self.get(key) is not None
78
+
79
+ def keys(self) -> list[str]:
80
+ # Materialize the list and drop expired entries on the way through
81
+ # so callers don't see stale ids.
82
+ live: list[str] = []
83
+ now = self._clock()
84
+ expired: list[str] = []
85
+ for key, item in self._data.items():
86
+ if item.expires_at is not None and now >= item.expires_at:
87
+ expired.append(key)
88
+ else:
89
+ live.append(key)
90
+ for key in expired:
91
+ del self._data[key]
92
+ return live
93
+
94
+ def size(self) -> int:
95
+ return len(self.keys())
96
+
97
+ def clear(self) -> None:
98
+ self._data.clear()
@@ -0,0 +1,122 @@
1
+ """SQLite-backed storage adapter.
2
+
3
+ Stdlib-only — no SQLAlchemy. Stores values as JSON text. TTL is enforced
4
+ on read (lazy eviction); a periodic ``vacuum_expired`` helper is
5
+ provided for callers that want eager cleanup.
6
+
7
+ The TTL clock defaults to :func:`time.time` (wall-clock epoch seconds)
8
+ so persisted ``expires_at`` values remain meaningful across process
9
+ restarts. Tests can inject a deterministic ``clock=`` callable.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sqlite3
16
+ import time
17
+ from collections.abc import Callable
18
+ from typing import Any
19
+
20
+ from atomicmemory.core.errors import ConfigError
21
+
22
+ _SCHEMA = """
23
+ CREATE TABLE IF NOT EXISTS atomicmemory_kv (
24
+ key TEXT PRIMARY KEY,
25
+ value TEXT NOT NULL,
26
+ created_at REAL NOT NULL,
27
+ expires_at REAL
28
+ );
29
+ """
30
+
31
+
32
+ class SQLiteStorageAdapter:
33
+ """SQLite-backed key/value store with optional TTL."""
34
+
35
+ def __init__(
36
+ self,
37
+ db_path: str = ":memory:",
38
+ *,
39
+ clock: Callable[[], float] | None = None,
40
+ ) -> None:
41
+ self._db_path = db_path
42
+ self._conn: sqlite3.Connection | None = None
43
+ self._clock: Callable[[], float] = clock or time.time
44
+
45
+ def initialize(self) -> None:
46
+ if self._conn is None:
47
+ self._conn = sqlite3.connect(self._db_path, isolation_level=None)
48
+ self._conn.execute(_SCHEMA)
49
+
50
+ def close(self) -> None:
51
+ if self._conn is not None:
52
+ self._conn.close()
53
+ self._conn = None
54
+
55
+ def _require(self) -> sqlite3.Connection:
56
+ if self._conn is None:
57
+ raise RuntimeError("SQLiteStorageAdapter is not initialized; call initialize() first.")
58
+ return self._conn
59
+
60
+ def get(self, key: str) -> Any | None:
61
+ row = self._require().execute("SELECT value, expires_at FROM atomicmemory_kv WHERE key = ?", (key,)).fetchone()
62
+ if row is None:
63
+ return None
64
+ value_text, expires_at = row
65
+ if expires_at is not None and self._clock() >= expires_at:
66
+ self.delete(key)
67
+ return None
68
+ return json.loads(value_text)
69
+
70
+ def set(
71
+ self,
72
+ key: str,
73
+ value: Any,
74
+ *,
75
+ ttl_seconds: float | None = None,
76
+ encrypt: bool = False,
77
+ ) -> None:
78
+ if encrypt:
79
+ raise ConfigError(
80
+ "SQLiteStorageAdapter does not implement encryption; "
81
+ "passing encrypt=True would silently store plaintext. "
82
+ "Omit the flag or use a backend that implements ciphered storage.",
83
+ context={"adapter": "SQLiteStorageAdapter"},
84
+ )
85
+ now = self._clock()
86
+ expires_at = now + ttl_seconds if ttl_seconds is not None else None
87
+ self._require().execute(
88
+ "INSERT INTO atomicmemory_kv(key, value, created_at, expires_at) "
89
+ "VALUES (?, ?, ?, ?) "
90
+ "ON CONFLICT(key) DO UPDATE SET "
91
+ "value=excluded.value, created_at=excluded.created_at, expires_at=excluded.expires_at",
92
+ (key, json.dumps(value), now, expires_at),
93
+ )
94
+
95
+ def delete(self, key: str) -> bool:
96
+ cursor = self._require().execute("DELETE FROM atomicmemory_kv WHERE key = ?", (key,))
97
+ return (cursor.rowcount or 0) > 0
98
+
99
+ def has(self, key: str) -> bool:
100
+ return self.get(key) is not None
101
+
102
+ def keys(self) -> list[str]:
103
+ self.vacuum_expired()
104
+ rows = self._require().execute("SELECT key FROM atomicmemory_kv ORDER BY key").fetchall()
105
+ return [row[0] for row in rows]
106
+
107
+ def size(self) -> int:
108
+ self.vacuum_expired()
109
+ row = self._require().execute("SELECT COUNT(*) FROM atomicmemory_kv").fetchone()
110
+ return int(row[0]) if row is not None else 0
111
+
112
+ def clear(self) -> None:
113
+ self._require().execute("DELETE FROM atomicmemory_kv")
114
+
115
+ def vacuum_expired(self) -> int:
116
+ """Delete rows whose ``expires_at`` is in the past. Returns the row count deleted."""
117
+ now = self._clock()
118
+ cursor = self._require().execute(
119
+ "DELETE FROM atomicmemory_kv WHERE expires_at IS NOT NULL AND expires_at <= ?",
120
+ (now,),
121
+ )
122
+ return cursor.rowcount or 0
@@ -0,0 +1,82 @@
1
+ """V3 memory provider abstractions.
2
+
3
+ Port of `atomicmemory-sdk/src/memory/`. Defines the core types and
4
+ provider interfaces; concrete providers live in
5
+ `atomicmemory.providers`.
6
+ """
7
+
8
+ from atomicmemory.memory.filters import FieldFilter, FieldFilterOp, FilterExpr
9
+ from atomicmemory.memory.types import (
10
+ Capabilities,
11
+ CapabilitiesExtensions,
12
+ CapabilitiesRequiredScope,
13
+ ContextPackage,
14
+ GraphEdge,
15
+ GraphNode,
16
+ GraphResult,
17
+ GraphSearchRequest,
18
+ HealthStatus,
19
+ IngestBase,
20
+ IngestInput,
21
+ IngestResult,
22
+ Insight,
23
+ ListRequest,
24
+ ListResultPage,
25
+ Memory,
26
+ MemoryKind,
27
+ MemoryRef,
28
+ MemoryVersion,
29
+ MemoryVersionEvent,
30
+ Message,
31
+ MessageIngest,
32
+ MessageRole,
33
+ PackageFormat,
34
+ PackageRequest,
35
+ Profile,
36
+ Provenance,
37
+ Scope,
38
+ SearchRequest,
39
+ SearchResult,
40
+ SearchResultPage,
41
+ TextIngest,
42
+ VerbatimIngest,
43
+ )
44
+
45
+ __all__ = [
46
+ "Capabilities",
47
+ "CapabilitiesExtensions",
48
+ "CapabilitiesRequiredScope",
49
+ "ContextPackage",
50
+ "FieldFilter",
51
+ "FieldFilterOp",
52
+ "FilterExpr",
53
+ "GraphEdge",
54
+ "GraphNode",
55
+ "GraphResult",
56
+ "GraphSearchRequest",
57
+ "HealthStatus",
58
+ "IngestBase",
59
+ "IngestInput",
60
+ "IngestResult",
61
+ "Insight",
62
+ "ListRequest",
63
+ "ListResultPage",
64
+ "Memory",
65
+ "MemoryKind",
66
+ "MemoryRef",
67
+ "MemoryVersion",
68
+ "MemoryVersionEvent",
69
+ "Message",
70
+ "MessageIngest",
71
+ "MessageRole",
72
+ "PackageFormat",
73
+ "PackageRequest",
74
+ "Profile",
75
+ "Provenance",
76
+ "Scope",
77
+ "SearchRequest",
78
+ "SearchResult",
79
+ "SearchResultPage",
80
+ "TextIngest",
81
+ "VerbatimIngest",
82
+ ]
@@ -0,0 +1,68 @@
1
+ """Recursive filter expression types.
2
+
3
+ Port of `atomicmemory-sdk/src/memory/types.ts:149-168`. `FilterExpr` is a
4
+ recursive discriminated union of and/or/not nodes plus a leaf
5
+ `FieldFilter`. We model it as a `RootModel` so consumers can construct
6
+ expressions ergonomically from dicts or by hand.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime
12
+ from typing import Annotated, Literal
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, RootModel
15
+
16
+ FieldFilterOp = Literal[
17
+ "eq",
18
+ "neq",
19
+ "gt",
20
+ "gte",
21
+ "lt",
22
+ "lte",
23
+ "in",
24
+ "contains",
25
+ "exists",
26
+ ]
27
+
28
+ FieldFilterValue = str | int | float | bool | datetime | list[str | int | float] | None
29
+
30
+
31
+ class FieldFilter(BaseModel):
32
+ """Leaf filter on a single record field."""
33
+
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ field: str
37
+ op: FieldFilterOp
38
+ value: FieldFilterValue = None
39
+
40
+
41
+ class FilterAnd(BaseModel):
42
+ model_config = ConfigDict(extra="forbid")
43
+ and_: list[FilterExpr] = Field(alias="and")
44
+
45
+
46
+ class FilterOr(BaseModel):
47
+ model_config = ConfigDict(extra="forbid")
48
+ or_: list[FilterExpr] = Field(alias="or")
49
+
50
+
51
+ class FilterNot(BaseModel):
52
+ model_config = ConfigDict(extra="forbid")
53
+ not_: FilterExpr = Field(alias="not")
54
+
55
+
56
+ FilterExprNode = Annotated[
57
+ FilterAnd | FilterOr | FilterNot | FieldFilter,
58
+ Field(union_mode="left_to_right"),
59
+ ]
60
+
61
+
62
+ class FilterExpr(RootModel[FilterExprNode]):
63
+ """A composable filter expression over memory metadata fields."""
64
+
65
+
66
+ FilterAnd.model_rebuild()
67
+ FilterOr.model_rebuild()
68
+ FilterNot.model_rebuild()
@@ -0,0 +1,42 @@
1
+ """Memory processing pipeline hooks.
2
+
3
+ Port of `atomicmemory-sdk/src/memory/pipeline.ts`. Pipelines let
4
+ providers (or wrappers) preprocess/postprocess ingest, search, get, and
5
+ list operations without modifying the provider itself.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Awaitable, Callable
11
+ from dataclasses import dataclass
12
+
13
+ from atomicmemory.memory.types import (
14
+ IngestInput,
15
+ IngestResult,
16
+ ListRequest,
17
+ ListResultPage,
18
+ Memory,
19
+ MemoryRef,
20
+ SearchRequest,
21
+ SearchResultPage,
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MemoryProcessingPipeline:
27
+ """Optional hook bundle for a provider.
28
+
29
+ Each hook may be ``None``. When present, the service awaits it around
30
+ the corresponding provider call.
31
+ """
32
+
33
+ preprocess_ingest: Callable[[IngestInput], Awaitable[list[IngestInput]]] | None = None
34
+ postprocess_ingest: Callable[[IngestResult, IngestInput], Awaitable[None]] | None = None
35
+ preprocess_search: Callable[[SearchRequest], Awaitable[SearchRequest]] | None = None
36
+ postprocess_search: Callable[[SearchResultPage, SearchRequest], Awaitable[SearchResultPage]] | None = None
37
+ preprocess_get: Callable[[MemoryRef], Awaitable[MemoryRef]] | None = None
38
+ postprocess_get: Callable[[Memory | None, MemoryRef], Awaitable[Memory | None]] | None = None
39
+ postprocess_list: Callable[[ListResultPage, ListRequest], Awaitable[ListResultPage]] | None = None
40
+
41
+
42
+ NOOP_PIPELINE = MemoryProcessingPipeline()