Skip to content

Commit d8726c2

Browse files
NiallJoeMaherclaude
andcommitted
docs: fix CHAPTER-1 to show correct systemPrompt and route patterns
- Updated system prompt section to show updating regularPrompt constant instead of replacing the entire systemPrompt function - Added note that system prompt already has requestHints for location - Updated route handler to show correct systemPrompt({ selectedChatModel, requestHints }) - Added explanation that full route uses createUIMessageStream 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 48410de commit d8726c2

File tree

1 file changed

+143
-114
lines changed

1 file changed

+143
-114
lines changed

CHAPTER-1.md

Lines changed: 143 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,6 @@
22

33
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.
44

5-
> **Branch**: `workshop/chapter-01-first-tool`
6-
> ```bash
7-
> git checkout workshop/chapter-01-first-tool
8-
> ```
9-
10-
---
11-
12-
## Teaching Notes for Presenters
13-
14-
### React Parallels
15-
16-
| AI SDK Concept | React Equivalent | Key Insight |
17-
|----------------|------------------|-------------|
18-
| `tool()` function | Custom hook definition | Encapsulates reusable logic with a defined interface |
19-
| `inputSchema` (Zod) | PropTypes / TypeScript props | Validates inputs before execution |
20-
| `execute` function | Event handler callback | Runs when the AI decides to use the tool |
21-
| Tool description | JSDoc / component documentation | Helps the AI "read" when to use it |
22-
23-
### Key Talking Points
24-
25-
1. **"Tools are just functions with extra metadata"**
26-
- The AI reads the `description` to decide when to call
27-
- Zod schemas ensure type-safe inputs (like TypeScript for AI)
28-
- `execute` is async - can fetch APIs, query DBs, anything
29-
30-
2. **"The AI decides, not you"**
31-
- You define WHAT the tool does, the AI decides WHEN
32-
- Good descriptions = accurate tool selection
33-
- Bad descriptions = confused AI, wrong tool calls
34-
35-
3. **"Tools don't break the chat"**
36-
- Tool results flow back into the conversation
37-
- The AI synthesizes results into natural language
38-
- Users see a seamless experience
39-
40-
### Live Demo Tips
41-
42-
- Show the Network tab to see tool calls as separate events
43-
- Intentionally ask something the tool can't handle ("What's the weather on Mars?")
44-
- Compare with/without tools: same question, different capabilities
45-
46-
### Common Questions
47-
48-
- **"Why Zod?"** - Runtime validation + TypeScript types in one schema
49-
- **"Can tools call other tools?"** - Not directly, but the AI can chain multiple tool calls
50-
- **"What if the API fails?"** - Handle errors gracefully in `execute`, return user-friendly messages
51-
52-
---
53-
545
## Learning Objectives
556

567
By the end of this chapter, you'll understand:
@@ -74,9 +25,6 @@ When you ask "What's the weather in London?", instead of guessing, the AI can **
7425

7526
Every tool has three parts:
7627

77-
<details>
78-
<summary>📄 <strong>Code: Tool Anatomy</strong> (click to expand)</summary>
79-
8028
```typescript
8129
import { tool } from "ai";
8230
import { z } from "zod";
@@ -98,86 +46,140 @@ const myTool = tool({
9846
});
9947
```
10048

101-
</details>
102-
103-
> 💡 **React Parallel**: This is like defining a custom hook with TypeScript props - the schema is your prop types, the execute is your hook logic, and the description is your JSDoc comment.
104-
10549
## The Weather Tool
10650

107-
Here's the complete weather tool implementation:
51+
Here's the complete weather tool implementation with city name geocoding:
10852

10953
### File: `lib/ai/tools/get-weather.ts`
11054

