LangChain Hands-on · Module 4 · Lab 17

LangGraph Basics: the Engine behind Agents

⏱ 20 min + hands-on🎯 Intermediate🖼 1 diagram🧪 3 live demos🐍 Python code

create_agent from Lab 13 is built on LangGraph. When you need your own flow (loops, branches, approvals, several agents) you use LangGraph directly. The idea is simple: a state dict flows through nodes (Python functions) connected by edges. In this lab you build a greeting graph, a pass/fail router, and then your own agent from scratch, with memory and a pause for approval.

In this session

  • Why LangGraph: custom flows
  • State, Node, Edge, Conditional edge, START / END
  • A first graph with no LLM
  • Conditional routing: pass or fail by marks
  • Your own agent: assistant ⇄ tools loop
  • Memory with a checkpointer, approval with interrupt_before

01The big idea: a metro map for your app

🚇

Think of the Mumbai Metro map. Stations are nodes, tracks are edges, and an interchange where you choose a line is a conditional edge. Your state is the passenger’s bag that travels through every station, and each station can add something to it. LangGraph lets you draw this map for your app.

WordMeaningIn code
Stateshared data (a dict) that flows through the graphclass State(TypedDict): name: str; greeting: str
Nodea Python function: reads state, returns updatesdef say_hello(state): return {"greeting": …}
Edgewhich node runs nextbuilder.add_edge("say_hello", "add_emoji")
Conditional edgechoose the next node with a functionadd_conditional_edges("check", route, {...})
START / ENDentry and exitadd_edge(START, "say_hello")
Demo · Conditional edge: move the marks

This is the notebook’s result graph. The real outputs for 72 and 20 marks are in the hands-on run below.

02Build your own agent

agent_graph.py
def assistant(state: MessagesState):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

b = StateGraph(MessagesState)
b.add_node("assistant", assistant)
b.add_node("tools", ToolNode(tools))
b.add_edge(START, "assistant")
b.add_conditional_edges("assistant", tools_condition)   # tool call? → "tools" else → END
b.add_edge("tools", "assistant")                        # loop back
agent_graph = b.compile()
STARTassistantLLM with toolstools_conditiontool call?toolsToolNode runs themENDno more calls
assistant → tools → assistant … until the LLM answers without a tool call. This is exactly the graph inside create_agent.
Demo · interrupt_before=["tools"]: pause, check, resume
NeedUse
prompt → llm → parserLCEL chain (Lab 04)
standard tool-calling agentcreate_agent (Lab 13)
custom steps, loops, branches, approvals, multi-agentLangGraph

03Hands-on: run it here

All six notebook sections: simple graph, routing, your own agent loop, memory in thread 1 (8 messages after two turns) and pause-and-resume before tools.

lab17_langgraph.py
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import InMemorySaver
from langchain.tools import tool
from standins import ScriptedModel, call

class State(TypedDict):
    name: str
    greeting: str
def say_hello(state: State): return {"greeting": f"Hello {state['name']}!"}
def add_emoji(state: State): return {"greeting": state["greeting"] + " 👋"}
builder = StateGraph(State)
builder.add_node("say_hello", say_hello); builder.add_node("add_emoji", add_emoji)
builder.add_edge(START, "say_hello"); builder.add_edge("say_hello", "add_emoji"); builder.add_edge("add_emoji", END)
print("1 simple   :", builder.compile().invoke({"name": "Ajeet"}))

class Result(TypedDict):
    marks: int
    message: str
b = StateGraph(Result)
b.add_node("check", lambda s: s); b.add_node("passed", lambda s: {"message": "🎉 Pass"}); b.add_node("failed", lambda s: {"message": "📚 Fail, try again"})
b.add_edge(START, "check")
b.add_conditional_edges("check", lambda s: "passed" if s["marks"] >= 35 else "failed", {"passed": "passed", "failed": "failed"})
b.add_edge("passed", END); b.add_edge("failed", END)
g = b.compile()
print("2 routing  :", g.invoke({"marks": 72}), g.invoke({"marks": 20}))

