axor-memory-sqlite 0.1.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.
@@ -0,0 +1,5 @@
1
+ """axor-memory-sqlite — SQLite MemoryProvider for axor-core."""
2
+ from axor_memory_sqlite.provider import SQLiteMemoryProvider
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["SQLiteMemoryProvider"]
@@ -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,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
+ [![CI](https://github.com/Bucha11/axor-memory-sqlite/actions/workflows/ci.yml/badge.svg)](https://github.com/Bucha11/axor-memory-sqlite/actions/workflows/ci.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/axor-memory-sqlite)](https://pypi.org/project/axor-memory-sqlite/)
28
+ [![Python](https://img.shields.io/pypi/pyversions/axor-memory-sqlite)](https://pypi.org/project/axor-memory-sqlite/)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ axor_memory_sqlite/__init__.py,sha256=T1yyp1lq90BOHBZ1BZhFgc_wAajvxb94kbOLFa6ZaRM,185
2
+ axor_memory_sqlite/provider.py,sha256=v0RQaVju8RE9_HfhsRl3NEEDIh67ALqTYbQcQypHNmc,9210
3
+ axor_memory_sqlite-0.1.0.dist-info/METADATA,sha256=CuD7yzxPVq_EDba6B-9Tl4dsyXsO0l3PlOTuRbSax04,7317
4
+ axor_memory_sqlite-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ axor_memory_sqlite-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any