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,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()
|