Build a Tool-Calling Agent with LangGraph
Build a stateful AI agent that calls tools and routes decisions using LangGraph.
Introduction
30 Second Summary
When you ask a smart assistant to check the weather, solve a math problem, or look up a word, it doesn't just guess. It decides which tool to use, runs that tool, and then answers you with the result.
In this project, you will build your own AI agent that makes those same decisions. Using LangGraph, you'll wire up a reasoning loop where your agent chooses between answering directly, looking up a word definition, or crunching numbers.
What You'll Build
A terminal-based personal assistant agent that autonomously decides when to call tools (a dictionary API and a calculator) and when to answer directly.
By the end of this project, you'll have:
- A working LangGraph agent that routes between tools and direct responses.
- Two custom tools (dictionary lookup and calculator) your agent can call on demand.
- A conditional routing function that drives the ReAct (Reason + Act) loop.
- Secret Mission: Add conversation memory so your agent remembers previous questions in the same session.
Are there any prerequisites?
You'll need basic Python knowledge (functions, type hints, and dictionaries). You'll also need an API key from OpenAI, Anthropic, or another LLM provider that supports tool calling.
Set Up Your Python Environment
Before you can build your LangGraph agent, you need a working Python environment with the right packages installed.
In this step, you'll set up Python, create an isolated project folder, install your dependencies, and configure your LLM API key.
In this step, get ready to:
- Install Python 3.10+ (if not already installed).
- Create a project folder with a virtual environment and install dependencies.
- Set your API key as an environment variable.
Check your Python version
LangGraph requires Python 3.10 or higher.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS/Linux) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
Linux
python3 --version
✔️ I see a version number
If you see Python 3.10 or higher (e.g. 3.11, 3.12, 3.13), you're good to go.
ⓧ Command not found
You need to install Python. Follow the instructions for your operating system below.
macOS
- Install Homebrew (if you don't have it) by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Install Python by running:
brew install python
- Verify the install by running python3 --version again. You should see a version number like 3.12.x.
Windows
- Download the latest Python installer from the official website:
- Run the installer.
- Check the box that says Add python.exe to PATH at the bottom of the installer window.
- Click Install Now.
- Close and reopen PowerShell, then run python --version again. You should see a version number like 3.12.x.
Linux
- Install Python using your package manager by running:
sudo apt update && sudo apt install python3 python3-venv python3-pip
- Verify the install by running python3 --version again. You should see a version number like 3.12.x.
Create your project folder and virtual environment
A virtual environment gives your project its own isolated set of packages. This prevents version conflicts between different projects on your machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir langgraph-agent
cd langgraph-agent
- Confirm the folder exists by running:
ls
You should see an empty directory (no output). That means you're inside your new langgraph-agent folder.
- Create a virtual environment by running:
macOS
python3 -m venv venv
Windows
python -m venv venv
Linux
python3 -m venv venv
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Linux
source venv/bin/activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you might need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try the activate command again.
On macOS/Linux, make sure you used source before the path. Running venv/bin/activate without source won't work.
Still stuck? Help me activate my Python virtual environment
- Install the project dependencies by running the command for your LLM provider:
OpenAI (ChatGPT)
pip install langgraph langchain-openai langchain-core requests
Anthropic (Claude)
pip install langgraph langchain-anthropic langchain-core requests
Other (OpenAI-compatible)
This works with any provider that exposes an OpenAI-compatible API (Groq, Together, Ollama, LM Studio, vLLM, etc.).
pip install langgraph langchain-openai langchain-core requests
What are these packages?
- langgraph is the framework for building your agent as a state machine with nodes and edges.
- langchain-openai or langchain-anthropic connects LangGraph to your LLM provider.
- langchain-core provides message types and the tool decorator you'll use to build your tools.
- requests lets you call the dictionary API from your lookup tool.
The install takes 30-60 seconds. You should see a success message ending with Successfully installed followed by a list of package names.
Seeing installation errors?
Make sure your virtual environment is active. You should see (venv) in your terminal prompt before running pip install.
If you see a permissions error, do not use sudo. Deactivate and recreate your virtual environment instead.
Help me fix my pip install error for LangGraph
Set your API key as an environment variable
Your agent needs an API key to communicate with the LLM. Setting it as an environment variable keeps it out of your code.
- Set your API key by running the command for your provider:
OpenAI (ChatGPT)
macOS
export OPENAI_API_KEY="your-api-key-here"
Windows
$env:OPENAI_API_KEY="your-api-key-here"
Linux
export OPENAI_API_KEY="your-api-key-here"
Replace your-api-key-here with your actual OpenAI API key (starts with sk-).
Anthropic (Claude)
macOS
export ANTHROPIC_API_KEY="your-api-key-here"
Windows
$env:ANTHROPIC_API_KEY="your-api-key-here"
Linux
export ANTHROPIC_API_KEY="your-api-key-here"
Replace your-api-key-here with your actual Anthropic API key.
Other (OpenAI-compatible)
macOS
export OPENAI_API_KEY="your-api-key-here"
Windows
$env:OPENAI_API_KEY="your-api-key-here"
Linux
export OPENAI_API_KEY="your-api-key-here"
Replace your-api-key-here with the API key from your provider. You'll set the custom base_url in your code later.
Why an environment variable?
Environment variables keep secrets out of your source code. The LangChain libraries automatically read OPENAI_API_KEY and ANTHROPIC_API_KEY from your environment, so you never paste your key directly into Python files.
💡 This variable only lasts for your current session
If you close your terminal, you'll need to re-run the export command (and re-activate your virtual environment) before running your agent again.
Your environment is ready. Next up, you'll define the two tools your agent can use: a dictionary lookup and a calculator.
Define Your Tools
Your Python environment is set up, your virtual environment is active, and all your dependencies are installed. Now it's time to give your agent something to actually do.
In this step, you will define two tools: one that looks up word definitions from a free dictionary API, and one that evaluates math expressions safely. These are the hands your agent uses to interact with the outside world.
In this step, get ready to:
- Define a dictionary lookup tool and test it standalone.
- Define a calculator tool and test it standalone.
- Confirm both tools work before connecting them to an agent.
Create tools.py and define the dictionary lookup tool
LangChain's @tool decorator turns a regular Python function into something an LLM can discover and call. The function's docstring becomes the tool's description, which the LLM reads to decide when to use it.
- Create a new file called tools.py in your project folder.
- Add the imports and the lookup_word tool by pasting this code:
import requests
from langchain_core.tools import tool
@tool
def lookup_word(word: str) -> str:
"""Look up the definition of an English word."""
url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
response = requests.get(url)
if response.status_code != 200:
return f"Could not find a definition for '{word}'."
data = response.json()
first_meaning = data[0]["meanings"][0]
part_of_speech = first_meaning["partOfSpeech"]
definition = first_meaning["definitions"][0]["definition"]
return f"{word} ({part_of_speech}): {definition}"
What does this code do?
requests lets you make HTTP calls to external APIs. The @tool decorator from LangChain registers the function so an LLM can call it by name.
The lookup_word function hits the Free Dictionary API with a word, checks for a valid response, and pulls out the first definition along with its part of speech. If the word is not found, it returns a friendly error message instead of crashing.
- Save tools.py.
- Test the dictionary tool by running this command in your terminal:
python -c "from tools import lookup_word; print(lookup_word.invoke('hello'))"
- You should see output like: hello (noun): a greeting or expression of goodwill. The exact definition may vary.
Getting a ModuleNotFoundError?
If you see ModuleNotFoundError: No module named 'requests', run pip install requests in your terminal and try again. If you see No module named 'langchain_core', make sure your virtual environment is still active.
help me fix a ModuleNotFoundError when testing my lookup_word tool
Your first tool is working. Now you'll add a second tool so the agent has more than one capability to choose from.
Define the calculator tool
LLMs are notoriously bad at arithmetic. A calculator tool lets the agent offload math to Python, where results are exact every time.
- In tools.py, add the calculate tool below the lookup_word function:
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression and return the result.
Use standard math operators: +, -, *, /, **, ().
"""
try:
# Only allow digits, math operators, spaces, and parentheses
allowed_chars = set("0123456789+-*/.() \n")
if not all(c in allowed_chars for c in expression):
return "Error: Expression contains invalid characters. Only numbers and math operators are allowed."
# Evaluate with builtins disabled to prevent code injection
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error evaluating expression: {e}"
What does this code do?
The calculate tool takes a math expression as a string and evaluates it using Python's eval(). Before evaluating, it checks every character against an allowlist of numbers and math operators.
Passing {"__builtins__": {}} to eval() disables all built-in Python functions inside the expression. Combined with the character allowlist, this prevents someone from injecting malicious code through the calculator.
💡 Why not use ast.literal_eval?
Python's ast.literal_eval is often recommended for safe evaluation, but it only handles literal values like numbers and strings. It cannot process math operators like + or *. The restricted eval() approach here gives you actual arithmetic while the character allowlist blocks anything dangerous.
- Save tools.py.
Seeing a syntax error when you save?
Make sure the @tool decorator sits directly above the def calculate line with no blank line between them. Also check that the triple-quoted docstring is on the line immediately after the function signature.
help me fix a syntax error in my tools.py file
✔️ Awesome, I've got everything!
Great. Double check you have saved tools.py before moving on to testing.
ⓧ I'd like to double check the full code
Here is the complete tools.py file for comparison.
import requests
from langchain_core.tools import tool
@tool
def lookup_word(word: str) -> str:
"""Look up the definition of an English word."""
url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
response = requests.get(url)
if response.status_code != 200:
return f"Could not find a definition for '{word}'."
data = response.json()
first_meaning = data[0]["meanings"][0]
part_of_speech = first_meaning["partOfSpeech"]
definition = first_meaning["definitions"][0]["definition"]
return f"{word} ({part_of_speech}): {definition}"
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression and return the result.
Use standard math operators: +, -, *, /, **, ().
"""
try:
# Only allow digits, math operators, spaces, and parentheses
allowed_chars = set("0123456789+-*/.() \n")
if not all(c in allowed_chars for c in expression):
return "Error: Expression contains invalid characters. Only numbers and math operators are allowed."
# Evaluate with builtins disabled to prevent code injection
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error evaluating expression: {e}"
Test the calculator tool
Just like before, you can test the calculator tool directly using LangChain's .invoke() method.
- Test the calculator tool by running this command:
python -c "from tools import calculate; print(calculate.invoke('247 * 38'))"
- You should see: 9386.
Seeing an error when testing calculate?
If you see ModuleNotFoundError: No module named 'langchain_core', make sure your virtual environment is still active. You should see your environment name in parentheses at the start of your terminal prompt.
If you see a SyntaxError, check that the @tool decorator sits directly above def calculate with no blank line between them.
help me fix an error when testing my calculate tool
Both tools are working independently. Next up, you'll connect them to an LLM so it can discover and call them on its own.
Build the Agent Graph
Connect Your LLM
Your tools are built and tested. Now you need to connect them to an LLM that can discover and call them.
In this step, you will configure an LLM provider and bind your tools to it. This gives the LLM a menu of capabilities it can choose from when answering questions.
In this step, get ready to:
- Create agent.py and configure your LLM provider.
- Bind your tools to the LLM so it can call them.
Create agent.py and configure your LLM
The agent file pulls together your tools, LLM, and graph logic into one module. You will start with the imports and LLM configuration.
- Create a new file called agent.py in your project folder (the same folder where tools.py lives).
- Add these imports at the top of the file:
from typing import Literal
from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.graph import END, START, MessagesState, StateGraph
from tools import lookup_word, calculate
What do these imports do?
- StateGraph is the builder class you use to define your agent's nodes and edges.
- MessagesState is a pre-built state type that holds a list of messages (the conversation so far).
- START and END are special markers for the graph's entry and exit points.
- SystemMessage and ToolMessage are message types the LLM uses to communicate with you and with tools.
You won't see any output yet. The next chunk configures your LLM provider.
Your agent needs an LLM with tool-calling abilities. The setup differs depending on which provider you are using.
- Below your imports in agent.py, add the provider setup section. Choose the tab that matches your API key:
OpenAI (ChatGPT)
# --- Provider Setup (choose one) ---
# OpenAI (ChatGPT)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
# Anthropic (Claude)
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# Other (OpenAI-compatible endpoint)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="your-model-name", base_url="https://your-provider.com/v1")
# --- End Provider Setup ---
This uses gpt-4o-mini, OpenAI's cheapest model that supports tool calling.
Anthropic (Claude)
# --- Provider Setup (choose one) ---
# OpenAI (ChatGPT)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="gpt-4o-mini")
# Anthropic (Claude)
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# Other (OpenAI-compatible endpoint)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="your-model-name", base_url="https://your-provider.com/v1")
# --- End Provider Setup ---
This uses claude-haiku-4-5-20251001, Anthropic's cheapest model that supports tool calling.
Other (OpenAI-compatible)
# --- Provider Setup (choose one) ---
# OpenAI (ChatGPT)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="gpt-4o-mini")
# Anthropic (Claude)
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# Other (OpenAI-compatible endpoint)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="your-model-name", base_url="https://your-provider.com/v1")
# --- End Provider Setup ---
Replace your-model-name and the base_url with your provider's details. This works with any OpenAI Chat Completions API compatible endpoint (Ollama, Groq, Together, etc.).
Bind your tools to the LLM
- After the provider setup section in agent.py, add the tool binding code:
tools = [lookup_word, calculate]
tools_by_name = {t.name: t for t in tools}
llm_with_tools = llm.bind_tools(tools)
What does binding tools do?
The bind_tools() method tells the LLM what tools are available. When the LLM receives a question, it can now choose to call one of these tools instead of answering directly.
The tools_by_name dictionary maps each tool's string name to the actual function. This lets your tool node look up and execute the right tool when the LLM requests one.
- Save agent.py.
- Verify the imports and LLM configuration work by running this command:
python -c "from agent import llm_with_tools; print('LLM configured successfully')"
- You should see LLM configured successfully printed in your terminal.
Seeing an import error?
- If you see ModuleNotFoundError: No module named 'langchain_openai', make sure your virtual environment is activated and you installed the correct provider package in Step 1.
- If you see an authentication error, confirm your API key environment variable is set in this terminal session.
- If you see ModuleNotFoundError: No module named 'tools', make sure you are running the command from the same folder that contains tools.py.
Help me debug my agent.py import error
Your LLM is connected and knows about your tools. Next up, you'll define the node functions and routing logic that turn this into a working agent loop.
Build the Agent Graph
Your LLM is configured and knows which tools exist. But it still has no way to decide when to use them or what to do with the results.
In LangGraph, you build an agent as a graph of nodes (functions that do work) connected by edges (rules that control flow). In this step, you will define two node functions and a routing function that together create the agent's decision loop.
In this step, get ready to:
- Define two node functions that handle LLM calls and tool execution.
- Write a conditional routing function that drives the agent loop.
Define the LLM call node
A node in LangGraph is a Python function that receives the current state and returns updates to it. Your agent needs two nodes: one that calls the LLM, and one that executes tools.
- In agent.py, add the llm_call node function below the llm_with_tools line:
def llm_call(state: MessagesState):
"""LLM decides whether to call a tool or respond directly."""
return {
"messages": [
llm_with_tools.invoke(
[
SystemMessage(
content="You are a helpful assistant. You can look up word definitions and perform math calculations using your tools. Use tools when appropriate."
)
]
+ state["messages"]
)
]
}
What does this node do?
This function takes the current conversation (all messages so far) and prepends a SystemMessage that tells the LLM its role and available capabilities.
The LLM then looks at the full message history and either responds directly or returns a tool call request. Either way, the response gets appended to the state's messages list.
You won't see a change yet. The next function completes the second half of the loop.
Define the tool execution node
- Below the llm_call function, add the tool_node function:
def tool_node(state: MessagesState):
"""Execute the tool(s) that the LLM requested."""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(
ToolMessage(content=str(observation), tool_call_id=tool_call["id"])
)
return {"messages": result}
What does this node do?
- It reads the tool_calls from the LLM's last message. Each tool call has a name, arguments, and an ID.
- It looks up the matching function in tools_by_name and runs it with the LLM-provided arguments.
- It wraps each result in a ToolMessage so the LLM can read the tool output on the next loop.
- Save agent.py.
- Confirm the node functions are defined correctly by running:
python -c "from agent import llm_call, tool_node; print('Nodes defined successfully')"
- You should see Nodes defined successfully printed.
Seeing a syntax error?
- Check that the parentheses and brackets in llm_call are balanced. The nested invoke() call has several closing parentheses and brackets at the end.
- Make sure both functions are at the top level of the file (no extra indentation).
Help me fix a syntax error in my agent.py node functions
Write the conditional routing function
The final piece is a routing function that inspects the LLM's response and decides what happens next. This is the core of the ReAct pattern: reason, then act, then check if more action is needed.
- Below the tool_node function in agent.py, add the should_continue function:
def should_continue(state: MessagesState) -> Literal["tool_node", "__end__"]:
"""Route to tools if the LLM made a tool call, otherwise end."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return END
What does this function do?
After the LLM node runs, this function checks the last message. If the LLM's response contains tool_calls, it routes the graph to the tool node. If not, the LLM answered directly and the graph ends.
This creates the loop: LLM calls tool, tool returns result, LLM sees result, decides if it needs another tool or can answer. The agent keeps looping until it has enough information.
- Save agent.py.
- Verify the full file is correct by running:
python -c "from agent import llm_call, tool_node, should_continue; print('All agent components ready')"
- You should see All agent components ready printed.
Import still failing?
- Make sure from typing import Literal is the very first line in your file. The should_continue function's return type annotation depends on it.
- Check that "__end__" in the Literal type hint uses two underscores on each side.
Help me debug my should_continue function
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete agent.py file as it should look at the end of this step. The example below shows the OpenAI provider active. If you are using Anthropic or another provider, your uncommented lines will differ in the provider setup section.
from typing import Literal
from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.graph import END, START, MessagesState, StateGraph
from tools import lookup_word, calculate
# --- Provider Setup (choose one) ---
# OpenAI (ChatGPT)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
# Anthropic (Claude)
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# Other (OpenAI-compatible endpoint)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="your-model-name", base_url="https://your-provider.com/v1")
# --- End Provider Setup ---
tools = [lookup_word, calculate]
tools_by_name = {t.name: t for t in tools}
llm_with_tools = llm.bind_tools(tools)
def llm_call(state: MessagesState):
"""LLM decides whether to call a tool or respond directly."""
return {
"messages": [
llm_with_tools.invoke(
[
SystemMessage(
content="You are a helpful assistant. You can look up word definitions and perform math calculations using your tools. Use tools when appropriate."
)
]
+ state["messages"]
)
]
}
def tool_node(state: MessagesState):
"""Execute the tool(s) that the LLM requested."""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(
ToolMessage(content=str(observation), tool_call_id=tool_call["id"])
)
return {"messages": result}
def should_continue(state: MessagesState) -> Literal["tool_node", "__end__"]:
"""Route to tools if the LLM made a tool call, otherwise end."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return END
Your agent's brain is ready. It can think (LLM node), act (tool node), and decide what to do next (conditional routing). In the next step, you will wire these pieces into a compiled graph and run your agent from the terminal.
Compile and Run Your Agent
You have all the building blocks ready: two nodes that do the thinking and tool execution, plus a routing function that decides where to go next.
Now it is time to wire them into a single executable graph. The StateGraph connects your nodes with edges, and once you compile it, you get an agent you can invoke from a simple terminal loop.
In this step, get ready to:
- Wire your nodes and edges into a compiled graph.
- Create a terminal interface to chat with your agent.
- Test the agent with three different question types.
Wire the graph together and compile it
A StateGraph needs three things: nodes (the functions to run), edges (the connections between them), and a compile step that turns the blueprint into an executable agent.
- In agent.py, scroll to the bottom of the file (after the should_continue function) and add the following code:
# Build the graph
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
What does this code do?
StateGraph(MessagesState) creates a new graph that uses a shared message list as its state.
- add_node() registers each function so the graph can call it by name.
- add_edge(START, "llm_call") means every conversation starts at the LLM node.
- add_conditional_edges() uses your should_continue function to decide whether to route to tool_node or stop.
- The edge from tool_node back to llm_call creates the loop. After a tool runs, the LLM gets the result and decides what to do next.
- compile() freezes the graph structure and returns an object you can invoke with messages.
💡 What is the ReAct pattern?
This loop implements the ReAct (Reason + Act) pattern. The LLM reasons about what to do, optionally acts by calling a tool, observes the result, then reasons again.
The cycle repeats until the LLM decides it has enough information to answer the user directly.
- Save agent.py.
Seeing a NameError?
- Make sure the graph-building code is at the bottom of agent.py, after all function definitions. Python needs the functions to exist before you reference them.
- Check that node names in add_node() match the strings in add_conditional_edges() exactly. A typo like "tools_node" vs "tool_node" will cause an error.
Help me debug my StateGraph setup in agent.py.
Create the terminal interface
Your compiled graph is ready to accept messages. You need a way to send it questions from the terminal and see its responses.
- Create a new file called main.py in the same folder as agent.py and tools.py.
- Add the following code to main.py:
from langchain_core.messages import HumanMessage
from agent import agent
def main():
print("LangGraph Agent Ready! Type 'quit' to exit.\n")
while True:
user_input = input("You: ")
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
ai_message = result["messages"][-1]
print(f"Agent: {ai_message.content}\n")
if __name__ == "__main__":
main()
What does this code do?
- from agent import agent imports your compiled graph from agent.py.
- The while True loop keeps asking for input until the user types quit, exit, or q.
- agent.invoke() sends the user's message through the entire graph. The agent reasons, optionally calls tools, and returns the full message history.
- result["messages"][-1] grabs the final message in the conversation, which is the agent's response.
- Save main.py.
Seeing an ImportError?
- Make sure main.py is in the same folder as agent.py. The from agent import agent line needs both files in the same directory.
- Confirm your virtual environment is still activated. If you see a package-not-found error, reactivate with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Help me fix my import error in main.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved both agent.py and main.py.
ⓧ I'd like to double check the full code
Here is the complete agent.py at this point:
from typing import Literal
from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.graph import END, START, MessagesState, StateGraph
from tools import lookup_word, calculate
# --- Provider Setup (choose one) ---
# OpenAI (ChatGPT)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
# Anthropic (Claude)
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# Other (OpenAI-compatible endpoint)
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="your-model-name", base_url="https://your-provider.com/v1")
# --- End Provider Setup ---
tools = [lookup_word, calculate]
tools_by_name = {t.name: t for t in tools}
llm_with_tools = llm.bind_tools(tools)
def llm_call(state: MessagesState):
"""LLM decides whether to call a tool or respond directly."""
return {
"messages": [
llm_with_tools.invoke(
[
SystemMessage(
content="You are a helpful assistant. You can look up word definitions and perform math calculations using your tools. Use tools when appropriate."
)
]
+ state["messages"]
)
]
}
def tool_node(state: MessagesState):
"""Execute the tool(s) that the LLM requested."""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(
ToolMessage(content=str(observation), tool_call_id=tool_call["id"])
)
return {"messages": result}
def should_continue(state: MessagesState) -> Literal["tool_node", "__end__"]:
"""Route to tools if the LLM made a tool call, otherwise end."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return END
# Build the graph
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
Here is the complete main.py:
from langchain_core.messages import HumanMessage
from agent import agent
def main():
print("LangGraph Agent Ready! Type 'quit' to exit.\n")
while True:
user_input = input("You: ")
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
ai_message = result["messages"][-1]
print(f"Agent: {ai_message.content}\n")
if __name__ == "__main__":
main()
Test the agent with three question types
Your agent should handle three scenarios differently: answer a factual question from its own knowledge, call the dictionary tool for a definition, and call the calculator tool for math.
- Make sure your virtual environment is activated and your API key environment variable is set.
- Start the agent by running this command:
python main.py
You should see LangGraph Agent Ready! Type 'quit' to exit. printed in your terminal.
- Test a direct knowledge question by typing:
What is the capital of France?
The agent answers directly (e.g. "The capital of France is Paris.") without calling any tools. It already knows this.
- Test the dictionary tool by typing:
Define ephemeral
This time the agent calls the lookup_word tool behind the scenes, gets the definition from the dictionary API, and presents it to you.
- Test the calculator tool by typing:
What is 247 * 38?
The agent calls the calculate tool and returns 9386 (or a sentence containing that number).
- Type quit to exit the agent loop.
Why does routing matter?
Notice you never told the agent which tool to use. The LLM reads the question, decides whether a tool would help, and picks the right one. This is the core value of the ReAct loop.
For the "capital of France" question, it skips tools entirely and responds from its training knowledge. For definitions and math, it autonomously routes through the tool node and back.
🙋♀️ Agent not calling tools?
- Make sure the system message in your llm_call function mentions "Use tools when appropriate." Without this hint, some models prefer to answer from memory.
- Check that you used llm_with_tools (with tools bound) inside llm_call, not the bare llm variable.
- Try a more explicit prompt like "Use the dictionary to define ephemeral" to confirm tools work, then experiment with less explicit phrasing.
Help me figure out why my agent is not calling tools.
Secret mission
Add Conversation Memory
Right now, your agent forgets everything after each question. Give it memory so it can reference earlier messages in the same session. Ask a follow-up question and watch it recall context from earlier in the conversation.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. API charges only occur when you actively run the agent and it makes LLM calls. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder on your Desktop (langgraph-agent).
- Python virtual environment (venv inside your project folder).
- Installed packages (langgraph, langchain-openai or langchain-anthropic, langchain-core, requests).
- API key environment variable (OPENAI_API_KEY or ANTHROPIC_API_KEY).
- Project files (tools.py, agent.py, main.py).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your agent or adding new tools.
- Your project folder, virtual environment, and files stay on your Desktop.
- Your API key environment variable persists only for the current terminal session (unless you added it to your shell profile).
- No costs are incurred unless you actively run the agent.
Pause - I'll come back to this later
Deactivate your virtual environment to free up your terminal. Your files and packages stay intact for next time.
- Deactivate the virtual environment by running:
deactivate
- Close your terminal window. Your environment variable will not persist to new sessions.
- When you come back, reactivate by navigating to your project folder and running source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your code, virtual environment, and installed packages.
- Deactivate the virtual environment if it is still active:
deactivate
- Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf langgraph-agent
- On Windows, use this command instead:
cd ~/Desktop
Remove-Item -Recurse -Force langgraph-agent
- If you added your API key to a shell profile file (like .zshrc or .bashrc), open that file and remove the export line.
- Consider revoking or rotating your API key in your provider's dashboard if you no longer need it.
Nice Work!
Nice Work!
You just built a tool-calling AI agent from scratch using LangGraph. Your agent can reason about questions, decide when to reach for a tool, and loop back for more information before responding.
Here's what you learned:
- Defined custom tools with the @tool decorator and wired them into an LLM that decides when to call them.
- Built a StateGraph with nodes, edges, and conditional routing to implement the ReAct loop.
- Compiled and invoked the graph from a terminal input loop, testing direct answers, dictionary lookups, and math calculations.
- Added persistent conversation memory with a checkpointer so the agent remembers earlier messages in a session.
Ready to quiz yourself?