AgentDock Core Documentation

Custom Tool Development

This guide provides a comprehensive overview of creating custom tools for AgentDock, with complete examples and best practices.

Introduction

Custom tools extend the capabilities of AI agents in AgentDock, allowing them to perform specialized tasks such as searching the web, analyzing data, or integrating with external APIs. Tools are essentially specialized nodes that follow a consistent pattern, making them easy to create and maintain.

Tool Structure in the Reference Implementation

In the AgentDock reference implementation, tools are organized as follows:

src/nodes/[tool-name]/
├── index.ts        # Main tool implementation
├── components.tsx  # React components for output
└── utils.ts        # Helper functions (optional)

Complete Custom Tool Example: Weather Tool

Let's walk through creating a complete weather forecasting tool.

Step 1: Create Directory Structure

src/nodes/weather/
├── index.ts        # Main implementation
├── components.tsx  # Output formatting
└── utils.ts        # API utilities

Step 2: Implement the Tool

// index.ts
import { z } from 'zod';
import { Tool } from '../types';
import { logger, LogCategory } from '@/lib/logger';
import { createToolResult, formatErrorMessage } from '@/lib/utils/markdown-utils';
import { WeatherForecast } from './components';
import { fetchWeatherData } from './utils';

// Parameter schema for the weather tool
const weatherSchema = z.object({
  location: z.string().describe('City name or location to get weather for'),
  days: z.number().optional().default(3).describe('Number of days to forecast (1-7)')
});

// Weather tool implementation
export const weatherTool: Tool = {
  name: 'weather',
  description: 'Get weather forecast for any location',
  parameters: weatherSchema,
  async execute({ location, days = 3 }, options) {
    try {
      // Validate input
      if (!location) {
        return createToolResult(
          'weather_error',
          formatErrorMessage('Error', 'Location is required')
        );
      }

      // Limit days to a reasonable range
      const forecastDays = Math.min(Math.max(days, 1), 7);
      
      // Fetch weather data
      const weatherData = await fetchWeatherData(location, forecastDays);
      
      // Return formatted results
      return WeatherForecast({
        location: weatherData.location.name,
        days: weatherData.forecast.forecastday
      });
    } catch (error) {
      // Log and handle errors
      logger.error(LogCategory.NODE, '[Weather]', 'Weather tool error:', { error, location });
      const errorMessage = error instanceof Error ? error.message : String(error);
      
      return createToolResult(
        'weather_error',
        formatErrorMessage('Error', `Unable to get weather for "${location}": ${errorMessage}`)
      );
    }
  }
};

// Export for auto-registration
export const tools = {
  weather: weatherTool
};

Step 3: Create Output Components

// components.tsx
import { formatBold, formatHeader, formatItalic, joinSections, createToolResult } from '@/lib/utils/markdown-utils';

// Types for weather data
export interface WeatherDay {
  date: string;
  day: {
    maxtemp_c: number;
    mintemp_c: number;
    condition: {
      text: string;
      icon: string;
    };
    daily_chance_of_rain: number;
  };
}

export interface WeatherForecastProps {
  location: string;
  days: WeatherDay[];
}

// Component to format weather forecast output
export function WeatherForecast(props: WeatherForecastProps) {
  const { location, days } = props;
  
  // Format each day's forecast
  const forecastDays = days.map(day => {
    const date = new Date(day.date).toLocaleDateString('en-US', {
      weekday: 'long',
      month: 'short',
      day: 'numeric'
    });
    
    return `${formatBold(date)}
Temperature: ${day.day.mintemp_c}°C to ${day.day.maxtemp_c}°C
Condition: ${day.day.condition.text}
Chance of rain: ${day.day.daily_chance_of_rain}%`;
  });
  
  // Combine into a single result
  return createToolResult(
    'weather_forecast',
    joinSections(
      formatHeader(`Weather forecast for ${location}`),
      forecastDays.join('\n\n')
    )
  );
}

Step 4: Implement API Utilities

// utils.ts
import { z } from 'zod';

// Type validation for API response
const weatherResponseSchema = z.object({
  location: z.object({
    name: z.string(),
    region: z.string(),
    country: z.string(),
  }),
  forecast: z.object({
    forecastday: z.array(z.object({
      date: z.string(),
      day: z.object({
        maxtemp_c: z.number(),
        mintemp_c: z.number(),
        condition: z.object({
          text: z.string(),
          icon: z.string(),
        }),
        daily_chance_of_rain: z.number(),
      })
    }))
  })
});

// Function to fetch weather data from API
export async function fetchWeatherData(location: string, days: number) {
  // Get API key from environment variable
  const apiKey = process.env.WEATHER_API_KEY;
  if (!apiKey) {
    throw new Error('Weather API key not configured');
  }
  
  // Make API request
  const response = await fetch(
    `https://api.weatherapi.com/v1/forecast.json?key=${apiKey}&q=${encodeURIComponent(location)}&days=${days}&aqi=no&alerts=no`
  );
  
  // Handle API errors
  if (!response.ok) {
    let errorText = `API error: ${response.status}`;
    try {
      const errorData = await response.json();
      if (errorData.error && errorData.error.message) {
        errorText = errorData.error.message;
      }
    } catch (e) {
      // Ignore JSON parsing errors
    }
    throw new Error(errorText);
  }
  
  // Parse and validate response
  const data = await response.json();
  return weatherResponseSchema.parse(data);
}

