ProductReadyProductReady

Custom Agent Tools

Create and use custom tools with AI agents for function calling

Custom Agent Tools

Custom tools extend your AI agents with specific capabilities through function calling. All agent providers in ProductReady support the AI SDK tool interface for seamless integration.


Overview

Tools allow AI agents to:

  • Execute functions - Call custom code during conversations
  • Access external data - Query APIs, databases, or services
  • Perform actions - Modify data, send messages, or trigger workflows
  • Return structured results - Provide typed responses to the agent

Tool Interface

All tools follow the AI SDK standard interface:

import { tool } from "ai";
import { z } from "zod";

const myTool = tool({
  description: "Clear description of what this tool does",
  inputSchema: z.object({
    // Zod schema defining expected inputs
  }),
  execute: async (input) => {
    // Implementation logic
    return result;
  },
});

Creating Custom Tools

Basic Tool Example

import { tool } from "ai";
import { z } from "zod";

const getWeatherTool = tool({
  description: "Get current weather for a specified location",
  inputSchema: z.object({
    location: z.string().describe("City name or address"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  }),
  execute: async ({ location, units }) => {
    // Call weather API
    const response = await fetch(
      `https://api.weather.com/v1/current?location=${location}&units=${units}`
    );
    const data = await response.json();
    
    return {
      temperature: data.temp,
      condition: data.condition,
      humidity: data.humidity,
    };
  },
});

Tool with Database Access

import { tool } from "ai";
import { z } from "zod";
import { db } from "~/db/client";
import { users } from "~/db/schema";
import { eq } from "drizzle-orm";

const getUserInfoTool = tool({
  description: "Retrieve user information from database",
  inputSchema: z.object({
    userId: z.string().describe("User ID to look up"),
  }),
  execute: async ({ userId }) => {
    const user = await db.query.users.findFirst({
      where: eq(users.id, userId),
    });
    
    if (!user) {
      return { error: "User not found" };
    }
    
    return {
      name: user.name,
      email: user.email,
      createdAt: user.createdAt,
    };
  },
});

Tool with Error Handling

import { tool } from "ai";
import { z } from "zod";

const calculateTool = tool({
  description: "Perform mathematical calculations",
  inputSchema: z.object({
    expression: z.string().describe("Mathematical expression to evaluate"),
  }),
  execute: async ({ expression }) => {
    try {
      // Safe evaluation (use a proper math library in production)
      const result = evaluateExpression(expression);
      
      return {
        success: true,
        result,
        expression,
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : "Calculation failed",
        expression,
      };
    }
  },
});

Using Tools with Providers

AI SDK Provider with Tools

import { AiSdkProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";

// Define your tools
const tools = {
  get_weather: tool({
    description: "Get weather for a location",
    inputSchema: z.object({
      location: z.string(),
    }),
    execute: async ({ location }) => {
      return { temperature: 72, condition: "sunny" };
    },
  }),
  
  search_web: tool({
    description: "Search the web for information",
    inputSchema: z.object({
      query: z.string(),
    }),
    execute: async ({ query }) => {
      // Implement web search
      return { results: [] };
    },
  }),
};

// Create provider with tools
const provider = new AiSdkProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools,
});

// Use the provider
const result = await provider.stream(messages);

Gaia Agent Provider with Tools

import { GaiaAgentProvider } from "agentlib/providers";
import { tool } from "ai";
import { z } from "zod";

const tools = {
  generate_article: tool({
    description: "Generate a blog article on a specific topic",
    inputSchema: z.object({
      title: z.string(),
      keywords: z.array(z.string()),
      minWords: z.number().default(500),
    }),
    execute: async ({ title, keywords, minWords }) => {
      // Article generation logic
      return {
        title,
        content: "Generated article content...",
        wordCount: 750,
      };
    },
  }),
};

const provider = new GaiaAgentProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools,
});

Advanced Tool Patterns

Tool with External API Integration

import { tool } from "ai";
import { z } from "zod";

