Time-Travel Debugging: Build a Conversational Research Agent with LangGraph Checkpoints
Why LangGraph for conversational research
LangGraph gives you structured control over multi-step conversations: you can add steps to a dialogue, save checkpoints of the conversation state, replay the entire state history, and resume from any past checkpoint. In this tutorial we put those capabilities into practice by building a research-oriented chatbot that uses a free Gemini model plus a Wikipedia search tool. The hands-on flow demonstrates how checkpoints and replay make debugging, reproducibility, and iterative development straightforward.
Setup and required packages
Install dependencies and initialize the environment. The original example uses pip to install LangGraph, LangChain and the Google generative API helper packages. Then it imports the needed Python modules and initializes a Gemini LLM via LangChain:
!pip -q install -U langgraph langchain langchain-google-genai google-generativeai typing_extensions
!pip -q install "requests==2.32.4"
import os
import json
import textwrap
import getpass
import time
from typing import Annotated, List, Dict, Any, Optional
from typing_extensions import TypedDict
from langchain.chat_models import init_chat_model
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import ToolNode, tools_condition
import requests
from requests.adapters import HTTPAdapter, Retry
if not os.environ.get("GOOGLE_API_KEY"):
os.environ["GOOGLE_API_KEY"] = getpass.getpass(" Enter your Google API Key (Gemini): ")
llm = init_chat_model("google_genai:gemini-2.0-flash")
Building a Wikipedia search tool
To enrich the assistant’s responses with factual references, the tutorial implements a small Wikipedia search tool that queries MediaWiki’s API. The implementation uses a requests.Session with retries and a polite User-Agent. That tool is then wrapped as a LangChain tool so the LLM can call it when needed.
WIKI_SEARCH_URL = "https://en.wikipedia.org/w/api.php"
_session = requests.Session()
_session.headers.update({
"User-Agent": "LangGraph-Colab-Demo/1.0 (contact: example@example.com)",
"Accept": "application/json",
})
retry = Retry(
total=5, connect=5, read=5, backoff_factor=0.5,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET", "POST")
)
_session.mount("https://", HTTPAdapter(max_retries=retry))
_session.mount("http://", HTTPAdapter(max_retries=retry))
def _wiki_search_raw(query: str, limit: int = 3) -> List[Dict[str, str]]:
"""
Use MediaWiki search API with:
- origin='*' (good practice for CORS)
- Polite UA + retries
Returns compact list of {title, snippet_html, url}.
"""
params = {
"action": "query",
"list": "search",
"format": "json",
"srsearch": query,
"srlimit": limit,
"srprop": "snippet",
"utf8": 1,
"origin": "*",
}
r = _session.get(WIKI_SEARCH_URL, params=params, timeout=15)
r.raise_for_status()
data = r.json()
out = []
for item in data.get("query", {}).get("search", []):
title = item.get("title", "")
page_url = f"https://en.wikipedia.org/wiki/{title.replace(' ', '_')}"
snippet = item.get("snippet", "")
out.append({"title": title, "snippet_html": snippet, "url": page_url})
return out
@tool
def wiki_search(query: str) -> List[Dict[str, str]]:
"""Search Wikipedia and return up to 3 results with title, snippet_html, and url."""
try:
results = _wiki_search_raw(query, limit=3)
return results if results else [{"title": "No results", "snippet_html": "", "url": ""}]
except Exception as e:
return [{"title": "Error", "snippet_html": str(e), "url": ""}]
TOOLS = [wiki_search]
State, graph and LLM integration
Define the conversation state, bind tools to the LLM, add a chatbot node to the StateGraph and compile it with an in-memory checkpointer. The example enforces a small policy: if the user asks to research something, the model should call the wiki_search tool at least once.
class State(TypedDict):
messages: Annotated[list, add_messages]
graph_builder = StateGraph(State)
llm_with_tools = llm.bind_tools(TOOLS)
SYSTEM_INSTRUCTIONS = textwrap.dedent("""
You are ResearchBuddy, a careful research assistant.
- If the user asks you to "research", "find info", "latest", "web", or references a library/framework/product,
you SHOULD call the `wiki_search` tool at least once before finalizing your answer.
- When you call tools, be concise in the text you produce around the call.
- After receiving tool results, cite at least the page titles you used in your summary.
""").strip()
def chatbot(state: State) -> Dict[str, Any]:
"""Single step: call the LLM (with tools bound) on the current messages."""
return {"messages": [llm_with_tools.invoke(state["msgs"])]]}
graph_builder.add_node("chatbot", chatbot)
memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
(Notice: the tutorial composes the LLM, tools and graph so that each step is checkpointed and the full state can be replayed.)
Utilities to inspect and choose checkpoints
A few helpers make it easy to print the last assistant message, list saved checkpoints, and locate a checkpoint that is about to call a particular node (for example, the tools step). These utilities simplify the ’time-travel’ flow.
def print_last_message(event: Dict[str, Any]):
"""Pretty-print the last message in an event if available."""
if "messages" in event and event["messages"]:
msg = event["messages"][-1]
try:
if isinstance(msg, BaseMessage):
msg.pretty_print()
else:
role = msg.get("role", "unknown")
content = msg.get("content", "")
print(f"\n[{role.upper()}]\n{content}\n")
except Exception:
print(str(msg))
def show_state_history(cfg: Dict[str, Any]) -> List[Any]:
"""Print a concise view of checkpoints; return the list as well."""
history = list(graph.get_state_history(cfg))
print("\n=== State history (most recent first) ===")
for i, st in enumerate(history):
n = st.next
n_txt = f"{n}" if n else "()"
print(f"{i:02d}) NumMessages={len(st.values.get('messages', []))} Next={n_txt}")
print("=== End history ===\n")
return history
def pick_checkpoint_by_next(history: List[Any], node_name: str = "tools") -> Optional[Any]:
"""Pick the first checkpoint whose `next` includes a given node (e.g., 'tools')."""
for st in history:
nxt = tuple(st.next) if st.next else tuple()
if node_name in nxt:
return st
return None
Running conversation steps, replay and resume
The example creates a config with a thread id, streams two user turns to the graph (so each becomes a checkpoint), then shows how to list history, pick a checkpoint near a tools invocation, and resume the graph from that checkpoint. The resume effectively ’time travels’ the conversation to a prior state and continues from there.
config = {"configurable": {"thread_id": "demo-thread-1"}}
first_turn = {
"messages": [
{"role": "system", "content": SYSTEM_INSTRUCTIONS},
{"role": "user", "content": "I'm learning LangGraph. Could you do some research on it for me?"},
]
}
print("\n==================== STEP 1: First user turn ====================")
events = graph.stream(first_turn, config, stream_mode="values")
for ev in events:
print_last_message(ev)
second_turn = {
"messages": [
{"role": "user", "content": "Ya. Maybe I'll build an agent with it!"}
]
}
print("\n==================== STEP 2: Second user turn ====================")
events = graph.stream(second_turn, config, stream_mode="values")
for ev in events:
print_last_message(ev)
print("\n==================== REPLAY: Full state history ====================")
history = show_state_history(config)
to_replay = pick_checkpoint_by_next(history, node_name="tools")
if to_replay is None:
to_replay = history[min(2, len(history) - 1)]
print("Chosen checkpoint to resume from:")
print(" Next:", to_replay.next)
print(" Config:", to_replay.config)
print("\n==================== RESUME from chosen checkpoint ====================")
for ev in graph.stream(None, to_replay.config, stream_mode="vals"):
print_last_message(ev)
MANUAL_INDEX = None
if MANUAL_INDEX is not None and 0 <= MANUAL_INDEX < len(history):
chosen = history[MANUAL_INDEX]
print(f"\n==================== MANUAL RESUME @ index {MANUAL_INDEX} ====================")
print("Next:", chosen.next)
print("Config:", chosen.config)
for ev in graph.stream(None, chosen.config, stream_mode="values"):
print_last_message(ev)
print("\n Done. You added steps, replayed history, and resumed from a prior checkpoint.")
Practical takeaways
- Checkpointing conversation state makes debugging and iterative testing much easier: you can inspect intermediate states and run from any saved point.
- Binding tools (like the wiki search) to your LLM gives the agent safe, auditable access to external knowledge sources.
- Replay and resume let you simulate different conversation branches or recover from mistakes without losing prior context.
This workflow is a strong foundation for building reproducible research assistants and autonomous agents where traceability and stepwise control matter.