axor-memory-sqlite 0.1.0__tar.gz
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.
- axor_memory_sqlite-0.1.0/.github/workflows/ci.yml +88 -0
- axor_memory_sqlite-0.1.0/.gitignore +6 -0
- axor_memory_sqlite-0.1.0/PKG-INFO +272 -0
- axor_memory_sqlite-0.1.0/README.md +249 -0
- axor_memory_sqlite-0.1.0/axor_memory_sqlite/__init__.py +5 -0
- axor_memory_sqlite-0.1.0/axor_memory_sqlite/provider.py +278 -0
- axor_memory_sqlite-0.1.0/pyproject.toml +37 -0
- axor_memory_sqlite-0.1.0/tests/test_provider.py +63 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*.*.*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Checkout axor-core
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
repository: ${{ github.repository_owner }}/axor-core
|
|
25
|
+
path: axor-core
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
cache: pip
|
|
31
|
+
|
|
32
|
+
- name: Install
|
|
33
|
+
run: |
|
|
34
|
+
pip install -e axor-core/
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: pytest -q
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
name: Publish to PyPI
|
|
42
|
+
needs: test
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
45
|
+
environment: pypi
|
|
46
|
+
|
|
47
|
+
permissions:
|
|
48
|
+
id-token: write
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
|
|
53
|
+
- uses: actions/setup-python@v5
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.12"
|
|
56
|
+
|
|
57
|
+
- name: Verify tag matches package version
|
|
58
|
+
run: |
|
|
59
|
+
python - << 'EOF'
|
|
60
|
+
import pathlib
|
|
61
|
+
import re
|
|
62
|
+
import sys
|
|
63
|
+
import tomllib
|
|
64
|
+
|
|
65
|
+
ref = "${{ github.ref_name }}"
|
|
66
|
+
m = re.fullmatch(r"v(\d+\.\d+\.\d+)", ref)
|
|
67
|
+
if not m:
|
|
68
|
+
print(f"Tag {ref!r} must match vX.Y.Z")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
tag_version = m.group(1)
|
|
72
|
+
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
|
|
73
|
+
pkg_version = data["project"]["version"]
|
|
74
|
+
|
|
75
|
+
if tag_version != pkg_version:
|
|
76
|
+
print(f"Version mismatch: tag={tag_version}, pyproject={pkg_version}")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
print(f"Version check passed: {pkg_version}")
|
|
80
|
+
EOF
|
|
81
|
+
|
|
82
|
+
- name: Build
|
|
83
|
+
run: |
|
|
84
|
+
pip install hatchling build
|
|
85
|
+
python -m build
|
|
86
|
+
|
|
87
|
+
- name: Publish to PyPI
|
|
88
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axor-memory-sqlite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQLite memory provider for axor-core
|
|
5
|
+
Project-URL: Repository, https://github.com/Bucha11/axor-memory-sqlite
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Bucha11/axor-memory-sqlite/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Bucha11/axor-memory-sqlite/releases
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agents,axor,memory,sqlite
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: axor-core>=0.1.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# axor-memory-sqlite
|
|
25
|
+
|
|
26
|
+
[](https://github.com/Bucha11/axor-memory-sqlite/actions/workflows/ci.yml)
|
|
27
|
+
[](https://pypi.org/project/axor-memory-sqlite/)
|
|
28
|
+
[](https://pypi.org/project/axor-memory-sqlite/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
**SQLite memory provider for [axor-core](https://github.com/Bucha11/axor-core).**
|
|
32
|
+
|
|
33
|
+
Persistent cross-session memory for governed agents. Zero extra dependencies — uses Python's built-in `sqlite3`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install axor-memory-sqlite
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires `axor-core >= 0.1.0`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import axor_claude
|
|
51
|
+
from axor_core import AgentDefinition, AgentDomain, FragmentValue, MemoryFragment
|
|
52
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
53
|
+
|
|
54
|
+
provider = SQLiteMemoryProvider("~/.axor/memory.db")
|
|
55
|
+
|
|
56
|
+
session = axor_claude.make_session(
|
|
57
|
+
api_key="sk-ant-...",
|
|
58
|
+
agent_def=AgentDefinition(
|
|
59
|
+
name="my-agent",
|
|
60
|
+
domain=AgentDomain.CODING,
|
|
61
|
+
personality="You are an expert Python engineer.",
|
|
62
|
+
memory_namespaces=("my-agent",), # loaded at every session start
|
|
63
|
+
),
|
|
64
|
+
memory_provider=provider,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result = await session.run("refactor the auth module")
|
|
68
|
+
|
|
69
|
+
# save what you want to remember next time
|
|
70
|
+
await provider.save([
|
|
71
|
+
MemoryFragment(
|
|
72
|
+
namespace="my-agent",
|
|
73
|
+
key="auth_module_status",
|
|
74
|
+
content="Auth module refactored to use JWT. Entry point: auth/jwt.py.",
|
|
75
|
+
value=FragmentValue.KNOWLEDGE,
|
|
76
|
+
),
|
|
77
|
+
])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## FragmentValue — what gets remembered how
|
|
83
|
+
|
|
84
|
+
Every `MemoryFragment` has a `value` that controls how the compressor treats it when it appears in `ContextView`:
|
|
85
|
+
|
|
86
|
+
| Value | Compressor behavior | Typical use |
|
|
87
|
+
|-------|--------------------|----|
|
|
88
|
+
| `PINNED` | Never touched — survives all turns | User preferences, system rules, agent personality |
|
|
89
|
+
| `KNOWLEDGE` | Dedup + error collapse only — no truncation | Project docs, domain context, API specs |
|
|
90
|
+
| `WORKING` | Normal compression pipeline | Task findings, recent tool results |
|
|
91
|
+
| `EPHEMERAL` | Aggressive compression — evicted first | Debug output, one-turn scratch |
|
|
92
|
+
|
|
93
|
+
Eviction priority: `EPHEMERAL` → `WORKING` → `KNOWLEDGE` → `PINNED` (never evicted).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `SQLiteMemoryProvider(db_path)`
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
103
|
+
|
|
104
|
+
provider = SQLiteMemoryProvider("~/.axor/memory.db") # persistent
|
|
105
|
+
provider = SQLiteMemoryProvider(":memory:") # in-memory, tests only
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
All methods are async. I/O runs in a thread pool — async callers are never blocked.
|
|
109
|
+
|
|
110
|
+
### `save(fragments)`
|
|
111
|
+
|
|
112
|
+
Upsert by `(namespace, key)` — existing fragments are overwritten:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
await provider.save([
|
|
116
|
+
MemoryFragment(
|
|
117
|
+
namespace="my-agent",
|
|
118
|
+
key="project_stack",
|
|
119
|
+
content="FastAPI + async SQLAlchemy + PostgreSQL",
|
|
120
|
+
value=FragmentValue.PINNED,
|
|
121
|
+
tags=["stack", "tech"],
|
|
122
|
+
),
|
|
123
|
+
])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `load(query)`
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from axor_core import MemoryQuery, FragmentValue
|
|
130
|
+
|
|
131
|
+
# load all from namespace, pinned first
|
|
132
|
+
fragments = await provider.load(MemoryQuery(
|
|
133
|
+
namespaces=("my-agent",),
|
|
134
|
+
max_results=20,
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# filter by value
|
|
138
|
+
pinned = await provider.load(MemoryQuery(
|
|
139
|
+
namespaces=("my-agent",),
|
|
140
|
+
values=(FragmentValue.PINNED, FragmentValue.KNOWLEDGE),
|
|
141
|
+
max_results=10,
|
|
142
|
+
))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Results are ordered by priority (`PINNED` first) then by `accessed_at` descending.
|
|
146
|
+
|
|
147
|
+
### `delete(namespace, keys)`
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
n = await provider.delete("my-agent", ["stale_key_1", "stale_key_2"])
|
|
151
|
+
print(f"deleted {n} fragments")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `evict(namespace, values, max_age_seconds)`
|
|
155
|
+
|
|
156
|
+
Remove stale fragments by value and/or age:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# evict all ephemeral fragments
|
|
160
|
+
await provider.evict("my-agent", values=(FragmentValue.EPHEMERAL,))
|
|
161
|
+
|
|
162
|
+
# evict working fragments older than 24 hours
|
|
163
|
+
await provider.evict(
|
|
164
|
+
"my-agent",
|
|
165
|
+
values=(FragmentValue.WORKING,),
|
|
166
|
+
max_age_seconds=86400,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `namespaces()`
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
ns = await provider.namespaces()
|
|
174
|
+
# ["my-agent", "shared", "project-x"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `close()`
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
await provider.close()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Call on shutdown. The provider can be used as an async context manager in tests:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
async with SQLiteMemoryProvider(":memory:") as provider:
|
|
187
|
+
await provider.save([...])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Namespaces
|
|
193
|
+
|
|
194
|
+
Namespaces are logical groupings — they do not require explicit creation. A namespace exists as soon as you save a fragment with that name.
|
|
195
|
+
|
|
196
|
+
Recommended pattern:
|
|
197
|
+
|
|
198
|
+
| Namespace | Content |
|
|
199
|
+
|-----------|---------|
|
|
200
|
+
| `{agent-name}` | Agent-specific memory — not shared |
|
|
201
|
+
| `shared` | Shared across all agents in a project |
|
|
202
|
+
| `project:{name}` | Project-specific facts |
|
|
203
|
+
| `user:{id}` | Per-user preferences |
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# agent reads from its own namespace + shared
|
|
207
|
+
agent = AgentDefinition(
|
|
208
|
+
name="billing-agent",
|
|
209
|
+
memory_namespaces=("billing-agent", "shared"),
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Schema
|
|
216
|
+
|
|
217
|
+
```sql
|
|
218
|
+
CREATE TABLE memory_fragments (
|
|
219
|
+
namespace TEXT NOT NULL,
|
|
220
|
+
key TEXT NOT NULL,
|
|
221
|
+
content TEXT NOT NULL,
|
|
222
|
+
value TEXT NOT NULL DEFAULT 'working',
|
|
223
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
224
|
+
tags TEXT NOT NULL DEFAULT '[]', -- JSON array
|
|
225
|
+
created_at TEXT NOT NULL,
|
|
226
|
+
accessed_at TEXT NOT NULL,
|
|
227
|
+
metadata TEXT NOT NULL DEFAULT '{}', -- JSON object
|
|
228
|
+
PRIMARY KEY (namespace, key)
|
|
229
|
+
);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The database file is a standard SQLite file — readable with any SQLite tool.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Testing
|
|
237
|
+
|
|
238
|
+
Use `:memory:` for tests — no file created, no cleanup needed:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
import pytest
|
|
242
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
243
|
+
from axor_core import MemoryFragment, FragmentValue, MemoryQuery
|
|
244
|
+
|
|
245
|
+
@pytest.mark.asyncio
|
|
246
|
+
async def test_memory():
|
|
247
|
+
p = SQLiteMemoryProvider(":memory:")
|
|
248
|
+
|
|
249
|
+
await p.save([
|
|
250
|
+
MemoryFragment(namespace="test", key="k1",
|
|
251
|
+
content="hello", value=FragmentValue.WORKING),
|
|
252
|
+
])
|
|
253
|
+
results = await p.load(MemoryQuery(namespaces=("test",), max_results=5))
|
|
254
|
+
assert len(results) == 1
|
|
255
|
+
assert results[0].content == "hello"
|
|
256
|
+
|
|
257
|
+
await p.close()
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Requirements
|
|
263
|
+
|
|
264
|
+
- Python 3.11+
|
|
265
|
+
- `axor-core >= 0.1.0`
|
|
266
|
+
- No extra dependencies — uses stdlib `sqlite3` + `asyncio`
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# axor-memory-sqlite
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Bucha11/axor-memory-sqlite/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/axor-memory-sqlite/)
|
|
5
|
+
[](https://pypi.org/project/axor-memory-sqlite/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**SQLite memory provider for [axor-core](https://github.com/Bucha11/axor-core).**
|
|
9
|
+
|
|
10
|
+
Persistent cross-session memory for governed agents. Zero extra dependencies — uses Python's built-in `sqlite3`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install axor-memory-sqlite
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires `axor-core >= 0.1.0`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import axor_claude
|
|
28
|
+
from axor_core import AgentDefinition, AgentDomain, FragmentValue, MemoryFragment
|
|
29
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
30
|
+
|
|
31
|
+
provider = SQLiteMemoryProvider("~/.axor/memory.db")
|
|
32
|
+
|
|
33
|
+
session = axor_claude.make_session(
|
|
34
|
+
api_key="sk-ant-...",
|
|
35
|
+
agent_def=AgentDefinition(
|
|
36
|
+
name="my-agent",
|
|
37
|
+
domain=AgentDomain.CODING,
|
|
38
|
+
personality="You are an expert Python engineer.",
|
|
39
|
+
memory_namespaces=("my-agent",), # loaded at every session start
|
|
40
|
+
),
|
|
41
|
+
memory_provider=provider,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
result = await session.run("refactor the auth module")
|
|
45
|
+
|
|
46
|
+
# save what you want to remember next time
|
|
47
|
+
await provider.save([
|
|
48
|
+
MemoryFragment(
|
|
49
|
+
namespace="my-agent",
|
|
50
|
+
key="auth_module_status",
|
|
51
|
+
content="Auth module refactored to use JWT. Entry point: auth/jwt.py.",
|
|
52
|
+
value=FragmentValue.KNOWLEDGE,
|
|
53
|
+
),
|
|
54
|
+
])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## FragmentValue — what gets remembered how
|
|
60
|
+
|
|
61
|
+
Every `MemoryFragment` has a `value` that controls how the compressor treats it when it appears in `ContextView`:
|
|
62
|
+
|
|
63
|
+
| Value | Compressor behavior | Typical use |
|
|
64
|
+
|-------|--------------------|----|
|
|
65
|
+
| `PINNED` | Never touched — survives all turns | User preferences, system rules, agent personality |
|
|
66
|
+
| `KNOWLEDGE` | Dedup + error collapse only — no truncation | Project docs, domain context, API specs |
|
|
67
|
+
| `WORKING` | Normal compression pipeline | Task findings, recent tool results |
|
|
68
|
+
| `EPHEMERAL` | Aggressive compression — evicted first | Debug output, one-turn scratch |
|
|
69
|
+
|
|
70
|
+
Eviction priority: `EPHEMERAL` → `WORKING` → `KNOWLEDGE` → `PINNED` (never evicted).
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `SQLiteMemoryProvider(db_path)`
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
80
|
+
|
|
81
|
+
provider = SQLiteMemoryProvider("~/.axor/memory.db") # persistent
|
|
82
|
+
provider = SQLiteMemoryProvider(":memory:") # in-memory, tests only
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
All methods are async. I/O runs in a thread pool — async callers are never blocked.
|
|
86
|
+
|
|
87
|
+
### `save(fragments)`
|
|
88
|
+
|
|
89
|
+
Upsert by `(namespace, key)` — existing fragments are overwritten:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
await provider.save([
|
|
93
|
+
MemoryFragment(
|
|
94
|
+
namespace="my-agent",
|
|
95
|
+
key="project_stack",
|
|
96
|
+
content="FastAPI + async SQLAlchemy + PostgreSQL",
|
|
97
|
+
value=FragmentValue.PINNED,
|
|
98
|
+
tags=["stack", "tech"],
|
|
99
|
+
),
|
|
100
|
+
])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `load(query)`
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from axor_core import MemoryQuery, FragmentValue
|
|
107
|
+
|
|
108
|
+
# load all from namespace, pinned first
|
|
109
|
+
fragments = await provider.load(MemoryQuery(
|
|
110
|
+
namespaces=("my-agent",),
|
|
111
|
+
max_results=20,
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
# filter by value
|
|
115
|
+
pinned = await provider.load(MemoryQuery(
|
|
116
|
+
namespaces=("my-agent",),
|
|
117
|
+
values=(FragmentValue.PINNED, FragmentValue.KNOWLEDGE),
|
|
118
|
+
max_results=10,
|
|
119
|
+
))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Results are ordered by priority (`PINNED` first) then by `accessed_at` descending.
|
|
123
|
+
|
|
124
|
+
### `delete(namespace, keys)`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
n = await provider.delete("my-agent", ["stale_key_1", "stale_key_2"])
|
|
128
|
+
print(f"deleted {n} fragments")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `evict(namespace, values, max_age_seconds)`
|
|
132
|
+
|
|
133
|
+
Remove stale fragments by value and/or age:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# evict all ephemeral fragments
|
|
137
|
+
await provider.evict("my-agent", values=(FragmentValue.EPHEMERAL,))
|
|
138
|
+
|
|
139
|
+
# evict working fragments older than 24 hours
|
|
140
|
+
await provider.evict(
|
|
141
|
+
"my-agent",
|
|
142
|
+
values=(FragmentValue.WORKING,),
|
|
143
|
+
max_age_seconds=86400,
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `namespaces()`
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
ns = await provider.namespaces()
|
|
151
|
+
# ["my-agent", "shared", "project-x"]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `close()`
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
await provider.close()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Call on shutdown. The provider can be used as an async context manager in tests:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
async with SQLiteMemoryProvider(":memory:") as provider:
|
|
164
|
+
await provider.save([...])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Namespaces
|
|
170
|
+
|
|
171
|
+
Namespaces are logical groupings — they do not require explicit creation. A namespace exists as soon as you save a fragment with that name.
|
|
172
|
+
|
|
173
|
+
Recommended pattern:
|
|
174
|
+
|
|
175
|
+
| Namespace | Content |
|
|
176
|
+
|-----------|---------|
|
|
177
|
+
| `{agent-name}` | Agent-specific memory — not shared |
|
|
178
|
+
| `shared` | Shared across all agents in a project |
|
|
179
|
+
| `project:{name}` | Project-specific facts |
|
|
180
|
+
| `user:{id}` | Per-user preferences |
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# agent reads from its own namespace + shared
|
|
184
|
+
agent = AgentDefinition(
|
|
185
|
+
name="billing-agent",
|
|
186
|
+
memory_namespaces=("billing-agent", "shared"),
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Schema
|
|
193
|
+
|
|
194
|
+
```sql
|
|
195
|
+
CREATE TABLE memory_fragments (
|
|
196
|
+
namespace TEXT NOT NULL,
|
|
197
|
+
key TEXT NOT NULL,
|
|
198
|
+
content TEXT NOT NULL,
|
|
199
|
+
value TEXT NOT NULL DEFAULT 'working',
|
|
200
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
201
|
+
tags TEXT NOT NULL DEFAULT '[]', -- JSON array
|
|
202
|
+
created_at TEXT NOT NULL,
|
|
203
|
+
accessed_at TEXT NOT NULL,
|
|
204
|
+
metadata TEXT NOT NULL DEFAULT '{}', -- JSON object
|
|
205
|
+
PRIMARY KEY (namespace, key)
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The database file is a standard SQLite file — readable with any SQLite tool.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Testing
|
|
214
|
+
|
|
215
|
+
Use `:memory:` for tests — no file created, no cleanup needed:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import pytest
|
|
219
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
220
|
+
from axor_core import MemoryFragment, FragmentValue, MemoryQuery
|
|
221
|
+
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_memory():
|
|
224
|
+
p = SQLiteMemoryProvider(":memory:")
|
|
225
|
+
|
|
226
|
+
await p.save([
|
|
227
|
+
MemoryFragment(namespace="test", key="k1",
|
|
228
|
+
content="hello", value=FragmentValue.WORKING),
|
|
229
|
+
])
|
|
230
|
+
results = await p.load(MemoryQuery(namespaces=("test",), max_results=5))
|
|
231
|
+
assert len(results) == 1
|
|
232
|
+
assert results[0].content == "hello"
|
|
233
|
+
|
|
234
|
+
await p.close()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Requirements
|
|
240
|
+
|
|
241
|
+
- Python 3.11+
|
|
242
|
+
- `axor-core >= 0.1.0`
|
|
243
|
+
- No extra dependencies — uses stdlib `sqlite3` + `asyncio`
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
SQLite-backed MemoryProvider for axor-core.
|
|
5
|
+
|
|
6
|
+
Zero external dependencies — uses Python's built-in sqlite3.
|
|
7
|
+
Thread-safe via asyncio.Lock + aiosqlite pattern (runs in thread pool).
|
|
8
|
+
|
|
9
|
+
Schema:
|
|
10
|
+
|
|
11
|
+
CREATE TABLE memory_fragments (
|
|
12
|
+
namespace TEXT NOT NULL,
|
|
13
|
+
key TEXT NOT NULL,
|
|
14
|
+
content TEXT NOT NULL,
|
|
15
|
+
value TEXT NOT NULL DEFAULT 'working',
|
|
16
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
tags TEXT NOT NULL DEFAULT '[]', -- JSON array
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
accessed_at TEXT NOT NULL,
|
|
20
|
+
metadata TEXT NOT NULL DEFAULT '{}', -- JSON object
|
|
21
|
+
PRIMARY KEY (namespace, key)
|
|
22
|
+
);
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
import sqlite3
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from axor_core.contracts.memory import (
|
|
33
|
+
MemoryFragment,
|
|
34
|
+
MemoryProvider,
|
|
35
|
+
MemoryQuery,
|
|
36
|
+
FragmentValue,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_SCHEMA = """
|
|
40
|
+
CREATE TABLE IF NOT EXISTS memory_fragments (
|
|
41
|
+
namespace TEXT NOT NULL,
|
|
42
|
+
key TEXT NOT NULL,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
value TEXT NOT NULL DEFAULT 'working',
|
|
45
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
accessed_at TEXT NOT NULL,
|
|
49
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
50
|
+
PRIMARY KEY (namespace, key)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_namespace ON memory_fragments(namespace);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_value ON memory_fragments(value);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_accessed ON memory_fragments(accessed_at);
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_NOW = lambda: datetime.now(timezone.utc).isoformat()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _row_to_fragment(row: tuple) -> MemoryFragment:
|
|
62
|
+
ns, key, content, value, token_count, tags_json, created_at, accessed_at, meta_json = row
|
|
63
|
+
return MemoryFragment(
|
|
64
|
+
namespace=ns,
|
|
65
|
+
key=key,
|
|
66
|
+
content=content,
|
|
67
|
+
value=FragmentValue(value),
|
|
68
|
+
token_count=token_count,
|
|
69
|
+
tags=json.loads(tags_json),
|
|
70
|
+
created_at=datetime.fromisoformat(created_at),
|
|
71
|
+
accessed_at=datetime.fromisoformat(accessed_at),
|
|
72
|
+
metadata=json.loads(meta_json),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SQLiteMemoryProvider(MemoryProvider):
|
|
77
|
+
"""
|
|
78
|
+
SQLite-backed MemoryProvider.
|
|
79
|
+
|
|
80
|
+
All I/O runs in a thread pool via asyncio.to_thread()
|
|
81
|
+
so async callers are never blocked.
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
|
|
85
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
86
|
+
from axor_core import GovernedSession, AgentDefinition
|
|
87
|
+
|
|
88
|
+
provider = SQLiteMemoryProvider("~/.axor/memory.db")
|
|
89
|
+
|
|
90
|
+
session = GovernedSession(
|
|
91
|
+
executor=...,
|
|
92
|
+
capability_executor=...,
|
|
93
|
+
agent_def=AgentDefinition(
|
|
94
|
+
name="my-agent",
|
|
95
|
+
memory_namespaces=("my-agent", "shared"),
|
|
96
|
+
),
|
|
97
|
+
memory_provider=provider,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# save memory after a session
|
|
101
|
+
await session.save_memory(
|
|
102
|
+
key="last_project",
|
|
103
|
+
content="Working on axor federation support",
|
|
104
|
+
value=FragmentValue.WORKING,
|
|
105
|
+
)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, db_path: str | Path = ":memory:") -> None:
|
|
109
|
+
self._db_path = str(Path(db_path).expanduser()) if db_path != ":memory:" else ":memory:"
|
|
110
|
+
self._lock = asyncio.Lock()
|
|
111
|
+
self._conn: sqlite3.Connection | None = None
|
|
112
|
+
|
|
113
|
+
def _open(self) -> sqlite3.Connection:
|
|
114
|
+
if self._conn is None:
|
|
115
|
+
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
116
|
+
self._conn.row_factory = sqlite3.Row
|
|
117
|
+
self._conn.executescript(_SCHEMA)
|
|
118
|
+
self._conn.commit()
|
|
119
|
+
return self._conn
|
|
120
|
+
|
|
121
|
+
async def _run(self, fn):
|
|
122
|
+
"""Run a blocking DB call in thread pool."""
|
|
123
|
+
return await asyncio.to_thread(fn)
|
|
124
|
+
|
|
125
|
+
# ── MemoryProvider interface ────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
async def load(self, query: MemoryQuery) -> list[MemoryFragment]:
|
|
128
|
+
def _load():
|
|
129
|
+
conn = self._open()
|
|
130
|
+
parts: list[str] = []
|
|
131
|
+
params: list[Any] = []
|
|
132
|
+
|
|
133
|
+
if query.namespaces:
|
|
134
|
+
placeholders = ",".join("?" * len(query.namespaces))
|
|
135
|
+
parts.append(f"namespace IN ({placeholders})")
|
|
136
|
+
params.extend(query.namespaces)
|
|
137
|
+
|
|
138
|
+
if query.values:
|
|
139
|
+
placeholders = ",".join("?" * len(query.values))
|
|
140
|
+
parts.append(f"value IN ({placeholders})")
|
|
141
|
+
params.extend(v.value for v in query.values)
|
|
142
|
+
|
|
143
|
+
where = ("WHERE " + " AND ".join(parts)) if parts else ""
|
|
144
|
+
sql = f"""
|
|
145
|
+
SELECT namespace, key, content, value, token_count, tags,
|
|
146
|
+
created_at, accessed_at, metadata
|
|
147
|
+
FROM memory_fragments
|
|
148
|
+
{where}
|
|
149
|
+
ORDER BY
|
|
150
|
+
CASE value
|
|
151
|
+
WHEN 'pinned' THEN 0
|
|
152
|
+
WHEN 'knowledge' THEN 1
|
|
153
|
+
WHEN 'working' THEN 2
|
|
154
|
+
WHEN 'ephemeral' THEN 3
|
|
155
|
+
END,
|
|
156
|
+
accessed_at DESC
|
|
157
|
+
LIMIT ?
|
|
158
|
+
"""
|
|
159
|
+
params.append(query.max_results)
|
|
160
|
+
rows = conn.execute(sql, params).fetchall()
|
|
161
|
+
return [_row_to_fragment(tuple(r)) for r in rows]
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
async with self._lock:
|
|
165
|
+
return await self._run(_load)
|
|
166
|
+
except Exception:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
async def save(self, fragments: list[MemoryFragment]) -> None:
|
|
170
|
+
def _save():
|
|
171
|
+
conn = self._open()
|
|
172
|
+
now = _NOW()
|
|
173
|
+
rows = [
|
|
174
|
+
(
|
|
175
|
+
f.namespace,
|
|
176
|
+
f.key,
|
|
177
|
+
f.content,
|
|
178
|
+
f.value.value,
|
|
179
|
+
f.token_count or len(f.content) // 4,
|
|
180
|
+
json.dumps(f.tags),
|
|
181
|
+
f.created_at.isoformat() if f.created_at else now,
|
|
182
|
+
now,
|
|
183
|
+
json.dumps(f.metadata),
|
|
184
|
+
)
|
|
185
|
+
for f in fragments
|
|
186
|
+
]
|
|
187
|
+
conn.executemany("""
|
|
188
|
+
INSERT INTO memory_fragments
|
|
189
|
+
(namespace, key, content, value, token_count, tags,
|
|
190
|
+
created_at, accessed_at, metadata)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
192
|
+
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
193
|
+
content = excluded.content,
|
|
194
|
+
value = excluded.value,
|
|
195
|
+
token_count = excluded.token_count,
|
|
196
|
+
tags = excluded.tags,
|
|
197
|
+
accessed_at = excluded.accessed_at,
|
|
198
|
+
metadata = excluded.metadata
|
|
199
|
+
""", rows)
|
|
200
|
+
conn.commit()
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
async with self._lock:
|
|
204
|
+
await self._run(_save)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
async def delete(self, namespace: str, keys: list[str]) -> int:
|
|
209
|
+
def _delete():
|
|
210
|
+
conn = self._open()
|
|
211
|
+
placeholders = ",".join("?" * len(keys))
|
|
212
|
+
cur = conn.execute(
|
|
213
|
+
f"DELETE FROM memory_fragments WHERE namespace=? AND key IN ({placeholders})",
|
|
214
|
+
[namespace, *keys],
|
|
215
|
+
)
|
|
216
|
+
conn.commit()
|
|
217
|
+
return cur.rowcount
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
async with self._lock:
|
|
221
|
+
return await self._run(_delete)
|
|
222
|
+
except Exception:
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
async def evict(
|
|
226
|
+
self,
|
|
227
|
+
namespace: str,
|
|
228
|
+
values: tuple[FragmentValue, ...] = (FragmentValue.EPHEMERAL,),
|
|
229
|
+
max_age_seconds: int | None = None,
|
|
230
|
+
) -> int:
|
|
231
|
+
def _evict():
|
|
232
|
+
conn = self._open()
|
|
233
|
+
parts: list[str] = ["namespace = ?"]
|
|
234
|
+
params: list[Any] = [namespace]
|
|
235
|
+
|
|
236
|
+
if values:
|
|
237
|
+
placeholders = ",".join("?" * len(values))
|
|
238
|
+
parts.append(f"value IN ({placeholders})")
|
|
239
|
+
params.extend(v.value for v in values)
|
|
240
|
+
|
|
241
|
+
if max_age_seconds is not None:
|
|
242
|
+
parts.append(
|
|
243
|
+
"accessed_at < datetime('now', ?)"
|
|
244
|
+
)
|
|
245
|
+
params.append(f"-{max_age_seconds} seconds")
|
|
246
|
+
|
|
247
|
+
where = " AND ".join(parts)
|
|
248
|
+
cur = conn.execute(
|
|
249
|
+
f"DELETE FROM memory_fragments WHERE {where}",
|
|
250
|
+
params,
|
|
251
|
+
)
|
|
252
|
+
conn.commit()
|
|
253
|
+
return cur.rowcount
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
async with self._lock:
|
|
257
|
+
return await self._run(_evict)
|
|
258
|
+
except Exception:
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
async def namespaces(self) -> list[str]:
|
|
262
|
+
def _ns():
|
|
263
|
+
conn = self._open()
|
|
264
|
+
rows = conn.execute(
|
|
265
|
+
"SELECT DISTINCT namespace FROM memory_fragments ORDER BY namespace"
|
|
266
|
+
).fetchall()
|
|
267
|
+
return [r[0] for r in rows]
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
async with self._lock:
|
|
271
|
+
return await self._run(_ns)
|
|
272
|
+
except Exception:
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
async def close(self) -> None:
|
|
276
|
+
if self._conn is not None:
|
|
277
|
+
self._conn.close()
|
|
278
|
+
self._conn = None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "axor-memory-sqlite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SQLite memory provider for axor-core"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["axor", "memory", "sqlite", "agents"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Database",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["axor-core>=0.1.0"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/Bucha11/axor-memory-sqlite"
|
|
29
|
+
"Bug Tracker" = "https://github.com/Bucha11/axor-memory-sqlite/issues"
|
|
30
|
+
Changelog = "https://github.com/Bucha11/axor-memory-sqlite/releases"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["axor_memory_sqlite"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
asyncio_mode = "auto"
|
|
37
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from axor_core.contracts.memory import FragmentValue, MemoryFragment, MemoryQuery
|
|
8
|
+
from axor_memory_sqlite import SQLiteMemoryProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_save_and_load_orders_by_value_priority() -> None:
|
|
13
|
+
provider = SQLiteMemoryProvider(":memory:")
|
|
14
|
+
try:
|
|
15
|
+
await provider.save([
|
|
16
|
+
MemoryFragment(namespace="agent", key="working", content="w", value=FragmentValue.WORKING),
|
|
17
|
+
MemoryFragment(namespace="agent", key="pinned", content="p", value=FragmentValue.PINNED),
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
fragments = await provider.load(MemoryQuery(namespaces=("agent",), max_results=10))
|
|
21
|
+
|
|
22
|
+
assert [fragment.key for fragment in fragments] == ["pinned", "working"]
|
|
23
|
+
finally:
|
|
24
|
+
await provider.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_delete_and_namespaces() -> None:
|
|
29
|
+
provider = SQLiteMemoryProvider(":memory:")
|
|
30
|
+
try:
|
|
31
|
+
await provider.save([
|
|
32
|
+
MemoryFragment(namespace="agent-a", key="one", content="a", value=FragmentValue.KNOWLEDGE),
|
|
33
|
+
MemoryFragment(namespace="agent-b", key="two", content="b", value=FragmentValue.EPHEMERAL),
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
namespaces = await provider.namespaces()
|
|
37
|
+
deleted = await provider.delete("agent-a", ["one"])
|
|
38
|
+
remaining = await provider.load(MemoryQuery(namespaces=("agent-a", "agent-b"), max_results=10))
|
|
39
|
+
|
|
40
|
+
assert namespaces == ["agent-a", "agent-b"]
|
|
41
|
+
assert deleted == 1
|
|
42
|
+
assert [fragment.namespace for fragment in remaining] == ["agent-b"]
|
|
43
|
+
finally:
|
|
44
|
+
await provider.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_evict_by_value() -> None:
|
|
49
|
+
provider = SQLiteMemoryProvider(":memory:")
|
|
50
|
+
try:
|
|
51
|
+
old = datetime(2000, 1, 1, tzinfo=timezone.utc)
|
|
52
|
+
await provider.save([
|
|
53
|
+
MemoryFragment(namespace="agent", key="old", content="x", value=FragmentValue.EPHEMERAL, created_at=old),
|
|
54
|
+
MemoryFragment(namespace="agent", key="keep", content="y", value=FragmentValue.PINNED, created_at=old),
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
removed = await provider.evict("agent", values=(FragmentValue.EPHEMERAL,))
|
|
58
|
+
remaining = await provider.load(MemoryQuery(namespaces=("agent",), max_results=10))
|
|
59
|
+
|
|
60
|
+
assert removed == 1
|
|
61
|
+
assert [fragment.key for fragment in remaining] == ["keep"]
|
|
62
|
+
finally:
|
|
63
|
+
await provider.close()
|