fix: update lessons on behalf of nova feedback

This commit is contained in:
Tim Rijkse
2026-02-02 08:20:57 +01:00
parent 03a5d437df
commit b39c4245c2
4 changed files with 722 additions and 389 deletions

View File

@@ -1,4 +1,4 @@
# Les 17: Vercel AI SDK - AI Features in je App
# Les 17: Vercel AI SDK, Tool Calling & Agents
---
@@ -6,7 +6,7 @@
**Deel 4: Advanced AI Features** (Les 13-18)
## Beschrijving
Bouw AI-powered features in je apps met de Vercel AI SDK. Leer hoe je chat interfaces, streaming responses en AI-gegenereerde content implementeert.
Bouw AI-powered features in je apps met de Vercel AI SDK. Leer niet alleen chat interfaces bouwen, maar ook hoe AI externe data kan ophalen via Tool Calling en autonome taken kan uitvoeren als Agent.
---
@@ -21,21 +21,22 @@ Bespreek klassikaal de Cursor .cursorrules ervaringen uit Les 16 - welke regels
- Streaming handmatig implementeren
- Error handling
- State management
- Tool calling implementeren
**De oplossing:** Vercel AI SDK
- Simpele React hooks
- Simpele React hooks (`useChat`, `useCompletion`)
- Built-in streaming
- Provider-agnostic (OpenAI, Anthropic, etc.)
- Edge-ready
- **Tool calling out-of-the-box**
- **Agent capabilities met `maxSteps`**
---
### Installatie & Setup
```bash
npm install ai @ai-sdk/openai
# of voor Anthropic:
npm install ai @ai-sdk/anthropic
npm install ai @ai-sdk/openai zod
# zod is nodig voor tool parameter validatie
```
**Environment variable:**
@@ -46,9 +47,9 @@ OPENAI_API_KEY=sk-xxxxx
---
### Core Hooks
### Deel 1: Basic Chat (Herhaling)
#### useChat - Voor Conversaties
#### useChat Hook
```tsx
'use client'
@@ -76,45 +77,16 @@ export function ChatComponent() {
placeholder="Type a message..."
className="w-full p-2 border rounded"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send'}
</button>
</form>
</div>
)
}
```
#### useCompletion - Voor Single Completions
#### Basic API Route
```tsx
import { useCompletion } from 'ai/react'
export function SummaryComponent() {
const { completion, input, handleInputChange, handleSubmit, isLoading } = useCompletion()
return (
<div>
<form onSubmit={handleSubmit}>
<textarea
value={input}
onChange={handleInputChange}
placeholder="Paste text to summarize..."
/>
<button type="submit">Summarize</button>
</form>
{completion && <p>{completion}</p>}
</div>
)
}
```
---
### API Routes
**app/api/chat/route.ts:**
```typescript
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
@@ -123,7 +95,7 @@ export async function POST(req: Request) {
const result = streamText({
model: openai('gpt-4o-mini'),
system: 'You are a helpful cooking assistant. Suggest recipes based on ingredients.',
system: 'You are a helpful assistant.',
messages,
})
@@ -131,151 +103,331 @@ export async function POST(req: Request) {
}
```
**Met custom system prompt:**
```typescript
const result = streamText({
model: openai('gpt-4o-mini'),
system: `You are a recipe assistant for the AI Recipe Generator app.
When the user provides ingredients:
1. Suggest 2-3 recipes they could make
2. List required additional ingredients (if any)
3. Provide brief cooking instructions
Be concise and practical.`,
messages,
})
```
---
### Streaming Responses
### Deel 2: Tool Calling - AI + Externe Data
**Waarom streaming?**
- Betere UX (user ziet direct resultaat)
- Snellere perceived performance
- Geen wachten op complete response
**Het probleem met basic chat:**
- AI kent alleen zijn training data
- Geen toegang tot realtime informatie
- Kan geen acties uitvoeren
**Hoe het werkt:**
1. Server stuurt tokens één voor één
2. Client rendert elke token direct
3. User ziet "typing" effect
**De oplossing: Tool Calling**
- Definieer "tools" die AI kan aanroepen
- AI besluit zelf wanneer een tool nodig is
- Tool haalt data op → AI interpreteert resultaat
**Loading indicator:**
```tsx
{isLoading && (
<div className="flex items-center gap-2">
<div className="animate-pulse"></div>
<span>AI is thinking...</span>
</div>
)}
```
---
### Integratie met Supabase
**Conversations opslaan:**
#### Voorbeeld: Cocktail Advisor met TheCocktailDB
```typescript
// Maak tabel in Supabase:
// conversations: id, user_id, created_at
// messages: id, conversation_id, role, content, created_at
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText, tool } from 'ai'
import { z } from 'zod'
// Na elke message:
async function saveMessage(conversationId: string, role: string, content: string) {
await supabase.from('messages').insert({
conversation_id: conversationId,
role,
content
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
system: `Je bent een cocktail expert.
Gebruik de tools om cocktails te zoeken en recepten op te halen.
Geef persoonlijk advies op basis van de resultaten.`,
messages,
tools: {
// Tool 1: Zoek cocktails op ingrediënt
searchByIngredient: tool({
description: 'Zoek cocktails die een specifiek ingrediënt bevatten',
parameters: z.object({
ingredient: z.string().describe('Het ingrediënt om op te zoeken, bijv. "rum" of "vodka"')
}),
execute: async ({ ingredient }) => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?i=${ingredient}`
)
const data = await res.json()
return data.drinks?.slice(0, 5) || []
}
}),
// Tool 2: Haal cocktail details op
getCocktailDetails: tool({
description: 'Haal het volledige recept van een cocktail op',
parameters: z.object({
cocktailId: z.string().describe('Het ID van de cocktail')
}),
execute: async ({ cocktailId }) => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${cocktailId}`
)
const data = await res.json()
return data.drinks?.[0] || null
}
}),
// Tool 3: Zoek non-alcoholische opties
searchNonAlcoholic: tool({
description: 'Zoek non-alcoholische cocktails/mocktails',
parameters: z.object({}),
execute: async () => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Non_Alcoholic`
)
const data = await res.json()
return data.drinks?.slice(0, 5) || []
}
})
}
})
return result.toDataStreamResponse()
}
```
**In je component:**
```tsx
const { messages, input, handleSubmit } = useChat({
onFinish: async (message) => {
await saveMessage(conversationId, message.role, message.content)
}
**Wat gebeurt er?**
```
User: "Ik heb rum en limoen, wat kan ik maken?"
AI denkt: "Ik moet zoeken op rum"
→ Roept searchByIngredient({ ingredient: "rum" }) aan
→ Krijgt: [{ name: "Mojito", id: "11000" }, { name: "Daiquiri", id: "11006" }, ...]
AI denkt: "Mojito klinkt goed met limoen, laat me het recept ophalen"
→ Roept getCocktailDetails({ cocktailId: "11000" }) aan
→ Krijgt: { name: "Mojito", ingredients: [...], instructions: "..." }
AI antwoordt: "Met rum en limoen kun je een heerlijke Mojito maken!
Je hebt nog nodig: verse munt en suiker..."
```
---
### Deel 3: Agents - Autonome Multi-Step AI
**Van Tool Calling naar Agent:**
- Tool calling = AI roept 1 tool aan, klaar
- Agent = AI blijft tools aanroepen totdat de taak af is
**Het verschil is één parameter: `maxSteps`**
```typescript
const result = streamText({
model: openai('gpt-4o-mini'),
system: `Je bent een cocktail party planner.
Plan een compleet menu met alle details.`,
messages,
tools: { /* ... tools ... */ },
maxSteps: 8 // ← Agent mag 8 tool-calls doen
})
```
---
#### Voorbeeld: Party Planner Agent
### Error Handling
```typescript
// app/api/party-planner/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText, tool } from 'ai'
import { z } from 'zod'
```tsx
const { messages, error, reload } = useChat()
export async function POST(req: Request) {
const { messages } = await req.json()
{error && (
<div className="p-4 bg-red-100 text-red-700 rounded">
<p>Something went wrong. Please try again.</p>
<button onClick={reload}>Retry</button>
</div>
)}
const result = streamText({
model: openai('gpt-4o'), // Gebruik slimmer model voor agent taken
system: `Je bent een professionele cocktail party planner.
Wanneer iemand een feest wil plannen:
1. Zoek eerst cocktails die passen bij de wensen
2. Haal recepten op van de beste opties
3. Denk aan non-alcoholische alternatieven
4. Geef een compleet overzicht met ingrediënten
Wees proactief en denk mee.`,
messages,
tools: {
searchByIngredient: tool({
description: 'Zoek cocktails met een ingrediënt',
parameters: z.object({
ingredient: z.string()
}),
execute: async ({ ingredient }) => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?i=${ingredient}`
)
return res.json()
}
}),
getCocktailDetails: tool({
description: 'Haal recept details op',
parameters: z.object({
cocktailId: z.string()
}),
execute: async ({ cocktailId }) => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${cocktailId}`
)
return res.json()
}
}),
searchNonAlcoholic: tool({
description: 'Zoek mocktails',
parameters: z.object({}),
execute: async () => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Non_Alcoholic`
)
return res.json()
}
}),
searchByCategory: tool({
description: 'Zoek cocktails per categorie (Cocktail, Shot, Beer, etc.)',
parameters: z.object({
category: z.string()
}),
execute: async ({ category }) => {
const res = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=${category}`
)
return res.json()
}
})
},
maxSteps: 10 // Agent kan tot 10 tool calls doen
})
return result.toDataStreamResponse()
}
```
**Agent in actie:**
```
User: "Plan cocktails voor mijn verjaardagsfeest.
15 mensen, een paar drinken geen alcohol,
we houden van citrus smaken."
Agent stappen:
1. searchByIngredient("lemon") → 12 cocktails
2. searchByIngredient("lime") → 15 cocktails
3. searchByIngredient("orange") → 10 cocktails
4. searchNonAlcoholic() → 8 mocktails
5. getCocktailDetails("11000") → Mojito recept
6. getCocktailDetails("11007") → Margarita recept
7. getCocktailDetails("12162") → Virgin Piña Colada recept
8. getCocktailDetails("12316") → Lemonade recept
Output: Compleet party plan met:
- 3 alcoholische cocktails met citrus
- 2 mocktails voor niet-drinkers
- Gecombineerde ingrediëntenlijst
- Tips voor bereiding
```
---
### Cost Management
### Gratis APIs voor Projecten
**Model keuze:**
| Model | Kosten | Gebruik voor |
|-------|--------|--------------|
| gpt-4o-mini | Goedkoop | Meeste taken |
| gpt-4o | Duur | Complexe reasoning |
| claude-3-haiku | Goedkoop | Simpele taken |
| claude-3-sonnet | Medium | Balans |
| API | Data | URL | Auth |
|-----|------|-----|------|
| TheCocktailDB | 636 cocktails, recepten | thecocktaildb.com/api.php | Geen (key=1) |
| TheMealDB | 597 recepten, ingrediënten | themealdb.com/api.php | Geen (key=1) |
| Open Trivia DB | 4000+ quiz vragen | opentdb.com/api_config.php | Geen |
| REST Countries | Landen data | restcountries.com | Geen |
| Open Library | Boeken data | openlibrary.org/developers | Geen |
**Bespaartips:**
1. Gebruik gpt-4o-mini als default
2. Korte system prompts
3. Beperk conversation history
4. Caching waar mogelijk
---
### Best Practices
**Tool Design:**
```typescript
// ✅ Goed: Specifieke, duidelijke tools
searchByIngredient: tool({
description: 'Zoek cocktails die een specifiek ingrediënt bevatten',
// ...
})
// ❌ Slecht: Vage tool
search: tool({
description: 'Zoek iets',
// ...
})
```
**Agent System Prompts:**
```typescript
// ✅ Goed: Geef duidelijke instructies
system: `Je bent een cocktail expert.
Wanneer je een vraag krijgt:
1. Zoek eerst relevante cocktails
2. Haal details op van de beste matches
3. Geef persoonlijk advies
Wees proactief en denk mee met de gebruiker.`
// ❌ Slecht: Te vaag
system: `Je bent een assistent.`
```
**Error Handling in Tools:**
```typescript
execute: async ({ ingredient }) => {
try {
const res = await fetch(`...`)
if (!res.ok) {
return { error: 'Kon geen cocktails vinden' }
}
return res.json()
} catch (error) {
return { error: 'API niet beschikbaar' }
}
}
```
---
## Tools
- Vercel AI SDK (`ai` package)
- Zod (parameter validatie)
- Next.js API Routes
- OpenAI API / Anthropic API
- Cursor
- Supabase
- Externe APIs (TheCocktailDB, TheMealDB, etc.)
---
## Lesopdracht (2 uur)
### Bouw een AI Chat Component
### Bouw een AI Agent met Externe Data
**Deel 1: Setup (20 min)**
1. `npm install ai @ai-sdk/openai`
**Deel 1: Setup (15 min)**
1. `npm install ai @ai-sdk/openai zod`
2. Voeg `OPENAI_API_KEY` toe aan `.env.local`
3. Maak `app/api/chat/route.ts`
3. Kies je API: TheCocktailDB of TheMealDB
**Deel 2: Basic Chat (40 min)**
1. Maak `components/Chat.tsx`
2. Implementeer `useChat` hook
3. Bouw chat UI met Tailwind
4. Test streaming werkt
**Deel 2: Basic Tool Calling (45 min)**
1. Maak `/api/chat/route.ts`
2. Implementeer 2 tools:
- Zoek op ingrediënt
- Haal details op
3. Test: "Wat kan ik maken met [ingrediënt]?"
**Deel 3: System Prompt (30 min)**
1. Schrijf system prompt voor je eindproject:
- Recipe Generator: cooking assistant
- Budget Buddy: financial advisor
- Travel Planner: travel expert
2. Test met relevante vragen
**Deel 3: Agent met maxSteps (45 min)**
1. Voeg `maxSteps: 5` toe
2. Voeg een 3e tool toe (bijv. zoek per categorie)
3. Verbeter je system prompt voor agent gedrag
4. Test: "Help me een menu plannen voor..."
**Deel 4: Supabase Integratie (30 min)**
1. Maak `messages` tabel
2. Sla berichten op met `onFinish`
3. Laad history bij page load
**Deel 4: Frontend (15 min)**
1. Bouw chat UI met `useChat`
2. Voeg loading indicator toe
3. Test de complete flow
### Deliverable
- Werkende AI chat met streaming
- Custom system prompt
- Messages opgeslagen in Supabase
- Werkende agent met minimaal 3 tools
- Chat interface
- Screenshot van agent die meerdere tools aanroept
---
@@ -283,50 +435,43 @@ const { messages, error, reload } = useChat()
### Bouw AI Feature voor Eindproject
**Deel 1: Core AI Feature (1 uur)**
**Deel 1: Agent Design (30 min)**
Implementeer de AI chat die past bij je eindproject:
Plan je agent voor de eindopdracht:
- Welke externe API gebruik je?
- Welke tools heeft je agent nodig? (minimaal 3)
- Wat is de typische flow?
| Project | AI Feature |
|---------|-----------|
| Recipe Generator | "Wat kan ik maken met kip en rijst?" |
| Budget Buddy | "Analyseer mijn uitgaven deze maand" |
| Travel Planner | "Plan een weekend Barcelona" |
Documenteer in `docs/AI-DECISIONS.md`
- Custom system prompt
- Context uit je database meegeven
**Deel 2: Implementatie (1 uur)**
**Deel 2: UX Polish (30 min)**
Bouw de agent voor je eindproject:
- Minimaal 3 tools
- `maxSteps` van minimaal 3
- Goede error handling
- Relevante system prompt
Voeg toe:
- Streaming indicator
- Suggested prompts / quick actions
- Copy response button
- Clear chat button
- Error handling
**Deel 3: Integratie (30 min)**
**Deel 3: Documentatie (30 min)**
Maak `docs/AI-FEATURE.md`:
- Welke AI feature heb je gebouwd?
- Wat doet de system prompt?
- Hoe integreert het met Supabase?
- Welke model keuzes heb je gemaakt?
Combineer met Supabase:
- Sla user preferences op
- Geef preferences mee als context aan agent
- Sla conversation history op
### Deliverable
- AI feature in eindproject
- Deployed preview
- AI-FEATURE.md documentatie
- Werkende agent in eindproject
- `docs/AI-DECISIONS.md` met agent design
- Minimaal 5 prompts in `PROMPT-LOG.md`
---
## Leerdoelen
Na deze les kan de student:
- Vercel AI SDK installeren en configureren
- `useChat` en `useCompletion` hooks gebruiken
- Streaming responses implementeren
- API routes opzetten voor AI providers
- Custom system prompts schrijven
- Chat history opslaan in Supabase
- Error handling en loading states implementeren
- Kostenbewust omgaan met AI APIs
- Tools definiëren met Zod parameters
- Tool Calling implementeren voor externe API integratie
- Agents bouwen met `maxSteps` voor autonome taken
- De juiste aanpak kiezen (basic chat vs tool calling vs agent)
- Error handling implementeren in tools
- Gratis externe APIs integreren in AI features