Learn how to create custom AI tools for your chatbot with advanced patterns and real-world examples
AI tools allow your chatbot to interact with external APIs, manipulate data, and create rich interactive experiences. This guide covers advanced tool building patterns from the chatbot’s implementation.
Let’s examine the weather tool, which demonstrates API integration and user approval:
lib/ai/tools/get-weather.ts
import { tool } from "ai";import { z } from "zod";async function geocodeCity( city: string): Promise<{ latitude: number; longitude: number } | null> { try { const response = await fetch( `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json` ); if (!response.ok) return null; const data = await response.json(); if (!data.results || data.results.length === 0) return null; const result = data.results[0]; return { latitude: result.latitude, longitude: result.longitude, }; } catch { return null; }}export const getWeather = tool({ description: "Get the current weather at a location. You can provide either coordinates or a city name.", inputSchema: z.object({ latitude: z.number().optional(), longitude: z.number().optional(), city: z .string() .describe("City name (e.g., 'San Francisco', 'New York', 'London')") .optional(), }), needsApproval: true, execute: async (input) => { let latitude: number; let longitude: number; if (input.city) { const coords = await geocodeCity(input.city); if (!coords) { return { error: `Could not find coordinates for "${input.city}". Please check the city name.`, }; } latitude = coords.latitude; longitude = coords.longitude; } else if (input.latitude !== undefined && input.longitude !== undefined) { latitude = input.latitude; longitude = input.longitude; } else { return { error: "Please provide either a city name or both latitude and longitude coordinates.", }; } const response = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto` ); const weatherData = await response.json(); if ("city" in input) { weatherData.cityName = input.city; } return weatherData; },});
The needsApproval: true flag requires user confirmation before executing the tool. This is useful for tools that make external API calls or perform sensitive operations.
The document creation tool demonstrates streaming, session handling, and complex state management:
lib/ai/tools/create-document.ts
import { tool, type UIMessageStreamWriter } from "ai";import type { Session } from "next-auth";import { z } from "zod";import { artifactKinds, documentHandlersByArtifactKind,} from "@/lib/artifacts/server";import type { ChatMessage } from "@/lib/types";import { generateUUID } from "@/lib/utils";type CreateDocumentProps = { session: Session; dataStream: UIMessageStreamWriter<ChatMessage>;};export const createDocument = ({ session, dataStream }: CreateDocumentProps) => tool({ description: "Create a document for a writing or content creation activities. This tool will call other functions that will generate the contents of the document based on the title and kind.", inputSchema: z.object({ title: z.string(), kind: z.enum(artifactKinds), }), execute: async ({ title, kind }) => { const id = generateUUID(); dataStream.write({ type: "data-kind", data: kind, transient: true, }); dataStream.write({ type: "data-id", data: id, transient: true, }); dataStream.write({ type: "data-title", data: title, transient: true, }); dataStream.write({ type: "data-clear", data: null, transient: true, }); const documentHandler = documentHandlersByArtifactKind.find( (documentHandlerByArtifactKind) => documentHandlerByArtifactKind.kind === kind ); if (!documentHandler) { throw new Error(`No document handler found for kind: ${kind}`); } await documentHandler.onCreateDocument({ id, title, dataStream, session, }); dataStream.write({ type: "data-finish", data: null, transient: true }); return { id, title, kind, content: "A document was created and is now visible to the user.", }; }, });
The suggestions tool demonstrates streaming structured output and nested AI calls:
lib/ai/tools/request-suggestions.ts
import { Output, streamText, tool, type UIMessageStreamWriter } from "ai";import type { Session } from "next-auth";import { z } from "zod";import { getDocumentById, saveSuggestions } from "@/lib/db/queries";import type { Suggestion } from "@/lib/db/schema";import type { ChatMessage } from "@/lib/types";import { generateUUID } from "@/lib/utils";import { getArtifactModel } from "../providers";type RequestSuggestionsProps = { session: Session; dataStream: UIMessageStreamWriter<ChatMessage>;};export const requestSuggestions = ({ session, dataStream,}: RequestSuggestionsProps) => tool({ description: "Request writing suggestions for an existing document artifact. Only use this when the user explicitly asks to improve or get suggestions for a document they have already created. Never use for general questions.", inputSchema: z.object({ documentId: z .string() .describe( "The UUID of an existing document artifact that was previously created with createDocument" ), }), execute: async ({ documentId }) => { const document = await getDocumentById({ id: documentId }); if (!document || !document.content) { return { error: "Document not found", }; } const suggestions: Omit< Suggestion, "userId" | "createdAt" | "documentCreatedAt" >[] = []; const { partialOutputStream } = streamText({ model: getArtifactModel(), system: "You are a help writing assistant. Given a piece of writing, please offer suggestions to improve the piece of writing and describe the change. It is very important for the edits to contain full sentences instead of just words. Max 5 suggestions.", prompt: document.content, output: Output.array({ element: z.object({ originalSentence: z.string().describe("The original sentence"), suggestedSentence: z.string().describe("The suggested sentence"), description: z .string() .describe("The description of the suggestion"), }), }), }); let processedCount = 0; for await (const partialOutput of partialOutputStream) { if (!partialOutput) continue; for (let i = processedCount; i < partialOutput.length; i++) { const element = partialOutput[i]; if ( !element?.originalSentence || !element?.suggestedSentence || !element?.description ) { continue; } const suggestion = { originalText: element.originalSentence, suggestedText: element.suggestedSentence, description: element.description, id: generateUUID(), documentId, isResolved: false, }; dataStream.write({ type: "data-suggestion", data: suggestion as Suggestion, transient: true, }); suggestions.push(suggestion); processedCount++; } } if (session.user?.id) { await saveSuggestions({ suggestions: suggestions.map((suggestion) => ({ ...suggestion, userId: session.user.id, createdAt: new Date(), documentCreatedAt: document.createdAt, })), }); } return { id: documentId, title: document.title, kind: document.kind, message: "Suggestions have been added to the document", }; }, });
The tool uses streamText within the execute function to generate structured suggestions:
const { partialOutputStream } = streamText({ model: getArtifactModel(), system: "You are a help writing assistant...", prompt: document.content, output: Output.array({ element: z.object({ originalSentence: z.string(), suggestedSentence: z.string(), description: z.string(), }), }),});
Streaming structured output
The tool processes suggestions as they’re generated and streams them to the UI:
let processedCount = 0;for await (const partialOutput of partialOutputStream) { if (!partialOutput) continue; for (let i = processedCount; i < partialOutput.length; i++) { const element = partialOutput[i]; // Process and stream each suggestion dataStream.write({ type: "data-suggestion", data: suggestion as Suggestion, transient: true, }); processedCount++; }}
Database persistence
After streaming all suggestions, they’re saved to the database:
Write detailed descriptions that help the AI understand when and how to use the tool:
description: "Request writing suggestions for an existing document artifact. Only use this when the user explicitly asks to improve or get suggestions for a document they have already created. Never use for general questions.",
2
Detailed parameter descriptions
Use Zod’s .describe() method to provide context for each parameter:
documentId: z .string() .describe( "The UUID of an existing document artifact that was previously created with createDocument" ),
3
Error handling
Return structured error objects instead of throwing:
if (!document) { return { error: "Document not found", };}