Skip to main content

Lightweight persistent chat service with optional semantic search

Project description

chatstore

PyPI version Python versions CI License: MIT

A lightweight, framework-agnostic persistent chat service for LLM applications.

No servers. No Docker. No cloud accounts. Just install and start building.


The Problem It Solves

Every LLM application needs the same three things:

  1. Persist conversation history across process restarts
  2. Feed a sliding window of recent messages to the LLM (not the entire history)
  3. Optionally retrieve semantically relevant past context using vector search

Most solutions force you into a framework (LangChain), require a running server (Zep), or call an LLM just to store a memory (Mem0).

chatstore does none of that. It is a single class, backed by SQLite, that works with any LLM provider — Gemini, OpenAI, Anthropic, Ollama, or your own model. Add semantic search later with one flag, no infrastructure changes.


Requirements

  • Python 3.10, 3.11, or 3.12

Install

Core (SQLite + token counting):

pip install chatstore

With semantic search (adds ChromaDB + sentence-transformers):

pip install chatstore[semantic]

Quickstart

v1 — Basic chat history

from chatstore import ChatService

chat = ChatService(project_id="my_app")

# Save messages
chat.save_message("user", "What is the capital of France?")
chat.save_message("assistant", "The capital of France is Paris.")

# Load full history
history = chat.load_history()
# [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]

# Build windowed context to pass to your LLM (last 10 turns by default)
context = chat.build_context()

Resume an existing session

session_id = chat.get_session_id()  # save this somewhere

# Later, in a new process:
chat = ChatService(project_id="my_app", session_id=session_id)
history = chat.load_history()  # full history is back

Configure the sliding window size

The sliding window controls how many recent messages are passed to the LLM on each call. Adjust it based on your token budget and use case:

# Default — last 10 messages
chat = ChatService(project_id="my_app")

# Larger window for complex, multi-turn conversations
chat = ChatService(project_id="my_app", max_history_turns=20)

# Smaller window for cost-sensitive or low-latency apps
chat = ChatService(project_id="my_app", max_history_turns=5)

The full conversation history is always stored in SQLite regardless of window size. The window only controls what gets passed to the LLM — you can always call load_history() to retrieve the complete history.

v2 — With semantic search

from chatstore import ChatService

chat = ChatService(
    project_id="my_app",
    enable_semantic_search=True,
)

# After getting a tool/search result, store it in vector memory
# On source: it's just a free-text metadata label stored alongside the chunk. You can pass anything — "web_search", "pdf_parser", "database_query", "user_upload" — the library doesn't validate or care. It's only there so you can trace back where a chunk came from.

chat.store_in_memory("Paris is the capital of France. It has a population of 2.1 million.", source="web_search")

# Before calling your LLM, retrieve relevant past context
past_context = chat.retrieve_from_memory("What do we know about France?")

# Pass it into build_context
context = chat.build_context(extra_context=past_context)
# Now feed `context` to your LLM

Integration Example (OpenAI)

from openai import OpenAI
from chatstore import ChatService

client = OpenAI()
chat = ChatService(project_id="my_app")

def ask(user_input: str) -> str:
    chat.save_message("user", user_input)
    context = chat.build_context()

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=context
    )
    reply = response.choices[0].message.content
    chat.save_message("assistant", reply)
    return reply

Integration Example (Google Gemini)

import google.generativeai as genai
from chatstore import ChatService

genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel("gemini-2.5-flash")
chat = ChatService(project_id="my_app")

def ask(user_input: str) -> str:
    chat.save_message("user", user_input)
    context = chat.build_context()

    # Convert to Gemini format
    gemini_history = [
        {"role": "user" if m["role"] == "user" else "model", "parts": [m["content"]]}
        for m in context
    ]
    response = model.generate_content(gemini_history)
    reply = response.text
    chat.save_message("assistant", reply)
    return reply

Configuration Reference

Parameter Default Description
project_id required Namespace — all sessions for one app share a project
session_id auto UUID Pass an existing ID to resume a session
db_path chat_history.db Path to the SQLite database file
max_history_turns 10 Sliding window size fed to the LLM
max_context_tokens 8000 Warns when context window approaches this limit
enable_semantic_search False Set True to enable v2 vector memory
chroma_db_path ./chroma_db ChromaDB storage directory (v2 only)
embedding_model all-MiniLM-L6-v2 Sentence-Transformers model (v2 only)
memory_top_k 3 Number of chunks to retrieve per query (v2 only)
chunk_size 400 Words per memory chunk (v2 only)

Adapting for Production

chatstore ships with SQLite for zero-setup local use. As your application grows, you can swap components without changing your application code.

Switch SQLite → PostgreSQL

The _conn() and _init_db() methods are the only two places that touch the database. Subclass ChatService and override them:

import psycopg2
from chatstore import ChatService

class PostgresChatService(ChatService):
    def __init__(self, *args, dsn: str, **kwargs):
        self._dsn = dsn
        super().__init__(*args, **kwargs)

    def _conn(self):
        return psycopg2.connect(self._dsn)

    def _init_db(self):
        with self._conn() as conn:
            with conn.cursor() as cur:
                cur.execute("""
                    CREATE TABLE IF NOT EXISTS messages (
                        id      SERIAL PRIMARY KEY,
                        project TEXT NOT NULL,
                        session TEXT NOT NULL,
                        role    TEXT NOT NULL,
                        content TEXT NOT NULL,
                        ts      TEXT NOT NULL
                    )
                """)
            conn.commit()

