Skip to content

Commit 8f10218

Browse files
NiallJoeMaherclaude
andcommitted
workshop: update Chapter 1 docs and add Chapter 2 stubs
- Update CHAPTER-1.md to show full geocoding implementation - Fix weather tool code block to match actual implementation - Update route.ts example with stepCountIs and experimental_activeTools - Add lib/ai/agents/ directory with Chapter 2 preview stubs: - types.ts: Agent type definitions - tutor.ts: Tutor agent stub - index.ts: Barrel export with TODOs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 13849f3 commit 8f10218

File tree

4 files changed

+212
-27
lines changed

4 files changed

+212
-27
lines changed

CHAPTER-1.md

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,69 +48,142 @@ const myTool = tool({
4848

4949
## The Weather Tool
5050

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

5353
### File: `lib/ai/tools/get-weather.ts`
5454

5555
```typescript
5656
import { tool } from "ai";
5757
import { z } from "zod";
5858

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+
5988
export const getWeather = tool({
6089
description:
61-
"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.",
6291
inputSchema: z.object({
63-
latitude: z.number().describe("Latitude coordinate"),
64-
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(),
6598
}),
66-
execute: async ({ latitude, longitude }) => {
67-
// In production, you'd call a real weather API
68-
// 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
69124
const response = await fetch(
70125
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`
71126
);
72127

73-
const data = await response.json();
128+
const weatherData = await response.json();
74129

75-
return {
76-
temperature: data.current.temperature_2m,
77-
unit: data.current_units.temperature_2m,
78-
};
130+
// Include city name in response if provided
131+
if ("city" in input) {
132+
weatherData.cityName = input.city;
133+
}
134+
135+
return weatherData;
79136
},
80137
});
81138
```
82139

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
146+
83147
## Wiring Tools into the Chat Route
84148

85149
Add the tool to your chat route:
86150

87151
### File: `app/(chat)/api/chat/route.ts`
88152

89153
```typescript
90-
import { streamText } from "ai";
154+
import { stepCountIs, streamText } from "ai";
91155
import { myProvider } from "@/lib/ai/providers";
92156
import { systemPrompt } from "@/lib/ai/prompts";
93157
import { getWeather } from "@/lib/ai/tools/get-weather";
94158

