Capstone: the support agent
The support agent brings the course together: one graph that reads a ticket, looks things up with a tool, remembers the conversation, and can be paused for a human before anything it cannot undo.
Last updated: 27 Sep, 2026 · LangGraph 1.2
Every piece here appeared in an earlier lesson. The agent loop calls a model and runs tools; a checkpointer gives it memory; an interrupt puts a human in front of a refund. Assembled, they are a support agent.
The pieces, and where each came from
| Piece | From lesson |
|---|---|
| Agent loop (call model, run tools, loop back) | The agent loop |
| A tool the model can call | Tools and bind_tools |
| Memory across turns | Checkpointer |
| A human approval before a refund | interrupt |
The imports
Start with the imports. Each line brings in one thing from an earlier lesson.
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessageThe lookup tool
Next, a tool the model can call. It is a plain function with the @tool decorator.
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order by id.""" # the docstring tells the model what the tool does
return f"Order {order_id}: shipped on 3 March."The model node
Now the model. A real chat model goes here; this stand-in stands in for one so the lesson runs without an API key. It reads the last message and decides the next move.
def call_model(state):
last = state["messages"][-1] # look at the newest message
if isinstance(last, ToolMessage): # a tool has run -> answer with its result
return {"messages": [AIMessage(last.content)]}
if isinstance(last, HumanMessage) and "order" in last.content.lower():
# user asked about an order -> ask to run the lookup tool
return {"messages": [AIMessage(content="", tool_calls=[
{"name": "lookup_order", "args": {"order_id": "A17"}, "id": "c1"}])]}
return {"messages": [AIMessage("How can I help with your order?")]} # otherwise, greet- If the last message is a
ToolMessage, the tool has run, so reply with its result. - If the user mentioned an order, ask to run
lookup_order(a tool call, not a final answer). - Otherwise, greet and wait.
Wiring the graph
Wire the three pieces into a graph. The edges are what make it loop.
b = StateGraph(MessagesState)
b.add_node("call_model", call_model)
b.add_node("tools", ToolNode([lookup_order])) # runs whatever tool the model asks for
b.add_edge(START, "call_model")
b.add_conditional_edges("call_model", tools_condition) # tool call? -> tools, else -> stop
b.add_edge("tools", "call_model") # after a tool, hand back to the model
agent = b.compile(checkpointer=InMemorySaver()) # the checkpointer is the memorytools_conditionsends a tool call to thetoolsnode, and anything else to the end.- The edge from
toolsback tocall_modelis the loop: run a tool, then let the model use the result. InMemorySaversaves the state per thread, so the agent remembers past turns.
Running two turns
Run two turns on the same thread so the second remembers the first.
cfg = {"configurable": {"thread_id": "1"}} # same thread id = same memory
agent.invoke({"messages": [HumanMessage("hi")]}, cfg) # turn 1
out = agent.invoke({"messages": [HumanMessage("where is my order A17?")]}, cfg) # turn 2
print(out["messages"][-1].content)The support agent end to end
The same pieces in one file, ready to run.
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessage
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order by id."""
return f"Order {order_id}: shipped on 3 March."
def call_model(state):
last = state["messages"][-1]
if isinstance(last, ToolMessage):
return {"messages": [AIMessage(last.content)]}
if isinstance(last, HumanMessage) and "order" in last.content.lower():
return {"messages": [AIMessage(content="", tool_calls=[
{"name": "lookup_order", "args": {"order_id": "A17"}, "id": "c1"}])]}
return {"messages": [AIMessage("How can I help with your order?")]}
b = StateGraph(MessagesState)
b.add_node("call_model", call_model)
b.add_node("tools", ToolNode([lookup_order]))
b.add_edge(START, "call_model")
b.add_conditional_edges("call_model", tools_condition)
b.add_edge("tools", "call_model")
agent = b.compile(checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "1"}}
agent.invoke({"messages": [HumanMessage("hi")]}, cfg)
out = agent.invoke({"messages": [HumanMessage("where is my order A17?")]}, cfg)
print(out["messages"][-1].content)What the two turns showed
- Turn one greeted; turn two, on the same thread, asked about an order, so the model requested the tool.
tools_conditionrouted to the tools node,ToolNoderanlookup_order, and the model answered from the result.- The checkpointer kept both turns on thread "1", so the agent carried the conversation across the two calls.
Going live
To make it real, swap the stand-in call_model for one that calls model.bind_tools([lookup_order]).invoke(state["messages"]), and add an interrupt before any refund tool. The rest of the graph is unchanged.
When to build an agent like this
- A support or operations assistant that looks things up and remembers a customer.
- The base an approval step and more tools bolt onto.
thread_id across turns. A new id each turn is the reason an agent forgets what was said a moment ago.Related
- Add a third turn on the same thread and confirm the earlier ones are still there.
- Add an
interruptin a refund node and resume it with an approval.
You understood something today that you didn't yesterday.