Using the LLM in Custom Tools

Custom tools can access the agent's LLM instance for generating content or analyzing data:

// Example: Using LLM in a news summarization tool
async execute({ query }, options) {
  try {
    // Fetch news articles
    const articles = await fetchNewsArticles(query);
    
    // Use LLM to generate a summary if available
    if (options.llmContext?.llm) {
      // Format articles for the LLM
      const articlesText = articles.map(a => 
        `TITLE: ${a.title}\nSUMMARY: ${a.description}`
      ).join('\n\n');
      
      // Create prompt for the LLM
      const messages = [
        {
          role: 'system',
          content: 'You are a news summarization assistant. Create a concise summary of these news articles about the given topic. Focus on the most important information and common themes.'
        },
        {
          role: 'user',
          content: `Topic: ${query}\n\nArticles:\n${articlesText}\n\nPlease create a concise summary of these news articles.`
        }
      ];
      
      // Generate summary with the LLM
      const result = await options.llmContext.llm.generateText({ 
        messages,
        temperature: 0.3,
        maxTokens: 500
      });
      
      // Return formatted result
      return NewsSummary({ 
        query, 
        articles,
        summary: result.text
      });
    }
    
    // Fallback if LLM is not available
    return NewsSummary({ 
      query, 
      articles,
      summary: "AI-generated summary not available. Please review the article excerpts below."
    });
  } catch (error) {
    // Error handling
    return createToolResult(
      'news_error',
      formatErrorMessage('Error', `Failed to fetch news: ${error.message}`)
    );
  }
}

Tool Registration Process

Tools are automatically registered when imported by the src/nodes/init.ts file:

// src/nodes/init.ts example
import { tools as searchTools } from './search';
import { tools as weatherTools } from './weather';
import { tools as stockPriceTools } from './stock-price';
// ... other tool imports

// Combine all tools into a single object
export const allTools = {
  ...searchTools,
  ...weatherTools,
  ...stockPriceTools,
  // ... other tools
};

// Register tools with the registry
export function registerTools() {
  const registry = getToolRegistry();
  Object.entries(allTools).forEach(([name, tool]) => {
    registry.registerTool(name, tool);
  });
}

Advanced Tool Features

1. Tool Chaining

Tools can use the results of previous tools:

// Research tool using search results
export const researchTool: Tool = {
  name: 'research',
  description: 'Research a topic in depth',
  parameters: researchSchema,
  async execute({ query, depth = 2 }, options) {
    // First, search for information
    const searchResults = await searchWeb(query);
    
    // Then, analyze the results with the LLM
    if (options.llmContext?.llm) {
      const analysis = await options.llmContext.llm.generateText({
        messages: [
          {
            role: 'system',
            content: 'Analyze these search results and identify key insights.'
          },
          {
            role: 'user',
            content: `Analyze these search results about "${query}": ${JSON.stringify(searchResults)}`
          }
        ]
      });
      
      return ResearchResults({ query, results: searchResults, analysis: analysis.text });
    }
    
    return ResearchResults({ query, results: searchResults });
  }
};

2. Multi-Step Tools

Tools can implement multi-step processes:

// Multi-step data analysis tool
export const dataAnalysisTool: Tool = {
  name: 'analyze_data',
  description: 'Analyze data in multiple steps',
  parameters: dataAnalysisSchema,
  async execute({ dataset, analysis_type }, options) {
    try {
      // Step 1: Load and validate data
      const data = await loadDataset(dataset);
      
      // Step 2: Perform statistical analysis
      const stats = performStatisticalAnalysis(data, analysis_type);
      
      // Step 3: Generate insights with LLM
      let insights = "Statistical analysis complete";
      if (options.llmContext?.llm) {
        const result = await options.llmContext.llm.generateText({
          messages: [
            {
              role: 'system',
              content: `You are a data analysis expert. Generate insights based on the statistical analysis of a dataset.`
            },
            {
              role: 'user',
              content: `Dataset: ${dataset}\nAnalysis Type: ${analysis_type}\nStatistics: ${JSON.stringify(stats)}\n\nWhat insights can we draw from this analysis?`
            }
          ]
        });
        insights = result.text;
      }
      
      // Return formatted results
      return DataAnalysisResults({
        dataset,
        analysis_type,
        statistics: stats,
        insights
      });
    } catch (error) {
      return createToolResult(
        'analysis_error',
        formatErrorMessage('Error', `Analysis failed: ${error.message}`)
      );
    }
  }
};

Best Practices

1. Input Validation