95159
export async function POST(request: Request) {
96-
const { messages } = await request.json();
160+
const { messages, selectedChatModel } = await request.json();
97161

98162
const result = streamText({
99-
model: myProvider.languageModel("chat-model"),
100-
system: systemPrompt(),
163+
model: myProvider.languageModel(selectedChatModel),
164+
system: systemPrompt({ selectedChatModel }),
101165
messages,
166+
// Stop after 5 tool call steps
167+
stopWhen: stepCountIs(5),
168+
// Disable tools for reasoning models
169+
experimental_activeTools:
170+
selectedChatModel === "chat-model-reasoning" ? [] : ["getWeather"],
102171
// Add tools here!
103172
tools: {
104173
getWeather,
105174
},
106-
// Let the AI call multiple tools if needed
107-
maxSteps: 5,
108175
});
109176

110177
return result.toDataStreamResponse();
111178
}
112179
```
113180

181+
### Key Configuration
182+
183+
- **`stopWhen: stepCountIs(5)`**: Limits tool call chains to 5 steps
184+
- **`experimental_activeTools`**: Conditionally enables/disables tools (disabled for reasoning model)
185+
- **`tools`**: Object containing all available tools
186+
114187
## How Tool Calling Works
115188

116189
```
@@ -120,9 +193,12 @@ export async function POST(request: Request) {
120193
│ AI thinks: "I should use the getWeather tool" │
121194
│ ↓ │
122195
│ AI generates tool call: │
123-
│ { name: "getWeather", args: { lat: 48.8, lon: 2.3 }}
196+
│ { name: "getWeather", args: { city: "Paris" }}
124197
│ ↓ │
125-
│ Tool executes and returns: { temperature: 18, unit: "°C"}│
198+
│ Tool executes: │
199+
│ 1. geocodeCity("Paris") → { lat: 48.8, lon: 2.3 } │
200+
│ 2. fetch weather data from Open-Meteo API │
201+
│ 3. returns: { temperature: 18, cityName: "Paris" } │
126202
│ ↓ │
127203
│ AI receives result and generates response: │
128204
│ "The current temperature in Paris is 18°C" │
@@ -139,19 +215,26 @@ Tool calls and results appear as special message parts. Here's how to render the
139215
"use client";
140216

141217
type WeatherProps = {
142-
temperature: number;
143-
unit: string;
218+
current: {
219+
temperature_2m: number;
220+
};
221+
current_units: {
222+
temperature_2m: string;
223+
};
224+
cityName?: string;
144225
};
145226

146-
export function Weather({ temperature, unit }: WeatherProps) {
227+
export function Weather({ current, current_units, cityName }: WeatherProps) {
147228
return (
148229
<div className="flex items-center gap-2 rounded-lg border p-4">
149230
<span className="text-2xl">🌡️</span>
150231
<div>
151-
<p className="font-semibold">Current Temperature</p>
232+
<p className="font-semibold">
233+
{cityName ? `Weather in ${cityName}` : "Current Weather"}
234+
</p>
152235
<p className="text-3xl">
153-
{temperature}
154-
{unit}
236+
{current.temperature_2m}
237+
{current_units.temperature_2m}
155238
</p>
156239
</div>
157240
</div>
@@ -214,7 +297,8 @@ Should I bring an umbrella to Seattle?
214297

215298
**Troubleshooting:**
216299
- 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
217-
- If coordinates seem wrong, the AI is inferring lat/long from city names - this is expected behavior
300+
- If the city isn't found, try using a more common spelling or a larger nearby city
301+
- Check the browser console for any API errors
218302

219303
---
220304

lib/ai/agents/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// TODO CHAPTER 2: Export agents as they are implemented
2+
//
3+
// Uncomment these exports as you implement each agent:
4+
5+
// Chapter 2: Tutor Agent
6+
// export { createTutorAgent } from "./tutor";
7+
8+
// Chapter 3: Multi-Agent System
9+
// export { createQuizMasterAgent } from "./quiz-master";
10+
// export { createPlannerAgent } from "./planner";
11+
// export { createAnalystAgent } from "./analyst";
12+
13+
// Re-export types for convenience
14+
export type { AgentResult, CreateAgentProps } from "./types";

lib/ai/agents/tutor.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { tool } from "ai";
2+
import { z } from "zod";
3+
4+
import type { AgentResult, CreateAgentProps } from "./types";
5+
6+
// TODO CHAPTER 2: Implement the Tutor Agent
7+
//
8+
// The Tutor Agent explains concepts with different teaching approaches.
9+
// It should:
10+
// 1. Accept a topic and teaching approach (eli5, technical, analogy, step-by-step)
11+
// 2. Use generateText to call the AI and create an explanation
12+
// 3. Return a structured AgentResult with the explanation
13+
//
14+
// Triggers: "explain", "teach me", "how does X work", "what is"
15+
//
16+
// See CHAPTER-2.md for the complete implementation.
17+
18+
/**
19+
* Tutor Agent - Explains concepts with different teaching approaches
20+
*
21+
* @param _props - Agent props (session, dataStream)
22+
* @returns AI SDK tool that can be used in streamText
23+
*/
24+
export const createTutorAgent = (_props: CreateAgentProps) =>
25+
tool({
26+
description:
27+
"Explain a concept or topic in depth. Use when the user wants to learn or understand something. " +
28+
"Triggers: explain, teach me, how does X work, what is, help me understand.",
29+
inputSchema: z.object({
30+
topic: z.string().describe("The topic or concept to explain"),
31+
approach: z
32+
.enum(["eli5", "technical", "analogy", "step-by-step"])
33+
.default("step-by-step")
34+
.describe(
35+
"Teaching approach: eli5 (simple), technical (detailed), analogy (comparisons), step-by-step"
36+
),
37+
priorKnowledge: z
38+
.string()
39+
.optional()
40+
.describe("What the user already knows about the topic"),
41+
}),
42+
execute: async ({ topic }): Promise<AgentResult> => {
43+
// TODO: Implement in Chapter 2
44+
// 1. Build a prompt with the topic and approach
45+
// 2. Call generateText with the prompt
46+
// 3. Return the result as an AgentResult
47+
48+
console.log(`[Tutor] TODO: Explain "${topic}"`);
49+
50+
// Placeholder await to satisfy linter (remove when implementing)
51+
await Promise.resolve();
52+
53+
return {
54+
agentName: "tutor",
55+
success: false,
56+
summary: `TODO: Implement tutor agent in Chapter 2 to explain "${topic}"`,
57+
};
58+
},
59+
});

lib/ai/agents/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { DataStreamWriter } from "ai";
2+
import type { Session } from "next-auth";
3+
4+
// TODO CHAPTER 2: Agent type definitions
5+
//
6+
// These types define the contract for all agents:
7+
// - CreateAgentProps: What gets passed to agent creators
8+
// - AgentResult: What agents return after execution
9+
//
10+
// See CHAPTER-2.md for complete implementation details.
11+
12+
/**
13+
* Props passed to every agent creator function
14+
*/
15+
export type CreateAgentProps = {
16+
session: Session | null;
17+
dataStream: DataStreamWriter;
18+
};
19+
20+
/**
21+
* Standard result format returned by all agents
22+
*/
23+
export type AgentResult = {
24+
agentName: string;
25+
success: boolean;
26+
summary: string;
27+
data?: Record<string, unknown>;
28+
};

0 commit comments

Comments
 (0)