In this chapter, we'll give the AI the ability to do things beyond just responding with text. We'll add a weather tool that demonstrates how AI can call functions to retrieve real-time information.
By the end of this chapter, you'll understand:
- What AI tools are and why they're powerful
- The anatomy of a tool (description, schema, execute)
- How the AI decides when to use tools
- How to render tool results in the UI
Tools allow the AI to:
- Fetch real-time data (weather, stock prices, etc.)
- Perform calculations
- Interact with external APIs
- Execute code
- Create documents
When you ask "What's the weather in London?", instead of guessing, the AI can call a tool to get the actual weather data.
Every tool has three parts:
import { tool } from "ai";
import { z } from "zod";
const myTool = tool({
// 1. Description - tells the AI when to use this tool
description: "Get current weather for a location",
// 2. Input Schema - what inputs the tool accepts (Zod schema)
inputSchema: z.object({
location: z.string().describe("City name"),
}),
// 3. Execute - the function that runs when called
execute: async ({ location }) => {
// Fetch weather data...
return { temperature: 72, conditions: "sunny" };
},
});Here's the complete weather tool implementation with city name geocoding:
import { tool } from "ai";
import { z } from "zod";
// Helper function to convert city names to coordinates
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(),
}),
execute: async (input) => {
let latitude: number;
let longitude: number;
// If city name provided, geocode it to coordinates
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.",
};
}
// Fetch weather data from Open-Meteo API
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();
// Include city name in response if provided
if ("city" in input) {
weatherData.cityName = input.city;
}
return weatherData;
},
});- Geocoding: The
geocodeCityhelper converts city names to coordinates using the free Open-Meteo Geocoding API - Flexible Input: Accepts either a city name OR latitude/longitude coordinates
- Error Handling: Returns helpful error messages if geocoding fails
- Real Data: Uses the Open-Meteo Weather API for actual weather data
Add the tool to your chat route. The route already has streaming set up - you just need to add the tool:
// Add this import at the top
import { getWeather } from "@/lib/ai/tools/get-weather";
// Inside the POST handler, the streamText call should include:
const result = streamText({
model: myProvider.languageModel(selectedChatModel),
system: systemPrompt({ selectedChatModel, requestHints }),
messages: convertToModelMessages(uiMessages),
// Stop after 5 tool call steps
stopWhen: stepCountIs(5),
// Disable tools for reasoning models
experimental_activeTools:
selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"],
experimental_transform: smoothStream({ chunking: "word" }),
// Add tools here!
tools: {
getWeather,
},
});The full route handler uses createUIMessageStream with JsonToSseTransformStream for streaming - the key thing is adding getWeather to the tools object and "getWeather" to experimental_activeTools.
stopWhen: stepCountIs(5): Limits tool call chains to 5 stepsexperimental_activeTools: Conditionally enables/disables tools (disabled for reasoning model)tools: Object containing all available toolsrequestHints: Contains user's location (useful for "What's the weather?" without a city)
┌─────────────────────────────────────────────────────────┐
│ User: "What's the weather in Paris?" │
│ ↓ │
│ AI thinks: "I should use the getWeather tool" │
│ ↓ │
│ AI generates tool call: │
│ { name: "getWeather", args: { city: "Paris" }} │
│ ↓ │
│ Tool executes: │
│ 1. geocodeCity("Paris") → { lat: 48.8, lon: 2.3 } │
│ 2. fetch weather data from Open-Meteo API │
│ 3. returns: { temperature: 18, cityName: "Paris" } │
│ ↓ │
│ AI receives result and generates response: │
│ "The current temperature in Paris is 18°C" │
└─────────────────────────────────────────────────────────┘
Tool calls and results appear as special message parts. Here's how to render them:
"use client";
type WeatherProps = {
current: {
temperature_2m: number;
};
current_units: {
temperature_2m: string;
};
cityName?: string;
};
export function Weather({ current, current_units, cityName }: WeatherProps) {
return (
<div className="flex items-center gap-2 rounded-lg border p-4">
<span className="text-2xl">🌡️</span>
<div>
<p className="font-semibold">
{cityName ? `Weather in ${cityName}` : "Current Weather"}
</p>
<p className="text-3xl">
{current.temperature_2m}
{current_units.temperature_2m}
</p>
</div>
</div>
);
}// In your message rendering component
{message.parts?.map((part, index) => {
if (part.type === "tool-result" && part.toolName === "getWeather") {
return <Weather key={index} {...part.result} />;
}
// ... handle other part types
})}The AI will use tools based on their description field, so you don't need to update the system prompt. However, you can optionally add tool documentation to help the AI understand when to use tools.
The system prompt is built from multiple parts. The simplest way to add tool info is to update the regularPrompt constant:
// lib/ai/prompts.ts
// Update this constant to include tool documentation
export const regularPrompt = `You are a friendly study buddy assistant! Keep your responses concise and helpful.
## Tools Available
- **getWeather**: Use this when users ask about weather conditions.
You can provide a city name like "Paris" or "Tokyo".
`;
// The systemPrompt function combines regularPrompt with location hints
// No need to change this function - it already works!
export const systemPrompt = ({
selectedChatModel,
requestHints,
}: {
selectedChatModel: string;
requestHints: RequestHints;
}) => {
const requestPrompt = getRequestPromptFromHints(requestHints);
return `${regularPrompt}\n\n${requestPrompt}`;
};Note: The requestHints add the user's location context (city, country, lat/lon), which is useful for the weather tool - the AI can use the user's location as a default if they just say "What's the weather?"
Now that you've wired up the weather tool, test it with these prompts. Click the "What is the weather in San Francisco?" button in the chat, or try these variations:
What is the weather in San Francisco?
What's the weather like in Tokyo right now?
Is it cold in London today?
What about New York?
Compare the weather in Paris and Berlin
Should I bring an umbrella to Seattle?
What to observe:
- The AI recognizes weather-related queries and calls the
getWeathertool - Tool calls appear in the message stream before the final response
- The weather data is formatted nicely in the chat UI
- The AI can handle follow-up location questions
Troubleshooting:
- If you see "I don't have access to real-time weather", check that the tool is properly added to the
toolsobject in your chat route - If the city isn't found, try using a more common spelling or a larger nearby city
- Check the browser console for any API errors
Create a simple calculator tool:
- Create
lib/ai/tools/calculator.ts:
import { tool } from "ai";
import { z } from "zod";
export const calculate = tool({
description: "Perform mathematical calculations",
inputSchema: z.object({
expression: z.string().describe("Math expression like '2 + 2' or '10 * 5'"),
}),
execute: async ({ expression }) => {
// Simple eval for demo (use a proper math library in production!)
const result = eval(expression);
return { expression, result };
},
});- Add it to the chat route alongside
getWeather - Test it: "What's 15 * 7?"
| Concept | Description |
|---|---|
description |
Tells AI when to use the tool |
inputSchema |
Zod schema defining expected inputs |
execute |
Async function that runs when called |
maxSteps |
How many tool calls allowed per request |
| Tool Result | The data returned from execute |
In Chapter 2, we'll take tools further by creating our first Agent. Instead of just fetching data, agents can make their own AI calls - think of them as specialized assistants with their own personality and capabilities.
| File | Changes |
|---|---|
lib/ai/tools/get-weather.ts |
New file - weather tool |
app/(chat)/api/chat/route.ts |
Added tools to streamText |
lib/ai/prompts.ts |
Updated with tool documentation |
components/weather.tsx |
New file - weather UI |