remembrane 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.
- remembrane-0.1.0/.github/workflows/ci.yml +21 -0
- remembrane-0.1.0/.gitignore +11 -0
- remembrane-0.1.0/CONTRIBUTING.md +22 -0
- remembrane-0.1.0/LICENSE +21 -0
- remembrane-0.1.0/PKG-INFO +192 -0
- remembrane-0.1.0/README.md +159 -0
- remembrane-0.1.0/pyproject.toml +51 -0
- remembrane-0.1.0/src/remembrane/__init__.py +17 -0
- remembrane-0.1.0/src/remembrane/adapters/__init__.py +10 -0
- remembrane-0.1.0/src/remembrane/adapters/crewai.py +47 -0
- remembrane-0.1.0/src/remembrane/adapters/langchain.py +73 -0
- remembrane-0.1.0/src/remembrane/cli.py +64 -0
- remembrane-0.1.0/src/remembrane/embedders.py +118 -0
- remembrane-0.1.0/src/remembrane/mcp_server.py +83 -0
- remembrane-0.1.0/src/remembrane/models.py +77 -0
- remembrane-0.1.0/src/remembrane/scoring.py +59 -0
- remembrane-0.1.0/src/remembrane/store.py +323 -0
- remembrane-0.1.0/tests/test_adapters.py +56 -0
- remembrane-0.1.0/tests/test_cli.py +30 -0
- remembrane-0.1.0/tests/test_embedders.py +38 -0
- remembrane-0.1.0/tests/test_scoring.py +44 -0
- remembrane-0.1.0/tests/test_store.py +136 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e .[dev]
|
|
20
|
+
- run: ruff check src tests
|
|
21
|
+
- run: pytest --cov=remembrane --cov-report=term-missing
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contributing to remembrane
|
|
2
|
+
|
|
3
|
+
Thanks for your interest!
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/satyasairay/remembrane
|
|
9
|
+
cd remembrane
|
|
10
|
+
pip install -e .[dev]
|
|
11
|
+
pytest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Guidelines
|
|
15
|
+
|
|
16
|
+
- Keep the core dependency-free. New integrations go in `src/remembrane/adapters/` as duck-typed, optional modules.
|
|
17
|
+
- Every PR needs tests. Run `pytest` and `ruff check src tests` before submitting.
|
|
18
|
+
- One feature per PR. Open an issue first for anything large.
|
|
19
|
+
|
|
20
|
+
## Ideas welcome
|
|
21
|
+
|
|
22
|
+
Good first contributions: new framework adapters, embedder backends, recall strategies (MMR, hybrid keyword+vector), benchmarks.
|
remembrane-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Satyasai Ray
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remembrane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first persistent memory for AI agents. SQLite-backed, zero required dependencies, pluggable embeddings, framework adapters and an MCP server.
|
|
5
|
+
Project-URL: Homepage, https://github.com/satyasairay/remembrane
|
|
6
|
+
Project-URL: Issues, https://github.com/satyasairay/remembrane/issues
|
|
7
|
+
Author-email: Satyasai Ray <satyasairay@yahoo.com>, Satyasai Ray <satyasairay2@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,crewai,langchain,llm,local-first,mcp,memory,sqlite
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
28
|
+
Provides-Extra: openai
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
30
|
+
Provides-Extra: sentence-transformers
|
|
31
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'sentence-transformers'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# remembrane
|
|
35
|
+
|
|
36
|
+
**Local-first persistent memory for AI agents.** SQLite-backed, zero required dependencies, pluggable embeddings, with adapters for LangChain and CrewAI and a built-in MCP server.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install remembrane
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Why
|
|
43
|
+
|
|
44
|
+
Agents forget everything between sessions. Existing memory solutions are cloud APIs, require a vector database, or drag in a heavyweight framework. `remembrane` is the opposite:
|
|
45
|
+
|
|
46
|
+
- **One file.** Your agent's entire memory is a SQLite database you can copy, back up, diff, or delete.
|
|
47
|
+
- **Zero required dependencies.** The default embedder is pure stdlib. `pip install remembrane` pulls in nothing else.
|
|
48
|
+
- **Human-like recall.** Results are ranked by a composite of semantic similarity, recency decay (memories halve in weight every week by default), and importance. Recalled memories are *reinforced* — spaced repetition for agents.
|
|
49
|
+
- **Framework-agnostic.** Use it bare, through the LangChain or CrewAI adapters, or expose it to any MCP-capable agent (like Claude) as an MCP server.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from remembrane import MemoryStore
|
|
55
|
+
|
|
56
|
+
mem = MemoryStore("agent.db") # or ":memory:" for ephemeral
|
|
57
|
+
|
|
58
|
+
mem.store("User prefers dark mode", importance=0.8)
|
|
59
|
+
mem.store("Deploy target is AWS us-east-1", namespace="ops")
|
|
60
|
+
|
|
61
|
+
results = mem.recall("what theme does the user like?")
|
|
62
|
+
print(results[0].memory.content) # → "User prefers dark mode"
|
|
63
|
+
print(results[0].score) # similarity × recency × importance
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Memory lifecycle
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
mem.reinforce(memory_id) # strengthen: slower decay, higher rank
|
|
70
|
+
mem.forget(memory_id) # delete one
|
|
71
|
+
mem.forget(namespace="ops") # delete a namespace
|
|
72
|
+
mem.forget(older_than_seconds=30*86400) # prune stale memories
|
|
73
|
+
mem.consolidate() # merge near-duplicates
|
|
74
|
+
mem.export() # plain dicts, ready for json.dump
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Tuning recall
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from remembrane import MemoryStore, ScoringConfig
|
|
81
|
+
|
|
82
|
+
mem = MemoryStore(
|
|
83
|
+
"agent.db",
|
|
84
|
+
scoring=ScoringConfig(
|
|
85
|
+
weight_similarity=0.7,
|
|
86
|
+
weight_recency=0.15,
|
|
87
|
+
weight_importance=0.15,
|
|
88
|
+
half_life_seconds=7 * 24 * 3600, # recency halves every week
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Embedders
|
|
94
|
+
|
|
95
|
+
The default `HashEmbedder` is deterministic, offline, and dependency-free — it hashes word and character n-grams. That makes similarity *lexical*, not semantic. It works well for typical agent memories (facts, preferences, short statements). For true semantic recall, plug in a real model:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from remembrane import MemoryStore, SentenceTransformerEmbedder, OpenAIEmbedder
|
|
99
|
+
|
|
100
|
+
mem = MemoryStore("agent.db", embedder=SentenceTransformerEmbedder()) # local, pip install remembrane[sentence-transformers]
|
|
101
|
+
mem = MemoryStore("agent.db", embedder=OpenAIEmbedder()) # API, pip install remembrane[openai]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Any object with `embed(texts) -> List[List[float]]` and a `dimension` attribute works.
|
|
105
|
+
|
|
106
|
+
Note: don't mix embedders in one database. Vectors from different embedders aren't comparable.
|
|
107
|
+
|
|
108
|
+
## LangChain
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from remembrane import MemoryStore
|
|
112
|
+
from remembrane.adapters import RemembraneChatMemory
|
|
113
|
+
|
|
114
|
+
memory = RemembraneChatMemory(MemoryStore("agent.db"), session_id="user-42")
|
|
115
|
+
|
|
116
|
+
memory.save_context({"input": "my favorite color is teal"}, {"output": "Noted!"})
|
|
117
|
+
memory.load_memory_variables({"input": "what color do I like?"})
|
|
118
|
+
# {'history': 'human: my favorite color is teal\nai: Noted!'}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Unlike buffer memory, this retrieves the exchanges *relevant to the current input* — the context window stays small no matter how long the history grows.
|
|
122
|
+
|
|
123
|
+
## CrewAI
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from remembrane import MemoryStore
|
|
127
|
+
from remembrane.adapters import RemembraneStorage
|
|
128
|
+
|
|
129
|
+
storage = RemembraneStorage(MemoryStore("crew.db"))
|
|
130
|
+
storage.save("the deadline is next friday", metadata={"task": "planning"})
|
|
131
|
+
storage.search("when is the deadline?")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## MCP server
|
|
135
|
+
|
|
136
|
+
Give any MCP-capable agent (e.g. Claude Desktop, Claude Code) persistent memory:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install remembrane[mcp]
|
|
140
|
+
remembrane-mcp --db ~/agent-memory.db
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"remembrane": {
|
|
147
|
+
"command": "remembrane-mcp",
|
|
148
|
+
"args": ["--db", "/path/to/agent-memory.db"]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Tools exposed: `memory_store`, `memory_recall`, `memory_forget`, `memory_reinforce`, `memory_stats`.
|
|
155
|
+
|
|
156
|
+
## CLI
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
remembrane --db agent.db store "the user prefers dark mode" --importance 0.8
|
|
160
|
+
remembrane --db agent.db recall "what theme?"
|
|
161
|
+
remembrane --db agent.db list
|
|
162
|
+
remembrane --db agent.db stats
|
|
163
|
+
remembrane --db agent.db export > backup.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## How ranking works
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
score = 0.7·similarity + 0.15·recency + 0.15·importance
|
|
170
|
+
recency = exp(−ln2 · age / half_life)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`age` is measured from the memory's **last access**, not creation — every recall resets the decay clock. Frequently-used memories stay vivid; untouched ones fade. All weights and the half-life are configurable.
|
|
174
|
+
|
|
175
|
+
## Design choices
|
|
176
|
+
|
|
177
|
+
- **SQLite over a vector DB** — agent memory stores are small (thousands, not billions, of rows). Brute-force cosine over a few thousand vectors is sub-millisecond, and you gain transactions, a single portable file, and zero infra.
|
|
178
|
+
- **No background daemon** — decay is computed at read time, so nothing runs when your agent doesn't.
|
|
179
|
+
- **Duck-typed adapters** — `remembrane` never imports langchain or crewai; the adapters match their interfaces structurally, so there are no version-pinning fights.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git clone https://github.com/satyasairay/remembrane
|
|
185
|
+
cd remembrane
|
|
186
|
+
pip install -e .[dev]
|
|
187
|
+
pytest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# remembrane
|
|
2
|
+
|
|
3
|
+
**Local-first persistent memory for AI agents.** SQLite-backed, zero required dependencies, pluggable embeddings, with adapters for LangChain and CrewAI and a built-in MCP server.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install remembrane
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Agents forget everything between sessions. Existing memory solutions are cloud APIs, require a vector database, or drag in a heavyweight framework. `remembrane` is the opposite:
|
|
12
|
+
|
|
13
|
+
- **One file.** Your agent's entire memory is a SQLite database you can copy, back up, diff, or delete.
|
|
14
|
+
- **Zero required dependencies.** The default embedder is pure stdlib. `pip install remembrane` pulls in nothing else.
|
|
15
|
+
- **Human-like recall.** Results are ranked by a composite of semantic similarity, recency decay (memories halve in weight every week by default), and importance. Recalled memories are *reinforced* — spaced repetition for agents.
|
|
16
|
+
- **Framework-agnostic.** Use it bare, through the LangChain or CrewAI adapters, or expose it to any MCP-capable agent (like Claude) as an MCP server.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from remembrane import MemoryStore
|
|
22
|
+
|
|
23
|
+
mem = MemoryStore("agent.db") # or ":memory:" for ephemeral
|
|
24
|
+
|
|
25
|
+
mem.store("User prefers dark mode", importance=0.8)
|
|
26
|
+
mem.store("Deploy target is AWS us-east-1", namespace="ops")
|
|
27
|
+
|
|
28
|
+
results = mem.recall("what theme does the user like?")
|
|
29
|
+
print(results[0].memory.content) # → "User prefers dark mode"
|
|
30
|
+
print(results[0].score) # similarity × recency × importance
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Memory lifecycle
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
mem.reinforce(memory_id) # strengthen: slower decay, higher rank
|
|
37
|
+
mem.forget(memory_id) # delete one
|
|
38
|
+
mem.forget(namespace="ops") # delete a namespace
|
|
39
|
+
mem.forget(older_than_seconds=30*86400) # prune stale memories
|
|
40
|
+
mem.consolidate() # merge near-duplicates
|
|
41
|
+
mem.export() # plain dicts, ready for json.dump
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Tuning recall
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from remembrane import MemoryStore, ScoringConfig
|
|
48
|
+
|
|
49
|
+
mem = MemoryStore(
|
|
50
|
+
"agent.db",
|
|
51
|
+
scoring=ScoringConfig(
|
|
52
|
+
weight_similarity=0.7,
|
|
53
|
+
weight_recency=0.15,
|
|
54
|
+
weight_importance=0.15,
|
|
55
|
+
half_life_seconds=7 * 24 * 3600, # recency halves every week
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Embedders
|
|
61
|
+
|
|
62
|
+
The default `HashEmbedder` is deterministic, offline, and dependency-free — it hashes word and character n-grams. That makes similarity *lexical*, not semantic. It works well for typical agent memories (facts, preferences, short statements). For true semantic recall, plug in a real model:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from remembrane import MemoryStore, SentenceTransformerEmbedder, OpenAIEmbedder
|
|
66
|
+
|
|
67
|
+
mem = MemoryStore("agent.db", embedder=SentenceTransformerEmbedder()) # local, pip install remembrane[sentence-transformers]
|
|
68
|
+
mem = MemoryStore("agent.db", embedder=OpenAIEmbedder()) # API, pip install remembrane[openai]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Any object with `embed(texts) -> List[List[float]]` and a `dimension` attribute works.
|
|
72
|
+
|
|
73
|
+
Note: don't mix embedders in one database. Vectors from different embedders aren't comparable.
|
|
74
|
+
|
|
75
|
+
## LangChain
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from remembrane import MemoryStore
|
|
79
|
+
from remembrane.adapters import RemembraneChatMemory
|
|
80
|
+
|
|
81
|
+
memory = RemembraneChatMemory(MemoryStore("agent.db"), session_id="user-42")
|
|
82
|
+
|
|
83
|
+
memory.save_context({"input": "my favorite color is teal"}, {"output": "Noted!"})
|
|
84
|
+
memory.load_memory_variables({"input": "what color do I like?"})
|
|
85
|
+
# {'history': 'human: my favorite color is teal\nai: Noted!'}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Unlike buffer memory, this retrieves the exchanges *relevant to the current input* — the context window stays small no matter how long the history grows.
|
|
89
|
+
|
|
90
|
+
## CrewAI
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from remembrane import MemoryStore
|
|
94
|
+
from remembrane.adapters import RemembraneStorage
|
|
95
|
+
|
|
96
|
+
storage = RemembraneStorage(MemoryStore("crew.db"))
|
|
97
|
+
storage.save("the deadline is next friday", metadata={"task": "planning"})
|
|
98
|
+
storage.search("when is the deadline?")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## MCP server
|
|
102
|
+
|
|
103
|
+
Give any MCP-capable agent (e.g. Claude Desktop, Claude Code) persistent memory:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install remembrane[mcp]
|
|
107
|
+
remembrane-mcp --db ~/agent-memory.db
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"remembrane": {
|
|
114
|
+
"command": "remembrane-mcp",
|
|
115
|
+
"args": ["--db", "/path/to/agent-memory.db"]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Tools exposed: `memory_store`, `memory_recall`, `memory_forget`, `memory_reinforce`, `memory_stats`.
|
|
122
|
+
|
|
123
|
+
## CLI
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
remembrane --db agent.db store "the user prefers dark mode" --importance 0.8
|
|
127
|
+
remembrane --db agent.db recall "what theme?"
|
|
128
|
+
remembrane --db agent.db list
|
|
129
|
+
remembrane --db agent.db stats
|
|
130
|
+
remembrane --db agent.db export > backup.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## How ranking works
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
score = 0.7·similarity + 0.15·recency + 0.15·importance
|
|
137
|
+
recency = exp(−ln2 · age / half_life)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`age` is measured from the memory's **last access**, not creation — every recall resets the decay clock. Frequently-used memories stay vivid; untouched ones fade. All weights and the half-life are configurable.
|
|
141
|
+
|
|
142
|
+
## Design choices
|
|
143
|
+
|
|
144
|
+
- **SQLite over a vector DB** — agent memory stores are small (thousands, not billions, of rows). Brute-force cosine over a few thousand vectors is sub-millisecond, and you gain transactions, a single portable file, and zero infra.
|
|
145
|
+
- **No background daemon** — decay is computed at read time, so nothing runs when your agent doesn't.
|
|
146
|
+
- **Duck-typed adapters** — `remembrane` never imports langchain or crewai; the adapters match their interfaces structurally, so there are no version-pinning fights.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/satyasairay/remembrane
|
|
152
|
+
cd remembrane
|
|
153
|
+
pip install -e .[dev]
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "remembrane"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local-first persistent memory for AI agents. SQLite-backed, zero required dependencies, pluggable embeddings, framework adapters and an MCP server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Satyasai Ray", email = "satyasairay@yahoo.com" },
|
|
14
|
+
{ name = "Satyasai Ray", email = "satyasairay2@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["ai", "agents", "memory", "llm", "mcp", "langchain", "crewai", "sqlite", "local-first"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/satyasairay/remembrane"
|
|
32
|
+
Issues = "https://github.com/satyasairay/remembrane/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
openai = ["openai>=1.0"]
|
|
36
|
+
sentence-transformers = ["sentence-transformers>=2.2"]
|
|
37
|
+
mcp = ["mcp>=1.0"]
|
|
38
|
+
dev = ["pytest>=7.0", "pytest-cov", "ruff"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
remembrane = "remembrane.cli:main"
|
|
42
|
+
remembrane-mcp = "remembrane.mcp_server:main"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/remembrane"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 100
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""remembrane — local-first persistent memory for AI agents."""
|
|
2
|
+
from .embedders import HashEmbedder, OpenAIEmbedder, SentenceTransformerEmbedder, cosine_similarity
|
|
3
|
+
from .models import Memory, RecallResult
|
|
4
|
+
from .scoring import ScoringConfig
|
|
5
|
+
from .store import MemoryStore
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MemoryStore",
|
|
10
|
+
"Memory",
|
|
11
|
+
"RecallResult",
|
|
12
|
+
"ScoringConfig",
|
|
13
|
+
"HashEmbedder",
|
|
14
|
+
"OpenAIEmbedder",
|
|
15
|
+
"SentenceTransformerEmbedder",
|
|
16
|
+
"cosine_similarity",
|
|
17
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Framework adapters for remembrane.
|
|
2
|
+
|
|
3
|
+
Adapters are duck-typed against their target frameworks so remembrane never
|
|
4
|
+
hard-depends on them. Import errors only occur if you actually use an adapter
|
|
5
|
+
without its framework installed.
|
|
6
|
+
"""
|
|
7
|
+
from .crewai import RemembraneStorage
|
|
8
|
+
from .langchain import RemembraneChatMemory
|
|
9
|
+
|
|
10
|
+
__all__ = ["RemembraneChatMemory", "RemembraneStorage"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""CrewAI-compatible storage adapter.
|
|
2
|
+
|
|
3
|
+
Duck-typed against crewai's external-memory Storage interface
|
|
4
|
+
(save / search / reset), so it works without importing crewai itself.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
from remembrane import MemoryStore
|
|
9
|
+
from remembrane.adapters import RemembraneStorage
|
|
10
|
+
|
|
11
|
+
storage = RemembraneStorage(MemoryStore("crew.db"))
|
|
12
|
+
# pass wherever CrewAI accepts a custom storage backend,
|
|
13
|
+
# or call .save() / .search() directly.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from ..store import MemoryStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RemembraneStorage:
|
|
23
|
+
"""Storage backend backed by a remembrane MemoryStore."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, store: MemoryStore, namespace: str = "crew"):
|
|
26
|
+
self.store = store
|
|
27
|
+
self.namespace = namespace
|
|
28
|
+
|
|
29
|
+
def save(self, value: Any, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
30
|
+
self.store.store(str(value), namespace=self.namespace, metadata=metadata or {})
|
|
31
|
+
|
|
32
|
+
def search(self, query: str, limit: int = 5, score_threshold: float = 0.0) -> List[Dict[str, Any]]:
|
|
33
|
+
results = self.store.recall(
|
|
34
|
+
query, k=limit, namespace=self.namespace, min_score=score_threshold
|
|
35
|
+
)
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
"id": r.memory.id,
|
|
39
|
+
"context": r.memory.content,
|
|
40
|
+
"metadata": r.memory.metadata,
|
|
41
|
+
"score": r.score,
|
|
42
|
+
}
|
|
43
|
+
for r in results
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def reset(self) -> None:
|
|
47
|
+
self.store.forget(namespace=self.namespace)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""LangChain-compatible memory adapter.
|
|
2
|
+
|
|
3
|
+
Duck-typed against LangChain's memory interface (load_memory_variables /
|
|
4
|
+
save_context / clear), so it works without importing langchain itself.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
from remembrane import MemoryStore
|
|
9
|
+
from remembrane.adapters import RemembraneChatMemory
|
|
10
|
+
|
|
11
|
+
memory = RemembraneChatMemory(MemoryStore("agent.db"), session_id="user-42")
|
|
12
|
+
# use anywhere LangChain expects a memory object,
|
|
13
|
+
# or call .save_context() / .load_memory_variables() directly.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List
|
|
18
|
+
|
|
19
|
+
from ..store import MemoryStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RemembraneChatMemory:
|
|
23
|
+
"""Persistent, semantically-recalled conversation memory.
|
|
24
|
+
|
|
25
|
+
Instead of replaying the full transcript, ``load_memory_variables`` returns
|
|
26
|
+
the memories most relevant to the *current input*, ranked by
|
|
27
|
+
similarity x recency x importance.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
memory_key: str = "history"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
store: MemoryStore,
|
|
35
|
+
session_id: str = "default",
|
|
36
|
+
k: int = 6,
|
|
37
|
+
input_key: str = "input",
|
|
38
|
+
output_key: str = "output",
|
|
39
|
+
):
|
|
40
|
+
self.store = store
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
self.k = k
|
|
43
|
+
self.input_key = input_key
|
|
44
|
+
self.output_key = output_key
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def memory_variables(self) -> List[str]:
|
|
48
|
+
return [self.memory_key]
|
|
49
|
+
|
|
50
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
51
|
+
"""Persist one exchange as two memories (human + ai)."""
|
|
52
|
+
human = str(inputs.get(self.input_key, "")).strip()
|
|
53
|
+
ai = str(outputs.get(self.output_key, "")).strip()
|
|
54
|
+
if human:
|
|
55
|
+
self.store.store(human, namespace=self.session_id, metadata={"role": "human"})
|
|
56
|
+
if ai:
|
|
57
|
+
self.store.store(ai, namespace=self.session_id, metadata={"role": "ai"})
|
|
58
|
+
|
|
59
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]:
|
|
60
|
+
"""Return the k memories most relevant to the current input."""
|
|
61
|
+
query = str(inputs.get(self.input_key, "")).strip()
|
|
62
|
+
if not query:
|
|
63
|
+
memories = self.store.all(self.session_id)[-self.k :]
|
|
64
|
+
lines = [f"{m.metadata.get('role', '?')}: {m.content}" for m in memories]
|
|
65
|
+
else:
|
|
66
|
+
results = self.store.recall(query, k=self.k, namespace=self.session_id)
|
|
67
|
+
lines = [
|
|
68
|
+
f"{r.memory.metadata.get('role', '?')}: {r.memory.content}" for r in results
|
|
69
|
+
]
|
|
70
|
+
return {self.memory_key: "\n".join(lines)}
|
|
71
|
+
|
|
72
|
+
def clear(self) -> None:
|
|
73
|
+
self.store.forget(namespace=self.session_id)
|