# Les 11 — Vercel AI SDK ## Lesstof **Vak:** AI-Assisted Development **Opleiding:** NOVI Hogeschool Utrecht **Onderwerp:** Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset **Demo:** Polderfest 2027 — fictief festival, 500 records --- ## Inhoudsopgave 1. [Wat is de Vercel AI SDK?](#1-wat-is-de-vercel-ai-sdk) 2. [Het modellen-landschap](#2-het-modellen-landschap) 3. [De vier kern-functies](#3-de-vier-kern-functies) 4. [Project setup van A tot Z](#4-project-setup-van-a-tot-z) 5. [Seed script: dummy data in Supabase](#5-seed-script-dummy-data-in-supabase) 6. [Chat-route + chat-UI implementeren](#6-chat-route--chat-ui-implementeren) 7. [Waarom data + AI samen krachtig zijn](#7-waarom-data--ai-samen-krachtig-zijn) 8. [Best practices & valkuilen](#8-best-practices--valkuilen) 9. [Wat komt hierna? Tool Calling teaser](#9-wat-komt-hierna) 10. [Bronnen](#10-bronnen) --- ## 1. Wat is de Vercel AI SDK? De Vercel AI SDK is een open-source TypeScript library waarmee je AI-features in je webapplicatie bouwt. Gemaakt door Vercel — de makers van Next.js — en daarom perfect geïntegreerd met React, Server Components en Server Actions. ### Wat krijg je? - **Unified API** — Zelfde code voor elk model (40+ providers) - **Streaming out-of-the-box** — Geen WebSocket-setup nodig - **React hooks** — `useChat`, `useCompletion` voor instant chat UI - **Tool Calling** — AI kan functies aanroepen die jij definieert (volgende les) - **Structured output** — Type-safe data via `generateObject` + Zod - **Multi-step agents** — Via `maxSteps` ### Eerste indruk ```typescript import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; const { text } = await generateText({ model: openai("gpt-4o-mini"), prompt: "Vat de Polderfest 2027 line-up samen", }); ``` Wil je naar Anthropic? Eén regel veranderen: ```typescript import { anthropic } from "@ai-sdk/anthropic"; const { text } = await generateText({ model: anthropic("claude-sonnet-4"), prompt: "Vat de Polderfest 2027 line-up samen", }); ``` Dat is de waarde. --- ## 2. Het modellen-landschap | Provider | Model | Sterke punten | Prijs (in/out per 1M tokens) | |----------|-------|---------------|------------------------------| | OpenAI | `gpt-4o-mini` | Snel, goedkoop, default | $0.15 / $0.60 | | OpenAI | `gpt-4o` | Multimodal (vision), krachtig | $2.50 / $10 | | OpenAI | `gpt-4.1` | Reasoning, voor agents | $2 / $8 | | Anthropic | `claude-sonnet-4` | Coding, lange context (200k) | $3 / $15 | | Google | `gemini-2.5-flash` | Multimodal, ultra goedkoop | $0.075 / $0.30 | | Groq | `llama-3.3-70b` | Ultra-fast inference | $0.59 / $0.79 | **Vuistregel:** start met `gpt-4o-mini`. Werkt het niet goed? Upgrade naar `gpt-4o`. Dan pas exotisch. **Wat kost onze Polderfest demo?** - Context = 500 bands → ~30.000 tokens per chat-request - 1 vraag = ~$0.005 (een halve cent) - 50 vragen = $0.25 (kwart euro) Met gpt-4o (15× duurder) zou dezelfde demo ~$4 kosten. Daarom: start mini. --- ## 3. De vier kern-functies ### `generateText` — Antwoord ophalen Wacht tot het AI-antwoord compleet is, returnt dan een string. ```typescript const { text } = await generateText({ model: openai("gpt-4o-mini"), prompt: "Geef 3 koffie-poll opties", }); ``` **Wanneer:** Korte antwoorden, server-only, niet-interactief. ### `streamText` — Streaming antwoord Streamt karakter voor karakter. Goed voor chat UI. ```typescript const result = streamText({ model: openai("gpt-4o-mini"), messages, }); return result.toDataStreamResponse(); ``` **Wanneer:** Chat UI, lange antwoorden, "ChatGPT-gevoel". Vandaag gebruiken we dit. ### `useChat` — React hook Aan client-kant: complete chat UI in 10 regels. ```tsx "use client"; import { useChat } from "ai/react"; export default function Chat() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return (
{messages.map(m =>
{m.role}: {m.content}
)}
); } ``` **Belangrijk:** werkt alleen met `streamText` als API endpoint. Vandaag gebruiken we dit ook. ### `generateObject` — Gestructureerde data In plaats van een string krijg je type-safe data terug — gevalideerd met Zod. ```typescript const { object } = await generateObject({ model: openai("gpt-4o-mini"), schema: z.object({ question: z.string(), options: z.array(z.string()).length(4), }), prompt: "Maak een poll over koffie", }); ``` **Wanneer:** Database inserts, formulieren vullen, classificatie. **Niet** vandaag — komt terug in latere lessen. --- ## 4. Project setup van A tot Z Dit is wat Tim in de les live deed. Voor je eigen project (lesopdracht/huiswerk): zelfde stappen. ### Stap 1 — Next.js scaffolden ```bash npx create-next-app@latest mijn-thema \ --typescript --tailwind --app --eslint --no-src-dir --turbopack cd mijn-thema ``` ### Stap 2 — Supabase project aanmaken - Ga naar https://supabase.com/dashboard - **New Project** → kies naam - Wacht ~2 min op deploy - Settings → API → kopieer: - Project URL - anon public key - service_role secret key ### Stap 3 — Schema definiëren Pas `schema.sql` aan voor jouw thema. Bv: ```sql create table items ( id bigserial primary key, name text not null, category text, rating int, description text, created_at timestamp default now() ); ``` Run in Supabase → SQL Editor. ### Stap 4 — Env variables `.env.local`: ``` NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=... SUPABASE_SERVICE_ROLE_KEY=... OPENAI_API_KEY=sk-proj-... ``` Belangrijke regels: - `NEXT_PUBLIC_*` = client-leesbaar - `SUPABASE_SERVICE_ROLE_KEY` = server-only, voor seed script (geen `NEXT_PUBLIC_`) - `OPENAI_API_KEY` = server-only (geen `NEXT_PUBLIC_`) ### Stap 5 — Packages installeren ```bash npm install @supabase/supabase-js ai @ai-sdk/openai zod npm install --save-dev tsx dotenv ``` ### Stap 6 — Supabase client `lib/supabase.ts`: ```typescript import { createClient } from "@supabase/supabase-js"; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, ); ``` --- ## 5. Seed script: dummy data in Supabase Een seed-script is een TypeScript bestand dat **éénmalig** je tabel vult met dummy data. Geen handmatige inserts — procedureel gegenereerd. ### Het Polderfest script (voorbeeld) Het volledige `seed-polderfest.ts` zit als bijlage bij deze les. Kernidee: ```typescript import { createClient } from "@supabase/supabase-js"; import "dotenv/config"; const supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, ); const adjectives = ["Lost", "Velvet", "Iron", "Neon", "Silent", ...]; const nouns = ["Tigers", "Wolves", "Mirrors", "Clouds", ...]; const genres = ["Indie Rock", "Electronic", ...]; function generateBand(i: number) { return { name: `${pick(adjectives)} ${pick(nouns)}`, genre: pick(genres), // ... }; } async function seed() { const bands = []; for (let i = 0; i < 500; i++) bands.push(generateBand(i)); for (let i = 0; i < bands.length; i += 100) { await supabase.from("bands").insert(bands.slice(i, i + 100)); } } seed(); ``` ### Runnen ```bash npx tsx scripts/seed-polderfest.ts ``` ### Waarom procedureel? - **Met 500 hard-coded records** = 500 regels handmatige data → mind-numbing - **Met combinaties** van 30 adjectives × 30 nouns = 900 unieke namen mogelijk - **Met seed-random** = reproduceerbaar (zelfde data bij re-run) ### Voor jouw eigen thema Open `seed-polderfest.ts`, kopieer de structuur, en vervang de **bouwstenen**: - Domein-specifieke arrays (in plaats van bands: restaurants, scripties, kunstwerken…) - Domein-specifieke velden - Domein-specifieke bio/beschrijving-fragmenten **Pro tip:** vraag een AI om dit te doen! "Pas het Polderfest seed-script aan voor [thema]." OpenCode of Cursor doet dit in 30 seconden. --- ## 6. Chat-route + chat-UI implementeren Dit is wat **alleen nieuw** is — Next.js + Supabase kennen jullie al. ### De chat-route `app/api/chat/route.ts`: ```typescript import { streamText } from "ai"; import { openai } from "@ai-sdk/openai"; import { createClient } from "@supabase/supabase-js"; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, ); export async function POST(req: Request) { const { messages } = await req.json(); // 1. Haal data op const { data: bands } = await supabase.from("bands").select("*"); // 2. Format als context const context = bands! .map((b) => `- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time})`, ) .join("\n"); // 3. System prompt met context const system = `Je bent een festival-assistent voor Polderfest 2027. Hier zijn alle bands: ${context} Beantwoord vragen op basis van bovenstaande data. Verzin niets. Antwoord in het Nederlands.`; // 4. Stream const result = streamText({ model: openai("gpt-4o-mini"), system, messages, }); return result.toDataStreamResponse(); } ``` ### De chat-pagina `app/chat/page.tsx`: ```tsx "use client"; import { useChat } from "ai/react"; export default function ChatPage() { const { messages, input, handleInputChange, handleSubmit, status } = useChat(); return (

Polderfest 2027 — vraag de AI

{messages.map((m) => (
{m.role === "user" ? "Jij" : "Festival AI"}
{m.content}
))}
); } ``` ### Voorbeeld-vragen die we live stelden | Vraag | Wat AI doet | |-------|-------------| | "Welke bands spelen zaterdag op de Beach Stage?" | Filtert door context | | "3 headliners met meeste populariteit" | Sorteert + select top | | "Hoeveel jazz fusion acts totaal?" | Telt | | "Vat de electronic-scene samen" | **Samenvatting** — alleen AI kan dit | | "Wie was hoofdact van Polderfest 2025?" | Eerlijk: "weet ik niet" — perfect | --- ## 7. Waarom data + AI samen krachtig zijn ### Data alleen - SQL queries: filter, sort, select - Geen interpretatie, geen taal, geen samenvatting - Gebruiker moet zelf SQL kunnen ### AI alleen - Kennis is generiek (training data) - Verzint vaak (hallucinatie) - Geen toegang tot live data of private data ### Data + AI - AI filtert via reasoning op tekst-context - Antwoorden in natuurlijke taal - Samenvattingen en interpretatie - Domein-kennis = jouw data, AI redeneert erover > "Een LLM zonder jouw data is een gewone chatbot. > Een LLM mét jouw data is een product." --- ## 8. Best practices & valkuilen ### Doen - **Begin met `gpt-4o-mini`** — upgrade pas als nodig - **System prompt is essentieel** — "gebruik alleen onze data, verzin niets" - **Stream alles** — `streamText` voelt 5× sneller dan `generateText` - **AI-calls altijd server-side** — keys blijven veilig - **Loading state** — AI duurt 1-5 sec, zonder feedback voelt het stuk - **Foutafhandeling** — `try/catch` rond elke AI-call ### Niet doen - **Geen `NEXT_PUBLIC_OPENAI_API_KEY`** — wordt zichtbaar in client - **Niet de output blind vertrouwen** — AI hallucineert - **Niet alle data altijd meesturen** — werkt voor 500 records, niet voor 50.000 (volgende les) - **Niet `gpt-4o` als default** — 15× duurder dan mini, vaak onnodig ### Veelvoorkomende fouten | Fout | Oorzaak | Oplossing | |------|---------|-----------| | `OPENAI_API_KEY is not defined` | `.env.local` niet geladen | Dev server herstarten | | `Cannot find module 'ai'` | npm install vergeten | `npm i ai @ai-sdk/openai` | | Seed: `permission denied` | Anon key i.p.v. service role | Gebruik `SUPABASE_SERVICE_ROLE_KEY` | | AI antwoordt in Engels | Niet expliciet om NL gevraagd | System prompt aanpassen | | AI verzint feiten | System prompt te zwak | Voeg toe: "verzin niets, gebruik alleen onze data" | | Chat laadt niet | `useChat` zonder `streamText` API | Endpoint moet `result.toDataStreamResponse()` returnen | --- ## 9. Wat komt hierna? ### Het schaalprobleem Vandaag sturen we **alle 500 bands** mee als context bij elke request. Dat is ~30k tokens. Werkt prima voor 500. Werkt **niet** voor: - 5.000 records → te duur, te traag - 50.000 records → past niet in context window - Real-time data → context wordt steeds opnieuw gebouwd ### Volgende les: Tool Calling (Les 12) In plaats van **alle data te sturen**, geef je de AI **tools** (functies). De AI besluit zelf welke te gebruiken: ```typescript const { text } = await generateText({ model: openai("gpt-4o-mini"), messages, tools: { searchBands: tool({ description: "Zoek bands op dag, stage, of genre", parameters: z.object({ day: z.string().optional(), stage: z.string().optional(), genre: z.string().optional(), }), execute: async ({ day, stage, genre }) => { // Supabase query const { data } = await supabase.from("bands").select("*") .eq("day", day || undefined) .eq("stage", stage || undefined); return data; }, }), }, maxSteps: 5, }); ``` Workflow: 1. User: "Welke bands op vrijdag?" 2. AI: "Ik roep `searchBands({ day: 'Vrijdag' })` aan" 3. Supabase: 60 bands terug 4. AI: "Op vrijdag spelen 60 bands. De headliners zijn..." Schaalbaar. Slim. Multi-step (combineer meerdere tools). ### Daarna in deze leerlijn - **Les 13:** Agents + `maxSteps` (autonome multi-step taken) - **Les 14:** RAG + embeddings (semantic search op heel grote datasets) - **Les 15-16:** Testing + Deployment + Performance - **Les 17-18:** Eindopdracht-werkdagen + Pitch --- ## 10. Bronnen ### Vercel AI SDK - Hoofdpagina: https://ai-sdk.dev/docs/introduction - Voorbeelden: https://ai-sdk.dev/examples - `streamText`: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text - `useChat`: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat - Tool Calling (volgende les): https://ai-sdk.dev/docs/foundations/tools ### Supabase - JS Client: https://supabase.com/docs/reference/javascript - Row Level Security: https://supabase.com/docs/guides/auth/row-level-security - Server-side usage: https://supabase.com/docs/guides/auth/server-side ### Inspiratie - v0.dev — Generative UI in actie - chat.vercel.ai — Officiële demo van AI SDK - Vercel templates met AI: https://vercel.com/templates?type=ai ### Tokens & kosten - OpenAI pricing: https://openai.com/api/pricing - Tokenizer: https://platform.openai.com/tokenizer - Usage dashboard: https://platform.openai.com/usage