dspy-lancedb-memory 0.1.4__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edward Boswell
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,561 @@
1
+ Metadata-Version: 2.4
2
+ Name: dspy-lancedb-memory
3
+ Version: 0.1.4
4
+ Summary: Add your description here
5
+ Keywords: dspy,ai,memory,lancedb
6
+ Author: Edward Boswell
7
+ Author-email: Edward Boswell <thememium@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Dist: dspy>=3.2.0
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Dist: lancedb<=0.25.3
19
+ Requires-Dist: litellm>=1.82.6
20
+ Requires-Dist: pyarrow>=24.0.0
21
+ Requires-Dist: pydantic>=2.13.3
22
+ Requires-Python: >=3.12
23
+ Project-URL: Homepage, https://github.com/thememium/dspy-lancedb-memory
24
+ Project-URL: Documentation, https://github.com/thememium/dspy-lancedb-memory
25
+ Project-URL: Repository, https://github.com/thememium/dspy-lancedb-memory.git
26
+ Project-URL: Issues, https://github.com/thememium/dspy-lancedb-memory/issues
27
+ Project-URL: Changelog, https://github.com/thememium/dspy-lancedb-memory/blob/master/CHANGELOG.md
28
+ Description-Content-Type: text/markdown
29
+
30
+ <a name="readme-top"></a>
31
+
32
+ <div align="center">
33
+ <h3 align="center">DSPy LanceDB Memory</h3>
34
+
35
+ <p align="center">
36
+ Persistent vector memory store for DSPy-powered AI agents — extract, store, and recall structured memories from conversation.
37
+ <br />
38
+ <a href="#table-of-contents"><strong>Explore the Documentation »</strong></a>
39
+ <br />
40
+ <a href="https://github.com/thememium/dspy-lancedb-memory/issues">Report Bug</a>
41
+ ·
42
+ <a href="https://github.com/thememium/dspy-lancedb-memory/issues">Request Feature</a>
43
+ </p>
44
+ </div>
45
+
46
+ <!-- TABLE OF CONTENTS -->
47
+
48
+ <a name="table-of-contents"></a>
49
+
50
+ <details>
51
+ <summary>Table of Contents</summary>
52
+ <ol>
53
+ <li><a href="#about">About</a></li>
54
+ <li><a href="#quick-start">Quick Start</a></li>
55
+ <li><a href="#usage">Usage</a></li>
56
+ <li><a href="#memory-taxonomy">Memory Taxonomy</a></li>
57
+ <li><a href="#development">Development</a></li>
58
+ <li><a href="#contributing">Contributing</a></li>
59
+ <li><a href="#license">License</a></li>
60
+ </ol>
61
+ </details>
62
+
63
+ <!-- ABOUT -->
64
+
65
+ ## About
66
+
67
+ DSPy Memory is a persistent vector memory store for DSPy-powered AI agents. It uses DSPy signatures to extract structured, categorized memories from conversation turns and stores them in LanceDB for efficient semantic retrieval.
68
+
69
+ - **Method-based SDK** — Single ``memory.configure()`` entry point for extraction LM, embedding model, and reranker
70
+ - **DSPy-native extraction** — Uses `ChainOfThought` with a typed `ExtractMemory` signature to pull salient information from conversations
71
+ - **Structured memory taxonomy** — Six memory categories (preference, semantic, episodic, procedural, summary, artifact) for fine-grained organization
72
+ - **Persistent vector storage** — LanceDB-backed with automatic text embeddings via the DSPy `Embedder`
73
+ - **Semantic search** — Query memories by user ID, session ID, conversation ID, memory type, or natural language
74
+ - **Optional reranking** — ``LiteLLMReranker`` wraps ``litellm.rerank()`` for cross-encoder reranking via Cohere, Jina, and any LiteLLM-compatible provider
75
+ - **Full CRUD** — Create, search, update, and delete individual memories or batch-extract from conversations
76
+
77
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
78
+
79
+ <!-- QUICK START -->
80
+
81
+ ## Quick Start
82
+
83
+ ### Prerequisites
84
+
85
+ Set the API key environment variable for your chosen provider. LiteLLM routes to the correct key automatically based on the model prefix:
86
+
87
+ ```bash
88
+ # Examples — set whichever matches your provider
89
+ export OPENAI_API_KEY="sk-..."
90
+ export ANTHROPIC_API_KEY="sk-ant-..."
91
+ export CO_API_KEY="..." # Cohere reranker
92
+ export JINA_API_KEY="..." # Jina reranker
93
+ ```
94
+
95
+ If you use a proxy or gateway (e.g. OpenRouter, LiteLLM proxy), set the base URL and key:
96
+
97
+ ```bash
98
+ export OPENAI_API_BASE="https://openrouter.ai/api/v1"
99
+ export OPENAI_API_KEY="sk-or-v1-..."
100
+ ```
101
+
102
+ ### Install
103
+
104
+ Install dspy-lancedb-memory with uv (recommended)
105
+
106
+ ```bash
107
+ uv add dspy-lancedb-memory
108
+ ```
109
+
110
+ Install with pip (alternative)
111
+
112
+ ```bash
113
+ pip install dspy-lancedb-memory
114
+ ```
115
+
116
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
117
+
118
+ <!-- USAGE -->
119
+
120
+ ## Usage
121
+
122
+ ### Basic Usage
123
+
124
+ ```python
125
+ from dspy_lancedb_memory import memory
126
+ import dspy
127
+
128
+ # Configure: extraction LM, embedding LM, and optional reranker LM
129
+ memory.configure(
130
+ extraction_lm=dspy.LM("openai/gpt-4o-mini"),
131
+ embedding_lm=dspy.LM("openai/text-embedding-3-small"),
132
+ reranker_lm=dspy.LM("cohere/rerank-4-fast"), # optional — omit to disable
133
+ )
134
+
135
+ # Create a store — picks up all defaults from configure()
136
+ store = memory.Store()
137
+
138
+ # Store a single memory
139
+ store.create_memory(
140
+ user_id="user_123",
141
+ content="Edward prefers DSPy signatures over ad-hoc prompts.",
142
+ memory_type="preference",
143
+ )
144
+
145
+ # Store with session and conversation scoping
146
+ store.create_memory(
147
+ user_id="user_123",
148
+ session_id="session_abc",
149
+ conversation_id="conv_456",
150
+ content="Remember this from our conversation about RAG pipelines.",
151
+ memory_type="episodic",
152
+ )
153
+
154
+ # Search memories (with reranking)
155
+ results = store.search_memories(
156
+ user_id="user_123",
157
+ session_id="session_abc", # optional — narrow to a session
158
+ query="What does Edward prefer?",
159
+ use_reranker=True,
160
+ )
161
+ print(results)
162
+ ```
163
+
164
+ ### Extract Memories from Conversation
165
+
166
+ The real power is automatic extraction. Pass a conversation turn and the LLM extracts all salient memories, each categorized by type.
167
+
168
+ ```python
169
+ from dspy_lancedb_memory import memory
170
+ import dspy
171
+
172
+ memory.configure(extraction_lm=dspy.LM("openai/gpt-4o-mini"))
173
+ store = memory.Store()
174
+
175
+ messages = [
176
+ {
177
+ "role": "user",
178
+ "content": "I really like using DSPy signatures instead of writing prompts by hand. "
179
+ "I'm building a CLI tool to organize my digital bookmarks by topic. "
180
+ "The PR is at github.com/example/bookmark-organizer/pull/42.",
181
+ },
182
+ ]
183
+
184
+ created = store.create_memories(
185
+ user_id="user_123",
186
+ contents=messages,
187
+ extract=True, # default; uses DSPy ChainOfThought to extract memories
188
+ )
189
+ # Returns multiple MemoryItems categorized automatically:
190
+ # preference: "User prefers DSPy signatures over writing prompts by hand"
191
+ # semantic: "User is building a CLI bookmark organizer"
192
+ # procedural: "User is organizing bookmarks by topic"
193
+ # artifact: "github.com/example/bookmark-organizer/pull/42"
194
+
195
+ for m in created:
196
+ print(f"[{m['memory_type']}] {m['content']}")
197
+ ```
198
+
199
+ ### Search with Memory Type and Reranking
200
+
201
+ ```python
202
+ from dspy_lancedb_memory import memory
203
+ import dspy
204
+
205
+ memory.configure(
206
+ extraction_lm=dspy.LM("openai/gpt-4o-mini"),
207
+ reranker_lm=dspy.LM("cohere/rerank-4-fast"),
208
+ )
209
+ store = memory.Store()
210
+
211
+ # Filter by memory type and optionally use reranking for better results
212
+ results = store.search_memories(
213
+ user_id="user_123",
214
+ query="What are Edward's tool preferences?",
215
+ memory_type="preference", # optional filter
216
+ limit=5,
217
+ use_reranker=True, # uses configured reranker endpoint
218
+ )
219
+ ```
220
+
221
+ ### Filtering by Session and Conversation
222
+
223
+ Every memory can be scoped to a ``session_id`` and ``conversation_id``. Both are optional and can be used independently or together.
224
+
225
+ ```python
226
+ # Scope to a specific session
227
+ session_memories = store.search_memories(
228
+ user_id="user_123",
229
+ session_id="session_abc",
230
+ query="What did we discuss last time?",
231
+ )
232
+
233
+ # Scope to a specific conversation
234
+ conversation_memories = store.search_memories(
235
+ user_id="user_123",
236
+ conversation_id="conv_456",
237
+ query="RAG pipeline details",
238
+ )
239
+
240
+ # Combine session + conversation for maximum precision
241
+ precise = store.search_memories(
242
+ user_id="user_123",
243
+ session_id="session_abc",
244
+ conversation_id="conv_456",
245
+ query="specific topic",
246
+ )
247
+
248
+ # Omit both to search across all sessions and conversations
249
+ all_results = store.search_memories(
250
+ user_id="user_123",
251
+ query="anything",
252
+ )
253
+ ```
254
+
255
+ ### Raw Store (No Extraction)
256
+
257
+ Store content verbatim without LLM extraction.
258
+
259
+ ```python
260
+ store.create_memories(
261
+ user_id="user_123",
262
+ contents=[{"role": "user", "content": "A raw fact worth storing."}],
263
+ extract=False,
264
+ memory_type="semantic",
265
+ )
266
+ ```
267
+
268
+ ### Update and Delete
269
+
270
+ ```python
271
+ # Update memory content (re-embeds automatically)
272
+ store.update_memory(
273
+ memory_id="some-uuid",
274
+ content="Updated memory text",
275
+ )
276
+
277
+ # Delete a memory
278
+ store.delete_memory(memory_id="some-uuid")
279
+ ```
280
+
281
+ ### Upsert — Insert, Update, or Skip
282
+
283
+ ``upsert_memory`` uses semantic similarity to decide what to do:
284
+
285
+ 1. **Exact match** — same ``content`` string exists → skip (no-op)
286
+ 2. **Semantic match** — similar content found (cosine similarity ≥ threshold) → **update** it
287
+ 3. **No match** — nothing close enough → **insert** a new memory
288
+
289
+ ```python
290
+ # First insert
291
+ store.upsert_memory(
292
+ user_id="user_123",
293
+ content="Edward is building a RAG pipeline for climate modeling.",
294
+ memory_type="semantic",
295
+ )
296
+
297
+ # Same content string → skip (returns existing row unchanged)
298
+ store.upsert_memory(
299
+ user_id="user_123",
300
+ content="Edward is building a RAG pipeline for climate modeling.",
301
+ )
302
+
303
+ # Semantically similar content → update that memory in place
304
+ store.upsert_memory(
305
+ user_id="user_123",
306
+ content="Edward is designing a RAG pipeline for climate data analysis.",
307
+ similarity_threshold=0.8, # lower = more aggressive updates
308
+ )
309
+
310
+ # Completely different content → insert a new row
311
+ store.upsert_memory(
312
+ user_id="user_123",
313
+ content="Edward prefers DSPy signatures over raw prompts.",
314
+ memory_type="preference",
315
+ )
316
+ ```
317
+
318
+ The ``similarity_threshold`` (default ``0.85``) controls how close two
319
+ memories must be to consider them the same. Higher values make upsert
320
+ more conservative (mostly inserts); lower values make it more aggressive
321
+ (mostly updates).
322
+
323
+ ### Batch Upsert with Extraction
324
+
325
+ ``upsert_memories`` mirrors ``create_memories`` exactly — same parameters,
326
+ same DSPy extraction — but each extracted memory goes through the upsert
327
+ decision instead of a blind insert.
328
+
329
+ ```python
330
+ from dspy_lancedb_memory import memory
331
+ import dspy
332
+
333
+ memory.configure(extraction_lm=dspy.LM("openai/gpt-4o-mini"))
334
+ store = memory.Store()
335
+
336
+ messages = [
337
+ {
338
+ "role": "user",
339
+ "content": "I really like using DSPy signatures instead of writing prompts by hand. "
340
+ "I'm working on a RAG pipeline for my thesis on climate modeling. "
341
+ "The PR is at github.com/example/climate-rag/pull/42.",
342
+ },
343
+ ]
344
+
345
+ upserted = store.upsert_memories(
346
+ user_id="user_123",
347
+ contents=messages,
348
+ extract=True,
349
+ )
350
+
351
+ for m in upserted:
352
+ # Each extracted memory was independently upserted:
353
+ # - exact match → skip
354
+ # - semantic match → update
355
+ # - no match → insert
356
+ print(f"[{m['memory_type']}] {m['content']}")
357
+ ```
358
+
359
+ ### Using the Reranker
360
+
361
+ The easiest way — configure via ``memory.configure(reranker_lm=...)`` with a ``dspy.LM`` and `memory.Store()` picks it up automatically:
362
+
363
+ ```python
364
+ from dspy_lancedb_memory import memory
365
+ import dspy
366
+
367
+ memory.configure(
368
+ extraction_lm=dspy.LM("openai/gpt-4o-mini"),
369
+ reranker_lm=dspy.LM("cohere/rerank-4-fast"),
370
+ )
371
+ store = memory.Store() # LiteLLMReranker auto-created from reranker_lm
372
+ ```
373
+
374
+ You can also pass a plain model string instead of a ``dspy.LM``:
375
+
376
+ ```python
377
+ memory.configure(reranker_lm="cohere/rerank-english-v3.0")
378
+ ```
379
+
380
+ For full control (custom column, top_n, etc.), build a ``LiteLLMReranker`` and pass it to ``Store()``:
381
+
382
+ ```python
383
+ from dspy_lancedb_memory import LiteLLMReranker
384
+
385
+ reranker = LiteLLMReranker(
386
+ model="cohere/rerank-english-v3.0",
387
+ column="content", # LanceDB column to rerank against
388
+ top_n=20, # optional: limit reranked candidates
389
+ )
390
+
391
+ store = memory.Store(reranker=reranker)
392
+ ```
393
+
394
+ The model string uses the same ``provider/model`` format as ``dspy.LM`` — e.g.
395
+ ``"cohere/rerank-english-v3.0"``, ``"jina/jina-reranker-v2-base-multilingual"``.
396
+ LiteLLM handles the routing.
397
+
398
+ ### Custom API Base and Key
399
+
400
+ When running behind a proxy, gateway, or self-hosted endpoint, pass ``api_base`` and ``api_key`` directly to ``LiteLLMReranker``:
401
+
402
+ ```python
403
+ from dspy_lancedb_memory import LiteLLMReranker
404
+
405
+ reranker = LiteLLMReranker(
406
+ model="my-provider/rerank-model",
407
+ api_base="https://my-gateway.example.com/v1",
408
+ api_key="sk-my-secret",
409
+ column="content",
410
+ top_n=20,
411
+ )
412
+
413
+ store = memory.Store(reranker=reranker)
414
+ ```
415
+
416
+ For embeddings behind a custom endpoint, pass the same ``api_base``/``api_key`` via a ``dspy.LM``:
417
+
418
+ ```python
419
+ import dspy
420
+
421
+ memory.configure(
422
+ embedding_lm=dspy.LM(
423
+ "openai/text-embedding-3-small",
424
+ api_base="https://my-gateway.example.com/v1",
425
+ api_key="sk-my-secret",
426
+ ),
427
+ )
428
+ ```
429
+
430
+ LiteLLM automatically routes to the correct provider based on the model prefix. If your
431
+ provider is not in LiteLLM's built-in list, ``LiteLLMReranker`` falls back to calling a
432
+ Cohere-compatible ``/rerank`` endpoint on your ``api_base``.
433
+
434
+ ### Custom Configuration
435
+
436
+ Everything — including LanceDB defaults — in one call:
437
+
438
+ ```python
439
+ from dspy_lancedb_memory import memory
440
+ import dspy
441
+
442
+ memory.configure(
443
+ extraction_lm=dspy.LM("anthropic/claude-sonnet-4-20250514"), # extraction LM
444
+ embedding_lm=dspy.LM("openai/text-embedding-3-small"), # embedding LM
445
+ embedding_dim=1536, # must match
446
+ reranker_lm=dspy.LM("cohere/rerank-4-fast"), # reranker model
447
+ uri=".my_memories", # LanceDB path
448
+ table_name="user_memories", # LanceDB table
449
+ )
450
+
451
+ store = memory.Store() # everything inherited from configure()
452
+ ```
453
+
454
+ Override individual fields on ``Store()`` when you need something different:
455
+
456
+ ```python
457
+ store = memory.Store(uri="./scratch", reranker=None)
458
+ ```
459
+
460
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
461
+
462
+ ## Memory Taxonomy
463
+
464
+ Every extracted memory is categorized into one of six types:
465
+
466
+ | Type | Description | Example |
467
+ |---|---|---|
468
+ | `preference` | User tastes, likes/dislikes, preferred formats, tone, tools | `"User prefers DSPy signatures over ad-hoc prompts"` |
469
+ | `semantic` | Facts, biographical data, stable knowledge about the user | `"User is a PhD student researching climate modeling"` |
470
+ | `episodic` | Events, tasks, decisions, or outcomes from a specific interaction | `"User decided to use LanceDB over Chroma for persistence"` |
471
+ | `procedural` | Learned rules, workflows, steps, patterns, or how-to knowledge | `"User's RAG pipeline uses hybrid search with reranking"` |
472
+ | `summary` | Compressed conversation or task summaries capturing the gist | `"User discussed their thesis work on climate RAG pipelines"` |
473
+ | `artifact` | Links, paths, IDs, or references to files, PRs, docs, outputs | `"github.com/example/climate-rag/pull/42"` |
474
+
475
+ When storing directly (without extraction), the default type is `semantic`.
476
+
477
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
478
+
479
+ ## Available API
480
+
481
+ | API | Description |
482
+ |---|---|
483
+ | [`memory`](#basic-usage) | SDK module — ``memory.configure()`` and ``memory.Store()`` |
484
+ | [`MemoryExtractor`](#extract-memories-from-conversation) | DSPy `ChainOfThought` module for memory extraction |
485
+ | [`LiteLLMReranker`](#using-the-reranker) | Cross-encoder reranker via ``litellm.rerank()`` — supports Cohere, Jina, and any LiteLLM-compatible provider |
486
+ | [`MemoryType`](#memory-taxonomy) | Enum of the six memory categories |
487
+ | [`MemoryItem`](#extract-memories-from-conversation) | Pydantic model for extracted memories |
488
+ | [`upsert_memory`](#upsert--insert-update-or-skip) | Semantic upsert — insert, update, or skip based on content similarity |
489
+ | `session_id` / `conversation_id` | Optional scoping fields on ``create_memory``, ``create_memories``, ``search_memories``, and ``upsert_memory`` |
490
+
491
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
492
+
493
+ <!-- DEVELOPMENT -->
494
+
495
+ ## Development
496
+
497
+ ### Code Quality
498
+
499
+ This project uses several tools to maintain code quality:
500
+
501
+ - **Ruff:** Linting and formatting
502
+ - **isort:** Import sorting
503
+ - **pytest:** Testing framework
504
+ - **deptry:** Dependency checking
505
+ - **ty:** Type checking (based on pyright)
506
+
507
+ **Available commands:**
508
+
509
+ ```sh
510
+ # Run all quality checks
511
+ uv run poe clean-full
512
+
513
+ # Individual checks
514
+ uv run poe lint # Ruff linting
515
+ uv run poe format # Ruff formatting
516
+ uv run poe sort # Import sorting
517
+ ```
518
+
519
+ ### Testing
520
+
521
+ Run tests using pytest:
522
+
523
+ ```sh
524
+ # Run all tests
525
+ uv run pytest
526
+
527
+ # Run specific test
528
+ uv run pytest path/to/test.py::test_name
529
+ ```
530
+
531
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
532
+
533
+ <!-- CONTRIBUTING -->
534
+
535
+ ## Contributing
536
+
537
+ Quick workflow:
538
+
539
+ 1. Fork and branch: `git checkout -b feature/name`
540
+ 2. Make changes
541
+ 3. Run checks: `uv run poe clean-full`
542
+ 4. Commit and push
543
+ 5. Open a Pull Request
544
+
545
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
546
+
547
+ <!-- LICENSE -->
548
+
549
+ ## License
550
+
551
+ MIT (as declared in `pyproject.toml`).
552
+
553
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
554
+
555
+ ---
556
+
557
+ <div align="center">
558
+ <p>
559
+ <sub>Built by <a href="https://github.com/thememium">thememium</a></sub>
560
+ </p>
561
+ </div>