RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Fredoline Eruo
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /How-to
  5. /How to Build Custom Tools for Agents
HOW-TO · RAG

How to Build Custom Tools for Agents

intermediate·20 min·By Fredoline Eruo
Target environment
Ubuntu 24.04 · Ollama 0.4.x
PREREQUISITES

Agent framework, Python 3.10+

What this does

Custom tools extend an agent's capabilities beyond built-in functions. Each tool wraps a Python function with a JSON schema declaration that the LLM uses to understand when and how to call it.

Steps

  • Write a tool as a plain function with docstring. The docstring becomes the tool description.
def get_stock_price(symbol: str) -> str:
    """Get the current stock price for a given ticker symbol."""
    prices = {"AAPL": "175.32", "GOOGL": "142.50", "MSFT": "378.20"}
    return prices.get(symbol.upper(), f"Price for {symbol} not found")
  • Wrap with LangChain's @tool decorator. This auto-generates the JSON schema.
from langchain.tools import tool

@tool
def get_stock_price(symbol: str) -> str:
    """Get the current stock price for a given ticker symbol."""
    prices = {"AAPL": "175.32", "GOOGL": "142.50", "MSFT": "378.20"}
    return prices.get(symbol.upper(), f"Price for {symbol} not found")

print(get_stock_price.name)  # get_stock_price
print(get_stock_price.args)  # {'symbol': {'type': 'string'}}
  • Create a tool class for complex tools. Extend BaseTool for tools with state.
from langchain.tools import BaseTool
from typing import Type, Optional

class DatabaseQueryTool(BaseTool):
    name: str = "query_database"
    description: str = "Execute SQL queries against the analytics database"
    db_connection: str = "sqlite:///analytics.db"

    def _run(self, sql: str) -> str:
        import sqlite3
        conn = sqlite3.connect(self.db_connection)
        cursor = conn.cursor()
        cursor.execute(sql)
        rows = cursor.fetchall()
        conn.close()
        return str(rows)

    def _arun(self, sql: str) -> str:
        raise NotImplementedError("Async not implemented")
  • Add structured arguments with Pydantic. Define an ArgsSchema for validation.
from pydantic import BaseModel, Field

class SendEmailInput(BaseModel):
    to: str = Field(description="Recipient email address")
    subject: str = Field(description="Email subject")
    body: str = Field(description="Email body content")
    priority: str = Field(default="normal", enum=["low", "normal", "high"])

@tool(args_schema=SendEmailInput)
def send_email(to: str, subject: str, body: str, priority: str = "normal") -> str:
    """Send an email to a recipient."""
    return f"Email sent to {to} with priority {priority}"
  • Include error handling inside the tool. Tools should never raise unhandled exceptions.
@tool
def safe_file_read(path: str) -> str:
    """Read the contents of a file safely."""
    import os
    if not os.path.exists(path):
        return f"Error: file {path} not found"
    if not path.endswith(".txt"):
        return f"Error: only .txt files allowed"
    try:
        with open(path, "r") as f:
            return f.read()
    except Exception as e:
        return f"Error reading file: {e}"

Verification

python -c "
from langchain.tools import tool
@tool
def add(a: int, b: int) -> int:
    '''Add numbers.'''
    return a + b
print(add.invoke({'a': 3, 'b': 4}))
# Expected: 7
"

Common failures

  • Missing type hints. Without type annotations, the schema generator cannot infer parameter types, leading to Any types that confuse the LLM.
  • Side effects without idempotency. Tools that insert records or send emails may be called multiple times on retry. Design tools to be idempotent.
  • Tool output too verbose. Returning megabytes of data exhausts the context window. Limit output length and provide pagination options.
  • Version mismatch - The installed package or runtime differs from the command shown; check the version first and rerun the smallest verification command.
  • Local environment drift - Another service, virtual environment, model, or path is being used; print the active binary path and configuration before changing the guide steps.

Related guides

  • How to Register and Call Custom Tools in Agents
  • How to Use OpenAI Function Calling with Tools
← All how-to guidesCourses →