const githubSearchTool = tool({
  description: "Search GitHub repositories",
  inputSchema: z.object({
    query: z.string().describe("Search query"),
    language: z.string().optional().describe("Programming language filter"),
    stars: z.number().optional().describe("Minimum star count"),
  }),
  execute: async ({ query, language, stars }) => {
    const params = new URLSearchParams({ q: query });
    if (language) params.append("language", language);
    if (stars) params.append("stars", `>=${stars}`);
    
    const response = await fetch(
      `https://api.github.com/search/repositories?${params}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
        },
      }
    );
    
    const data = await response.json();
    
    return {
      total: data.total_count,
      repositories: data.items.slice(0, 5).map((repo: any) => ({
        name: repo.full_name,
        description: repo.description,
        stars: repo.stargazers_count,
        url: repo.html_url,
      })),
    };
  },
});

Tool with File Operations

import { tool } from "ai";
import { z } from "zod";
import { promises as fs } from "fs";
import path from "path";

const saveFileTool = tool({
  description: "Save content to a file in the workspace",
  inputSchema: z.object({
    filename: z.string().describe("Name of the file"),
    content: z.string().describe("Content to write"),
    directory: z.string().default("output"),
  }),
  execute: async ({ filename, content, directory }) => {
    const dirPath = path.join(process.cwd(), directory);
    const filePath = path.join(dirPath, filename);
    
    // Ensure directory exists
    await fs.mkdir(dirPath, { recursive: true });
    
    // Write file
    await fs.writeFile(filePath, content, "utf-8");
    
    return {
      success: true,
      path: filePath,
      size: content.length,
    };
  },
});

Tool with Async Operations

import { tool } from "ai";
import { z } from "zod";

const processLargeDataTool = tool({
  description: "Process large dataset asynchronously",
  inputSchema: z.object({
    datasetId: z.string(),
    operation: z.enum(["analyze", "transform", "export"]),
  }),
  execute: async ({ datasetId, operation }) => {
    // Start async operation
    const jobId = await startBackgroundJob(datasetId, operation);
    
    // Poll for completion (simplified)
    const result = await waitForJobCompletion(jobId);
    
    return {
      jobId,
      status: "completed",
      result,
    };
  },
});

Tool Organization

Grouping Tools by Feature

// tools/weather.ts
export const weatherTools = {
  get_weather: tool({ /* ... */ }),
  get_forecast: tool({ /* ... */ }),
};

// tools/search.ts
export const searchTools = {
  search_web: tool({ /* ... */ }),
  search_docs: tool({ /* ... */ }),
};

// tools/index.ts
import { weatherTools } from "./weather";
import { searchTools } from "./search";

export const allTools = {
  ...weatherTools,
  ...searchTools,
};

Context-Specific Tools

import { GaiaAgentProvider } from "agentlib/providers";
import { weatherTools } from "./tools/weather";
import { searchTools } from "./tools/search";
import { databaseTools } from "./tools/database";

function getToolsForContext(context: string) {
  switch (context) {
    case "weather-app":
      return weatherTools;
    case "search-app":
      return searchTools;
    case "admin-panel":
      return { ...searchTools, ...databaseTools };
    default:
      return {};
  }
}

// Use in provider
const provider = new GaiaAgentProvider({
  model: "gpt-4",
  apiKey: process.env.OPENAI_API_KEY,
  tools: getToolsForContext("weather-app"),
});

Best Practices

1. Clear Tool Descriptions

// ✅ Good: Specific, actionable description
const goodTool = tool({
  description: "Search GitHub repositories by keyword, language, and minimum stars. Returns top 5 matching repositories with metadata.",
  // ...
});

// ❌ Bad: Vague description
const badTool = tool({
  description: "Search stuff",
  // ...
});

2. Detailed Input Schemas

// ✅ Good: Well-documented schema with descriptions
const goodSchema = z.object({
  query: z.string().describe("Search query (e.g., 'react hooks')"),
  language: z.string().optional().describe("Programming language filter (e.g., 'typescript')"),
  minStars: z.number().min(0).optional().describe("Minimum star count (default: 0)"),
});

// ❌ Bad: No descriptions
const badSchema = z.object({
  q: z.string(),
  lang: z.string().optional(),
  stars: z.number().optional(),
});

3. Robust Error Handling

const robustTool = tool({
  description: "Fetch user profile",
  inputSchema: z.object({
    userId: z.string(),
  }),
  execute: async ({ userId }) => {
    try {
      const user = await fetchUser(userId);
      
      if (!user) {
        return {
          success: false,
          error: "User not found",
          userId,
        };
      }
      
      return {
        success: true,
        user,
      };
    } catch (error) {
      console.error("Tool execution error:", error);
      return {
        success: false,
        error: error instanceof Error ? error.message : "Unknown error",
      };
    }
  },
});

4. Return Structured Data

// ✅ Good: Structured, typed response
execute: async ({ location }) => {
  return {
    location,
    temperature: 72,
    unit: "fahrenheit",
    condition: "sunny",
    humidity: 45,
    timestamp: new Date().toISOString(),
  };
}

// ❌ Bad: Unstructured string
execute: async ({ location }) => {
  return `It's 72°F and sunny in ${location}`;
}

5. Input Validation

const validatingTool = tool({
  description: "Process payment",
  inputSchema: z.object({
    amount: z.number().positive().max(10000),
    currency: z.enum(["USD", "EUR", "GBP"]),
    description: z.string().min(5).max(200),
  }),
  execute: async ({ amount, currency, description }) => {
    // Zod validates inputs automatically
    // Additional business logic validation
    if (amount > 1000 && !description.includes("verified")) {
      return {
        success: false,
        error: "Large payments require verification in description",
      };
    }
    
    // Process payment
    return { success: true, transactionId: "..." };
  },
});

Tool Security

Sanitize User Inputs

import { tool } from "ai";
import { z } from "zod";

const secureSearchTool = tool({
  description: "Search with sanitized input",
  inputSchema: z.object({
    query: z.string().max(200),
  }),
  execute: async ({ query }) => {
    // Sanitize input
    const sanitized = query
      .replace(/[^\w\s-]/g, "") // Remove special chars
      .trim()
      .toLowerCase();
    
    // Use sanitized input
    const results = await searchDatabase(sanitized);
    return results;
  },
});

Restrict File Access

const safeFileReadTool = tool({
  description: "Read file contents safely",
  inputSchema: z.object({
    filename: z.string(),
  }),
  execute: async ({ filename }) => {
    // Validate filename to prevent path traversal
    if (filename.includes("..") || filename.startsWith("/")) {
      return {
        error: "Invalid filename",
      };
    }
    
    // Restrict to allowed directory
    const allowedDir = path.join(process.cwd(), "data");
    const filePath = path.join(allowedDir, filename);
    
    if (!filePath.startsWith(allowedDir)) {
      return {
        error: "Access denied",
      };
    }
    
    const content = await fs.readFile(filePath, "utf-8");
    return { content };
  },
});

Rate Limiting

import { tool } from "ai";
import { z } from "zod";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = new Redis({ /* config */ });
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "1 m"),
});