111-
<details>
112-
<summary>📄 <strong>Code: Complete Weather Tool</strong> (click to expand)</summary>
113-
11455
```typescript
11556
import { tool } from "ai";
11657
import { z } from "zod";
11758

59+
// Helper function to convert city names to coordinates
60+
async function geocodeCity(
61+
city: string
62+
): Promise<{ latitude: number; longitude: number } | null> {
63+
try {
64+
const response = await fetch(
65+
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`
66+
);
67+
68+
if (!response.ok) {
69+
return null;
70+
}
71+
72+
const data = await response.json();
73+
74+
if (!data.results || data.results.length === 0) {
75+
return null;
76+
}
77+
78+
const result = data.results[0];
79+
return {
80+
latitude: result.latitude,
81+
longitude: result.longitude,
82+
};
83+
} catch {
84+
return null;
85+
}
86+
}
87+
11888
export const getWeather = tool({
11989
description:
120-
"Get the current weather at a location. Use this when users ask about weather.",
90+
"Get the current weather at a location. You can provide either coordinates or a city name.",
12191
inputSchema: z.object({
122-
latitude: z.number().describe("Latitude coordinate"),
123-
longitude: z.number().describe("Longitude coordinate"),
92+
latitude: z.number().optional(),
93+
longitude: z.number().optional(),
94+
city: z
95+
.string()
96+
.describe("City name (e.g., 'San Francisco', 'New York', 'London')")
97+
.optional(),
12498
}),
125-
execute: async ({ latitude, longitude }) => {
126-
// In production, you'd call a real weather API
127-
// For demo, we return mock data based on coordinates
99+
execute: async (input) => {
100+
let latitude: number;
101+
let longitude: number;
102+
103+
// If city name provided, geocode it to coordinates
104+
if (input.city) {
105+
const coords = await geocodeCity(input.city);
106+
if (!coords) {
107+
return {
108+
error: `Could not find coordinates for "${input.city}". Please check the city name.`,
109+
};
110+
}
111+
latitude = coords.latitude;
112+
longitude = coords.longitude;
113+
} else if (input.latitude !== undefined && input.longitude !== undefined) {
114+
latitude = input.latitude;
115+
longitude = input.longitude;
116+
} else {
117+
return {
118+
error:
119+
"Please provide either a city name or both latitude and longitude coordinates.",
120+
};
121+
}
122+
123+
// Fetch weather data from Open-Meteo API
128124
const response = await fetch(
129125
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`
130126
);
131127

132-
const data = await response.json();
128+
const weatherData = await response.json();
133129

134-
return {
135-
temperature: data.current.temperature_2m,
136-
unit: data.current_units.temperature_2m,
137-
};
130+
// Include city name in response if provided
131+
if ("city" in input) {
132+
weatherData.cityName = input.city;
133+
}
134+
135+
return weatherData;
138136
},
139137
});
140138
```
141139

142-
</details>
140+
### Key Features
141+
142+
1. **Geocoding**: The `geocodeCity` helper converts city names to coordinates using the free Open-Meteo Geocoding API
143+
2. **Flexible Input**: Accepts either a city name OR latitude/longitude coordinates
144+
3. **Error Handling**: Returns helpful error messages if geocoding fails
145+
4. **Real Data**: Uses the Open-Meteo Weather API for actual weather data
143146

144147
## Wiring Tools into the Chat Route
145148

146-
Add the tool to your chat route:
149+
Add the tool to your chat route. The route already has streaming set up - you just need to add the tool:
147150

148151
### File: `app/(chat)/api/chat/route.ts`
149152

150-
<details>
151-
<summary>📄 <strong>Code: Add Tool to Route</strong> (click to expand)</summary>
152-
153153
```typescript
154-
import { streamText } from "ai";
155-
import { myProvider } from "@/lib/ai/providers";
156-
import { systemPrompt } from "@/lib/ai/prompts";
154+
// Add this import at the top
157155
import { getWeather } from "@/lib/ai/tools/get-weather";
158156

159-
export async function POST(request: Request) {
160-
const { messages } = await request.json();
161-
162-
const result = streamText({
163-
model: myProvider.languageModel("chat-model"),
164-
system: systemPrompt(),
165-
messages,
166-
// Add tools here!
167-
tools: {
168-
getWeather,
169-
},
170-
// Let the AI call multiple tools if needed
171-
maxSteps: 5,
172-
});
173-
174-
return result.toDataStreamResponse();
175-
}
157+
// Inside the POST handler, the streamText call should include:
158+
const result = streamText({
159+
model: myProvider.languageModel(selectedChatModel),
160+
system: systemPrompt({ selectedChatModel, requestHints }),
161+
messages: convertToModelMessages(uiMessages),
162+
// Stop after 5 tool call steps
163+
stopWhen: stepCountIs(5),
164+
// Disable tools for reasoning models
165+
experimental_activeTools:
166+
selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"],
167+
experimental_transform: smoothStream({ chunking: "word" }),
168+
// Add tools here!
169+
tools: {
170+
getWeather,
171+
},
172+
});
176173
```
177174

178-
</details>
175+
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`.
176+
177+
### Key Configuration
179178

180-
> 💡 **Key Insight**: `maxSteps: 5` allows the AI to make multiple tool calls in sequence - like calling `getWeather` for two cities to compare them.
179+
- **`stopWhen: stepCountIs(5)`**: Limits tool call chains to 5 steps
180+
- **`experimental_activeTools`**: Conditionally enables/disables tools (disabled for reasoning model)
181+
- **`tools`**: Object containing all available tools
182+
- **`requestHints`**: Contains user's location (useful for "What's the weather?" without a city)
181183

182184
## How Tool Calling Works
183185

@@ -188,9 +190,12 @@ export async function POST(request: Request) {
188190
│ AI thinks: "I should use the getWeather tool" │
189191
│ ↓ │
190192
│ AI generates tool call: │
191-
│ { name: "getWeather", args: { lat: 48.8, lon: 2.3 }}
193+
│ { name: "getWeather", args: { city: "Paris" }}
192194
│ ↓ │
193-
│ Tool executes and returns: { temperature: 18, unit: "°C"}│
195+
│ Tool executes: │
196+
│ 1. geocodeCity("Paris") → { lat: 48.8, lon: 2.3 } │
197+
│ 2. fetch weather data from Open-Meteo API │
198+
│ 3. returns: { temperature: 18, cityName: "Paris" } │
194199
│ ↓ │
195200
│ AI receives result and generates response: │
196201
│ "The current temperature in Paris is 18°C" │
@@ -207,19 +212,26 @@ Tool calls and results appear as special message parts. Here's how to render the
207212
"use client";
208213

209214
type WeatherProps = {
210-
temperature: number;
211-
unit: string;
215+
current: {
216+
temperature_2m: number;
217+
};
218+
current_units: {
219+
temperature_2m: string;
220+
};
221+
cityName?: string;
212222
};
213223

214-
export function Weather({ temperature, unit }: WeatherProps) {
224+
export function Weather({ current, current_units, cityName }: WeatherProps) {
215225
return (
216226
<div className="flex items-center gap-2 rounded-lg border p-4">
217227
<span className="text-2xl">🌡️</span>
218228
<div>
219-
<p className="font-semibold">Current Temperature</p>
229+
<p className="font-semibold">
230+
{cityName ? `Weather in ${cityName}` : "Current Weather"}
231+
</p>
220232
<p className="text-3xl">
221-
{temperature}
222-
{unit}
233+
{current.temperature_2m}
234+
{current_units.temperature_2m}
223235
</p>
224236
</div>
225237
</div>
@@ -239,23 +251,39 @@ export function Weather({ temperature, unit }: WeatherProps) {
239251
})}
240252
```
241253

242-
## Updating the System Prompt
254+
## Updating the System Prompt (Optional)
243255

244-
Help the AI know when to use tools:
256+
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.
257+
258+
The system prompt is built from multiple parts. The simplest way to add tool info is to update the `regularPrompt` constant:
245259

246260
```typescript
247261
// lib/ai/prompts.ts
248-
export const systemPrompt = () => `
249-
You are a helpful AI assistant.
262+
263+
// Update this constant to include tool documentation
264+
export const regularPrompt = `You are a friendly study buddy assistant! Keep your responses concise and helpful.
250265
251266
## Tools Available
252267
- **getWeather**: Use this when users ask about weather conditions.
253-
Ask for a city name if not provided.
254-
255-
Today's date is ${new Date().toLocaleDateString()}.
268+
You can provide a city name like "Paris" or "Tokyo".
256269
`;
270+
271+
// The systemPrompt function combines regularPrompt with location hints
272+
// No need to change this function - it already works!
273+
export const systemPrompt = ({
274+
selectedChatModel,
275+
requestHints,
276+
}: {
277+
selectedChatModel: string;
278+
requestHints: RequestHints;
279+
}) => {
280+
const requestPrompt = getRequestPromptFromHints(requestHints);
281+
return `${regularPrompt}\n\n${requestPrompt}`;
282+
};
257283
```
258284

285+
**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?"
286+
259287
## Try It Out: Weather Tool
260288

261289
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:
@@ -282,7 +310,8 @@ Should I bring an umbrella to Seattle?
282310

283311
**Troubleshooting:**
284312
- If you see "I don't have access to real-time weather", check that the tool is properly added to the `tools` object in your chat route
285-
- If coordinates seem wrong, the AI is inferring lat/long from city names - this is expected behavior
313+
- If the city isn't found, try using a more common spelling or a larger nearby city
314+
- Check the browser console for any API errors
286315

287316
---
288317

0 commit comments

Comments
 (0)