What are Custom Tools?

Custom tools are functions that extend your AI agent’s capabilities beyond the built-in functionality. They allow agents to interact with external systems, perform specialized operations, and execute complex workflows. What do they do?
  • Execute specific business logic or API calls
  • Interact with databases, file systems, or external services
  • Perform data processing, calculations, or transformations
  • Enable human-in-the-loop workflows with confirmation and input requirements
  • Provide caching and performance optimization capabilities
What are the parts of a custom tool?
  • Function Definition: The core logic that performs the actual work
  • Type Hints: Required parameter and return type annotations for the LLM to understand usage
  • Documentation: Clear docstrings explaining the tool’s purpose and parameters
  • Configuration: Optional behavioral settings like confirmation requirements, caching, or external execution
  • Error Handling: Robust error management for production reliability

Core Principles For Custom Tools

When creating custom tools, ensure you define these elements:
  • Clear Purpose: Each tool should have a single, well-defined responsibility
  • Type Safety: All parameters and return values must have explicit type hints
  • Documentation: Comprehensive docstrings that explain usage and expected behavior
  • Error Handling: Graceful failure handling with meaningful error messages
  • Configuration: Appropriate behavioral settings for your use case

Defining Tool Functions

The function definition is the foundation of your custom tool. Follow these guidelines:
  • Single Responsibility: Each tool should do one thing well
  • Type Annotations: Every parameter and return value must have type hints
  • Clear Naming: Use descriptive function names that indicate the tool’s purpose
  • Documentation: Write comprehensive docstrings that explain parameters, behavior, and return values
Good Tool Definition:
@tool
async def analyze_website_content(url: str, analysis_type: str = "general") -> Dict[str, Any]:
    """
    Analyzes the content of a website and provides insights based on the specified analysis type.
    
    Args:
        url: The website URL to analyze (must be a valid HTTP/HTTPS URL)
        analysis_type: Type of analysis to perform ('general', 'seo', 'accessibility', 'performance')
    
    Returns:
        Dictionary containing analysis results with keys: 'status', 'insights', 'recommendations'
    
    Raises:
        ValueError: If the URL is invalid or unreachable
        ConnectionError: If the website cannot be accessed
    """
    # Tool implementation here
    pass
Bad Tool Definition:
@tool
def analyze(url, type):
    # Missing type hints, docstring, and error handling
    pass

Tool Best Practices

1. Error Handling

Always implement proper error handling in your tools:
@tool
async def robust_tool(param: str) -> Dict[str, Any]:
    """A tool with comprehensive error handling."""
    try:
        # Tool logic here
        result = await perform_operation(param)
        return {"status": "success", "data": result}
    except ValueError as e:
        return {"status": "error", "error": f"Invalid parameter: {e}"}
    except Exception as e:
        return {"status": "error", "error": f"Unexpected error: {e}"}

2. Input Validation

Validate inputs before processing:
@tool
async def validated_tool(url: str, timeout: int = 30) -> str:
    """A tool with input validation."""
    if not url.startswith(('http://', 'https://')):
        raise ValueError("URL must start with http:// or https://")
    
    if timeout <= 0 or timeout > 300:
        raise ValueError("Timeout must be between 1 and 300 seconds")
    
    # Tool logic here
    return "Validated and processed"

3. Clear Documentation

Always provide comprehensive docstrings:
@tool
async def well_documented_tool(param: str, option: bool = True) -> Dict[str, Any]:
    """
    Clear description of what the tool does.
    
    Args:
        param: Description of the parameter and its expected format
        option: Description of the optional parameter and its default behavior
        
    Returns:
        Description of the return value structure and expected keys
        
    Raises:
        ValueError: When param is invalid
        ConnectionError: When external service is unavailable
    """
    # Tool implementation
    pass

Let’s Create Custom Tools for a Website Analysis Agent

In this example, we’ll create basic tools and show how to add user confirmation for sensitive operations.
# Upsonic Docs: Create Custom Tools
# https://docs.upsonic.ai/guides/create_custom_tools

