Stateless to stateful with JSONL
Real conversations need context. When you say 'make it shorter', the bot needs to know what 'it' refers to. Since the provider is stateless, we have to store the conversation on our side and replay it into every call.
JSONL is one JSON object per line. It is the smallest persistence format that survives a crash mid-write: each line either lands intact or does not land at all. You can cat it, grep it, and tail -f it. Perfect for sessions.
{"role": "user", "content": "My name is Param"}
{"role": "assistant", "content": "Nice to meet you, Param!"}
{"role": "user", "content": "What's my name?"}
{"role": "assistant", "content": "Your name is Param."}One line per turn. Each line is a complete, parseable JSON object. Open it in any editor or pipe it through jq.
def load_session(user_id: str) -> list[dict]:
"""Load conversation history from a JSONL file."""
path = SESSIONS_DIR / f"{user_id}.jsonl"
if not path.exists():
return []
messages = []
for line in path.read_text().splitlines():
if line.strip():
messages.append(json.loads(line))
return messages
def append_message(user_id: str, message: dict):
"""Append a single message to the user's JSONL session file."""
path = SESSIONS_DIR / f"{user_id}.jsonl"
with open(path, "a") as f:
f.write(json.dumps(message) + "\n")Two helpers. load_session parses the file line by line. append_message tacks one new line on. No locks, no transactions, no databases.
def reply_with_history(user_id: str, user_text: str) -> str:
"""Load history, append user msg, call LLM, save reply, return reply."""
messages = load_session(user_id)
user_msg = {"role": "user", "content": user_text}
messages.append(user_msg)
append_message(user_id, user_msg)
response = client.chat.completions.create(
model=MODEL,
max_tokens=1024,
messages=messages,
)
reply = response.choices[0].message.content or ""
assistant_msg = {"role": "assistant", "content": reply}
append_message(user_id, assistant_msg)
return replyThe core helper. Load the history, append the user turn, call the model with the full array, save the assistant reply.
Because the data model is a log. Appends only. No queries. No joins. One file per user. SQLite would work, but it adds a process boundary and a connection lifecycle for zero benefit. When we add long-term memory, we use markdown files for the same reason: store data in the shape you read it.
Matching exercise: Match each property to the format that best fits a chat session
Loading practice…
Quiz: Quiz
Loading practice…