Always validate inputs with Zod schemas:

const stockPriceSchema = z.object({
  symbol: z.string().describe('Stock ticker symbol (e.g., AAPL, MSFT)'),
  period: z.enum(['1d', '1w', '1m', '3m', '6m', '1y', '5y'])
    .default('1m')
    .describe('Time period for historical data')
});

2. Error Handling

Implement comprehensive error handling:

try {
  // Tool logic
} catch (error) {
  logger.error(LogCategory.NODE, '[MyTool]', 'Execution error:', { error });
  
  // Provide user-friendly error messages
  let errorMessage = 'An unexpected error occurred';
  if (error instanceof Error) {
    errorMessage = error.message;
  } else if (typeof error === 'string') {
    errorMessage = error;
  }
  
  return createToolResult(
    'error',
    formatErrorMessage('Error', errorMessage)
  );
}

3. Output Formatting

Format outputs consistently:

return createToolResult(
  'my_tool_result',
  joinSections(
    formatHeader(`Results for ${query}`),
    formatBold('Key Findings:'),
    results.join('\n\n')
  )
);

4. API Security

Secure API access:

// Never include API keys in the code
const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
  throw new Error('API key not configured');
}

// Use HTTPS for all external requests
const response = await fetch(`https://api.example.com/data?key=${apiKey}&q=${encodeURIComponent(query)}`);

// Validate API responses
if (!response.ok) {
  throw new Error(`API error: ${response.status}`);
}

5. Performance

Optimize performance:

// Cache expensive operations
const cachedResults = await redis.get(`cache:${cacheKey}`);
if (cachedResults) {
  return JSON.parse(cachedResults);
}

// Set reasonable timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
  const response = await fetch(url, { signal: controller.signal });
  // Process response
} finally {
  clearTimeout(timeoutId);
}

Troubleshooting

Common issues and solutions:

1. Tool Not Registered

If your tool isn't appearing in the agent:

// Make sure you're exporting the tools object
export const tools = {
  my_tool: myTool
};

// Check that your tool is imported in src/nodes/init.ts

2. Parameter Errors

If parameters aren't working correctly:

// Use descriptive parameter names
const searchSchema = z.object({
  query: z.string().describe('Search query to look up'),
  // NOT: q: z.string().describe('Search query')
});

// Set reasonable defaults for optional parameters
const limit = z.number().optional().default(10).describe('Maximum results');

3. Output Not Displaying

If tool output isn't displaying correctly:

// Make sure you're using createToolResult
return createToolResult('my_tool_result', formattedOutput);

// NOT: return formattedOutput;

More Examples

Stock Price Tool

// stock-price/index.ts
import { z } from 'zod';
import { Tool } from '../types';
import { StockPriceResults } from './components';
import { fetchStockData } from './utils';

const stockPriceSchema = z.object({
  symbol: z.string().describe('Stock ticker symbol (e.g., AAPL, MSFT)'),
  period: z.enum(['1d', '1w', '1m', '3m', '6m', '1y', '5y'])
    .default('1m')
    .describe('Time period for historical data')
});

export const stockPriceTool: Tool = {
  name: 'stock_price',
  description: 'Get stock price information and historical data',
  parameters: stockPriceSchema,
  async execute({ symbol, period = '1m' }) {
    try {
      const stockData = await fetchStockData(symbol, period);
      return StockPriceResults({ symbol, period, data: stockData });
    } catch (error) {
      return createToolResult(
        'stock_price_error',
        formatErrorMessage('Error', `Unable to get stock data for ${symbol}: ${error.message}`)
      );
    }
  }
};

export const tools = {
  stock_price: stockPriceTool
};

Image Analysis Tool

// image-analysis/index.ts
import { z } from 'zod';
import { Tool } from '../types';
import { analyzeImage } from './utils';
import { ImageAnalysisResults } from './components';

const imageAnalysisSchema = z.object({
  url: z.string().url().describe('URL of the image to analyze'),
  analysis_type: z.enum(['objects', 'faces', 'text', 'colors'])
    .default('objects')
    .describe('Type of analysis to perform on the image')
});

export const imageAnalysisTool: Tool = {
  name: 'analyze_image',
  description: 'Analyze an image to detect objects, faces, text, or colors',
  parameters: imageAnalysisSchema,
  async execute({ url, analysis_type = 'objects' }) {
    try {
      const results = await analyzeImage(url, analysis_type);
      return ImageAnalysisResults({ url, analysis_type, results });
    } catch (error) {
      return createToolResult(
        'image_analysis_error',
        formatErrorMessage('Error', `Image analysis failed: ${error.message}`)
      );
    }
  }
};

export const tools = {
  analyze_image: imageAnalysisTool
};

Conclusion

Custom tools are a powerful way to extend AgentDock's capabilities. By following the patterns and practices outlined in this guide, you can create sophisticated tools that enhance your AI agents with specialized functionality.

For more information, refer to the Node System documentation and the AgentDock Core API reference.