posit-dev / posit-dev/chatlas

Conversation persistence: chat.save() / Chat.load() (+ optional local conversation log)

Open
#352 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ai-triage:needs-review enhancement
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) serialize Turns (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's get_turns()/set_turns(). ellmer#1042 ("public API to switch model/provider on an existing Chat while preserving conversation history") is adjacent: it was resolved with Chat$set_model() for same-provider switches, leaving cross-provider switching (relevant to Chat.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's Turn-only JSON persistence avoids this by construction (no API key ever enters a Turn), but this should be called out explicitly in docs for chat.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. A chat.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 constructed Chat/Provider (safer, matches ellmer's credential-safety lesson), or default to ChatAuto(provider_model) for convenience, with an explicit opt-in?
  • Should kwargs_chat and _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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.