AI Agent Tools Tutorial: How to Give Agents Real-World Capabilities
AI Agent Tools Tutorial: How to Give Agents Real-World Capabilities
An AI agent without tools is a chatbot. Tools are what let agents take action: search the web, read files, call APIs, send emails, query databases. This tutorial teaches you how to design, build, and test tools that make AI agents genuinely useful.
You'll learn the function calling protocol, tool design patterns that prevent errors, and get 10 ready-to-use tool implementations.
In this tutorial:
- How agent tools work
- Tool design principles
- Building your first tool
- 10 production-ready tools
- Tool testing strategies
- Common tool design mistakes
Related tutorials: AI Agent API Integration · AI Agent Python Tutorial · Build AI Agent From Scratch
How Agent Tools Work
Tools use the function calling protocol. Here's the flow:
Agent decides it needs to search the web
│
Agent outputs: {"name": "web_search", "arguments": {"query": "Python 3.13 features"}}
│
Your code executes: web_search("Python 3.13 features")
│
Result returns: "Python 3.13 includes improved error messages, JIT compiler..."
│
Agent uses the result to answer the user
The key insight: the agent doesn't run the tool itself. It produces a structured request, and your code executes it. You have full control over what happens.
The OpenAI Function Calling Format
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Get the current stock price for a given ticker symbol",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Stock ticker symbol like AAPL or GOOGL"
},
"include_history": {
"type": "boolean",
"description": "Whether to include 30-day price history",
"default": false
}
},
"required": ["symbol"]
}
}
}
Tool Design Principles
Principle 1: Be Specific in Descriptions
The description is what the agent reads to decide when and how to use the tool. Vague descriptions cause wrong tool usage.
# Bad
description = "Search the web"
# Good
description = (
"Search the web for current information about a specific topic. "
"Returns titles, URLs, and content snippets from the top 5 results. "
"Use when you need up-to-date facts, news, prices, or data. "
"Do NOT use for general knowledge questions."
)
Principle 2: Validate All Inputs
Never trust agent-generated arguments. Validate everything:
def search_web(query: str, max_results: int = 5) -> str:
if not query or len(query.strip()) == 0:
return "Error: Search query cannot be empty"
if len(query) > 500:
return "Error: Query too long (max 500 characters)"
max_results = min(max(1, max_results), 10)
try:
response = requests.get(...)
return format_results(response.json())
except requests.RequestException as e:
return f"Error: Search failed - {str(e)}"
Principle 3: Return Strings
Tools must return strings because that's what goes back into the LLM's context. Format complex data as readable text:
# Bad
return {"temperature": 72, "condition": "sunny", "humidity": 45}
# Good
return "Weather in San Francisco: 72°F, Sunny, 45% humidity. Source: Weather API."
Principle 4: Include Error Messages
When a tool fails, return a descriptive error that helps the agent adjust:
# Bad
return "Error"
# Good
return "Error: Could not find stock price for 'XYZZ'. Verify the ticker symbol is correct. Common tickers: AAPL, GOOGL, MSFT."
Building Your First Tool
Here's a complete tool implementation with all best practices:
import os
import requests
import json
from typing import Optional
TOOL_DEFINITION = {
"type": "function",
"function": {
"name": "web_search",
"description": (
"Search the web for current information. Returns top results with "
"titles, URLs, and content snippets. Use for finding recent news, "
"prices, dates, or any information that changes frequently."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Specific search query, e.g. 'Python 3.13 release date'"
},
"num_results": {
"type": "integer",
"description": "Number of results to return (1-10)",
"default": 5
}
},
"required": ["query"]
}
}
}
def web_search(query: str, num_results: int = 5) -> str:
if not query.strip():
return "Error: Empty search query"
num_results = max(1, min(num_results, 10))
api_key = os.environ.get("TAVILY_API_KEY")
if not api_key:
return "Error: Web search not configured (missing TAVILY_API_KEY)"
try:
response = requests.post(
"https://api.tavily.com/search",
json={
"query": query,
"api_key": api_key,
"max_results": num_results,
"include_answer": True
},
timeout=10
)
response.raise_for_status()
data = response.json()
results = []
if data.get("answer"):
results.append(f"Summary: {data['answer']}")
for r in data.get("results", []):
results.append(f"- {r['title']}\n {r['url']}\n {r['content'][:200]}")
return "\n\n".join(results) if results else "No results found"
except requests.Timeout:
return "Error: Search timed out. Try a simpler query."
except requests.RequestException as e:
return f"Error: Search failed - {e}"
10 Production-Ready Tools
Tool 1: File Reader
file_read_tool = {
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a text file. Supports .txt, .md, .csv, .json files.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path relative to workspace"},
"max_lines": {"type": "integer", "description": "Maximum lines to read", "default": 100}
},
"required": ["path"]
}
}
}
Get AI agent tips in your inbox
Multi-agent workflows, BYOK tips, and product updates. No spam.
def read_file(path: str, max_lines: int = 100) -> str: allowed_extensions = {".txt", ".md", ".csv", ".json", ".py", ".js", ".ts"} ext = os.path.splitext(path)[1].lower()
if ext not in allowed_extensions:
return f"Error: Cannot read {ext} files. Allowed: {allowed_extensions}"
if not os.path.exists(path):
return f"Error: File not found: {path}"
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()[:max_lines]
content = "".join(lines)
return f"File: {path} ({len(lines)} lines)\n{content}"
except Exception as e:
return f"Error reading file: {e}"
### Tool 2: File Writer
```python
def write_file(path: str, content: str) -> str:
allowed_extensions = {".txt", ".md", ".csv", ".json", ".py", ".js", ".ts"}
ext = os.path.splitext(path)[1].lower()
if ext not in allowed_extensions:
return f"Error: Cannot write {ext} files"
try:
os.makedirs(os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return f"Successfully wrote {len(content)} characters to {path}"
except Exception as e:
return f"Error writing file: {e}"
Tool 3: Calculator
import math
def calculate(expression: str) -> str:
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "len": len, "pow": pow,
"pi": math.pi, "e": math.e,
"sqrt": math.sqrt, "log": math.log, "log10": math.log10,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"ceil": math.ceil, "floor": math.floor,
}
try:
code = compile(expression, "<string>", "eval")
for name in code.co_names:
if name not in allowed_names:
return f"Error: '{name}' is not allowed. Available: {list(allowed_names.keys())}"
result = eval(code, {"__builtins__": {}}, allowed_names)
return f"Result: {result}"
except Exception as e:
return f"Error: {e}"
Tool 4: HTTP Request
def http_request(url: str, method: str = "GET", headers: dict = None, body: str = None) -> str:
if not url.startswith(("http://", "https://")):
return "Error: URL must start with http:// or https://"
allowed_methods = {"GET", "POST", "PUT", "DELETE"}
method = method.upper()
if method not in allowed_methods:
return f"Error: Method must be one of {allowed_methods}"
try:
response = requests.request(
method=method,
url=url,
headers=headers,
data=body,
timeout=30
)
return f"Status: {response.status_code}\nHeaders: {dict(response.headers)}\nBody: {response.text[:2000]}"
except requests.Timeout:
return "Error: Request timed out (30s)"
except requests.RequestException as e:
return f"Error: {e}"
Tool 5: JSON Data Extractor
def extract_json(data: str, jsonpath: str) -> str:
try:
parsed = json.loads(data)
except json.JSONDecodeError:
return "Error: Invalid JSON input"
keys = jsonpath.strip(".").split(".")
current = parsed
for key in keys:
if isinstance(current, dict):
if key not in current:
return f"Error: Key '{key}' not found. Available keys: {list(current.keys())}"
current = current[key]
elif isinstance(current, list):
try:
idx = int(key)
current = current[idx]
except (ValueError, IndexError):
return f"Error: Cannot access index '{key}' on array of length {len(current)}"
else:
return f"Error: Cannot traverse into {type(current).__name__}"
return json.dumps(current, indent=2) if isinstance(current, (dict, list)) else str(current)
Tool 6: Date and Time
from datetime import datetime, timedelta
def get_datetime(format: str = "%Y-%m-%d %H:%M:%S", offset_days: int = 0) -> str:
now = datetime.now() + timedelta(days=offset_days)
try:
return now.strftime(format)
except ValueError:
return f"Error: Invalid format string. Example: '%Y-%m-%d %H:%M:%S'"
Tool 7: List Directory
def list_directory(path: str = ".", pattern: str = "*") -> str:
import glob
if not os.path.exists(path):
return f"Error: Directory not found: {path}"
matches = glob.glob(os.path.join(path, pattern))
if not matches:
return f"No files matching '{pattern}' in {path}"
entries = []
for m in sorted(matches)[:50]:
size = os.path.getsize(m) if os.path.isfile(m) else "-"
entry_type = "DIR" if os.path.isdir(m) else "FILE"
entries.append(f"[{entry_type}] {os.path.basename(m)} ({size} bytes)")
return "\n".join(entries)
Tool 8: Text Statistics
def text_stats(text: str) -> str:
words = text.split()
sentences = text.count(".") + text.count("!") + text.count("?")
paragraphs = text.count("\n\n") + 1
import re
reading_time = len(words) / 200
return (
f"Words: {len(words)}\n"
f"Characters: {len(text)}\n"
f"Sentences: {max(sentences, 1)}\n"
f"Paragraphs: {paragraphs}\n"
f"Reading time: {reading_time:.1f} minutes\n"
f"Average word length: {sum(len(w) for w in words) / max(len(words), 1):.1f} characters"
)
Tool 9: Regex Search
import re
def regex_search(text: str, pattern: str, max_results: int = 20) -> str:
try:
matches = re.findall(pattern, text)
except re.error as e:
return f"Error: Invalid regex pattern - {e}"
if not matches:
return "No matches found"
matches = matches[:max_results]
if isinstance(matches[0], tuple):
return f"Found {len(matches)} matches:\n" + "\n".join(str(m) for m in matches)
return f"Found {len(matches)} matches:\n" + "\n".join(f"- {m}" for m in matches)
Tool 10: Data Table Formatter
def format_table(headers: list[str], rows: list[list]) -> str:
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(str(cell)))
def format_row(cells):
return " | ".join(str(c).ljust(col_widths[i]) for i, c in enumerate(cells))
separator = "-+-".join("-" * w for w in col_widths)
output = format_row(headers) + "\n" + separator + "\n"
for row in rows:
output += format_row(row) + "\n"
return output
Tool Testing Strategies
Unit Testing Individual Tools
import unittest
class TestTools(unittest.TestCase):
def test_calculator_basic(self):
result = calculate("2 + 2")
self.assertIn("4", result)
def test_calculator_invalid(self):
result = calculate("import os")
self.assertIn("Error", result)
def test_calculator_functions(self):
result = calculate("sqrt(16)")
self.assertIn("4", result)
def test_file_read_nonexistent(self):
result = read_file("/nonexistent/file.txt")
self.assertIn("Error", result)
def test_text_stats(self):
result = text_stats("Hello world. This is a test.")
self.assertIn("Words: 6", result)
self.assertIn("Sentences:", result)
if __name__ == "__main__":
unittest.main()
Integration Testing with Agent
def test_tool_with_agent():
tools = [TOOL_DEFINITION]
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Use tools to answer questions."},
{"role": "user", "content": "What is 15% of 340?"}
],
tools=[{"type": "function", "function": {
"name": "calculate",
"description": "Evaluate a math expression",
"parameters": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"]
}
}}]
)
tool_calls = response.choices[0].message.tool_calls
assert tool_calls is not None, "Agent should use calculator tool"
assert "340" in tool_calls[0].function.arguments
Common Tool Design Mistakes
Mistake 1: Vague Descriptions
# Bad: Agent won't know when to use this
"description": "Get data"
# Good: Agent knows exactly when and how
"description": "Get the current stock price for a ticker symbol. Use when asked about stock prices, market caps, or financial data."
Mistake 2: Missing Parameter Descriptions
# Bad
"parameters": {"type": "object", "properties": {"q": {"type": "string"}}}
# Good
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A specific search query like 'Tesla stock price Q4 2025'"
}
}
}
Mistake 3: Returning Complex Objects
# Bad: LLM can't use this effectively
return {"status": 200, "data": {"items": [...]}}
# Good: LLM can reason about this
return "Found 5 items:\n1. Apple ($150)\n2. Banana ($120)\n..."
Mistake 4: No Timeout or Retry
# Bad: Can hang forever
response = requests.get(url)
# Good: Fails fast
response = requests.get(url, timeout=10)
Mistake 5: Overly Broad Tools
# Bad: One tool does everything poorly
def do_api_call(method, url, headers, body): ...
# Good: Specific, well-defined tools
def send_email(to, subject, body): ...
def search_web(query): ...
def read_file(path): ...
Tools Built In: Ivern AI
Ivern AI comes with pre-built tools for common tasks:
- Web search -- find current information
- File operations -- read, write, and manage files
- Data processing -- extract, transform, and format data
- Research tools -- gather and synthesize information
- No custom tool code needed -- configure tools through the interface
Build agents with built-in tools: ivern.ai/signup
Key Takeaways
- Tools are the bridge between AI reasoning and real-world action
- Descriptions drive usage -- write them carefully, the agent reads them to decide when to use each tool
- Validate everything -- never trust agent-generated inputs
- Return readable strings -- not complex objects
- Test tools independently before integrating with the agent
Next tutorials: AI Agent API Integration · AI Agent Prompt Engineering · AI Agent Security
Related Articles
AI Agents vs Bots: 7 Key Differences That Matter for Your Business (2026)
AI agents and bots are fundamentally different technologies. Agents are autonomous, use tools, make decisions, and execute multi-step workflows. Bots follow scripts, handle single tasks, and can't adapt. We break down 7 differences with real examples and explain when your business needs agents vs bots.
AI Agent API Integration Tutorial: Connect Agents to Any External Service
Step-by-step tutorial for connecting AI agents to external APIs and services. Covers REST API integration, authentication, error handling, rate limiting, and building a tool layer that lets agents interact with any service.
AI Agent Collaboration Tutorial: How to Make Multiple Agents Work Together
Learn how to build collaborative AI agent systems where multiple specialized agents share context, hand off tasks, and produce results together. Covers communication patterns, context sharing, and real implementation examples.
Want to try multi-agent AI for free?
Generate a blog post, Twitter thread, LinkedIn post, and newsletter from one prompt. No signup required.
Try the Free DemoAI Content Factory -- Free to Start
One prompt generates blog posts, social media, and emails. Free tier, BYOK, zero markup.
No spam. Unsubscribe anytime.