Skip to main content

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

User Interaction Features

Requires Confirmation

Pause execution and require user confirmation before a tool runs. Perfect for sensitive operations like deletions, sending emails, or financial transactions.
@tool(requires_confirmation=True)
def delete_file(file_path: str) -> str:
    """Delete a file from the system."""
    return f"File '{file_path}' deleted successfully."

task = Task(
    description="Delete the old_data.txt file",
    tools=[delete_file]
)

agent = Agent(model="openai/gpt-4o")
result = agent.do(task)
# User will be prompted: "Proceed? (y/n): "

Requires User Input

Collect specific field values from the user at runtime. Useful for credentials, personal information, or dynamic parameters.
@tool(
    requires_user_input=True, 
    user_input_fields=["api_key", "secret_token"]
)
def authenticate_service(service_name: str, api_key: str = "", secret_token: str = "") -> str:
    """Authenticate with an external service."""
    return f"Authenticated with {service_name}"

task = Task(
    description="Authenticate with the payment service",
    tools=[authenticate_service]
)

agent = Agent(model="openai/gpt-4o")
result = agent.do(task)
# User will be prompted for each field:
# Enter value for 'api_key': 
# Enter value for 'secret_token':

Combined User Interaction

Use both features together for maximum control:
@tool(
    requires_user_input=True,
    user_input_fields=["password"]
)
def reset_database(database_name: str, password: str = "") -> str:
    """Reset a database (requires confirmation and password)."""
    return f"Database '{database_name}' has been reset."

External Tool Execution (Human-in-the-Loop)

Execute tools outside the agent’s automatic flow, enabling human-in-the-loop workflows for security-sensitive operations, external integrations, and cost control.

Basic External Execution

@tool(external_execution=True)
def execute_shell_command(command: str) -> str:
    """Execute a shell command externally."""
    import subprocess
    return subprocess.check_output(command, shell=True).decode("utf-8")

task = Task(
    description="List files in the current directory",
    tools=[execute_shell_command]
)

agent = Agent(model="openai/gpt-4o")

# Initial execution - agent pauses when tool is needed
result = agent.do(task)

# Check if execution paused
if task.is_paused and task.tools_awaiting_external_execution:
    # Execute each tool externally
    for tool_call in task.tools_awaiting_external_execution:
        print(f"Tool: {tool_call.tool_name}")
        print(f"Args: {tool_call.args}")
        
        # Execute the tool yourself
        external_result = execute_shell_command(**tool_call.args)
        
        # Set the result back
        tool_call.result = external_result
    
    # Continue execution with the results
    final_result = agent.continue_run(task)

Security-Sensitive Operations

@tool(external_execution=True)
def delete_production_data(table_name: str) -> str:
    """Delete data from production (requires manual execution)."""
    return f"Deleted data from {table_name}"

# Review before executing
if task.is_paused:
    for tool_call in task.tools_awaiting_external_execution:
        print(f"⚠️  About to delete from: {tool_call.args['table_name']}")
        confirm = input("Proceed? (yes/no): ")
        
        if confirm == "yes":
            tool_call.result = delete_production_data(**tool_call.args)
        else:
            tool_call.result = "Operation cancelled by user"
    
    final_result = agent.continue_run(task)

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)

When to Use Each Feature

Use requires_confirmation for:
  • Destructive operations (delete, reset, drop)
  • Sensitive actions (send email, publish content)
  • Financial transactions (transfer money, make payment)
Use requires_user_input for:
  • Credentials (API keys, passwords, tokens)
  • Personal information (name, age, address)
  • Dynamic parameters that change per execution
  • Sensitive data that shouldn’t be in code
Use external_execution for:
  • Security-sensitive operations requiring manual review
  • External integrations with special credentials
  • Hardware or network resources not accessible to the agent
  • Cost control over expensive operations
  • Compliance and audit requirements

Best Practices Summary

  1. Clear Purpose: Each tool should have a single, well-defined responsibility
  2. Type Safety: All parameters and return values must have explicit type hints
  3. Documentation: Comprehensive docstrings explaining usage and expected behavior
  4. Error Handling: Graceful failure handling with meaningful error messages
  5. Security: Use requires_user_input for credentials, never hardcode sensitive data
  6. Validation: Always validate inputs before processing
  7. User Experience: Provide clear prompts and helpful error messages
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.
I