What is a Tool?
A Tool is a function or service that a language model can call to perform a specific task.
Instead of generating an answer solely from its training data, the model can use a tool to retrieve information, perform calculations, interact with APIs, or execute code.
Simple definition:
A Tool is a function that extends the capabilities of a language model by allowing it to perform real-world tasks.
Why Do We Need Tools?
Large Language Models (LLMs) are excellent at understanding and generating text, but they have limitations:
- They may not know the latest news.
- They cannot perform precise calculations reliably.
- They cannot access private databases.
- They cannot send emails or interact with external applications by themselves.
Tools overcome these limitations by allowing the model to interact with external systems.
For example:
- Search the web for current information
- Perform mathematical calculations
- Query a SQL database
- Read a PDF document
- Call an external API
- Send an email
- Retrieve documents from a knowledge base
LangChain Tools β Complete TutorialΒΆ
Give your LLM superpowersΒΆ
LangChain: 1.3.6 | Python: 3.11.9
Prerequisite: Chains tutorial
What We’ll CoverΒΆ
| # | Topic | Tools Covered |
|---|---|---|
| 1 | Why Tools? | The problem LLMs cannot solve alone |
| 2 | Custom Tools | @tool decorator β any Python function |
| 3 | Math & Data | Calculator, REPL, statistics, unit converter |
| 4 | Web & Search | DuckDuckGo, Wikipedia, web fetch |
| 5 | Datetime & Files | Dates, read/write files |
| 6 | Tools Inside Chains | Combine tools with LCEL |
| 7 | LLM Tool Calling | Let the LLM pick the right tool |
| 8 | Interesting Tools | Passwords, Morse code, readability + more |
| 9 | All Together | Full demo with 17 tools |
No extra API keys needed for most tools β DuckDuckGo, Wikipedia, Python REPL are all free.
SetupΒΆ
!pip install -q langchain langchain-openai langchain-community openai
!pip install -q duckduckgo-search wikipedia requests
[notice] A new release of pip is available: 24.0 -> 26.1.2 [notice] To update, run: python.exe -m pip install --upgrade pip
import importlib.metadata
# List the distribution package names
packages = ["langchain", "langchain-community", "langchain-openai", "openai",
"langchain_core", "duckduckgo-search",]
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 openai version: 2.36.0 langchain_core version: 1.4.1 duckduckgo-search version: 8.1.1
import os
from dotenv import load_dotenv
load_dotenv() # read .env file
# TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(api_key = OPENAI_API_KEY,
model="gpt-4o-mini",
temperature=0
) # temperature=0 for tool use
response = llm.invoke("Confirm connection by replying with the word 'Active'")
print("\nConnection Successful!")
print(f"AI Response: '{response.content.strip()}'")
Connection Successful! AI Response: 'Active'
1οΈ) Why Do We Need Tools?ΒΆ
LLMs Are Smart But LimitedΒΆ
| LLM Can β | LLM Cannot β |
|---|---|
| Reason, explain, summarise | Know today’s news |
| Write, translate, code | Do precise arithmetic |
| Answer questions from training data | Access the internet |
| Understand context | Check current stock prices |
The GapΒΆ
llm.invoke('What is 2847 * 3921?')
# LLM might say: 11,162,487 -- WRONG! Correct: 11,155,287
llm.invoke('What is today\'s date?')
# LLM: 'I don\'t have access to real-time information...'
llm.invoke('What is the temperature in New Delhi ?')
# LLM: 'I don\'t have access to real-time information...'
Tools bridge this gap β they let the LLM call real code and real APIs.
How Tools WorkΒΆ
User: 'What is 2847 * 3921?'
β
LLM sees the question + available tools
β
LLM decides: 'I should use the calculator tool'
β
Tool runs: calculator('2847 * 3921') -> 11,155,287
β
LLM forms response: '2847 x 3921 = 11,155,287'
# Demonstrate the problem β LLMs are unreliable at arithmetic
response = llm.invoke('What is 2847 * 3921? Give just the number, no explanation.')
print('LLM answer: ', response.content)
print('Correct answer:', 2847 * 3921)
print('Match?', str(2847 * 3921) in response.content.replace(',',''))
LLM answer: 11,155,787 Correct answer: 11163087 Match? False
2οΈ Custom Tools β The @tool DecoratorΒΆ
The simplest way to create a tool: any Python function with @tool.
Three things the LLM uses:ΒΆ
- Function name β identifies the tool
- Docstring β LLM reads this to decide WHEN to use it
- Type hints β tells LangChain input/output types
@tool
def my_tool(input: str) -> str:
"""Describe WHAT this does and WHEN to use it.
The LLM reads this like instructions."""
return "result"
from langchain_core.tools import tool
# Simplest possible tool
@tool
def greet(name: str) -> str:
"""
Greet a person by name. Use when you need to say hello to someone.
"""
return f"Hello, {name}! Welcome to LangChain Tools! π"
print(greet.invoke({"name": "Priya"}))
print("--- Tool Metadata ---")
print("Name: ", greet.name)
print("Description:", greet.description)
print("Args schema:", greet.args)
Hello, Priya! Welcome to LangChain Tools! π
--- Tool Metadata ---
Name: greet
Description: Greet a person by name. Use when you need to say hello to someone.
Args schema: {'name': {'title': 'Name', 'type': 'string'}}
# Tool with multiple parameters
@tool
def calculate_bmi(weight_kg: float, height_m: float) -> str:
"""
Calculate Body Mass Index (BMI) given weight in kg and height in meters.
Use when someone asks about BMI or healthy weight range.
"""
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5: category = 'Underweight'
elif bmi < 25: category = 'Normal weight'
elif bmi < 30: category = 'Overweight'
else: category = 'Obese'
return f"BMI: {bmi:.1f} β {category}"
print(calculate_bmi.invoke({"weight_kg": 70, "height_m": 1.75}))
print(calculate_bmi.invoke({"weight_kg": 50, "height_m": 1.70}))
BMI: 22.9 β Normal weight BMI: 17.3 β Underweight
from typing import Optional
# Tool with optional parameters
@tool
def format_currency(amount: float, currency: str = 'USD', decimals: int = 2) -> str:
"""
Format a number as currency. Defaults to USD with 2 decimal places.
Supports USD, EUR, GBP, INR, JPY. Use for any money formatting request.
"""
symbols = {'USD': '$', 'EUR': 'β¬', 'GBP': 'Β£', 'INR': 'βΉ', 'JPY': 'Β₯'}
symbol = symbols.get(currency.upper(), currency + ' ')
return f"{symbol}{amount:,.{decimals}f}"
print(format_currency.invoke({"amount": 1234567.89}))
print(format_currency.invoke({"amount": 9999.5, "currency": "INR"}))
print(format_currency.invoke({"amount": 500, "currency": "EUR", "decimals": 0}))
$1,234,567.89 βΉ9,999.50 β¬500
# Best practice demo: vague vs descriptive docstrings
@tool
def bad_tool(x: str) -> str:
"""
Does stuff with input.
"""
return x
@tool
def word_counter(text: str) -> str:
"""
Count the number of words in a piece of text.
Use when asked 'how many words', 'word count', or the length of a text in words.
Input should be the full text string.
"""
count = len(text.split())
return f"The text contains {count} words."
print("Bad tool:", bad_tool.description)
print("Good tool:", word_counter.description)
Bad tool: Does stuff with input. Good tool: Count the number of words in a piece of text. Use when asked 'how many words', 'word count', or the length of a text in words. Input should be the full text string.
3οΈ. Math & Data ToolsΒΆ
LLMs are notoriously unreliable at arithmetic. Always use a tool for calculations.
| Tool | Use case |
|---|---|
| Calculator | Safe math expression evaluation |
| Stats calculator | Mean, median, std dev etc. |
| Python REPL | Run arbitrary Python |
| Unit converter | Length, weight, temperature, speed |
import math
@tool
def calculator(expression: str) -> str:
"""
Evaluate a mathematical expression. Input must be valid Python math.
Supports: +, -, *, /, **, sqrt, sin, cos, log, pi, e.
Examples: '2847 * 3921', 'math.sqrt(144)', 'math.pi * 5**2'
ALWAYS use for any calculation instead of computing mentally.
"""
try:
allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith('_')}
allowed['abs'] = abs
allowed['round'] = round
result = eval(expression, {"__builtins__": {}}, allowed)
return f"{expression} = {result}"
except Exception as e:
return f"Error evaluating '{expression}': {e}"
print(calculator.invoke({"expression": "2847 * 3921"}))
print(calculator.invoke({"expression": "math.sqrt(144)"}))
print(calculator.invoke({"expression": "math.pi * 7**2"}))
print(calculator.invoke({"expression": "math.log(1000, 10)"}))
print(calculator.invoke({"expression": "(15 + 27 + 33 + 42) / 4"}))
2847 * 3921 = 11163087 Error evaluating 'math.sqrt(144)': name 'math' is not defined Error evaluating 'math.pi * 7**2': name 'math' is not defined Error evaluating 'math.log(1000, 10)': name 'math' is not defined (15 + 27 + 33 + 42) / 4 = 29.25
import statistics
@tool
def stats_calculator(numbers: str) -> str:
"""
Calculate statistics for a list of numbers.
Input: comma-separated numbers like '10, 20, 30, 40, 50'
Returns: mean, median, std deviation, min, max, range.
"""
try:
nums = [float(x.strip()) for x in numbers.split(',')]
result = {
'count': len(nums),
'mean': round(statistics.mean(nums), 4),
'median': statistics.median(nums),
'std_dev': round(statistics.stdev(nums), 4) if len(nums) > 1 else 0,
'min': min(nums),
'max': max(nums),
'range': max(nums) - min(nums)
}
return "\n".join(f"{k}: {v}" for k, v in result.items())
except Exception as e:
return f"Error: {e}"
scores = "72, 85, 91, 68, 77, 95, 83, 79, 88, 64"
print("Exam Score Statistics:")
print(stats_calculator.invoke({"numbers": scores}))
Exam Score Statistics: count: 10 mean: 80.2 median: 81.0 std_dev: 10.0973 min: 64.0 max: 95.0 range: 31.0
# Python REPL Tool β run actual Python code
# Most powerful data tool β great for complex data manipulation
import sys
from io import StringIO
# Clean replacement β no extra packages needed
import sys
from io import StringIO
from langchain_core.tools import tool
@tool
def python_repl(code: str) -> str:
"""
Execute Python code and return the printed output.
Use for data manipulation, calculations, sorting, or any Python logic.
Input should be valid Python code as a string.
"""
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
exec(code, {})
output = sys.stdout.getvalue()
except Exception as e:
output = f"Error: {e}"
finally:
sys.stdout = old_stdout
return output if output else "Code ran successfully (no output)"
# Same test β works identically
result = python_repl.invoke({"code": """
students = [
{"name": "Alice", "score": 85},
{"name": "Bob", "score": 72},
{"name": "Carol", "score": 91},
{"name": "Dave", "score": 68},
{"name": "Eve", "score": 95},
]
ranked = sorted(students, key=lambda x: x['score'], reverse=True)
print("=== CLASS RANKING ===")
for i, s in enumerate(ranked, 1):
grade = "A" if s["score"] >= 90 else "B" if s["score"] >= 80 else "C"
print(f"{i}. {s['name']:10s} {s['score']} Grade: {grade}")
avg = sum(s['score'] for s in students) / len(students)
print(f"\\nClass average: {avg:.1f}")
"""})
print(result)
=== CLASS RANKING === 1. Eve 95 Grade: A 2. Carol 91 Grade: A 3. Alice 85 Grade: B 4. Bob 72 Grade: C 5. Dave 68 Grade: C Class average: 82.2
@tool
def unit_converter(value: float, from_unit: str, to_unit: str) -> str:
"""
Convert between common units of measurement.
Supports length (km/miles/meters/feet/cm/inches),
weight (kg/pounds/grams), temperature (celsius/fahrenheit/kelvin),
volume (liters/gallons/ml), speed (kmh/mph).
Example: unit_converter(100, 'km', 'miles')
"""
conversions = {
('km','miles'): lambda x: x * 0.621371,
('miles','km'): lambda x: x * 1.60934,
('meters','feet'): lambda x: x * 3.28084,
('feet','meters'): lambda x: x * 0.3048,
('cm','inches'): lambda x: x * 0.393701,
('inches','cm'): lambda x: x * 2.54,
('kg','pounds'): lambda x: x * 2.20462,
('pounds','kg'): lambda x: x * 0.453592,
('celsius','fahrenheit'):lambda x: x * 9/5 + 32,
('fahrenheit','celsius'):lambda x: (x - 32) * 5/9,
('celsius','kelvin'): lambda x: x + 273.15,
('kelvin','celsius'): lambda x: x - 273.15,
('liters','gallons'): lambda x: x * 0.264172,
('gallons','liters'): lambda x: x * 3.78541,
('kmh','mph'): lambda x: x * 0.621371,
('mph','kmh'): lambda x: x * 1.60934,
}
key = (from_unit.lower(), to_unit.lower())
if key in conversions:
result = conversions[key](value)
return f"{value} {from_unit} = {result:.4f} {to_unit}"
return f"Conversion from {from_unit!r} to {to_unit!r} not supported."
print(unit_converter.invoke({"value": 100, "from_unit": "km", "to_unit": "miles"}))
print(unit_converter.invoke({"value": 37, "from_unit": "celsius", "to_unit": "fahrenheit"}))
print(unit_converter.invoke({"value": 70, "from_unit": "kg", "to_unit": "pounds"}))
print(unit_converter.invoke({"value": 120, "from_unit": "kmh", "to_unit": "mph"}))
100.0 km = 62.1371 miles 37.0 celsius = 98.6000 fahrenheit 70.0 kg = 154.3234 pounds 120.0 kmh = 74.5645 mph
4οΈ. Web & Search ToolsΒΆ
The most important category β these give LLMs access to current information. Your LLM training data has a cutoff date. Search tools fix that.
| Tool | Free? | Best for |
|---|---|---|
| DuckDuckGo Search | β Free | General web search |
| Wikipedia | β Free | Encyclopedic knowledge |
| Web Fetch (requests) | β Free | Read any webpage |
| Tavily Search | API key | High quality search results |
from langchain_community.tools import DuckDuckGoSearchRun, DuckDuckGoSearchResults
# Option A: plain text summary β easiest to use
search = DuckDuckGoSearchRun()
result = search.invoke("latest developments in quantum computing 2025")
print("=== DuckDuckGoSearchRun (plain text) ===")
print(result[:600])
=== DuckDuckGoSearchRun (plain text) === August 19, 2025 - Insights from the βQuantum Index Report 2025β include the following: Quantum processor performance is improving, with the U. S. leading the field. Two-dozen manufacturers are now commercially offering more than 40 quantum processing units ... November 19, 2025 - But as the quantum computers got bigger, the error rates would go up even faster, meaning that it was impossible to catch up. In the last couple of years, researchers started to find ways around this problem, both in making the individual qubits less prone to errors, and by improving the error-correction mechanisms. T
# Option B: structured results with title + URL + snippet
search_results = DuckDuckGoSearchResults(num_results=3)
results = search_results.invoke("Python programming news 2025")
print("=== DuckDuckGoSearchResults (structured) ===")
print(results[:800])
=== DuckDuckGoSearchResults (structured) === snippet: November 4, 2025 - The Python Language Summit of 2025 revealed that βSomewhere between one-quarter and one-third of all native code being uploaded to PyPI for new projects uses Rustβ, indicating that βpeople are choosing to start new projects using Rustβ. ..., title: The State of Python 2025: Trends and Survey Insights - The JetBrains Blog, link: https://blog.jetbrains.com/pycharm/2025/08/the-state-of-python-2025/, snippet: 2 weeks ago - Discover the latest Python development news, repositories, and conferences at Hackertab., title: Stay Ahead with Python News β Updated Every Hour, link: https://hackertab.dev/topics/python, snippet: December 8, 2025 - PEP 810 brings lazy imports to Python 3.15, PyPI tightens 2FA security, and Django 6.0 reaches release candidate. Catch up on all t
from langchain_tavily import TavilySearch
tavily_search = TavilySearch()
extracted_info = tavily_search.run("What is recent development Agentic AI?")
extracted_info
{'query': 'What is recent development Agentic AI?',
'follow_up_questions': None,
'answer': None,
'images': [],
'results': [{'url': 'https://www.ibm.com/think/insights/agentic-ai',
'title': "Agentic AI: 4 reasons why it's the next big thing in AI research - IBM",
'content': 'Agentic AI refers to a system or program that is capable of autonomously performing tasks on behalf of a user or another system by designing its workflow and',
'score': 0.7304634,
'raw_content': None},
{'url': 'https://www.rishabhsoft.com/blog/agentic-ai-in-software-development',
'title': 'Agentic AI in Software Development Accelerating Modern Delivery',
'content': "From multi-agent systems to self-improving solutions, Agentic AI marks the beginning of a new game in software development. Let's explore",
'score': 0.5769478,
'raw_content': None},
{'url': 'https://about.gitlab.com/the-source/ai/emerging-agentic-ai-trends-reshaping-software-development',
'title': 'Emerging agentic AI trends reshaping software development - GitLab',
'content': 'Discover how agentic AI transforms development from isolated coding to intelligent workflows that enhance productivity while maintaining security.',
'score': 0.50432175,
'raw_content': None},
{'url': 'https://www.vastdata.com/blog/evolution-of-ai-from-machine-learning-to-agentic-systems',
'title': 'The Evolution of AI: From Machine Learning to Agentic Systems',
'content': 'Agentic AI can manage complex data flows, integrate signals from many sources, and support decision-making at scale. Its design enables',
'score': 0.48879638,
'raw_content': None},
{'url': 'https://mitsloan.mit.edu/ideas-made-to-matter/agentic-ai-explained',
'title': 'Agentic AI, explained | MIT Sloan',
'content': "The age of agentic AI β systems that are semi- or fully autonomous and can act on their own β has arrived. Here's what you need to know,",
'score': 0.40655622,
'raw_content': None}],
'response_time': 0.69,
'request_id': 'cb491584-1ddd-460d-8830-a161955fcf9d'}
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
wiki = WikipediaQueryRun(
api_wrapper=WikipediaAPIWrapper(
top_k_results=1,
doc_content_chars_max=1000
)
)
result = wiki.invoke("Large Language Models history")
print("=== Wikipedia Result ===")
print(result)
=== Wikipedia Result === Page: Large language model Summary: A large language model (LLM) is a neural network trained on a vast amount of text for natural language processing tasks, especially language generation. LLMs can typically generate, summarize, translate, and analyze text in many contexts, and are a foundational technology behind modern chatbots. Biased or inaccurate training data can make an LLM's output less reliable. LLMs are typically based on transformer architecture. Generative pre-trained transformers (GPTs) are a type of LLM that is pre-trained to predict the next word. GPTs are then often fine-tuned to follow instructions and to behave as assistants. Benchmark evaluations for LLMs attempt to measure model reasoning, factual accuracy, alignment, and safety.
import requests, re
@tool
def fetch_webpage(url: str) -> str:
"""
Fetch and return the text content of any webpage URL.
Use when you need to read the content of a specific website.
Input should be a full URL starting with https://
"""
try:
headers = {"User-Agent": "Mozilla/5.0"}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
text = re.sub(r'<[^>]+>', ' ', response.text)
text = re.sub(r'\s+', ' ', text).strip()
return f"Content from {url}:\n{text[:1500]}..."
except Exception as e:
return f"Could not fetch {url}: {e}"
# result = fetch_webpage.invoke({"url": "https://httpbin.org/json"})
result = fetch_webpage.invoke({"url": "https://loxfordacademy.com/blog/uncategorized/hugging-face-the-github-of-ai-models/"})
print(result[:1000])
Content from https://loxfordacademy.com/blog/uncategorized/hugging-face-the-github-of-ai-models/:
/* */ /* */ :root { --lp-container-max-width: 1290px; --lp-cotainer-padding: 1rem; --lp-primary-color: #ffb606; --lp-secondary-color: #442e66; } Hugging Face: The GitHub of AI Models – loxfordacademy.com img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px} /*# sourceUR
import datetime
@tool
def get_current_datetime() -> str:
"""
Get the current date and time.
Use when asked about todays date, current time, what day it is,
or anything involving now or today.
"""
now = datetime.datetime.now()
return (
f"Date: {now.strftime('%A, %B %d, %Y')}\n"
f"Time: {now.strftime('%I:%M %p')}\n"
f"ISO: {now.isoformat()}"
)
@tool
def days_between_dates(date1: str, date2: str) -> str:
"""Calculate the number of days between two dates.
Input format: YYYY-MM-DD for both dates.
Use for deadline calculations, age calculations, or any date difference."""
try:
d1 = datetime.datetime.strptime(date1, "%Y-%m-%d")
d2 = datetime.datetime.strptime(date2, "%Y-%m-%d")
delta = abs((d2 - d1).days)
weeks, days = divmod(delta, 7)
return f"{delta} days ({weeks} weeks and {days} days) between {date1} and {date2}"
except ValueError as e:
return f"Error: {e}. Use YYYY-MM-DD format."
@tool
def add_days_to_date(date: str, days: int) -> str:
"""Add or subtract days from a date. Input: YYYY-MM-DD format.
Use negative days to go back in time. Good for scheduling and deadlines."""
try:
d = datetime.datetime.strptime(date, "%Y-%m-%d")
result = d + datetime.timedelta(days=days)
return f"{date} + {days} days = {result.strftime('%A, %B %d, %Y')}"
except ValueError as e:
return f"Error: {e}"
print(get_current_datetime.invoke({}))
print()
print(days_between_dates.invoke({"date1": "2025-01-01", "date2": "2025-12-31"}))
print()
print(add_days_to_date.invoke({"date": "2025-06-01", "days": 90}))
Date: Sunday, July 05, 2026 Time: 11:39 AM ISO: 2026-07-05T11:39:09.723990 364 days (52 weeks and 0 days) between 2025-01-01 and 2025-12-31 2025-06-01 + 90 days = Saturday, August 30, 2025
import os
@tool
def write_file(filename: str, content: str) -> str:
"""Write content to a text file.
Use when the user asks to save, export, or create a file."""
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
size = os.path.getsize(filename)
return f"Written {size} bytes to {filename!r}"
except Exception as e:
return f"Error: {e}"
@tool
def read_file(filename: str) -> str:
"""Read and return the contents of a text file.
Use when asked to open, read, or load a file."""
try:
with open(filename, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
return f"File {filename!r} not found."
except Exception as e:
return f"Error: {e}"
@tool
def list_files(directory: str = '.') -> str:
"""List all files in a directory. Default is current directory.
Use when asked what files exist, to browse a folder, or find files."""
try:
items = sorted(os.listdir(directory))
if not items:
return f"Directory {directory!r} is empty."
lines = []
for f in items:
icon = 'π' if os.path.isdir(os.path.join(directory, f)) else 'π'
lines.append(f" {icon} {f}")
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
print(write_file.invoke({"filename": "notes.txt", "content": "LangChain tools are awesome!\nLine 2."}))
print(read_file.invoke({"filename": "notes.txt"}))
print()
print("Files:")
print(list_files.invoke({}))
Written 37 bytes to 'notes.txt' LangChain tools are awesome! Line 2. Files: π .env π .ipynb_checkpoints π LangChain_2_Tools_Tutorial.ipynb π ReAct_Agent.ipynb π notes.txt
6οΈ. Using Tools Inside ChainsΒΆ
Tools do not have to be used by agents. Call them inside chains with RunnableLambda.
Input
β
Chain Step 1: LLM generates data
β
Chain Step 2: Tool processes the data (no LLM needed)
β
Chain Step 3: LLM interprets the result
β
Output
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
# Step 1: LLM generates exam scores
generate_data = (
ChatPromptTemplate.from_template(
"Generate 8 realistic exam scores (50-100) for a {subject} class. "
"Return ONLY comma-separated numbers, nothing else."
)
| llm
| StrOutputParser()
)
# Step 2: Tool processes the data β no LLM needed here
run_stats = RunnableLambda(lambda scores: stats_calculator.invoke({'numbers': scores}))
# Step 3: LLM interprets the stats
interpret = (
ChatPromptTemplate.from_template(
"Here are statistics for a class exam:\n{stats}\n\n"
"Write a 2-sentence teacher comment on the class performance."
)
| llm | StrOutputParser()
)
pipeline = (
{"scores": generate_data, "subject": RunnablePassthrough()}
| RunnablePassthrough.assign(stats=lambda x: run_stats.invoke(x['scores']))
| RunnablePassthrough.assign(comment=lambda x: interpret.invoke({'stats': x['stats']}))
)
result = pipeline.invoke({"subject": "mathematics"})
print("Scores:", result["scores"])
print("\nStats:")
print(result["stats"])
print("\nTeacher Comment:")
print(result["comment"])
Scores: 76, 85, 92, 67, 88, 94, 73, 81 Stats: count: 8 mean: 82.0 median: 83.0 std_dev: 9.5019 min: 67.0 max: 94.0 range: 27.0 Teacher Comment: The class demonstrated a solid understanding of the material, with a mean score of 82.0 and a median of 83.0, indicating that most students performed well. However, the range of scores suggests there is room for improvement, particularly for those who scored lower, and I encourage everyone to seek help if needed to enhance their understanding.
# Search β summarise β a very common real-world pattern
summarise_prompt = ChatPromptTemplate.from_template(
"Based on this search result, write a clear 3-bullet summary:\n\n"
"{search_result}\n\nTopic: {topic}\nKeep each bullet under 20 words."
)
research_chain = (
RunnablePassthrough.assign(
search_result=lambda x: search.invoke(x['topic'])
)
| summarise_prompt
| llm
| StrOutputParser()
)
print(research_chain.invoke({"topic": "benefits of meditation"}))
- Meditation enhances calmness, happiness, and self-awareness while reducing stress. - Various meditation types provide similar mental and physical health benefits. - Meditation shifts perspective, revealing deeper aspects of existence beyond the physical world.
7οΈ. LLM Tool Calling β Let the Model DecideΒΆ
Instead of you choosing which tool to call, the LLM decides based on the question.
llm_with_tools = llm.bind_tools([tool_a, tool_b, tool_c])
The LLM can respond with:
- A text answer (no tool needed)
- A tool call (tool name + arguments)
You execute the tool and feed the result back for the final answer.
# Bind tools to the LLM
tools_basic = [calculator, get_current_datetime, unit_converter, stats_calculator, wiki]
llm_with_tools = llm.bind_tools(tools_basic)
# Ask questions β watch which trigger tool calls vs direct answers
questions = [
"What is 1337 * 42?",
"What is the capital of France?", # no tool needed
"What day is it today?",
"Convert 100 miles to km",
]
for q in questions:
response = llm_with_tools.invoke(q)
if response.tool_calls:
print(f"\nβ {q}")
print(f" π§ Tool: {response.tool_calls[0]['name']}")
print(f" π₯ Args: {response.tool_calls[0]['args']}")
else:
print(f"\nβ {q}")
print(f" π¬ Direct answer: {response.content}")
β What is 1337 * 42?
π§ Tool: calculator
π₯ Args: {'expression': '1337 * 42'}
β What is the capital of France?
π§ Tool: wikipedia
π₯ Args: {'query': 'Capital of France'}
β What day is it today?
π§ Tool: get_current_datetime
π₯ Args: {}
β Convert 100 miles to km
π§ Tool: unit_converter
π₯ Args: {'value': 100, 'from_unit': 'miles', 'to_unit': 'km'}
from langchain_core.messages import HumanMessage, ToolMessage
tool_map = {t.name: t for t in tools_basic}
def ask_with_tools(question: str) -> str:
"""Full round-trip: question -> tool call -> result -> final answer."""
messages = [HumanMessage(content=question)]
response = llm_with_tools.invoke(messages)
messages.append(response)
if response.tool_calls:
for tc in response.tool_calls:
print(f" π§ Calling {tc['name']!r} with {tc['args']}")
result = tool_map[tc['name']].invoke(tc['args'])
messages.append(ToolMessage(
content=str(result),
tool_call_id=tc['id']
))
final = llm_with_tools.invoke(messages)
return final.content
return response.content
test_questions = [
"What is 999 * 888?",
"Convert 37 celsius to fahrenheit",
"What is today's date?",
# "Tell me about Python in one sentence", # no tool
]
for q in test_questions:
print(f"\nβ {q}")
print('β
', ask_with_tools(q))
β What is 999 * 888?
π§ Calling 'calculator' with {'expression': '999 * 888'}
β
The result of \( 999 \times 888 \) is 887,112.
β Convert 37 celsius to fahrenheit
π§ Calling 'unit_converter' with {'value': 37, 'from_unit': 'celsius', 'to_unit': 'fahrenheit'}
β
37 degrees Celsius is equal to 98.6 degrees Fahrenheit.
β What is today's date?
π§ Calling 'get_current_datetime' with {}
β
Today's date is Sunday, June 14, 2026.
# Multiple tool calls β some questions need more than one tool
result = ask_with_tools(
"What is today's date, and also calculate 365 / 12 rounded to 2 decimal places?"
)
print(result)
π§ Calling 'get_current_datetime' with {}
π§ Calling 'calculator' with {'expression': '365 / 12'}
Today's date is Sunday, June 14, 2026.
The result of \( 365 / 12 \) rounded to two decimal places is approximately 30.42.
8οΈ. Interesting & Fun ToolsΒΆ
| Tool | What it does |
|---|---|
| Password generator | Creates secure random passwords |
| Morse code | Encode and decode Morse code |
| Readability scorer | Flesch-Kincaid reading ease score |
| Timezone converter | Convert times across timezones |
| Number to words | 1234 to ‘one thousand two hundred…” |
| Word tools | Palindromes, ROT13, vowel counter |
import random, string
@tool
def generate_password(length: int = 16, include_symbols: bool = True) -> str:
"""
Generate a secure random password.
Length defaults to 16. Set include_symbols=False for alphanumeric only.
Use when asked to create a password or generate a secure string.
"""
chars = string.ascii_letters + string.digits
if include_symbols:
chars += "!@#$%^&*"
password = [
random.choice(string.ascii_uppercase),
random.choice(string.ascii_lowercase),
random.choice(string.digits),
]
if include_symbols:
password.append(random.choice("!@#$%^&*"))
password += [random.choice(chars) for _ in range(length - len(password))]
random.shuffle(password)
return ''.join(password)
print("Sample passwords:")
for _ in range(5):
print(" ", generate_password.invoke({"length": 16, "include_symbols": True}))
Sample passwords: O&F6GY91@iYdHbpS b8n%l$J8AH1iYt$s H*Yxt3nj26Q3A8G& DiZk0C94D#xzgM^^ F*h*Qso*SzB0hm1T
@tool
def morse_code(text: str, mode: str = 'encode') -> str:
"""
Encode text to Morse code or decode Morse back to text.
mode: use 'encode' to convert text to morse, 'decode' for morse to text.
Morse uses dots and dashes, words separated by ' / '.
Example encode input: HELLO β Example decode input: .... . .-.. .-.. ---
"""
CODE = {
'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.','H':'....','I':'..','J':'.---',
'K':'-.-','L':'.-..','M':'--','N':'-.','O':'---','P':'.--.','Q':'--.-','R':'.-.','S':'...','T':'-',
'U':'..-','V':'...-','W':'.--','X':'-..-','Y':'-.--','Z':'--..',
'0':'-----','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.',
}
DECODE = {v: k for k, v in CODE.items()}
if mode == 'encode':
result = []
for word in text.upper().split():
result.append(' '.join(CODE.get(c, '?') for c in word))
return ' / '.join(result)
else:
words = text.strip().split(' / ')
return ' '.join(''.join(DECODE.get(c, '?') for c in word.split()) for word in words)
encoded = morse_code.invoke({"text": "HELLO WORLD", "mode": "encode"})
print("Encoded:", encoded)
decoded = morse_code.invoke({"text": encoded, "mode": "decode"})
print("Decoded:", decoded)
Encoded: .... . .-.. .-.. --- / .-- --- .-. .-.. -.. Decoded: HELLO WORLD
@tool
def readability_score(text: str) -> str:
"""
Calculate the Flesch-Kincaid readability score of a text (0-100).
Higher score = easier to read.
Score 90-100: Very Easy, 60-70: Standard, 30-50: Difficult, 0-30: Very Difficult.
Use to analyse writing complexity or compare texts.
"""
sentences = max(text.count('.') + text.count('!') + text.count('?'), 1)
words = text.split()
if not words:
return "No text provided."
def count_syllables(word):
word = word.lower().strip('.,!?;:')
vowels = 'aeiouy'
count = sum(1 for i, c in enumerate(word) if c in vowels and (i == 0 or word[i-1] not in vowels))
return max(count, 1)
syllables = sum(count_syllables(w) for w in words)
score = 206.835 - 1.015*(len(words)/sentences) - 84.6*(syllables/len(words))
score = max(0, min(100, score))
if score >= 90: level = 'Very Easy (5th grade)'
elif score >= 70: level = 'Fairly Easy (7th grade)'
elif score >= 60: level = 'Standard (8th-9th grade)'
elif score >= 50: level = 'Fairly Difficult (10th-12th grade)'
elif score >= 30: level = 'Difficult (college level)'
else: level = 'Very Difficult (professional)'
return f"Score: {score:.1f}/100 β {level} | Words: {len(words)} | Sentences: {sentences}"
easy = "The cat sat on the mat. It was a big cat. The cat was very happy."
hard = "The epistemological implications of quantum indeterminacy necessitate fundamental reexamination of classical causality."
print("Easy:", readability_score.invoke({"text": easy}))
print("Hard:", readability_score.invoke({"text": hard}))
Easy: Score: 100.0/100 β Very Easy (5th grade) | Words: 16 | Sentences: 3 Hard: Score: 0.0/100 β Very Difficult (professional) | Words: 12 | Sentences: 1
@tool
def convert_timezone(time_str: str, from_offset: str, to_offset: str) -> str:
"""
Convert a time between UTC offsets.
time_str: HH:MM in 24-hour format.
from_offset and to_offset: UTC offset like '+5:30', '-8', '+0'.
Use for scheduling across countries or converting meeting times.
"""
try:
h, m = map(int, time_str.split(':'))
def parse(tz):
tz = tz.strip().lstrip('UTC').strip()
if not tz or tz == '+0' or tz == '0': return 0
sign = 1 if '+' in tz else -1
tz = tz.replace('+','').replace('-','')
parts = tz.split(':')
return sign * (int(parts[0]) * 60 + (int(parts[1]) if len(parts) > 1 else 0))
total = h * 60 + m - parse(from_offset) + parse(to_offset)
total = total % (24 * 60)
return f"{time_str} (UTC{from_offset}) = {total//60:02d}:{total%60:02d} (UTC{to_offset})"
except Exception as e:
return f"Error: {e}"
meeting = "14:30"
print(f"Meeting at {meeting} UTC:")
print(convert_timezone.invoke({"time_str": meeting, "from_offset": "+0", "to_offset": "+5:30"}), " India")
print(convert_timezone.invoke({"time_str": meeting, "from_offset": "+0", "to_offset": "-8"}), " US Pacific")
print(convert_timezone.invoke({"time_str": meeting, "from_offset": "+0", "to_offset": "+9"}), " Japan")
Meeting at 14:30 UTC: 14:30 (UTC+0) = 20:00 (UTC+5:30) India 14:30 (UTC+0) = 06:30 (UTC-8) US Pacific 14:30 (UTC+0) = 23:30 (UTC+9) Japan
@tool
def number_to_words(number: int) -> str:
"""
Convert a number to its English word representation.
Useful for writing cheques, formal documents, or accessibility.
Works for numbers up to 999,999,999.
"""
if number == 0: return 'zero'
ones = ['','one','two','three','four','five','six','seven','eight','nine',
'ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen',
'seventeen','eighteen','nineteen']
tens = ['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety']
def three(n):
if n == 0: return ''
elif n < 20: return ones[n]
elif n < 100: return tens[n//10] + ('-' + ones[n%10] if n%10 else '')
else: return ones[n//100] + ' hundred' + (' and ' + three(n%100) if n%100 else '')
result = ''
if number >= 1_000_000:
result += three(number // 1_000_000) + ' million '
number %= 1_000_000
if number >= 1_000:
result += three(number // 1_000) + ' thousand '
number %= 1_000
if number > 0:
result += ('and ' if result else '') + three(number)
return result.strip()
for n in [42, 1000, 15750, 1_000_000, 999_999_999]:
print(f"{n:>12,} -> {number_to_words.invoke({'number': n})}")
42 -> forty-two
1,000 -> one thousand
15,750 -> fifteen thousand and seven hundred and fifty
1,000,000 -> one million
999,999,999 -> nine hundred and ninety-nine million nine hundred and ninety-nine thousand and nine hundred and ninety-nine
@tool
def word_tools(word: str, operation: str) -> str:
"""
Perform word operations.
operation choices:
- palindrome: check if word/phrase is a palindrome
- reverse: reverse the string
- vowels: count vowels and consonants
- rot13: apply ROT13 Caesar cipher
"""
w = word.lower().replace(' ', '')
if operation == 'palindrome':
is_p = w == w[::-1]
return f"{word!r} {'IS β
' if is_p else 'is NOT β'} a palindrome"
elif operation == 'reverse':
return f"Reversed: {word[::-1]!r}"
elif operation == 'vowels':
v = sum(1 for c in word.lower() if c in 'aeiou')
c = sum(1 for c in word.lower() if c.isalpha() and c not in 'aeiou')
return f"{word!r}: {v} vowels, {c} consonants"
elif operation == 'rot13':
result = ''.join(
chr((ord(c)-65+13)%26+65) if c.isupper()
else chr((ord(c)-97+13)%26+97) if c.islower()
else c for c in word)
return f"ROT13: {word!r} -> {result!r}"
return f"Unknown operation: {operation}"
print(word_tools.invoke({"word": "racecar", "operation": "palindrome"}))
print(word_tools.invoke({"word": "A man a plan a canal Panama", "operation": "palindrome"}))
print(word_tools.invoke({"word": "LangChain", "operation": "vowels"}))
print(word_tools.invoke({"word": "Hello World", "operation": "rot13"}))
'racecar' IS β a palindrome 'A man a plan a canal Panama' IS β a palindrome 'LangChain': 3 vowels, 6 consonants ROT13: 'Hello World' -> 'Uryyb Jbeyq'
all_tools = [
# Math & Data
calculator, stats_calculator, unit_converter, python_repl,
# Web & Search
search, wiki, fetch_webpage,
# Datetime
get_current_datetime, days_between_dates, add_days_to_date,
# Files
write_file, read_file, list_files,
# Interesting
generate_password, morse_code, readability_score,
convert_timezone, number_to_words, word_tools,
]
print(f"Total tools: {len(all_tools)}")
for t in all_tools:
print(f" π§ {t.name:30s} {t.description[:50]}...")
Total tools: 19
π§ calculator Evaluate a mathematical expression. Input must be ...
π§ stats_calculator Calculate statistics for a list of numbers.
In...
π§ unit_converter Convert between common units of measurement.
S...
π§ python_repl Execute Python code and return the printed output....
π§ duckduckgo_search A wrapper around DuckDuckGo Search. Useful for whe...
π§ wikipedia A wrapper around Wikipedia. Useful for when you ne...
π§ fetch_webpage Fetch and return the text content of any webpage U...
π§ get_current_datetime Get the current date and time.
Use when asked ...
π§ days_between_dates Calculate the number of days between two dates.
...
π§ add_days_to_date Add or subtract days from a date. Input: YYYY-MM-D...
π§ write_file Write content to a text file.
Use when the use...
π§ read_file Read and return the contents of a text file.
U...
π§ list_files List all files in a directory. Default is current ...
π§ generate_password Generate a secure random password.
Length defa...
π§ morse_code Encode text to Morse code or decode Morse back to ...
π§ readability_score Calculate the Flesch-Kincaid readability score of ...
π§ convert_timezone Convert a time between UTC offsets.
time_str: ...
π§ number_to_words Convert a number to its English word representatio...
π§ word_tools Perform word operations.
operation choices:
...
llm_all = llm.bind_tools(all_tools)
tool_map_all = {t.name: t for t in all_tools}
def smart_ask(question: str) -> str:
from langchain_core.messages import HumanMessage, ToolMessage
messages = [HumanMessage(content=question)]
response = llm_all.invoke(messages)
messages.append(response)
if response.tool_calls:
for tc in response.tool_calls:
print(f" π§ {tc['name']}({tc['args']})")
result = tool_map_all[tc['name']].invoke(tc['args'])
messages.append(ToolMessage(content=str(result), tool_call_id=tc['id']))
return llm_all.invoke(messages).content
return response.content
showcase = [
"What is 4096 / 64?",
"Encode LANGCHAIN in Morse code",
"Generate a 12-character password without symbols",
"Convert 500 pounds to kg",
"Is Never odd or even a palindrome?",
"Write 1234567 in words",
"What day of the week is it today?",
"How many days between 2025-01-01 and 2025-12-31?",
]
for q in showcase:
print(f"\nβ {q}")
print('β
', smart_ask(q))
β What is 4096 / 64?
π§ calculator({'expression': '4096 / 64'})
β
The result of \( 4096 \div 64 \) is \( 64.0 \).
β Encode LANGCHAIN in Morse code
π§ morse_code({'text': 'LANGCHAIN', 'mode': 'encode'})
β
The Morse code for "LANGCHAIN" is: **.-.. .- -. --. -.-. .... .- .. -.**
β Generate a 12-character password without symbols
π§ generate_password({'length': 12, 'include_symbols': False})
β
Here is your 12-character password without symbols: **rn2lePUAKmK6**
β Convert 500 pounds to kg
π§ unit_converter({'value': 500, 'from_unit': 'pounds', 'to_unit': 'kg'})
β
500 pounds is approximately 226.80 kilograms.
β Is Never odd or even a palindrome?
π§ word_tools({'word': 'Never odd or even', 'operation': 'palindrome'})
β
Yes, "Never odd or even" is a palindrome!
β Write 1234567 in words
π§ number_to_words({'number': 1234567})
β
The number 1,234,567 in words is: **one million two hundred and thirty-four thousand and five hundred and sixty-seven**.
β What day of the week is it today?
π§ get_current_datetime({})
β
Today is Sunday.
β How many days between 2025-01-01 and 2025-12-31?
π§ days_between_dates({'date1': '2025-01-01', 'date2': '2025-12-31'})
β
There are 364 days between January 1, 2025, and December 31, 2025.
Summary β Tools Cheat SheetΒΆ
# 1. CREATE A TOOL
from langchain_core.tools import tool
@tool
def my_tool(param: str) -> str:
"""Descriptive docstring β the LLM reads this to decide when to call it."""
return "result"
my_tool.invoke({'param': 'value'}) # call directly
# 2. BUILT-IN COMMUNITY TOOLS
from langchain_community.tools import DuckDuckGoSearchRun # web search (free)
from langchain_community.tools import WikipediaQueryRun # Wikipedia (free)
from langchain_community.tools import PythonREPLTool # run Python (free)
# 3. BIND TO LLM β let LLM decide which to call
llm_with_tools = llm.bind_tools([tool_a, tool_b, tool_c])
# 4. HANDLE TOOL CALLS
from langchain_core.messages import HumanMessage, ToolMessage
response = llm_with_tools.invoke([HumanMessage(content='question')])
if response.tool_calls:
for tc in response.tool_calls:
result = tool_map[tc['name']].invoke(tc['args'])
# feed result back as ToolMessage