# Imports
from upsonic import Agent, Task
from upsonic.tools import tool
from typing import Dict, List, Any
import requests
from bs4 import BeautifulSoup
import time

# Tool 1: Basic Website Content Fetcher
@tool
async def fetch_website_content(url: str) -> Dict[str, Any]:
    """
    Fetches and returns the HTML content of a website.
    
    Args:
        url: The website URL to fetch content from
        
    Returns:
        Dictionary containing 'status', 'content', 'headers', and 'response_time'
        
    Raises:
        requests.RequestException: If the request fails
        ValueError: If the URL is invalid
    """
    start_time = time.time()
    
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        
        return {
            "status": "success",
            "content": response.text,
            "headers": dict(response.headers),
            "response_time": time.time() - start_time
        }
    except requests.RequestException as e:
        return {
            "status": "error",
            "error": str(e),
            "response_time": time.time() - start_time
        }

# Tool 2: SEO Analysis Tool with Human-in-the-Loop via Hooks
from upsonic.tools.tool import ToolHooks

def before_analysis_hook(html_content: str):
    """Hook that runs before SEO analysis to get user confirmation."""
    print(f"🔍 About to start SEO analysis on content ({len(html_content)} characters)")
    
    # Ask user for confirmation
    user_input = input("Do you want to proceed with SEO analysis? (y/n): ").lower().strip()
    
    if user_input != 'y':
        raise Exception("Not allowed, rejected by user")
    
    print("📊 User approved. Starting SEO analysis...")

def after_analysis_hook(result):
    """Hook that runs after SEO analysis - currently not used."""
    pass

@tool(tool_hooks=ToolHooks(before=before_analysis_hook, after=after_analysis_hook))
async def analyze_seo_metrics(html_content: str) -> Dict[str, Any]:
    """
    Analyzes HTML content for SEO metrics with human-in-the-loop hooks.
    
    Args:
        html_content: The HTML content to analyze
        
    Returns:
        Dictionary containing enhanced SEO metrics, recommendations, and priority actions
    """
    soup = BeautifulSoup(html_content, 'html.parser')
    
    # Extract SEO elements
    title = soup.find('title')
    meta_description = soup.find('meta', attrs={'name': 'description'})
    h1_tags = soup.find_all('h1')
    
    seo_score = 0
    recommendations = []
    
    # Analyze title
    if title and title.text.strip():
        seo_score += 30
        if len(title.text) > 60:
            recommendations.append("Title is too long (should be under 60 characters)")
    else:
        recommendations.append("Missing title tag")
    
    # Analyze meta description
    if meta_description and meta_description.get('content'):
        seo_score += 30
    else:
        recommendations.append("Missing meta description")
    
    # Analyze H1 tags
    if h1_tags:
        seo_score += 40
        if len(h1_tags) > 1:
            recommendations.append("Multiple H1 tags found (should have only one)")
    else:
        recommendations.append("Missing H1 tag")
    
    return {
        "seo_score": seo_score,
        "title": title.text.strip() if title else None,
        "meta_description": meta_description.get('content') if meta_description else None,
        "h1_count": len(h1_tags),
        "recommendations": recommendations
    }

# Agent Creation with Custom Tools
website_analyzer_agent = Agent(
    name="Website Analysis Expert",
    role="Website Performance and SEO Specialist",
    goal="Provide comprehensive website analysis with actionable insights for optimization"
)

# Example Task Using Custom Tools
analysis_task = Task(
    description="Analyze the website 'example.com' for SEO optimization",
    tools=[
        fetch_website_content,
        analyze_seo_metrics
    ]
)

# Execute the analysis
result = website_analyzer_agent.do(analysis_task)
print("Analysis Complete:", result)
Need more advanced features? The @tool decorator supports many powerful configuration options including:
  • User Confirmation: Require manual approval before executing sensitive tools
  • User Input Collection: Prompt users for specific values during execution
  • External Execution: Pause tool execution for external processes
  • Result Caching: Cache expensive operations with TTL control
  • Result Display: Show tool outputs directly to users
  • Execution Control: Stop agent execution after specific tools
For detailed examples and advanced patterns, see our comprehensive Tools Documentation.