Conversation persistence: chat.save() / Chat.load() (+ optional local conversation log)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 176
- Forks
- 28
- Avg merge
- 18h 42m
- Merged PRs (30d)
- 16
Description
Motivation
llm logs every prompt/response/tool-call to a local SQLite DB by default, and it's the tool's single most-praised feature (https://llm.datasette.io/en/stable/logging.html). LangChain/LangGraph ship checkpointers (SQLite/Postgres) for the same reason: conversations outlive a single process. chatlas is most of the way there already — Turn (chatlas/_turn.py) is a pydantic BaseModel with a working JSON round trip (including base64-encoded bytes fields as of the CHANGELOG 0.18.0 fix for ContentPDF.data / thought_signature), and Chat.export() (chatlas/_chat.py) already writes markdown/HTML from turns. What's missing is a first-class way to persist and restore a Chat's state (not just a human-readable export), and, optionally, an llm-style append-only log across sessions.
Proposed approach
Phase 1 — chat.save() / Chat.load()
from chatlas import ChatAnthropic, Chat
chat = ChatAnthropic(system_prompt="You are terse.")
chat.chat("Hello", echo="none")
chat.save("conversation.json")
# Writes: provider name + model (e.g. "anthropic/claude-sonnet-4-6", matching
# the `provider_model` string ChatAuto() already accepts — see chatlas/_auto.py),
# system_prompt, and `[t.model_dump(mode="json") for t in chat.get_turns()]`.
# Option A: reconstruct the provider automatically via ChatAuto
chat2 = Chat.load("conversation.json")
# Option B: caller supplies an already-configured Chat (credentials, base_url,
# custom kwargs_chat aren't always representable as a provider/model string)
chat3 = ChatAnthropic(api_key=os.environ["MY_KEY"])
chat3 = Chat.load("conversation.json", chat=chat3)
Grounded in existing code: Chat.__init__ (chatlas/_chat.py:151) takes a provider: Provider, and Provider.name/Provider.model (chatlas/_provider.py:142,149) already give exactly the two strings ChatAuto()'s provider_model argument expects (chatlas/_auto.py:86, e.g. "anthropic/claude-sonnet-4-0"). set_turns() (chatlas/_chat.py:357) already restores conversation state from a list of Turns, and Turn.model_validate/model_validate_json (chatlas/_turn.py:124,172) already dispatch to UserTurn/SystemTurn/AssistantTurn by role — save/load mostly needs to wrap these existing primitives plus a small metadata envelope (provider/model, system_prompt).
Phase 2 — optional local conversation log (explicitly opt-in, llm-inspired)
chat = ChatOpenAI(log_to="~/.chatlas/history.sqlite") # or JSONL; opt-in only
Appends each completed turn (+ token usage/cost from chatlas/_tokens.py) for later search/audit, independent of any particular Chat instance's lifetime — this is deliberately a separate concern from save/load, which round-trips one conversation.
Alternatives / prior art
- ellmer has direct prior art:
contents_record()/contents_replay()(tidyverse/ellmer#503, #689, #895, #906) serializeTurns (and custom subclasses) to plain R lists and back, and #895 specifically extended this to operate on a list of turns rather than one at a time — the same shape as chatlas'sget_turns()/set_turns(). ellmer#1042 ("public API to switch model/provider on an existing Chat while preserving conversation history") is adjacent: it was resolved withChat$set_model()for same-provider switches, leaving cross-provider switching (relevant toChat.load()reconstructing a provider) as an open gap there too. - ellmer#534 ("Too easy to save chat object, accidentally exposing API key") is a cautionary tale: naive
saveRDS(chat)serialized live credentials. chatlas'sTurn-only JSON persistence avoids this by construction (no API key ever enters aTurn), but this should be called out explicitly in docs forchat.save(). - Reference chatlas#121 ("more export types: dataframe, csv, string") as related: that issue wants
.export()refactored so its string content is accessible without a file side-effect. Achat.save()JSON format is a natural complement (round-trippable) rather than a competitor (human/pipeline-readable) to that work.
Open questions
- Default provider-reconstruction story: should
Chat.load()require the caller to pass a constructedChat/Provider(safer, matches ellmer's credential-safety lesson), or default toChatAuto(provider_model)for convenience, with an explicit opt-in? - Should
kwargs_chatand_standard_model_params(chatlas/_chat.py:176,190) be persisted too, or left to the caller to reapply? - Format: plain JSON (simplest, human-inspectable) vs. something more structured — no evidence yet that anything beyond JSON is needed for phase 1.
- Should phase 2's log schema be designed to also serve chatlas's own future needs (e.g. supporting issue #8-style conversation-size limits by keeping cost/token history outside the live
Chat)?
Drafted from a competitive review of llm / Pydantic AI / LangChain / LiteLLM (July 2026); filed via Claude Code on behalf of @cpsievert.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Chat.init and set_turns() in chatlas/_chat.py, then inspect Provider.name/model in chatlas/_provider.py, ChatAuto() in chatlas/_auto.py, and Turn validation in chatlas/_turn.py. Implement the first persistence phase only if the scope is narrowed: a JSON envelope that restores provider metadata, system_prompt, and turns without credentials. Done means a saved conversation can be loaded into an automatically selected or caller-supplied Chat with equivalent state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, sqlite
- Domain
- backend, database
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100