Token-aware context memory package supporting multiple storage backends for LLM integrations and general context storage.
Project description
ContextStore
A persistence layer that reliably stores and retrieves LLM interaction context, with pluggable backends and both sync/async APIs.
Features
- Pluggable Backends: In-memory and SQLite storage, easily extensible
- Sync/Async APIs: Full async support with sync wrappers
- Token-Aware Context: Automatic truncation to fit model token limits
- Session Management: Organize interactions by session ID
- Semantic Retrieval: Embedding-based search over conversation history
- Unified SessionStore: Integrated memory + retrieval with auto-embedding
- Deterministic: Same inputs always produce identical outputs
- Type Hints: Full type annotation support
Installation
pip install contextstore
Quick Start
Async API (Recommended)
import asyncio
from contextstore import SQLiteMemory
async def main():
memory = SQLiteMemory("chat.db")
# Save context
await memory.save_context("session-1", [
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi there!"}
])
# Load context
history = await memory.load_context("session-1")
print(history)
asyncio.run(main())
In-Memory Storage
from contextstore import InMemoryMemory
memory = InMemoryMemory()
await memory.save_context("session-1", [{"role": "user", "content": "Hello!"}])
history = await memory.load_context("session-1")
Token-Aware Context Building
Automatically truncate context to fit within model token limits:
from contextstore import ContextBuilder
builder = ContextBuilder.from_model('gpt-4')
messages = [
{'id': '1', 'role': 'user', 'content': 'Hello!', 'timestamp': '2024-01-01T00:00:00Z'},
{'id': '2', 'role': 'assistant', 'content': 'Hi!', 'timestamp': '2024-01-01T00:01:00Z'},
# ... many more messages
]
result = builder.build(messages, max_tokens=4000)
print(result.messages) # Messages that fit
print(result.total_tokens) # Token count
print(result.approximate) # True if using fallback tokenizer
Truncation Strategies
# Drop oldest messages first (default)
result = builder.build(messages, max_tokens=4000, strategy='truncate_oldest')
# Keep only recent messages
result = builder.build(messages, max_tokens=4000, strategy='recent_only')
# Summarize oldest messages
result = builder.build(
messages,
max_tokens=4000,
strategy='summarize_oldest',
strategy_opts={'summarizer': lambda msgs: "Summary: ...", 'chunk_size': 5},
)
Tokenizer Options
from contextstore import tokenizer_from_name
# Model-specific (requires tiktoken)
tokenizer = tokenizer_from_name('gpt-4')
# Explicit fallback (no dependencies)
tokenizer = tokenizer_from_name('fallback')
Full Example with OpenAI
from uuid import uuid4
from datetime import datetime
from openai import OpenAI
from contextstore import ContextBuilder, SQLiteMemory
client = OpenAI()
memory = SQLiteMemory("chat.db")
builder = ContextBuilder.from_model('gpt-4')
async def chat(session_id: str, user_message: str):
# Load existing context
history = await memory.load_context(session_id)
# Add new message
history.append({
'id': str(uuid4()),
'role': 'user',
'content': user_message,
'timestamp': datetime.now().isoformat(),
})
# Truncate to fit token limit
result = builder.build(history, max_tokens=4000)
# Call OpenAI
response = client.chat.completions.create(
model='gpt-4',
messages=[{'role': m['role'], 'content': m['content']} for m in result.messages],
)
# Save updated context
assistant_msg = {
'id': str(uuid4()),
'role': 'assistant',
'content': response.choices[0].message.content,
'timestamp': datetime.now().isoformat(),
}
history.append(assistant_msg)
await memory.save_context(session_id, history)
return assistant_msg['content']
Custom Backends
Extend MemoryBackend to create custom storage:
from contextstore import MemoryBackend
from typing import List, Dict, Any, Optional
class RedisBackend(MemoryBackend):
async def load_context(self, session_id: str, k: Optional[int] = None) -> List[Dict[str, Any]]:
# Your Redis load logic
pass
async def save_context(self, session_id: str, context: List[Dict[str, Any]]) -> None:
# Your Redis save logic
pass
async def append_context(self, session_id: str, context: List[Dict[str, Any]]) -> None:
# Your Redis append logic
pass
async def delete_session(self, session_id: str) -> None:
# Your Redis delete logic
pass
async def delete_interaction(self, session_id: str, interaction_id: str) -> None:
# Your Redis delete interaction logic
pass
API Reference
MemoryBackend Methods
| Method | Description |
|---|---|
load_context(session_id, k=None) |
Load context (optionally last k interactions) |
save_context(session_id, context) |
Save/replace context |
append_context(session_id, context) |
Append to existing context |
delete_session(session_id) |
Delete entire session |
delete_interaction(session_id, interaction_id) |
Delete specific interaction |
ContextBuilder
ContextBuilder(tokenizer=None, default_strategy='truncate_oldest')
ContextBuilder.from_model(model_name) # Factory method
builder.build(
messages, # List of message dicts
max_tokens, # Token budget
strategy=None, # See strategies below
strategy_opts=None, # Strategy-specific options
pre_filter=None, # Filter before processing
post_filter=None, # Filter after truncation
) -> BuildResult
BuildResult
| Attribute | Type | Description |
|---|---|---|
messages |
List[Dict] |
Messages within budget |
total_tokens |
int |
Token count |
approximate |
bool |
True if using fallback tokenizer |
strategy_used |
str |
Strategy applied |
metadata |
Dict |
Additional info (dropped_ids, etc.) |
Truncation Strategies
| Strategy | Description |
|---|---|
truncate_oldest |
Drop oldest messages until under budget (default) |
recent_only |
Keep only recent messages that fit within budget |
summarize_oldest |
Summarize oldest messages via user-provided callback |
SessionStore
SessionStore(memory, retrieval=None, config=None)
# Methods
store.load_context(session_id, k=None)
store.save_context(session_id, context)
store.append_context(session_id, context)
store.retrieve_relevant(session_id, query, k=5)
store.spawn_background_embedding(session_id, message_id, text, metadata=None)
store.wait_for_embeddings()
RetrievalBackend
| Method | Description |
|---|---|
add(session_id, message_id, vector, metadata) |
Add embedding vector |
search(session_id, query_vector, k) |
Search for similar vectors |
has_embedding(session_id, message_id) |
Check if embedding exists |
Semantic Retrieval (v0.4.0+)
Retrieve relevant messages from conversation history using embeddings:
from contextstore import InMemoryEmbeddingStore, retrieve_relevant
# Your embedding function (sync or async)
def embed_fn(texts: list[str]) -> list[list[float]]:
# Use OpenAI, sentence-transformers, etc.
return [[0.1, 0.2, ...] for _ in texts]
store = InMemoryEmbeddingStore()
# Add embeddings
await store.add("session-1", "msg-1", embed_fn(["Hello!"])[0], {"text": "Hello!"})
await store.add("session-1", "msg-2", embed_fn(["How are you?"])[0], {"text": "How are you?"})
# Search for relevant messages
results = await retrieve_relevant("session-1", "greeting", embed_fn, store, k=5)
for item in results:
print(f"{item.message_id}: {item.score:.3f} - {item.metadata}")
SessionStore - Unified Workflow
SessionStore combines memory storage with embedding-based retrieval:
from contextstore import SessionStore, SessionStoreConfig, SQLiteMemory, InMemoryEmbeddingStore
memory = SQLiteMemory("chat.db")
retrieval = InMemoryEmbeddingStore()
config = SessionStoreConfig(
auto_embed=True,
embed_fn=your_embed_function,
)
store = SessionStore(memory, retrieval, config)
# Save context (automatically embeds when auto_embed=True)
await store.save_context("session-1", [
{"id": "1", "role": "user", "content": "What is Python?"},
{"id": "2", "role": "assistant", "content": "Python is a programming language."},
])
# Semantic search over history
relevant = await store.retrieve_relevant("session-1", "programming languages", k=3)
# Background embedding (non-blocking)
store.spawn_background_embedding("session-1", "msg-3", "Some text to embed")
await store.wait_for_embeddings() # Wait for completion
Requirements
- Python 3.8+
- Optional:
tiktokenfor accurate token counting - Optional:
numpyfor embedding-based retrieval
License
MIT License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contextstore-1.0.1.tar.gz.
File metadata
- Download URL: contextstore-1.0.1.tar.gz
- Upload date:
- Size: 30.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59a7a377d76f1614a52224596bedac15987b655f98947b6764b98136ff9486a6
|
|
| MD5 |
68f399cc4c8e77e8966330613bc4b42b
|
|
| BLAKE2b-256 |
7c84291ec960e32852a1b6fd28ccfb58f22b27ed63c3a5333238061363a3c9e2
|
File details
Details for the file contextstore-1.0.1-py3-none-any.whl.
File metadata
- Download URL: contextstore-1.0.1-py3-none-any.whl
- Upload date:
- Size: 23.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b98f57032b795029044785228658d00764f55cbe67c9d2e621596d855b655bc2
|
|
| MD5 |
a545a676ba9c05bb5c6004d4d86f4e54
|
|
| BLAKE2b-256 |
7fa66fb5fe5f5dfb10f4d86a0d8d0eb9176e3a925bc1c5e30817b75976382b37
|