n4nAI

Building a session replay tool for chatbot conversations

Build a session replay tool chatbot engineers can use to log and replay LLM conversations for debugging, with Python and OpenAI-compatible APIs.

n4n Team2 min read541 words

Audio narration

Coming soon — every post will get a voice note here.

A session replay tool chatbot developers can rely on needs to capture exact request and response cycles, not just the final answer. This tutorial builds a lightweight logging and replay harness in Python that works against any OpenAI-compatible API, so you can reproduce production conversations locally and diff model behavior.

Prerequisites

  • Python 3.10 or newer
  • openai Python package (v1.x): pip install openai
  • API credentials for an OpenAI-compatible endpoint (OpenAI, Azure, or a gateway)
  • Basic comfort with JSON Lines and writing small CLI scripts

If you route through n4n.ai, its OpenAI-compatible endpoint addresses 240+ models and automatically falls back when a provider is rate-limited, which keeps replays from stalling on upstream errors.

Designing the session log format

Start with a flat JSONL file. One line per model call keeps appends cheap and lets you stream-read later. Each record must store the model, the exact input messages, the returned message, and token usage.

{
  "ts": "2024-05-12T08:31:02Z",
  "model": "gpt-4o",
  "messages": [{"role": "user", "content": "Refund policy for orders over 30 days?"}],
  "response": {"role": "assistant", "content": "Our policy allows refunds up to 60 days."},
  "usage": {"prompt_tokens": 12, "completion_tokens": 9}
}

Do not store just the transcript. Store the raw messages array exactly as sent, because system prompts and few-shot examples are where bugs hide.

Instrumenting your chatbot

Wrap the chat completion call. The wrapper writes a record after the response returns. Keep it synchronous for clarity; async works the same with AsyncOpenAI.

import openai, json, datetime, os

client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def chat_with_logging(messages, model="gpt-4o", log_path="sessions.jsonl"):
    resp = client.chat.completions.create(model=model, messages=messages)
    record = {
        "ts": datetime.datetime.utcnow().isoformat() + "Z",
        "model": model,
        "messages": messages,
        "response": {"role": "assistant", "content": resp.choices[0].message.content},
        "usage": resp.usage.model_dump() if hasattr(resp.usage, "model_dump") else resp.usage,
    }
    with open(log_path, "a") as f:
        f.write(json.dumps(record) + "\n")
    return resp

# Example usage in your bot
if __name__ == "__main__":
    msgs = [{"role": "user", "content": "What is your refund window?"}]
    r = chat_with_logging(msgs)
    print(r.choices[0].message.content)

Run it once:

python bot.py

Expected output (content varies):

Our standard refund window is 30 days from delivery.

And sessions.jsonl now contains one line. That file is the backbone of your session replay tool chatbot debugging flow.

Writing the replay harness

The replay script reads each line and re-issues the same messages. You can override the model to compare providers or versions.

import json, openai, sys, os

def replay(log_path, model_override=None, base_url=None, api_key=None):
    client = openai.OpenAI(api_key=api_key or os.environ["OPENAI_API_KEY"], base_url=base_url)
    with open(log_path) as f:
        for i, line in enumerate(f):
            rec = json.loads(line)
            model = model_override or rec["model"]
            print(f"[{i}] {rec['ts']} -> replaying with {model}")
            try:
                resp = client.chat.completions.create(model=model, messages=rec["messages"])
            except Exception as e:
                print(f"  ERROR: {e}")
                continue
            orig = rec["response"]["content"]
            new = resp.choices[0].message.content
            print(f"  ORIG: {orig}")
            print(f"  REPL: {new}")
            print("---")

if __name__ == "__main__":
    path = sys.argv[1] if len(sys.argv) > 1 else "sessions.jsonl"
    model = sys.argv[2] if len(sys.argv) > 2 else None
    replay(path, model_override=model)

Run a replay against a cheaper model:

python replay.py sessions.jsonl gpt-4o-mini

Expected output:

[0] 2024-05-12T08:31:02Z -> replaying with gpt-4o-mini
  ORIG: Our standard refund window is 30 days from delivery.
  REPL: You can request a refund within 30 days of delivery.
---

The session replay tool chatbot loop now reproduces the exact context that produced a bad answer.

Diffing outputs for regression checks

Printing side-by-side is fine for one record, but a real replay harness should flag divergences. Use difflib to get a unified diff.

import difflib

def show_diff(a, b):
    a_lines = a.splitlines()
    b_lines = b.splitlines()
    diff = difflib.unified_diff(a_lines, b_lines, lineterm="", fromfile="orig", tofile="replay")
    return "\n".join(diff)

# Inside replay loop, after getting new:
if orig != new:
    print(show_diff(orig, new))

For a changed refund number, you would see:

--- orig
+++ replay
@@ -1 +1 @@
-Our standard refund window is 30 days from delivery.
+You can request a refund within 45 days of delivery.

That diff is the signal you care about when a model swap silently changes policy statements.

Handling streaming and tool calls

Production chatbots often stream or call functions. Extend the schema:

{
  "ts": "2024-05-12T09:00:00Z",
  "model": "gpt-4o",
  "messages": [{"role": "user", "content": "Check order #123"}],
  "tools": [{"type": "function", "function": {"name": "get_order"}}],
  "response": {"role": "assistant", "tool_calls": [{"id": "call_1", "function": {"name": "get_order", "arguments": "{\"id\":123}"}}]},
  "usage": {"prompt_tokens": 20, "completion_tokens": 15}
}

When replaying, pass tools=rec.get("tools") and check for tool_calls instead of content. Streaming can be captured by accumulating chunks during logging; replay can use stream=False to simplify diffing. Determinism is more important than latency in a session replay tool chatbot debugger.

Running end-to-end with a gateway

If you point the client at a gateway, set base_url. This is where automatic fallback matters: a replay over hundreds of sessions should not abort because one provider returned 429.

replay(
    "sessions.jsonl",
    model_override="anthropic/claude-3-sonnet",
    base_url="https://api.n4n.ai/v1",
    api_key=os.environ["N4N_API_KEY"],
)

Because the gateway honors client routing directives and forwards provider cache-control hints, cached prompts from the original call may still hit cache on replay, keeping token metering sane.

Limitations and next steps

JSONL is great until you need to query by user ID or session ID. Add a session_id field and load into SQLite or Postgres when volume grows. A web UI that renders the diff inline beats scrolling terminal output.

The session replay tool chatbot approach here is deliberately minimal: it captures the contract between your code and the model. Once that contract is recorded, you can test prompt changes, model upgrades, or provider migrations without asking users to reproduce bugs. That is the whole point of replay debugging—remove the “works on my machine” from LLM behavior.

Tagssession-replaychatbotdebuggingtooling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All chatbot session replay & debugging posts →