const rateLimitedTool = tool({
  description: "API call with rate limiting",
  inputSchema: z.object({
    query: z.string(),
    userId: z.string(),
  }),
  execute: async ({ query, userId }) => {
    // Check rate limit
    const { success } = await ratelimit.limit(userId);
    
    if (!success) {
      return {
        error: "Rate limit exceeded. Please try again later.",
      };
    }
    
    // Execute tool logic
    const result = await performQuery(query);
    return result;
  },
});

Testing Tools

Unit Testing

import { describe, it, expect } from "vitest";
import { getWeatherTool } from "./weather-tools";

describe("getWeatherTool", () => {
  it("should return weather data for valid location", async () => {
    const result = await getWeatherTool.execute({
      location: "San Francisco",
      units: "celsius",
    });
    
    expect(result).toHaveProperty("temperature");
    expect(result).toHaveProperty("condition");
    expect(result.temperature).toBeTypeOf("number");
  });
  
  it("should handle invalid location", async () => {
    const result = await getWeatherTool.execute({
      location: "InvalidCity123",
      units: "celsius",
    });
    
    expect(result).toHaveProperty("error");
  });
});

Integration Testing

import { describe, it, expect } from "vitest";
import { GaiaAgentProvider } from "agentlib/providers";
import { myTools } from "./tools";

describe("Agent with custom tools", () => {
  it("should use tool correctly in conversation", async () => {
    const provider = new GaiaAgentProvider({
      model: "gpt-4",
      apiKey: process.env.OPENAI_API_KEY,
      tools: myTools,
    });
    
    const messages = [
      { role: "user", content: "What's the weather in Tokyo?" },
    ];
    
    const result = await provider.stream(messages);
    
    // Verify tool was called
    expect(result).toBeDefined();
  });
});

Troubleshooting

Tool Not Being Called

Issue: Agent doesn't use the tool even when it should.

Solutions:

  1. Improve tool description to be more specific
  2. Make input schema descriptions clearer
  3. Provide examples in the description
  4. Check if tool name is intuitive
// Before
tool({
  description: "Get data",
  // ...
});

// After
tool({
  description: "Get current weather data for any city. Example: 'What's the weather in Paris?' Returns temperature, condition, and humidity.",
  // ...
});

Tool Execution Errors

Issue: Tool throws errors during execution.

Solutions:

  1. Add comprehensive error handling
  2. Validate inputs beyond Zod schema
  3. Add logging for debugging
  4. Return meaningful error messages
execute: async (input) => {
  try {
    // Log inputs
    console.log("Tool called with:", input);
    
    // Validate
    if (!isValid(input)) {
      return { error: "Invalid input" };
    }
    
    // Execute
    const result = await performAction(input);
    return result;
  } catch (error) {
    console.error("Tool error:", error);
    return {
      error: error instanceof Error ? error.message : "Unknown error",
    };
  }
}

Schema Validation Failures

Issue: Inputs don't match expected schema.

Solutions:

  1. Make optional fields truly optional with .optional()
  2. Provide sensible defaults with .default()
  3. Add clear descriptions to all fields
  4. Test schema with various inputs

Next Steps

On this page