sandclaw-memory 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.
- sandclaw_memory-0.1.0/LICENSE +21 -0
- sandclaw_memory-0.1.0/PKG-INFO +308 -0
- sandclaw_memory-0.1.0/README.md +272 -0
- sandclaw_memory-0.1.0/pyproject.toml +88 -0
- sandclaw_memory-0.1.0/sandclaw_memory/__init__.py +71 -0
- sandclaw_memory-0.1.0/sandclaw_memory/brain.py +656 -0
- sandclaw_memory-0.1.0/sandclaw_memory/dispatcher.py +163 -0
- sandclaw_memory-0.1.0/sandclaw_memory/exceptions.py +103 -0
- sandclaw_memory-0.1.0/sandclaw_memory/loader.py +141 -0
- sandclaw_memory-0.1.0/sandclaw_memory/permanent.py +975 -0
- sandclaw_memory-0.1.0/sandclaw_memory/py.typed +0 -0
- sandclaw_memory-0.1.0/sandclaw_memory/renderer.py +155 -0
- sandclaw_memory-0.1.0/sandclaw_memory/session.py +538 -0
- sandclaw_memory-0.1.0/sandclaw_memory/summary.py +209 -0
- sandclaw_memory-0.1.0/sandclaw_memory/types.py +93 -0
- sandclaw_memory-0.1.0/sandclaw_memory/utils.py +185 -0
- sandclaw_memory-0.1.0/sandclaw_memory.egg-info/PKG-INFO +308 -0
- sandclaw_memory-0.1.0/sandclaw_memory.egg-info/SOURCES.txt +29 -0
- sandclaw_memory-0.1.0/sandclaw_memory.egg-info/dependency_links.txt +1 -0
- sandclaw_memory-0.1.0/sandclaw_memory.egg-info/requires.txt +8 -0
- sandclaw_memory-0.1.0/sandclaw_memory.egg-info/top_level.txt +1 -0
- sandclaw_memory-0.1.0/setup.cfg +4 -0
- sandclaw_memory-0.1.0/tests/test_brain.py +252 -0
- sandclaw_memory-0.1.0/tests/test_dispatcher.py +57 -0
- sandclaw_memory-0.1.0/tests/test_exceptions.py +189 -0
- sandclaw_memory-0.1.0/tests/test_integration.py +489 -0
- sandclaw_memory-0.1.0/tests/test_loader.py +80 -0
- sandclaw_memory-0.1.0/tests/test_permanent.py +279 -0
- sandclaw_memory-0.1.0/tests/test_real_world.py +489 -0
- sandclaw_memory-0.1.0/tests/test_session.py +224 -0
- sandclaw_memory-0.1.0/tests/test_summary.py +85 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kokogo100
|
|
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,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandclaw-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-growing tag-dictionary RAG that runs on a $200 laptop.
|
|
5
|
+
Author-email: kokogo100 <kokogo100@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kokogo100/sandclaw-memory
|
|
8
|
+
Project-URL: Documentation, https://github.com/kokogo100/sandclaw-memory#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/kokogo100/sandclaw-memory
|
|
10
|
+
Project-URL: Issues, https://github.com/kokogo100/sandclaw-memory/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/kokogo100/sandclaw-memory/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: rag,memory,llm,ai-agent,tag-based-rag,temporal-rag,self-growing,gpu-free,lightweight,local-first,privacy,sqlite,zero-dependencies
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pyright>=1.1.350; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<h1 align="center">sandclaw-memory</h1>
|
|
39
|
+
<p align="center"><strong>Self-Growing Tag-Dictionary RAG for Any Device</strong></p>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://pypi.org/project/sandclaw-memory/"><img src="https://img.shields.io/pypi/v/sandclaw-memory?color=blue" alt="PyPI"></a>
|
|
44
|
+
<a href="https://pypi.org/project/sandclaw-memory/"><img src="https://img.shields.io/pypi/pyversions/sandclaw-memory" alt="Python"></a>
|
|
45
|
+
<a href="https://github.com/kokogo100/sandclaw-memory/blob/main/LICENSE"><img src="https://img.shields.io/github/license/kokogo100/sandclaw-memory" alt="License"></a>
|
|
46
|
+
<a href="https://github.com/kokogo100/sandclaw-memory/actions"><img src="https://img.shields.io/github/actions/workflow/status/kokogo100/sandclaw-memory/ci.yml?label=tests" alt="Tests"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
Give your AI **long-term memory that grows smarter over time.**
|
|
52
|
+
|
|
53
|
+
No GPU. No vector database. No external dependencies.
|
|
54
|
+
Just `pip install` and go.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from sandclaw_memory import BrainMemory
|
|
58
|
+
|
|
59
|
+
brain = BrainMemory(tag_extractor=my_ai_func)
|
|
60
|
+
brain.save("User loves Python and React")
|
|
61
|
+
context = brain.recall("what does the user like?")
|
|
62
|
+
# -> Returns relevant memories as Markdown, ready for LLM injection
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why sandclaw-memory?
|
|
66
|
+
|
|
67
|
+
| Feature | sandclaw-memory | Vector DB (Pinecone, Weaviate) | mem0 |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| **Setup** | `pip install` (done) | Docker + API keys + config | `pip install` + API key |
|
|
70
|
+
| **GPU required** | No | Often yes | No |
|
|
71
|
+
| **Cost over time** | Decreases (self-growing) | Constant | Constant |
|
|
72
|
+
| **Search** | FTS5 + BM25 (proven) | Cosine similarity | Embedding-based |
|
|
73
|
+
| **Privacy** | 100% local | Cloud required | Cloud optional |
|
|
74
|
+
| **Dependencies** | 0 (stdlib only) | Many | Several |
|
|
75
|
+
| **Size** | ~50KB | 100MB+ Docker | ~5MB |
|
|
76
|
+
|
|
77
|
+
### The Self-Growing Advantage
|
|
78
|
+
|
|
79
|
+
Traditional RAG calls AI for **every** search. sandclaw-memory learns:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Day 1: "React로 페이지 만듦" → AI extracts: [react, frontend, page]
|
|
83
|
+
keyword_map registers: "React" → react, "페이지" → page
|
|
84
|
+
|
|
85
|
+
Day 30: "React Native 시작" → keyword_map instant match! No AI needed.
|
|
86
|
+
Cost: $0.00
|
|
87
|
+
|
|
88
|
+
Day 90: 80% of saves match keyword_map → AI calls reduced by 80%
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**The more you use it, the cheaper it gets.**
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
### Install
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install sandclaw-memory
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3-Line Memory
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from sandclaw_memory import BrainMemory
|
|
105
|
+
|
|
106
|
+
with BrainMemory(tag_extractor=my_tag_func) as brain:
|
|
107
|
+
brain.save("Important: migrate to FastAPI by Q2")
|
|
108
|
+
print(brain.recall("what's the migration plan?"))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### With OpenAI
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import json, openai
|
|
115
|
+
from sandclaw_memory import BrainMemory
|
|
116
|
+
|
|
117
|
+
def tag_extractor(content: str) -> list[str]:
|
|
118
|
+
resp = openai.chat.completions.create(
|
|
119
|
+
model="gpt-4o-mini",
|
|
120
|
+
messages=[{
|
|
121
|
+
"role": "system",
|
|
122
|
+
"content": "Extract 3-7 keyword tags. Return JSON array only."
|
|
123
|
+
}, {"role": "user", "content": content}],
|
|
124
|
+
temperature=0,
|
|
125
|
+
)
|
|
126
|
+
return json.loads(resp.choices[0].message.content)
|
|
127
|
+
|
|
128
|
+
with BrainMemory(
|
|
129
|
+
db_path="./my_memory",
|
|
130
|
+
tag_extractor=tag_extractor,
|
|
131
|
+
) as brain:
|
|
132
|
+
brain.start_polling() # Background tag extraction every 15s
|
|
133
|
+
|
|
134
|
+
brain.save("User prefers Python and TypeScript")
|
|
135
|
+
brain.save("Decided to use PostgreSQL", source="archive")
|
|
136
|
+
|
|
137
|
+
context = brain.recall("what tech stack?")
|
|
138
|
+
# Inject 'context' into your LLM's system prompt
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### With Claude
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import json, anthropic
|
|
145
|
+
from sandclaw_memory import BrainMemory
|
|
146
|
+
|
|
147
|
+
client = anthropic.Anthropic()
|
|
148
|
+
|
|
149
|
+
def tag_extractor(content: str) -> list[str]:
|
|
150
|
+
resp = client.messages.create(
|
|
151
|
+
model="claude-haiku-4-5-20251001",
|
|
152
|
+
max_tokens=200,
|
|
153
|
+
messages=[{"role": "user",
|
|
154
|
+
"content": f"Extract 3-7 tags as JSON array:\n{content}"}],
|
|
155
|
+
)
|
|
156
|
+
return json.loads(resp.content[0].text)
|
|
157
|
+
|
|
158
|
+
brain = BrainMemory(tag_extractor=tag_extractor)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> See [`examples/`](examples/) for LangChain, custom callbacks, and scheduling patterns.
|
|
162
|
+
|
|
163
|
+
## Architecture
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
┌─────────────────────────────────────────────────┐
|
|
167
|
+
│ BrainMemory │
|
|
168
|
+
│ save() → recall() → start_polling() │
|
|
169
|
+
├─────────────────────────────────────────────────┤
|
|
170
|
+
│ │
|
|
171
|
+
│ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │
|
|
172
|
+
│ │ L1 │ │ L2 │ │ L3 │ │
|
|
173
|
+
│ │ Session │ │ Summary │ │ Archive │ │
|
|
174
|
+
│ │ │ │ │ │ │ │
|
|
175
|
+
│ │ 3-day │ │ 30-day │ │ Permanent │ │
|
|
176
|
+
│ │ rolling │ │ AI/text │ │ SQLite + FTS5 │ │
|
|
177
|
+
│ │ Markdown│ │ summary │ │ + self-growing │ │
|
|
178
|
+
│ │ logs │ │ │ │ tag dict │ │
|
|
179
|
+
│ └────┬────┘ └────┬─────┘ └───────┬────────┘ │
|
|
180
|
+
│ │ │ │ │
|
|
181
|
+
│ ┌────┴────────────┴────────────────┴────────┐ │
|
|
182
|
+
│ │ Intent Dispatcher │ │
|
|
183
|
+
│ │ CASUAL → L1 │ STANDARD → L1+L2 │ │
|
|
184
|
+
│ │ │ DEEP → L1+L2+L3 │ │
|
|
185
|
+
│ └───────────────┴───────────────────────────┘ │
|
|
186
|
+
│ │
|
|
187
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
188
|
+
│ │ Self-Growing Tag Pipeline │ │
|
|
189
|
+
│ │ Stage 1: keyword_map (instant, free) │ │
|
|
190
|
+
│ │ Stage 2: AI callback (async, queue) │ │
|
|
191
|
+
│ │ → keyword_map grows → Stage 2 shrinks │ │
|
|
192
|
+
│ └──────────────────────────────────────────┘ │
|
|
193
|
+
└─────────────────────────────────────────────────┘
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Three Memory Layers
|
|
197
|
+
|
|
198
|
+
| Layer | What | Retention | Storage | Speed |
|
|
199
|
+
|---|---|---|---|---|
|
|
200
|
+
| **L1** Session | Conversation logs | 3 days (rolling) | Markdown files | Instant |
|
|
201
|
+
| **L2** Summary | Period summaries | 30 days | In-memory | Instant |
|
|
202
|
+
| **L3** Archive | Important memories | Forever | SQLite + FTS5 | ~1ms |
|
|
203
|
+
|
|
204
|
+
### Intent-Based Depth
|
|
205
|
+
|
|
206
|
+
The dispatcher auto-detects how deep to search:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
brain.recall("what time is it?") # → CASUAL (L1 only)
|
|
210
|
+
brain.recall("summarize this month") # → STANDARD (L1 + L2)
|
|
211
|
+
brain.recall("why did we pick React?") # → DEEP (L1 + L2 + L3)
|
|
212
|
+
|
|
213
|
+
# Or override manually:
|
|
214
|
+
brain.recall("anything", depth="deep")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## API Reference
|
|
218
|
+
|
|
219
|
+
### BrainMemory
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
BrainMemory(
|
|
223
|
+
db_path="./memory", # Where to store files
|
|
224
|
+
tag_extractor=my_func, # REQUIRED: (str) -> list[str]
|
|
225
|
+
promote_checker=None, # Optional: (str) -> bool
|
|
226
|
+
depth_detector=None, # Optional: (str) -> str
|
|
227
|
+
duplicate_checker=None, # Optional: (str, str) -> bool
|
|
228
|
+
conflict_resolver=None, # Optional: (str, str) -> str
|
|
229
|
+
polling_interval=15, # Seconds between maintenance cycles
|
|
230
|
+
rolling_days=3, # L1 retention period
|
|
231
|
+
summary_days=30, # L2 summary period
|
|
232
|
+
max_context_chars=15_000, # Context budget for LLM injection
|
|
233
|
+
encryption_key=None, # Optional SQLCipher encryption
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Core Methods
|
|
238
|
+
|
|
239
|
+
| Method | Description |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `save(content, source="chat", tags=None)` | Save to memory. `source="archive"` forces L3. |
|
|
242
|
+
| `recall(query, depth=None)` | Recall relevant memories as Markdown. |
|
|
243
|
+
| `search(query, limit=20)` | Search L3 archive, returns `list[MemoryEntry]`. |
|
|
244
|
+
| `promote(content, tags=None)` | Manually save to L3. Returns memory ID. |
|
|
245
|
+
| `summarize(llm_callback=None)` | Generate a 30-day summary. |
|
|
246
|
+
| `start_polling()` | Start background maintenance loop. |
|
|
247
|
+
| `stop_polling()` | Stop background maintenance loop. |
|
|
248
|
+
| `run_maintenance()` | Run one maintenance cycle manually. |
|
|
249
|
+
| `get_stats()` | Get stats from all layers. |
|
|
250
|
+
| `get_tag_stats()` | Get tag usage counts. |
|
|
251
|
+
| `export_json(path=None)` | Export all data as JSON. |
|
|
252
|
+
| `on(event, callback)` | Register an event hook. |
|
|
253
|
+
| `close()` | Stop polling and close DB. |
|
|
254
|
+
|
|
255
|
+
### The 5 AI Callbacks
|
|
256
|
+
|
|
257
|
+
| Callback | Signature | Default |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `tag_extractor` | `(str) -> list[str]` | **REQUIRED** |
|
|
260
|
+
| `promote_checker` | `(str) -> bool` | `len(content) > 200` |
|
|
261
|
+
| `depth_detector` | `(str) -> str` | Keyword matching |
|
|
262
|
+
| `duplicate_checker` | `(str, str) -> bool` | SequenceMatcher > 0.85 |
|
|
263
|
+
| `conflict_resolver` | `(str, str) -> str` | Keep newer text |
|
|
264
|
+
|
|
265
|
+
### Event Hooks
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
brain.on("before_save", lambda content, source: ...)
|
|
269
|
+
brain.on("after_save", lambda content, source: ...)
|
|
270
|
+
brain.on("before_promote", lambda content: ...)
|
|
271
|
+
brain.on("after_promote", lambda content, mem_id: ...)
|
|
272
|
+
brain.on("after_recall", lambda query, depth, result: ...)
|
|
273
|
+
brain.on("after_cycle", lambda stats: ...)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Examples
|
|
277
|
+
|
|
278
|
+
| File | Description |
|
|
279
|
+
|---|---|
|
|
280
|
+
| [`basic_usage.py`](examples/basic_usage.py) | Simplest setup with OpenAI |
|
|
281
|
+
| [`with_openai.py`](examples/with_openai.py) | All 5 callbacks with OpenAI |
|
|
282
|
+
| [`with_anthropic.py`](examples/with_anthropic.py) | Claude API integration |
|
|
283
|
+
| [`with_langchain.py`](examples/with_langchain.py) | LangChain agent + memory |
|
|
284
|
+
| [`custom_callbacks.py`](examples/custom_callbacks.py) | Custom callbacks + hooks |
|
|
285
|
+
| [`scheduling.py`](examples/scheduling.py) | Polling, FastAPI, Celery |
|
|
286
|
+
|
|
287
|
+
## Compatibility
|
|
288
|
+
|
|
289
|
+
- **Python**: 3.9, 3.10, 3.11, 3.12, 3.13
|
|
290
|
+
- **OS**: Windows, macOS, Linux
|
|
291
|
+
- **Dependencies**: None (Python stdlib only)
|
|
292
|
+
- **SQLite**: Uses built-in `sqlite3` module (FTS5 optional, graceful fallback)
|
|
293
|
+
|
|
294
|
+
## Roadmap
|
|
295
|
+
|
|
296
|
+
| Version | Codename | Status |
|
|
297
|
+
|---|---|---|
|
|
298
|
+
| v0.1.0 | "Remembering AI" | Current |
|
|
299
|
+
| v0.2.0 | "Growing AI" | Planned -- tag trees + built-in AI (Gemma 4) |
|
|
300
|
+
| v0.3.0 | "Thinking AI" | Planned -- contradiction detection (CKN) |
|
|
301
|
+
|
|
302
|
+
## Contributing
|
|
303
|
+
|
|
304
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">sandclaw-memory</h1>
|
|
3
|
+
<p align="center"><strong>Self-Growing Tag-Dictionary RAG for Any Device</strong></p>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://pypi.org/project/sandclaw-memory/"><img src="https://img.shields.io/pypi/v/sandclaw-memory?color=blue" alt="PyPI"></a>
|
|
8
|
+
<a href="https://pypi.org/project/sandclaw-memory/"><img src="https://img.shields.io/pypi/pyversions/sandclaw-memory" alt="Python"></a>
|
|
9
|
+
<a href="https://github.com/kokogo100/sandclaw-memory/blob/main/LICENSE"><img src="https://img.shields.io/github/license/kokogo100/sandclaw-memory" alt="License"></a>
|
|
10
|
+
<a href="https://github.com/kokogo100/sandclaw-memory/actions"><img src="https://img.shields.io/github/actions/workflow/status/kokogo100/sandclaw-memory/ci.yml?label=tests" alt="Tests"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Give your AI **long-term memory that grows smarter over time.**
|
|
16
|
+
|
|
17
|
+
No GPU. No vector database. No external dependencies.
|
|
18
|
+
Just `pip install` and go.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from sandclaw_memory import BrainMemory
|
|
22
|
+
|
|
23
|
+
brain = BrainMemory(tag_extractor=my_ai_func)
|
|
24
|
+
brain.save("User loves Python and React")
|
|
25
|
+
context = brain.recall("what does the user like?")
|
|
26
|
+
# -> Returns relevant memories as Markdown, ready for LLM injection
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why sandclaw-memory?
|
|
30
|
+
|
|
31
|
+
| Feature | sandclaw-memory | Vector DB (Pinecone, Weaviate) | mem0 |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| **Setup** | `pip install` (done) | Docker + API keys + config | `pip install` + API key |
|
|
34
|
+
| **GPU required** | No | Often yes | No |
|
|
35
|
+
| **Cost over time** | Decreases (self-growing) | Constant | Constant |
|
|
36
|
+
| **Search** | FTS5 + BM25 (proven) | Cosine similarity | Embedding-based |
|
|
37
|
+
| **Privacy** | 100% local | Cloud required | Cloud optional |
|
|
38
|
+
| **Dependencies** | 0 (stdlib only) | Many | Several |
|
|
39
|
+
| **Size** | ~50KB | 100MB+ Docker | ~5MB |
|
|
40
|
+
|
|
41
|
+
### The Self-Growing Advantage
|
|
42
|
+
|
|
43
|
+
Traditional RAG calls AI for **every** search. sandclaw-memory learns:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Day 1: "React로 페이지 만듦" → AI extracts: [react, frontend, page]
|
|
47
|
+
keyword_map registers: "React" → react, "페이지" → page
|
|
48
|
+
|
|
49
|
+
Day 30: "React Native 시작" → keyword_map instant match! No AI needed.
|
|
50
|
+
Cost: $0.00
|
|
51
|
+
|
|
52
|
+
Day 90: 80% of saves match keyword_map → AI calls reduced by 80%
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**The more you use it, the cheaper it gets.**
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install sandclaw-memory
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3-Line Memory
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from sandclaw_memory import BrainMemory
|
|
69
|
+
|
|
70
|
+
with BrainMemory(tag_extractor=my_tag_func) as brain:
|
|
71
|
+
brain.save("Important: migrate to FastAPI by Q2")
|
|
72
|
+
print(brain.recall("what's the migration plan?"))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With OpenAI
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import json, openai
|
|
79
|
+
from sandclaw_memory import BrainMemory
|
|
80
|
+
|
|
81
|
+
def tag_extractor(content: str) -> list[str]:
|
|
82
|
+
resp = openai.chat.completions.create(
|
|
83
|
+
model="gpt-4o-mini",
|
|
84
|
+
messages=[{
|
|
85
|
+
"role": "system",
|
|
86
|
+
"content": "Extract 3-7 keyword tags. Return JSON array only."
|
|
87
|
+
}, {"role": "user", "content": content}],
|
|
88
|
+
temperature=0,
|
|
89
|
+
)
|
|
90
|
+
return json.loads(resp.choices[0].message.content)
|
|
91
|
+
|
|
92
|
+
with BrainMemory(
|
|
93
|
+
db_path="./my_memory",
|
|
94
|
+
tag_extractor=tag_extractor,
|
|
95
|
+
) as brain:
|
|
96
|
+
brain.start_polling() # Background tag extraction every 15s
|
|
97
|
+
|
|
98
|
+
brain.save("User prefers Python and TypeScript")
|
|
99
|
+
brain.save("Decided to use PostgreSQL", source="archive")
|
|
100
|
+
|
|
101
|
+
context = brain.recall("what tech stack?")
|
|
102
|
+
# Inject 'context' into your LLM's system prompt
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### With Claude
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import json, anthropic
|
|
109
|
+
from sandclaw_memory import BrainMemory
|
|
110
|
+
|
|
111
|
+
client = anthropic.Anthropic()
|
|
112
|
+
|
|
113
|
+
def tag_extractor(content: str) -> list[str]:
|
|
114
|
+
resp = client.messages.create(
|
|
115
|
+
model="claude-haiku-4-5-20251001",
|
|
116
|
+
max_tokens=200,
|
|
117
|
+
messages=[{"role": "user",
|
|
118
|
+
"content": f"Extract 3-7 tags as JSON array:\n{content}"}],
|
|
119
|
+
)
|
|
120
|
+
return json.loads(resp.content[0].text)
|
|
121
|
+
|
|
122
|
+
brain = BrainMemory(tag_extractor=tag_extractor)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> See [`examples/`](examples/) for LangChain, custom callbacks, and scheduling patterns.
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
┌─────────────────────────────────────────────────┐
|
|
131
|
+
│ BrainMemory │
|
|
132
|
+
│ save() → recall() → start_polling() │
|
|
133
|
+
├─────────────────────────────────────────────────┤
|
|
134
|
+
│ │
|
|
135
|
+
│ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │
|
|
136
|
+
│ │ L1 │ │ L2 │ │ L3 │ │
|
|
137
|
+
│ │ Session │ │ Summary │ │ Archive │ │
|
|
138
|
+
│ │ │ │ │ │ │ │
|
|
139
|
+
│ │ 3-day │ │ 30-day │ │ Permanent │ │
|
|
140
|
+
│ │ rolling │ │ AI/text │ │ SQLite + FTS5 │ │
|
|
141
|
+
│ │ Markdown│ │ summary │ │ + self-growing │ │
|
|
142
|
+
│ │ logs │ │ │ │ tag dict │ │
|
|
143
|
+
│ └────┬────┘ └────┬─────┘ └───────┬────────┘ │
|
|
144
|
+
│ │ │ │ │
|
|
145
|
+
│ ┌────┴────────────┴────────────────┴────────┐ │
|
|
146
|
+
│ │ Intent Dispatcher │ │
|
|
147
|
+
│ │ CASUAL → L1 │ STANDARD → L1+L2 │ │
|
|
148
|
+
│ │ │ DEEP → L1+L2+L3 │ │
|
|
149
|
+
│ └───────────────┴───────────────────────────┘ │
|
|
150
|
+
│ │
|
|
151
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
152
|
+
│ │ Self-Growing Tag Pipeline │ │
|
|
153
|
+
│ │ Stage 1: keyword_map (instant, free) │ │
|
|
154
|
+
│ │ Stage 2: AI callback (async, queue) │ │
|
|
155
|
+
│ │ → keyword_map grows → Stage 2 shrinks │ │
|
|
156
|
+
│ └──────────────────────────────────────────┘ │
|
|
157
|
+
└─────────────────────────────────────────────────┘
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Three Memory Layers
|
|
161
|
+
|
|
162
|
+
| Layer | What | Retention | Storage | Speed |
|
|
163
|
+
|---|---|---|---|---|
|
|
164
|
+
| **L1** Session | Conversation logs | 3 days (rolling) | Markdown files | Instant |
|
|
165
|
+
| **L2** Summary | Period summaries | 30 days | In-memory | Instant |
|
|
166
|
+
| **L3** Archive | Important memories | Forever | SQLite + FTS5 | ~1ms |
|
|
167
|
+
|
|
168
|
+
### Intent-Based Depth
|
|
169
|
+
|
|
170
|
+
The dispatcher auto-detects how deep to search:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
brain.recall("what time is it?") # → CASUAL (L1 only)
|
|
174
|
+
brain.recall("summarize this month") # → STANDARD (L1 + L2)
|
|
175
|
+
brain.recall("why did we pick React?") # → DEEP (L1 + L2 + L3)
|
|
176
|
+
|
|
177
|
+
# Or override manually:
|
|
178
|
+
brain.recall("anything", depth="deep")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## API Reference
|
|
182
|
+
|
|
183
|
+
### BrainMemory
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
BrainMemory(
|
|
187
|
+
db_path="./memory", # Where to store files
|
|
188
|
+
tag_extractor=my_func, # REQUIRED: (str) -> list[str]
|
|
189
|
+
promote_checker=None, # Optional: (str) -> bool
|
|
190
|
+
depth_detector=None, # Optional: (str) -> str
|
|
191
|
+
duplicate_checker=None, # Optional: (str, str) -> bool
|
|
192
|
+
conflict_resolver=None, # Optional: (str, str) -> str
|
|
193
|
+
polling_interval=15, # Seconds between maintenance cycles
|
|
194
|
+
rolling_days=3, # L1 retention period
|
|
195
|
+
summary_days=30, # L2 summary period
|
|
196
|
+
max_context_chars=15_000, # Context budget for LLM injection
|
|
197
|
+
encryption_key=None, # Optional SQLCipher encryption
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Core Methods
|
|
202
|
+
|
|
203
|
+
| Method | Description |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `save(content, source="chat", tags=None)` | Save to memory. `source="archive"` forces L3. |
|
|
206
|
+
| `recall(query, depth=None)` | Recall relevant memories as Markdown. |
|
|
207
|
+
| `search(query, limit=20)` | Search L3 archive, returns `list[MemoryEntry]`. |
|
|
208
|
+
| `promote(content, tags=None)` | Manually save to L3. Returns memory ID. |
|
|
209
|
+
| `summarize(llm_callback=None)` | Generate a 30-day summary. |
|
|
210
|
+
| `start_polling()` | Start background maintenance loop. |
|
|
211
|
+
| `stop_polling()` | Stop background maintenance loop. |
|
|
212
|
+
| `run_maintenance()` | Run one maintenance cycle manually. |
|
|
213
|
+
| `get_stats()` | Get stats from all layers. |
|
|
214
|
+
| `get_tag_stats()` | Get tag usage counts. |
|
|
215
|
+
| `export_json(path=None)` | Export all data as JSON. |
|
|
216
|
+
| `on(event, callback)` | Register an event hook. |
|
|
217
|
+
| `close()` | Stop polling and close DB. |
|
|
218
|
+
|
|
219
|
+
### The 5 AI Callbacks
|
|
220
|
+
|
|
221
|
+
| Callback | Signature | Default |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `tag_extractor` | `(str) -> list[str]` | **REQUIRED** |
|
|
224
|
+
| `promote_checker` | `(str) -> bool` | `len(content) > 200` |
|
|
225
|
+
| `depth_detector` | `(str) -> str` | Keyword matching |
|
|
226
|
+
| `duplicate_checker` | `(str, str) -> bool` | SequenceMatcher > 0.85 |
|
|
227
|
+
| `conflict_resolver` | `(str, str) -> str` | Keep newer text |
|
|
228
|
+
|
|
229
|
+
### Event Hooks
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
brain.on("before_save", lambda content, source: ...)
|
|
233
|
+
brain.on("after_save", lambda content, source: ...)
|
|
234
|
+
brain.on("before_promote", lambda content: ...)
|
|
235
|
+
brain.on("after_promote", lambda content, mem_id: ...)
|
|
236
|
+
brain.on("after_recall", lambda query, depth, result: ...)
|
|
237
|
+
brain.on("after_cycle", lambda stats: ...)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Examples
|
|
241
|
+
|
|
242
|
+
| File | Description |
|
|
243
|
+
|---|---|
|
|
244
|
+
| [`basic_usage.py`](examples/basic_usage.py) | Simplest setup with OpenAI |
|
|
245
|
+
| [`with_openai.py`](examples/with_openai.py) | All 5 callbacks with OpenAI |
|
|
246
|
+
| [`with_anthropic.py`](examples/with_anthropic.py) | Claude API integration |
|
|
247
|
+
| [`with_langchain.py`](examples/with_langchain.py) | LangChain agent + memory |
|
|
248
|
+
| [`custom_callbacks.py`](examples/custom_callbacks.py) | Custom callbacks + hooks |
|
|
249
|
+
| [`scheduling.py`](examples/scheduling.py) | Polling, FastAPI, Celery |
|
|
250
|
+
|
|
251
|
+
## Compatibility
|
|
252
|
+
|
|
253
|
+
- **Python**: 3.9, 3.10, 3.11, 3.12, 3.13
|
|
254
|
+
- **OS**: Windows, macOS, Linux
|
|
255
|
+
- **Dependencies**: None (Python stdlib only)
|
|
256
|
+
- **SQLite**: Uses built-in `sqlite3` module (FTS5 optional, graceful fallback)
|
|
257
|
+
|
|
258
|
+
## Roadmap
|
|
259
|
+
|
|
260
|
+
| Version | Codename | Status |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| v0.1.0 | "Remembering AI" | Current |
|
|
263
|
+
| v0.2.0 | "Growing AI" | Planned -- tag trees + built-in AI (Gemma 4) |
|
|
264
|
+
| v0.3.0 | "Thinking AI" | Planned -- contradiction detection (CKN) |
|
|
265
|
+
|
|
266
|
+
## Contributing
|
|
267
|
+
|
|
268
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sandclaw-memory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Self-growing tag-dictionary RAG that runs on a $200 laptop."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "kokogo100", email = "kokogo100@users.noreply.github.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"rag",
|
|
17
|
+
"memory",
|
|
18
|
+
"llm",
|
|
19
|
+
"ai-agent",
|
|
20
|
+
"tag-based-rag",
|
|
21
|
+
"temporal-rag",
|
|
22
|
+
"self-growing",
|
|
23
|
+
"gpu-free",
|
|
24
|
+
"lightweight",
|
|
25
|
+
"local-first",
|
|
26
|
+
"privacy",
|
|
27
|
+
"sqlite",
|
|
28
|
+
"zero-dependencies",
|
|
29
|
+
]
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Development Status :: 3 - Alpha",
|
|
32
|
+
"Intended Audience :: Developers",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3.9",
|
|
36
|
+
"Programming Language :: Python :: 3.10",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Programming Language :: Python :: 3.13",
|
|
40
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
41
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
42
|
+
"Typing :: Typed",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/kokogo100/sandclaw-memory"
|
|
47
|
+
Documentation = "https://github.com/kokogo100/sandclaw-memory#readme"
|
|
48
|
+
Repository = "https://github.com/kokogo100/sandclaw-memory"
|
|
49
|
+
Issues = "https://github.com/kokogo100/sandclaw-memory/issues"
|
|
50
|
+
Changelog = "https://github.com/kokogo100/sandclaw-memory/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
[project.optional-dependencies]
|
|
53
|
+
dev = [
|
|
54
|
+
"pytest>=7.0",
|
|
55
|
+
"pytest-cov>=4.0",
|
|
56
|
+
"ruff>=0.8.0",
|
|
57
|
+
"pyright>=1.1.350",
|
|
58
|
+
"build>=1.0",
|
|
59
|
+
"twine>=5.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# ═══════════════════════════════════════════════════════════
|
|
63
|
+
# Ruff — fast Python linter & formatter
|
|
64
|
+
# ═══════════════════════════════════════════════════════════
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py39"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.format]
|
|
70
|
+
quote-style = "double"
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
74
|
+
|
|
75
|
+
# ═══════════════════════════════════════════════════════════
|
|
76
|
+
# Pytest
|
|
77
|
+
# ═══════════════════════════════════════════════════════════
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
pythonpath = ["."]
|
|
81
|
+
|
|
82
|
+
# ═══════════════════════════════════════════════════════════
|
|
83
|
+
# Pyright — type checking
|
|
84
|
+
# ═══════════════════════════════════════════════════════════
|
|
85
|
+
[tool.pyright]
|
|
86
|
+
pythonVersion = "3.9"
|
|
87
|
+
typeCheckingMode = "basic"
|
|
88
|
+
include = ["sandclaw_memory"]
|