@tool
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
@tool
def get_weather(city: str) -> str:
    """Get weather of a city."""
    return f"{city}: 29°C, sunny"
tools = [multiply, get_weather]

def make_graph(llm, **compile_kw):
    def assistant(state: MessagesState): return {"messages": [llm.invoke(state["messages"])]}
    b = StateGraph(MessagesState)
    b.add_node("assistant", assistant); b.add_node("tools", ToolNode(tools))
    b.add_edge(START, "assistant")
    b.add_conditional_edges("assistant", tools_condition)      # tool call? -> "tools" else -> END
    b.add_edge("tools", "assistant")                           # loop back
    return b.compile(**compile_kw)

from langchain.messages import AIMessage
llm = ScriptedModel([AIMessage("", tool_calls=[{"name": "multiply", "args": {"a": 23, "b": 17}, "id": "m"}, {"name": "get_weather", "args": {"city": "Pune"}, "id": "w"}]),
                     "23 × 17 = 391, and Pune is 29°C and sunny."])
out = make_graph(llm).invoke({"messages": [{"role": "user", "content": "What is 23 * 17, and weather in Pune?"}]})
print("3 agent    :", " → ".join(m.type + (f"[{', '.join(c['name'] for c in m.tool_calls)}]" if getattr(m, "tool_calls", None) else "") for m in out["messages"]))
print("             final:", out["messages"][-1].content)

mem = make_graph(ScriptedModel([call("multiply", {"a": 6, "b": 7}), "Nice to meet you Ajeet! 6 × 7 = 42.",
                                call("multiply", {"a": 42, "b": 2}), "Your name is Ajeet, and 42 × 2 = 84."]), checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "1"}}
mem.invoke({"messages": [{"role": "user", "content": "My name is Ajeet. Multiply 6 and 7."}]}, cfg)
r = mem.invoke({"messages": [{"role": "user", "content": "What is my name? Multiply the last answer by 2."}]}, cfg)
print("4 memory   :", r["messages"][-1].content, f"({len(r['messages'])} messages in thread 1)")

hitl = make_graph(ScriptedModel([call("multiply", {"a": 12, "b": 12}), "12 × 12 = 144."]), checkpointer=InMemorySaver(), interrupt_before=["tools"])
cfg = {"configurable": {"thread_id": "approve-1"}}
hitl.invoke({"messages": [{"role": "user", "content": "Multiply 12 and 12"}]}, cfg)
print("5 paused   : next =", hitl.get_state(cfg).next, "| waiting tool call:", hitl.get_state(cfg).values["messages"][-1].tool_calls[0]["args"])
print("  resumed  :", hitl.invoke(None, cfg)["messages"][-1].content)
1 simple : {'name': 'Ajeet', 'greeting': 'Hello Ajeet! 👋'} 2 routing : {'marks': 72, 'message': '🎉 Pass'} {'marks': 20, 'message': '📚 Fail, try again'} 3 agent : human → ai[multiply, get_weather] → tool → tool → ai final: 23 × 17 = 391, and Pune is 29°C and sunny. 4 memory : Your name is Ajeet, and 42 × 2 = 84. (8 messages in thread 1) 5 paused : next = ('tools',) | waiting tool call: {'a': 12, 'b': 12} resumed : 12 × 12 = 144.
Runs here without any API key. A small stand-in plays the LLM (RuleModel or ScriptedModel from standins.py), so every output below is from a real run of this code. In Colab you swap one line: llm = ChatGroq(model="openai/gpt-oss-20b").

04Hands-on: Ajeetkumar’s Colab notebook

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 17

05Where to go next

  1. Build a graph: classify (complaint / query) → complaint_node or query_node.
  2. Add a convert_currency tool to the agent graph.
  3. Use interrupt_before to approve a send_email tool.

Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.

06Summary

07Quick quiz

1. What flows through a LangGraph graph?

2. What does a node return?

3. marks=20, threshold 35. Which node runs?

4. What does tools_condition do?

5. How do you resume after interrupt_before?

08References

Created by Ajeetkumar