AI Agent Tools Tutorial: How to Give Agents Real-World Capabilities

TutorialsBy Ivern AI Team14 min read

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:

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"
    )
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

  1. Tools are the bridge between AI reasoning and real-world action
  2. Descriptions drive usage -- write them carefully, the agent reads them to decide when to use each tool
  3. Validate everything -- never trust agent-generated inputs
  4. Return readable strings -- not complex objects
  5. Test tools independently before integrating with the agent

Next tutorials: AI Agent API Integration · AI Agent Prompt Engineering · AI Agent Security

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 Demo

AI Content Factory -- Free to Start

One prompt generates blog posts, social media, and emails. Free tier, BYOK, zero markup.

No spam. Unsubscribe anytime.