LangGraph Getting Started
This guide walks you through a minimal LangGraph chatbot first, then adds incremental memory-service integration. The goal is to keep code changes small while unlocking memory features one step at a time.
Make sure you have completed LangGraph Dev Setup first.
Also complete Step 2 on that page (build local memory-service-langchain wheel + UV_FIND_LINKS); this is temporary until the package is released.
Step 1: Start with a Minimal LangGraph Chatbot
Starting checkpoint: python/examples/langgraph/doc-checkpoints/01-basic-langgraph
Create a minimal LangGraph chatbot using StateGraph(MessagesState) and expose it over HTTP with FastAPI (no memory-service imports yet):
from __future__ import annotations
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse
from langchain_openai import ChatOpenAI
from langgraph.graph import START, StateGraph
from langgraph.graph.message import MessagesState
openai_base_url = os.getenv("OPENAI_BASE_URL")
if openai_base_url and not openai_base_url.rstrip("/").endswith("/v1"):
openai_base_url = openai_base_url.rstrip("/") + "/v1"
model = ChatOpenAI(
model=os.getenv("OPENAI_MODEL", "gpt-4o"),
openai_api_base=openai_base_url,
api_key=os.getenv("OPENAI_API_KEY", "not-needed-for-tests"),
)
def call_model(state: MessagesState) -> dict:
messages = [{"role": "system", "content": "You are a helpful assistant."}] + list(state["messages"])
response = model.invoke(messages)
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
graph = builder.compile()
app = FastAPI(title="LangGraph Chatbot")
@app.get("/ready")
async def ready() -> dict[str, str]:
return {"status": "ok"}
@app.post("/chat")
async def chat(request: Request) -> PlainTextResponse:
user_message = (await request.body()).decode("utf-8").strip()
if not user_message:
raise HTTPException(400, "message is required")
result = await graph.ainvoke(
{"messages": [{"role": "user", "content": user_message}]}
)
message = result["messages"][-1]
content = getattr(message, "content", "")
return PlainTextResponse(content if isinstance(content, str) else str(content)) Why: StateGraph(MessagesState) is LangGraph’s way of describing the computation as a directed graph where each node transforms state. MessagesState gives the graph a built-in messages list so nodes can append to it naturally. Starting with a single node keeps the structure clear before adding memory features.
Add LangGraph dependencies:
[project]
name = "langgraph-doc-checkpoint-01-basic-langgraph"
version = "0.1.0"
description = "Memory Service LangGraph docs checkpoint app"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.115.0,<1.0.0",
"langgraph>=1.0.0,<2.0.0",
"langchain-openai>=1.0.0,<2.0.0",
"uvicorn>=0.34.0,<1.0.0",
]
[tool.uv]
package = false The checkpoint reads model configuration from environment variables (OPENAI_MODEL, OPENAI_BASE_URL, OPENAI_API_KEY) so local dev and site-tests can inject provider settings without code changes.
Run the app:
cd python/examples/langgraph/doc-checkpoints/01-basic-langgraph
uv sync --frozen
uv run uvicorn app:app --host 0.0.0.0 --port 9090Test it with curl:
curl -NsSfX POST http://localhost:9090/chat \
-H "Content-Type: text/plain" \
-d "What is 1 + 1?" Example output:
1 + 1 = 2. Each request is stateless — the chatbot has no memory of previous turns.
Step 2: Enable Memory-Backed Conversations
Starting checkpoint: python/examples/langgraph/doc-checkpoints/01-basic-langgraph
Checkpoint 02 adds MemoryServiceCheckpointSaver so the LangGraph graph persists conversation state across requests, keyed by conversation_id.
from __future__ import annotations
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse
from langchain_openai import ChatOpenAI
from langgraph.graph import START, StateGraph
from langgraph.graph.message import MessagesState
from memory_service_langchain import MemoryServiceCheckpointSaver, install_fastapi_authorization_middleware
openai_base_url = os.getenv("OPENAI_BASE_URL")
if openai_base_url and not openai_base_url.rstrip("/").endswith("/v1"):
openai_base_url = openai_base_url.rstrip("/") + "/v1" What changed: MemoryServiceCheckpointSaver and install_fastapi_authorization_middleware are imported from memory_service_langchain, and a checkpointer = MemoryServiceCheckpointSaver.from_env() instance is created at module level.
Why: MemoryServiceCheckpointSaver is a LangGraph BaseCheckpointSaver that stores and loads graph state in the Memory Service context channel. install_fastapi_authorization_middleware reads the Authorization: Bearer <token> header from every incoming request and stores it in a request-scoped context variable so the checkpointer can forward it to Memory Service without you passing it explicitly.
def call_model(state: MessagesState) -> dict:
messages = [{"role": "system", "content": "You are a helpful assistant."}] + list(state["messages"])
response = model.invoke(messages)
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model") What changed: checkpointer=checkpointer is passed to builder.compile(), and install_fastapi_authorization_middleware(app) is called after the FastAPI app is created.
Why: Passing checkpointer to compile() makes LangGraph automatically save the graph state after every node and load it at the start of every subsequent call with the same thread_id. This is the LangGraph pattern — unlike the LangChain create_agent() API, the checkpointer is attached at compile time rather than agent-creation time.
The endpoint extracts conversation_id from the path and threads it into the graph as thread_id:
@app.get("/ready")
async def ready() -> dict[str, str]:
return {"status": "ok"}
install_fastapi_authorization_middleware(app)
@app.post("/chat/{conversation_id}")
async def chat(conversation_id: str, request: Request) -> PlainTextResponse:
user_message = (await request.body()).decode("utf-8").strip()
if not user_message:
raise HTTPException(400, "message is required")
result = await graph.ainvoke( What changed: The route changes from POST /chat to POST /chat/{conversation_id}, and config={"configurable": {"thread_id": conversation_id}} is passed to graph.ainvoke().
Why: By mapping thread_id to conversation_id from the URL path, each unique path corresponds to one persistent conversation thread. Two calls to /chat/abc share the same message history; a call to /chat/xyz starts a fresh one.
Make sure Memory Service and Keycloak are running, then define a helper to get a user token:
function get-token() {
curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=memory-service-client" \
-d "client_secret=change-me" \
-d "grant_type=password" \
-d "username=bob" \
-d "password=bob" \
| jq -r '.access_token'
}
curl -NsSfX POST http://localhost:9090/chat/08c5bb33-05aa-4e4d-85cd-a1dd7ab1e137 \
-H "Authorization: Bearer $(get-token)" \
-H "Content-Type: text/plain" \
-d "Hi, I'm Hiram, who are you?" Example output:
Hi Hiram! I am a LangGraph memory-service demo agent. curl -NsSfX POST http://localhost:9090/chat/08c5bb33-05aa-4e4d-85cd-a1dd7ab1e137 \
-H "Authorization: Bearer $(get-token)" \
-H "Content-Type: text/plain" \
-d "Who am I?" Example output:
You are Hiram, as you told me in your introduction. Next Steps
Continue to Conversation History to record user/AI turns and expose conversation APIs for frontend clients.