# 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 (
); } ``` **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 (