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…(after asking about food, etc.)
User: Where do I work?
Case – Without memory:
I don’t know.
Case – 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.
Think of it this way:
- A Chain is like a recipe: follow step 1, then step 2, then step 3.
- An Agent is like a chef: given a goal, the chef decides which techniques to apply, checks the result, and adjusts until the dish is ready.
The ReAct Loop: How Agents Think¶
Most LangChain agents use the ReAct pattern (Reasoning + Acting):
User Input (E.g.: Find a cold weather vacation, cheap spot in China and book a flight.)
│
▼
┌─────────────────────────────────────┐
│ 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.
LangChain 1.x: The New Agent API¶
In LangChain 1.x the agent system was completely redesigned.
- ✅
create_agent: The new single function for all agents
The new create_agent is built on LangGraph under the hood. It returns a compiled graph that you invoke directly — no AgentExecutor wrapper needed.
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 (replaces AgentExecutor) |
| 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",
"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
# langgraph version: 1.2.11
# langchain_core version: 1.6.2
langchain version: 1.3.6 langchain-community version: 0.4.2 langchain-openai version: 1.2.2 langgraph version: 1.2.11 langchain_core version: 1.6.2
Step 2: Imports¶
In LangChain 1.x, the agent lives in langchain.agents and is simply called create_agent.
There is no AgentExecutor — the compiled graph returned by create_agent is the executor.
import os
import math
from datetime import datetime
# ── LangChain 1.x Agent API
from langchain.agents import create_agent
# ── 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: 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¶
LangChain 1.x uses MemorySaver (a LangGraph checkpointer) for persistent memory.
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!")
# Turn 1
r1 = chat("What is 250 miles in kilometers?")
print("🤖", r1)
# Turn 2 — refers to the previous answer; agent should REMEMBER '250 miles'
r2 = chat("Now convert that same distance to feet.")
print("🤖", r2)
# 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)
Following is optional (only if time permits)¶
Step 9: 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.
# NOTE: Gave a buggy response
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 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)
🤖 Agent is live! Try math, dates, unit conversions, or text analysis. Type 'quit' to exit. ------------------------------------------------------------
--------------------------------------------------------------------------- KeyboardInterrupt Traceback (most recent call last) Cell In[25], line 16 12 print(" Type 'quit' to exit.") 13 print("-" * 60) 14 15 while True: ---> 16 user_input = input("You: ").strip() 17 if not user_input: 18 continue 19 if user_input.lower() in ("quit", "exit", "bye"): File ~\Desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\Lib\site-packages\ipykernel\kernelbase.py:1403, in Kernel.raw_input(self, prompt) 1401 msg = "raw_input was called, but this frontend does not support input requests." 1402 raise StdinNotImplementedError(msg) -> 1403 return self._input_request( 1404 str(prompt), 1405 self._get_shell_context_var(self._shell_parent_ident), 1406 self.get_parent("shell"), 1407 password=False, 1408 ) File ~\Desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\Lib\site-packages\ipykernel\kernelbase.py:1448, in Kernel._input_request(self, prompt, ident, parent, password) 1445 except KeyboardInterrupt: 1446 # re-raise KeyboardInterrupt, to truncate traceback 1447 msg = "Interrupted by user" -> 1448 raise KeyboardInterrupt(msg) from None 1449 except Exception: 1450 self.log.warning("Invalid Message:", exc_info=True) KeyboardInterrupt: Interrupted by user
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 |
What’s Next?¶
- Custom Tools — Connect your agent to databases, REST APIs, or file systems
- RAG + Agents — Add a retrieval tool so the agent can search documents
- Structured Output — Force the agent to return JSON with a defined schema
- Multi-Agent Systems — Multiple specialized agents collaborating
- Middleware — The new LangChain 1.x feature for intercepting agent behavior