# Usage — identical to the base class
chat = PostgresChatService(
    project_id="my_app",
    dsn="postgresql://user:password@localhost:5432/mydb"
)
chat.save_message("user", "Hello!")

Switch ChromaDB → Pinecone / Weaviate / Qdrant (v2)

Override VectorMemory in chatstore/memory.py — implement the same three methods (store, retrieve, clear_session) against your preferred vector DB:

from chatstore.memory import VectorMemory
import pinecone

class PineconeChatMemory(VectorMemory):
    def __init__(self, project_id, api_key, index_name, **kwargs):
        # skip ChromaDB init entirely
        self._index = pinecone.Index(index_name)
        self._top_k = kwargs.get("top_k", 3)
        self._chunk_size = kwargs.get("chunk_size", 400)

    def store(self, text, source, session_id):
        # your Pinecone upsert logic here
        ...

    def retrieve(self, query, session_id):
        # your Pinecone query logic here
        ...

Then pass an instance directly to ChatService._memory:

chat = ChatService(project_id="my_app")
chat._memory = PineconeChatMemory("my_app", api_key="...", index_name="chatstore")

Use a custom embedding model

chat = ChatService(
    project_id="my_app",
    enable_semantic_search=True,
    embedding_model="BAAI/bge-large-en-v1.5",   # any Sentence-Transformers model
)

Multiple projects sharing one database

# Each project_id is fully isolated — same db_path, zero bleed
support_chat = ChatService(project_id="support", db_path="shared.db")
sales_chat   = ChatService(project_id="sales",   db_path="shared.db")

API Reference

# Core (v1)
ChatService(project_id, session_id, db_path, max_history_turns, max_context_tokens)

chat.save_message(role: str, content: str)         # persist a message
chat.load_history(last_n=None) -> list[dict]        # full or partial history
chat.load_history_windowed()   -> list[dict]        # last max_history_turns messages
chat.build_context(extra_context="") -> list[dict]  # windowed history + optional prefix
chat.count_tokens(messages) -> int                  # approximate token count
chat.clear_session()                                # delete this session's messages
chat.get_session_id() -> str                        # current session UUID
chat.list_sessions() -> list[str]                   # all session IDs for this project

# Semantic search (v2, enable_semantic_search=True)
chat.store_in_memory(text: str, source: str)        # chunk and embed text
chat.retrieve_from_memory(query: str) -> str        # fetch relevant past chunks
chat.clear_memory()                                 # delete vector memory for session

Roadmap

v1.0 — Core ✅ Released

  • SQLite-backed persistent chat history
  • Sliding window context for LLM calls
  • Token counting via tiktoken
  • Multi-project and multi-session support

v2.0 — Semantic Search ✅ Released

  • ChromaDB-backed vector memory
  • Local sentence-transformers embeddings (no API key required)
  • Session-scoped retrieval — sessions never bleed into each other
  • Optional install via pip install chatstore[semantic]

v2.1 — Coming next

  • Async supportAsyncChatService for FastAPI / async frameworks
  • PostgreSQL adapter built-in (no subclassing required), enabled via db_url="postgresql://..."
  • Message metadata — attach arbitrary {key: value} tags to any message for filtering
  • Session summaries — auto-summarise old turns when approaching token limit instead of just dropping them

v3.0 — Future

  • Built-in adapters for Pinecone, Qdrant, and Weaviate (drop-in via vector_backend="pinecone")
  • Multi-modal memory — store and retrieve image descriptions alongside text
  • TTL / expiry — auto-expire sessions older than N days
  • Export / import — serialize a session to JSON for backup or migration

License

MIT

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

chatstore-2.0.1.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chatstore-2.0.1-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

File details

Details for the file chatstore-2.0.1.tar.gz.

File metadata

  • Download URL: chatstore-2.0.1.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for chatstore-2.0.1.tar.gz
Algorithm Hash digest
SHA256 bef6feb0465e5d2af28711af939560fd1b939ab0c1da20a03a23b915973778ab
MD5 17e0cc3ecf2456cd7c2981be76b6aec4
BLAKE2b-256 403122540ab078c5163ac1c3395f1a0a9108b6b23907f63b7cda5d48d2f6c835

See more details on using hashes here.

Provenance

The following attestation bundles were made for chatstore-2.0.1.tar.gz:

Publisher: publish.yml on namankr/chatstore

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file chatstore-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: chatstore-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 11.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for chatstore-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 92de0a232bb7f2d4985edd205b004c18f1eae9deda942d989c24e60df1a99958
MD5 0331a0f4d1b1b1684f1d540e3822739b
BLAKE2b-256 f3fb6cbf9eac8bf61f4f8aed7597cd8fdec95c94cad21be77772644f4533ea5f

See more details on using hashes here.

Provenance

The following attestation bundles were made for chatstore-2.0.1-py3-none-any.whl:

Publisher: publish.yml on namankr/chatstore

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page