An Agent is an AI system that can reason, make decisions, and choose which tools to use in order to complete a task.
Unlike a Chain, which follows a fixed sequence of steps, an Agent dynamically decides what actions to take based on the user’s request.
Example
User asks:
“Find the latest AI research paper, summarize it, and email me the summary.”
An Agent might:
- Use a Search Tool to find the paper.
- Use an LLM to generate a summary.
- Use an Email Tool to send the summary.
The Agent decides which tools to use and when to use them.
What is Memory in LangChain?
Memory allows an AI application to remember information from previous interactions and use it in future conversations. Without memory, each conversation is treated independently. With memory, the AI can maintain context and provide more natural interactions.
Example
User: I work in Loxford Academy.
Later…
User: Where do I work?
Without memory:
I don’t know.
With memory:
You work in Loxford Academy.
Why Memory is Important
Memory helps an AI:
- Remember user preferences
- Maintain conversation context
- Build personalized assistants
- Support long-running interactions
- Create more human-like conversations
🤖 LangChain Agents: A Complete Introduction¶
What is an Agent?¶
In the previous notebooks, you learned about:
- Tools — functions that the LLM can call
- Chains — predefined sequences of steps you define in advance
Both of those have a fixed flow — you decide what happens and in what order.
Agents are fundamentally different. An agent lets the LLM itself decide
- what steps to take,
- in what order, and
- when to stop
— based on the user’s goal.
- In a Chain you follow a predefined sequence of steps: follow step 1, then step 2, then step 3.
- An Agent has a brain/reasoning capability: given a goal, the agent decides which techniques to apply, checks the result, and adjusts until the output is ready.
The ReAct Loop: How Agents Think¶
Most LangChain agents use the ReAct pattern (Reasoning + Acting):
User Input
│
▼
┌─────────────────────────────────────┐
│ THOUGHT: What do I need to do? │
│ ACTION: Call a tool │ ◄── repeats
│ OBSERVE: See the result │
│ ...repeat until done... │
│ FINAL ANSWER: Respond to user │
└─────────────────────────────────────┘
The agent keeps looping through Thought → Action → Observation until it decides it has enough information to give a final answer.
Key Components¶
| Component | Role |
|---|---|
| LLM | The “brain” — reasons and decides which tools to call |
| Tools | Python functions the agent can invoke |
| create_agent | Builds the agent graph
| Messages | Agent input/output is always a list of messages |
| MemorySaver | Optional: gives the agent persistent memory across turns |
Step 1: Verify Your Library Versions¶
import importlib.metadata
# List the distribution package names
packages = ["langchain", "langchain-community", "langchain-openai",
"llama-index", "langgraph","langchain_core"]
for package in packages:
try:
version = importlib.metadata.version(package)
print(f"{package} version: {version}")
except importlib.metadata.PackageNotFoundError:
print(f"{package} is not installed in this environment.")
langchain version: 1.3.6 langchain-community version: 0.4.2 langchain-openai version: 1.2.2 llama-index version: 0.14.21 langgraph version: 1.2.4 langchain_core version: 1.4.1
Step 2: Imports¶
import os
import math
from datetime import datetime
# ── LangChain 1.x Agent API ──────────────────────────────────────────────────
from langchain.agents import create_agent # NEW in 1.x — replaces AgentExecutor
# ── LLM ──────────────────────────────────────────────────────────────────────
from langchain_openai import ChatOpenAI
# ── Tools ─────────────────────────────────────────────────────────────────────
from langchain_core.tools import tool
# ── Messages (used for input and memory) ──────────────────────────────────────
from langchain_core.messages import HumanMessage, AIMessage
# ── Memory (optional, for multi-turn conversations) ───────────────────────────
from langgraph.checkpoint.memory import MemorySaver
print("✅ All imports successful!")
✅ All imports successful!
Step 3: Set Up the LLM¶
import os
from dotenv import load_dotenv
# This automatically searches for and loads your .env file
load_dotenv()
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
llm = ChatOpenAI(
api_key=OPENAI_API_KEY,
model="gpt-4o-mini",
temperature=0 # deterministic output — best for tool-calling agents
)
response = llm.invoke("Hello! Respond with just the word 'Success' if you can read this.")
print("LLM Response:", response.content)
print("LLM ready!")
LLM Response: Success LLM ready!
Step 4: Define Tools¶
Tools are Python functions decorated with @tool.
⚠️ The docstring is critical. The LLM reads it to understand what the tool does and when to use it. Always write clear, specific docstrings.
We’ll define 4 tools that need no external APIs:
- Calculator — math expressions
- Word Counter — text statistics
- Date/Time — current date and time
- Unit Converter — length, weight, temperature
# ── Tool 1: Calculator
@tool
def calculator(expression: str) -> str:
"""
Evaluates a mathematical expression and returns the result.
Use this for any arithmetic or math calculation.
Supports: +, -, *, /, ** (power), sqrt(), sin(), cos(), log(), pi, e.
Examples: '2 + 2', 'sqrt(144)', '3 ** 4', '100 / 7', 'pi * 5 ** 2'
"""
try:
safe_env = {k: v for k, v in math.__dict__.items() if not k.startswith('_')}
result = eval(expression, {"__builtins__": {}}, safe_env)
return f"Result: {result}"
except Exception as e:
return f"Error: {e}"
# ── Tool 2: Word Counter
@tool
def word_counter(text: str) -> str:
"""
Counts the number of words, characters, and sentences in a given text.
Use this when the user wants statistics or analysis of a piece of text.
"""
words = len(text.split())
chars_with = len(text)
chars_without = len(text.replace(" ", ""))
sentences = len([s for s in text.split(".") if s.strip()])
return (
f"Words: {words}\n"
f"Characters (with spaces): {chars_with}\n"
f"Characters (without spaces): {chars_without}\n"
f"Approximate sentences: {sentences}"
)
# ── Tool 3: Date and Time
@tool
def get_current_datetime(format: str = "full") -> str:
"""
Returns the current date and/or time.
Use this when the user asks about today's date, current time, day of week, or year.
format options:
'full' → both date and time (default)
'date' → date only
'time' → time only
"""
now = datetime.now()
if format == "date":
return now.strftime("Date: %A, %B %d, %Y")
elif format == "time":
return now.strftime("Time: %H:%M:%S")
return now.strftime("Date & Time: %A, %B %d, %Y at %H:%M:%S")
# ── Tool 4: Unit Converter
@tool
def unit_converter(value: float, from_unit: str, to_unit: str) -> str:
"""
Converts a value from one unit to another.
Use this for any unit conversion task.
Supported categories:
Temperature : celsius, fahrenheit, kelvin
Length : meters, kilometers, miles, feet, inches
Weight : grams, kg, kilograms, pounds
Example call: value=100, from_unit='celsius', to_unit='fahrenheit'
"""
f = from_unit.lower().strip()
t = to_unit.lower().strip()
# Temperature (special-cased)
temp = {"celsius", "fahrenheit", "kelvin"}
if f in temp or t in temp:
try:
if f == "celsius" and t == "fahrenheit":
r = value * 9/5 + 32
elif f == "fahrenheit" and t == "celsius":
r = (value - 32) * 5/9
elif f == "celsius" and t == "kelvin":
r = value + 273.15
elif f == "kelvin" and t == "celsius":
r = value - 273.15
elif f == "fahrenheit" and t == "kelvin":
r = (value - 32) * 5/9 + 273.15
elif f == "kelvin" and t == "fahrenheit":
r = (value - 273.15) * 9/5 + 32
else:
return f"Cannot convert {f} to {t}"
return f"{value} {f} = {round(r, 4)} {t}"
except Exception as e:
return str(e)
# Other units → convert via base unit
to_base = {
"meters": 1.0, "kilometers": 1000.0, "miles": 1609.34,
"feet": 0.3048, "inches": 0.0254,
"grams": 1.0, "kg": 1000.0, "kilograms": 1000.0, "pounds": 453.592,
}
if f in to_base and t in to_base:
result = value * to_base[f] / to_base[t]
return f"{value} {f} = {round(result, 6)} {t}"
return f"Conversion from '{from_unit}' to '{to_unit}' is not supported."
# ── Collect tools
tools = [calculator, word_counter, get_current_datetime, unit_converter]
print("✅ Tools defined:")
for t in tools:
print(f" • {t.name}")
✅ Tools defined: • calculator • word_counter • get_current_datetime • unit_converter
Step 5: Create the Agent¶
create_agent¶
create_agent returns a compiled LangGraph graph .
You call .invoke() directly on it, passing a messages list.
The input format is always:
agent.invoke({"messages": [HumanMessage(content="your question")]})
The output is a dict with a messages key — the last message is the agent’s final answer.
SYSTEM_PROMPT = """You are a helpful assistant with access to tools.
Always use a tool when the question involves math, dates, unit conversion, or text analysis.
Think step by step. Never guess a number — calculate it."""
# create_agent builds the agent graph automatically
# It handles the Thought → Action → Observation loop internally
agent = create_agent(
model=llm,
tools=tools,
system_prompt=SYSTEM_PROMPT
)
print("Agent created! Type:", type(agent).__name__)
print(" .invoke() and .stream() are both available.")
✅ Agent created! Type: CompiledStateGraph .invoke() and .stream() are both available.
Step 6: Helper Function¶
We’ll write a small helper so we don’t repeat the message-wrapping boilerplate in every cell.
def ask(question: str, config: dict = None) -> str:
"""
Send a single question to the agent and return the final answer.
config is used for memory (thread_id) — see Step 8.
"""
kwargs = {"messages": [HumanMessage(content=question)]}
result = agent.invoke(kwargs, config=config)
# The last message in the output is always the final AI response
return result["messages"][-1].content
print("Helper ready!")
✅ Helper ready!
answer = ask("What is the square root of 1764? Then square that result.")
print("🤖", answer)
🤖 The square root of 1764 is 42. When you square that result, you get 1764.
Example 2: Date and Time¶
answer = ask("What is today's date and what day of the week is it?")
print("🤖", answer)
🤖 Today's date is June 25, 2026, and it is a Thursday.
Example 3: Unit Conversion¶
answer = ask("Convert 37 degrees Celsius to Fahrenheit. Is that a fever?")
print("🤖", answer)
🤖 37 degrees Celsius is equal to 98.6 degrees Fahrenheit. In terms of fever, a normal body temperature is typically around 98.6°F (37°C). A fever is generally considered to be a body temperature of 100.4°F (38°C) or higher. Therefore, 37°C (98.6°F) is not a fever.
Example 4: Multi-Tool Question¶
This is where agents shine — a single question that requires multiple tools in sequence.
answer = ask(
"If I invest 5000 dollars at 8% annual compound interest for 10 years, "
"how much will I have? Also, what year will that be?"
)
print("🤖", answer)
🤖 If you invest $5,000 at an 8% annual compound interest for 10 years, you will have approximately $10,794.62. Starting from today, which is June 25, 2026, the investment will mature in 10 years, making it June 25, 2036.
Example 5: Text Analysis¶
sample = (
"Artificial intelligence is transforming industries worldwide. "
"From healthcare to finance, companies are adopting machine learning rapidly. "
"Researchers continue pushing the boundaries of what is possible with neural networks."
)
answer = ask(f"Count the words and characters in this text: '{sample}'")
print("🤖", answer)
🤖 The text contains: - **Words:** 28 - **Characters (with spaces):** 224 - **Characters (without spaces):** 197 - **Approximate sentences:** 3
Step 8: Watching the Agent’s Intermediate Steps¶
Use .stream() with stream_mode="updates" to see every step the agent takes — which tool it called, what it passed, and what it got back. This is the equivalent of verbose=True in the old API.
def ask_verbose(question: str):
"""Ask the agent and print every intermediate step."""
print(f"❓ Question: {question}")
print("=" * 60)
step_num = 0
final_answer = ""
for chunk in agent.stream(
{"messages": [HumanMessage(content=question)]},
stream_mode="updates"
):
for node_name, node_output in chunk.items():
messages = node_output.get("messages", [])
for msg in messages:
# Tool call requests (agent deciding which tool to use)
if hasattr(msg, "tool_calls") and msg.tool_calls:
step_num += 1
for tc in msg.tool_calls:
print(f"\n🔧 Step {step_num} — Tool Call")
print(f" Tool : {tc['name']}")
print(f" Input : {tc['args']}")
# Tool results (observation)
elif hasattr(msg, "name") and msg.name is not None:
print(f" Result: {msg.content}")
# Final AI text response
elif hasattr(msg, "content") and msg.content and not hasattr(msg, "tool_calls"):
final_answer = msg.content
print("\n" + "=" * 60)
print("FINAL ANSWER:", final_answer)
print("=" * 60)
# Try it out
ask_verbose(
"A room is 5 meters by 4 meters. "
"What is its area in square meters, and convert that to square feet? "
"(1 meter = 3.28084 feet)"
)
❓ Question: A room is 5 meters by 4 meters. What is its area in square meters, and convert that to square feet? (1 meter = 3.28084 feet)
============================================================
🔧 Step 1 — Tool Call
Tool : calculator
Input : {'expression': '5 * 4'}
🔧 Step 1 — Tool Call
Tool : unit_converter
Input : {'value:20,': 'square meters', 'to_unit': 'square feet'}
Result: Result: 20
Result: Error invoking tool 'unit_converter' with kwargs {'value:20,': 'square meters', 'to_unit': 'square feet'} with error:
value: Field required
from_unit: Field required
Please fix the error and try again.
🔧 Step 2 — Tool Call
Tool : calculator
Input : {'expression': '5 * 4'}
🔧 Step 2 — Tool Call
Tool : unit_converter
Input : {'value': 20, 'from_unit': 'meters', 'to_unit': 'feet'}
Result: Result: 20
Result: 20.0 meters = 65.616798 feet
============================================================
✅ FINAL ANSWER:
============================================================
Step 9: Multi-Turn Memory with MemorySaver¶
The Problem with Stateless Agents¶
By default, each .invoke() call is completely independent — the agent forgets everything after each call.
The Solution: MemorySaver + thread_id¶
You pass a thread_id in the config — all calls with the same thread_id share memory.
# Create a fresh agent WITH memory
memory = MemorySaver()
agent_with_memory = create_agent(
model=llm,
tools=tools,
system_prompt=SYSTEM_PROMPT,
checkpointer=memory # ← this enables multi-turn memory
)
# Each "conversation" is identified by a thread_id
SESSION_CONFIG = {"configurable": {"thread_id": "student-session-1"}}
def chat(question: str) -> str:
"""Chat with the memory-enabled agent."""
result = agent_with_memory.invoke(
{"messages": [HumanMessage(content=question)]},
config=SESSION_CONFIG
)
return result["messages"][-1].content
print("Memory-enabled agent ready!")
✅ Memory-enabled agent ready!
# Turn 1
r1 = chat("What is 250 miles in kilometers?")
print("🤖", r1)
🤖 250 miles is approximately 402.34 kilometers.
# Turn 2 — refers to the previous answer; agent should REMEMBER '250 miles'
r2 = chat("Now convert that same distance to feet.")
print("🤖", r2)
🤖 250 miles is approximately 1,319,996.72 feet.
# Turn 3 — chains off the previous context
r3 = chat("If I drive at 100 km/h, how many hours to cover that distance?")
print("🤖", r3)
🤖 If you drive at 100 km/h, it will take you approximately 4.02 hours to cover the distance of 402.34 kilometers.
Step 10: Edge Cases¶
A well-designed agent should handle questions that don’t need tools, unsupported operations, and multi-step chained reasoning.
# No tool needed — LLM answers from knowledge
print("── Case 1: No tool needed ──")
print(ask("What is the capital of France?"))
── Case 1: No tool needed ── The capital of France is Paris.
# Chained reasoning across tools
print("── Case 2: Multi-step chained reasoning ──")
print(ask(
"Find 15% of 340. Then convert that number from USD to INR "
"using an exchange rate of 83 (i.e. multiply by 83)."
))
── Case 2: Multi-step chained reasoning ── 15% of 340 is 51 USD. When converted to INR at an exchange rate of 83, it amounts to 4233 INR.
Step 11: Interactive Chat Loop¶
Run this cell for a live chat session with the memory-enabled agent. Type quit to exit.
# Fresh memory session
fresh_memory = MemorySaver()
interactive_agent = create_agent(
model=llm,
tools=tools,
system_prompt=SYSTEM_PROMPT,
checkpointer=fresh_memory
)
CHAT_CONFIG = {"configurable": {"thread_id": "interactive-session"}}
print("🤖 Agent is live! Try math, dates, unit conversions, or text analysis.")
print(" Type 'quit' to exit.")
print("-" * 60)
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "bye"):
print("🤖 Goodbye!")
break
result = interactive_agent.invoke(
{"messages": [HumanMessage(content=user_input)]},
config=CHAT_CONFIG
)
print(f"🤖 {result['messages'][-1].content}")
print("-" * 60)
Summary¶
| Concept | Key Takeaway |
|---|---|
| Agent vs Chain | Chains have fixed steps; agents decide dynamically |
| ReAct Loop | Thought → Action → Observation, repeated until done |
@tool + docstring |
How you expose a function to the agent |
create_agent |
The single new API in LangChain 1.x (no AgentExecutor) |
| Input format | Always {"messages": [HumanMessage(...)]} |
| Output format | result["messages"][-1].content is the final answer |
.stream(stream_mode="updates") |
See every intermediate step |
MemorySaver + thread_id |
Multi-turn persistent memory |
