Skip to content

Commit 16a79ea

Browse files
joaoh82claude
andauthored
feat(examples): SQLR-39 Python LLM agent with persistent memory in SQLRite (#140)
The first SQLR-38 example app. A Python CLI chat agent whose long-term memory is one .sqlrite file. Each turn embeds the user input, runs hybrid recall (vector KNN via HNSW + BM25 via Phase 8 fts_match), assembles a system prompt from the recalled facts/summaries/messages, calls the LLM, and writes the new turn back. Persists across process restarts because the entire memory layer is a regular SQLRite database — the demo's whole point. Package: examples/python-agent/ (Python 3.11+, pinned to sqlrite>=0.10,<0.11). Binds only to the SDK's documented surface (sqlrite.connect, Connection, Cursor); no internals. Layers: - db.py: schema + migrations + SQL (3 tables, HNSW + FTS indexes) - sqlutil.py: safe SQL-literal inlining (the SDK doesn't bind params yet) - embeddings.py: hash / OpenAI / sentence-transformers - facts.py: regex-based (subject, predicate, object) extraction - memory.py: hybrid recall (vector + lexical + facts) with merge - chat.py: Anthropic / Echo (offline fallback so zero-key first run works) - agent.py: turn loop, prompt assembly, manual summarize_window() - cli.py: interactive REPL + /facts /recall /summarize slash commands 31 offline tests; runs end-to-end without an API key. Adds /examples landing page to sqlritedb.com (nav link + sitemap entry). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a38023a commit 16a79ea

23 files changed

Lines changed: 2454 additions & 0 deletions

examples/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ Phase 5 lands these incrementally — each sub-phase fills in one language. The
1616

1717
See [docs/roadmap.md](../docs/roadmap.md) for what each sub-phase delivers.
1818

19+
## End-to-end example apps
20+
21+
Beyond the per-SDK quick-start tours above, the [SQLR-38 umbrella](../docs/roadmap.md) tracks longer, opinionated example apps that exercise SQLRite end-to-end in real-world shapes:
22+
23+
| App | Language / SDK | What it shows | Directory |
24+
|---|---|---|---|
25+
| LLM agent with persistent memory | Python | Vector + lexical recall, fact extraction, summaries — all in one `.sqlrite` file | [`python-agent/`](python-agent/) |
26+
1927
## Running the Rust quickstart
2028

2129
```bash
@@ -63,6 +71,17 @@ python examples/python/hello.py
6371

6472
Mirrors the Rust quickstart shape via the DB-API: `sqlrite.connect(":memory:")``cursor.execute` → iterate tuples, plus a BEGIN/ROLLBACK block. See [`python/hello.py`](python/hello.py) and [`sdk/python/README.md`](../sdk/python/README.md) for the full API tour.
6573

74+
## Running the Python LLM agent (SQLR-39)
75+
76+
```bash
77+
cd examples/python-agent
78+
python3 -m venv .venv && source .venv/bin/activate
79+
pip install -e .
80+
python -m sqlrite_agent # works offline; no API key required
81+
```
82+
83+
A full CLI chat agent whose long-term memory is one `.sqlrite` file. Embeds each turn, hybrid-searches over past messages and a structured `facts` table on every recall, and survives process restarts. Read [`python-agent/README.md`](python-agent/README.md) for the demo script and architecture diagram.
84+
6685
## Running the Node.js sample
6786

6887
```bash

examples/python-agent/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.egg-info/
4+
.pytest_cache/
5+
.coverage
6+
build/
7+
dist/
8+
9+
# Local agent state.
10+
*.sqlrite
11+
*.sqlrite-wal
12+
*.sqlrite-lock

examples/python-agent/README.md

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# sqlrite-agent — a Python LLM agent with persistent memory in SQLRite
2+
3+
A small CLI chat agent whose entire long-term memory lives in **one
4+
`.sqlrite` file on disk**. Every turn the agent:
5+
6+
1. Embeds the user's message.
7+
2. Hybrid-searches its memory: top-k vector KNN (HNSW) over past
8+
messages and summaries, plus keyword recall over the structured
9+
`facts` table.
10+
3. Injects the recalled context into the system prompt.
11+
4. Sends prompt + recent turns to an LLM and prints the reply.
12+
5. Writes the new turn back to the same SQLRite database.
13+
14+
Close the process, reopen it days later — your assistant still knows
15+
your dog's name. No Postgres, no Pinecone, no Redis. One file.
16+
17+
> **Why this example?** Single-file embedded storage is the *right*
18+
> architecture for a local agent, and SQLRite's HNSW vector index +
19+
> structured SQL gives you semantic *and* deterministic recall from one
20+
> store. This is a place where the database genuinely fits the
21+
> workload, not just a demo.
22+
23+
## Architecture
24+
25+
```mermaid
26+
flowchart LR
27+
User[/"User input"/] --> Embed["Embedder<br/>(hash / OpenAI / local)"]
28+
Embed --> Recall["Memory.recall()"]
29+
Recall -->|vector KNN| Msgs[("messages<br/>HNSW(embedding)")]
30+
Recall -->|vector KNN| Sums[("summaries<br/>HNSW(embedding)")]
31+
Recall -->|keyword| Facts[("facts<br/>SQL")]
32+
Recall --> Prompt["Prompt assembly<br/>(system + recent turns)"]
33+
Prompt --> LLM["LLM<br/>(Anthropic / echo)"]
34+
LLM --> Reply[/"Assistant reply"/]
35+
Reply --> Writeback["Memory.log_message()"]
36+
Writeback --> Msgs
37+
User -.-> Writeback
38+
```
39+
40+
Three tables, one file:
41+
42+
| Table | Purpose | Indexed how |
43+
|-------------|-------------------------------------------------------------|------------------------------------------|
44+
| `messages` | Every user / assistant turn, plus its 384-dim embedding. | HNSW on `embedding` |
45+
| `summaries` | Periodic rollups for old context that's too long to inline. | HNSW on `embedding` |
46+
| `facts` | Structured `(subject, predicate, object)` triples extracted from user turns. | Plain SQL — keyword recall |
47+
48+
## Install
49+
50+
```bash
51+
# 1. Clone the rust_sqlite repo (this example ships inside it).
52+
git clone https://github.com/joaoh82/rust_sqlite
53+
cd rust_sqlite/examples/python-agent
54+
55+
# 2. Create a virtualenv and install the example.
56+
python3 -m venv .venv && source .venv/bin/activate
57+
pip install -e .
58+
59+
# 3. (Optional) Install an LLM provider extra. Without one you get
60+
# the offline "echo" agent — the recall pipeline still runs.
61+
pip install 'sqlrite-agent[anthropic]' # default LLM
62+
# pip install 'sqlrite-agent[openai]' # use OpenAI embeddings too
63+
# pip install 'sqlrite-agent[local-embeddings]' # 384-dim sentence-transformer
64+
```
65+
66+
The `sqlrite` Python wheel comes from PyPI automatically (pinned to the
67+
0.10.x release that introduced `VECTOR(N)` + HNSW indexes).
68+
69+
## Run
70+
71+
```bash
72+
# Zero config — runs against a fresh in-memory hash embedder and the
73+
# offline echo "LLM". You see the recall pipeline work end-to-end
74+
# without an API key.
75+
python -m sqlrite_agent
76+
77+
# With Anthropic — set ANTHROPIC_API_KEY and run.
78+
export ANTHROPIC_API_KEY=sk-ant-...
79+
python -m sqlrite_agent
80+
81+
# Pick where the DB lives (default: ~/.sqlrite-agent.sqlrite).
82+
python -m sqlrite_agent --db ./my-agent.sqlrite
83+
84+
# Multiple parallel conversations in one DB.
85+
python -m sqlrite_agent --conversation work
86+
python -m sqlrite_agent --conversation personal
87+
88+
# Force a specific embedder.
89+
python -m sqlrite_agent --embedder local # sentence-transformers
90+
python -m sqlrite_agent --embedder openai # text-embedding-3-small
91+
```
92+
93+
## CLI commands
94+
95+
While the REPL is running, anything starting with `/` is a command:
96+
97+
| Command | What it does |
98+
|--------------------|----------------------------------------------------------------|
99+
| `/help` | Show all commands. |
100+
| `/stats` | Counts of messages, summaries, facts. |
101+
| `/facts` | List every extracted fact. |
102+
| `/recent` | Last 10 turns in chronological order. |
103+
| `/recall <query>` | Show what *would* be recalled for a query, without replying. |
104+
| `/summarize` | Summarize the last 20 turns into a single `summaries` row. |
105+
| `/quit` | Exit. `Ctrl-D` also works. |
106+
107+
## 60-second demo script
108+
109+
Run this top-to-bottom to see persistent memory survive a process
110+
restart. Uses the zero-key default (`hash` embedder + `echo` chat).
111+
112+
```bash
113+
# Session 1 — drop some facts, then quit.
114+
$ python -m sqlrite_agent --db agent.sqlrite
115+
sqlrite-agent 0.1.0 — db=agent.sqlrite, ...
116+
loaded memory: 0 messages, 0 summaries, 0 facts.
117+
118+
you> My dog's name is Mochi.
119+
agent> [echo agent ...]
120+
121+
you> Mochi loves carrots more than treats.
122+
agent> [echo agent ...]
123+
124+
you> I live in Lisbon, Portugal.
125+
agent> [echo agent ...]
126+
127+
you> /facts
128+
user.dog.name = Mochi
129+
user.location = Lisbon, Portugal
130+
131+
you> /quit
132+
133+
# Session 2 — fresh process, same DB.
134+
$ python -m sqlrite_agent --db agent.sqlrite
135+
sqlrite-agent 0.1.0 — db=agent.sqlrite, ...
136+
loaded memory: 6 messages, 0 summaries, 2 facts.
137+
138+
you> What's my dog's name?
139+
[recalled: 1 facts, 0 summaries, 4 messages]
140+
agent> [echo agent ... — but the recall block above includes
141+
user.dog.name = Mochi]
142+
```
143+
144+
With `ANTHROPIC_API_KEY` set, the second turn answers "Mochi" instead
145+
of the canned echo because the LLM sees the recalled fact in its
146+
system prompt.
147+
148+
## Open the DB yourself with the SQLRite REPL
149+
150+
The memory file is plain SQLRite — open it from anywhere:
151+
152+
```bash
153+
$ cargo install sqlrite-engine # or grab a binary from GitHub Releases
154+
$ sqlrite agent.sqlrite
155+
SQLRite v0.10.0
156+
sqlrite> SELECT role, content FROM messages ORDER BY id LIMIT 5;
157+
sqlrite> SELECT subject, predicate, object FROM facts;
158+
sqlrite> SELECT id, content
159+
...> FROM messages
160+
...> ORDER BY vec_distance_cosine(embedding, (SELECT embedding FROM messages WHERE id = 1))
161+
...> LIMIT 3;
162+
```
163+
164+
This is the demo's whole point: **the agent's memory is just SQL**.
165+
You can query it, back it up, copy it between machines, or load it
166+
into the Node / Go / WASM SDK without converting anything.
167+
168+
## How recall works
169+
170+
`Memory.recall(query)` runs three searches in parallel and merges the
171+
results. Pseudocode:
172+
173+
```python
174+
embedding = embedder.embed(query)
175+
keywords = query_keywords(query) # filtered to content words
176+
177+
vector_messages = SELECT ... FROM messages
178+
ORDER BY vec_distance_cosine(embedding, ?)
179+
LIMIT k_messages
180+
181+
vector_summaries = SELECT ... FROM summaries
182+
ORDER BY vec_distance_cosine(embedding, ?)
183+
LIMIT k_summaries
184+
185+
lexical_messages = SELECT ... FROM messages
186+
WHERE fts_match(content, ?)
187+
ORDER BY bm25_score(content, ?) DESC
188+
LIMIT k_messages -- Phase 8, BM25 over the inverted index
189+
190+
facts = SELECT * FROM facts
191+
WHERE subject LIKE ...
192+
OR predicate LIKE ...
193+
OR object LIKE ...
194+
LIMIT k_facts
195+
```
196+
197+
The vector and lexical message lists are merged in Python (dedupe by
198+
`id`, vector ranking primary) — that's the simplest correct shape for
199+
hybrid retrieval: vector finds conceptual neighbors even with zero
200+
lexical overlap, and BM25 surfaces exact-term matches the vector
201+
embedding might rank too low. See [`docs/fts.md`](../../docs/fts.md)
202+
for the BM25 surface and [`examples/hybrid-retrieval/`](../hybrid-retrieval/)
203+
for an example that fuses both into a single `ORDER BY` arithmetic.
204+
205+
## Embedding-provider tradeoffs
206+
207+
| Provider | Dependencies | API key | Real semantic recall | First-run friction |
208+
|----------------|-----------------------|---------|----------------------|--------------------|
209+
| `hash` (default) | None — stdlib only | No | Bag-of-words approximation only. Good enough for the demo, mediocre for real RAG. | Zero. |
210+
| `openai` | `openai` package | `OPENAI_API_KEY` | Excellent. `text-embedding-3-small` constrained to 384 dims. | ~30s install. |
211+
| `local` | `sentence-transformers` (~500 MB with torch) | No | Excellent. `all-MiniLM-L6-v2`, fully offline. | ~5 min install. |
212+
213+
Swap with `--embedder hash | openai | local`. The dimension is fixed
214+
at 384 to match `VECTOR(384)` in the schema; if you need a different
215+
dim, change `DEFAULT_DIM` in `sqlrite_agent/db.py` and start with a
216+
fresh DB.
217+
218+
## Known simplifications
219+
220+
This is an *example*, not a production agent. Things v1 punts on:
221+
222+
- **Memory eviction.** No automatic rolling-window or summarize-and-evict
223+
loop yet — run `/summarize` manually when the conversation grows
224+
unwieldy.
225+
- **Fact extraction.** Six hand-written regex patterns. A real agent
226+
would call the LLM to extract facts so it catches phrasings the
227+
regex misses. Easy upgrade: wrap an LLM call in `facts.extract_facts`.
228+
- **Single-query hybrid.** The agent merges vector hits and BM25 hits
229+
in Python. The engine also supports a single SQL query that fuses
230+
both into one `ORDER BY` arithmetic (`0.5 * bm25_score(...) + 0.5 *
231+
(1.0 - vec_distance_cosine(...))`) — see [`examples/hybrid-retrieval/`](../hybrid-retrieval/).
232+
The merge approach handles conceptual queries with no token overlap;
233+
the single-query approach is tighter when you always want BM25 to
234+
pre-filter. Pick per workload.
235+
- **Concurrency.** The agent assumes single-user single-process. The
236+
engine supports concurrent reads + a single writer via fs2 advisory
237+
locks; running two `sqlrite-agent` processes against the same DB
238+
works, but they won't see each other's in-flight writes until commit.
239+
240+
## Development
241+
242+
```bash
243+
# Tests run fully offline with the hash embedder and echo chat — no
244+
# API keys, no network.
245+
pip install -e '.[dev]'
246+
pytest
247+
```
248+
249+
## Layout
250+
251+
```
252+
examples/python-agent/
253+
├── pyproject.toml # package metadata + pinned sqlrite dep
254+
├── README.md # this file
255+
├── sqlrite_agent/
256+
│ ├── __init__.py
257+
│ ├── __main__.py # python -m sqlrite_agent → cli.main()
258+
│ ├── agent.py # turn loop, prompt assembly, summarization
259+
│ ├── chat.py # LLM provider abstraction (Anthropic / Echo)
260+
│ ├── cli.py # interactive REPL + slash commands
261+
│ ├── db.py # schema, migrations, all SQL
262+
│ ├── embeddings.py # Embedder abstraction (hash / OpenAI / local)
263+
│ ├── facts.py # regex-based fact extractor
264+
│ ├── memory.py # hybrid recall over messages + summaries + facts
265+
│ └── sqlutil.py # safe SQL-literal inlining
266+
└── tests/ # offline pytest suite (31 tests)
267+
```
268+
269+
The agent binds only to the SQLRite Python SDK's documented public
270+
surface (`sqlrite.connect`, `Connection`, `Cursor`). It does not reach
271+
into internals.
272+
273+
## License
274+
275+
MIT — same as the rest of the rust_sqlite repo.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "sqlrite-agent"
7+
version = "0.1.0"
8+
description = "Example: a Python CLI chat agent with persistent long-term memory backed entirely by SQLRite (vector + lexical recall, periodic summarization, structured fact extraction)."
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = { text = "MIT" }
12+
authors = [{ name = "SQLRite contributors" }]
13+
keywords = ["sqlrite", "llm", "agent", "rag", "memory", "vector-search"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Programming Language :: Python :: 3.11",
17+
"Programming Language :: Python :: 3.12",
18+
"License :: OSI Approved :: MIT License",
19+
"Topic :: Database",
20+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
21+
]
22+
23+
# The agent binds only to the public SQLRite Python SDK surface
24+
# (`sqlrite.connect`, `Connection`, `Cursor`). Pinned to the 0.10.x
25+
# release that introduced VECTOR + HNSW + the `ask` feature.
26+
dependencies = [
27+
"sqlrite>=0.10.0,<0.11.0",
28+
]
29+
30+
[project.optional-dependencies]
31+
# Cloud LLM + embedding providers — install whichever you want.
32+
anthropic = ["anthropic>=0.40"]
33+
openai = ["openai>=1.40"]
34+
# Local embedding model — heavy install (~500 MB with torch), but
35+
# means no API key is needed for the embedding half of recall.
36+
local-embeddings = ["sentence-transformers>=2.7"]
37+
# Everything at once for a full demo.
38+
all = ["anthropic>=0.40", "openai>=1.40", "sentence-transformers>=2.7"]
39+
dev = ["pytest>=7", "pytest-cov"]
40+
41+
[project.scripts]
42+
sqlrite-agent = "sqlrite_agent.cli:main"
43+
44+
[project.urls]
45+
Homepage = "https://sqlritedb.com"
46+
Repository = "https://github.com/joaoh82/rust_sqlite"
47+
"Bug Tracker" = "https://github.com/joaoh82/rust_sqlite/issues"
48+
49+
[tool.setuptools.packages.find]
50+
where = ["."]
51+
include = ["sqlrite_agent*"]
52+
53+
[tool.pytest.ini_options]
54+
testpaths = ["tests"]
55+
addopts = "-ra --tb=short"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""sqlrite_agent — an example LLM chat agent with persistent memory in SQLRite."""
2+
3+
__version__ = "0.1.0"
4+
__all__ = ["__version__"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from sqlrite_agent.cli import main
2+
3+
if __name__ == "__main__":
4+
raise SystemExit(main())

0 commit comments

Comments
 (0)