# Les 11 — Vercel AI SDK ## Docenttekst (Klas A — 3 uur, fysiek, demo-driven) **Les:** 11 van 18 **Onderwerp:** Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset **Duur:** 180 minuten **Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = thuis. **Demo-app:** Polderfest 2027 — fictief muziekfestival, 500 records in Supabase. --- ## Hoe deze tekst werkt Dit document is een **lopend script**. Je kunt 'm letterlijk volgen op je laptop terwijl je lesgeeft. - `[SLIDE X]` — Klik naar slide X op de beamer - `[SCHERM: slides | terminal | editor | browser | supabase]` — Welk scherm op de beamer - **Vertel:** "..." — Letterlijk wat je zegt (mag in eigen woorden) - `*[stage direction]*` — Korte instructie voor jezelf, niet uitspreken - Code blocks = wat je typt - 💬 = verwachte studentenvraag --- ## VÓÓR DE LES — Setup (60 min) ### 1. Tools open op je laptop - VS Code / Cursor — leeg - Terminal — open in `~/` - Browser tabs: - https://supabase.com/dashboard (ingelogd) - https://platform.openai.com (key paraat) - `localhost:3000` tab (nog niets) - Dit docenttekst-bestand - De slides PDF / PPTX ### 2. Demo-repo `polderfest-demo` voorbereiden ```bash cd ~ npx create-next-app@latest polderfest-demo \ --typescript --tailwind --app --eslint --no-src-dir --turbopack cd polderfest-demo npm i @supabase/supabase-js ai @ai-sdk/openai zod dotenv npm i tsx --save-dev git init && git add . && git commit -m "init" ``` ### 3. Nieuwe Supabase project - Dashboard → **New Project** → naam `polderfest-demo` - Wacht ~2 min op deploy - Settings → API → kopieer URL + anon key + service role key - `.env.local`: ``` NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=... SUPABASE_SERVICE_ROLE_KEY=... OPENAI_API_KEY=sk-proj-... ``` ### 4. Schema runnen - Supabase → SQL Editor → plak `schema.sql` → Run - Check Table Editor → `bands` tabel bestaat, leeg ### 5. Seed test (verwijder daarna) - Plaats `seed-polderfest.ts` in `polderfest-demo/scripts/` - `npx tsx scripts/seed-polderfest.ts` → 500 records erin → check Table Editor - **Wis** alle records vóór de les: `delete from bands;` (zodat je live kunt seeden in demo 2) - **Verwijder** `app/chat/page.tsx` + `app/api/chat/route.ts` (maken we live in demo 3) ### 6. Backup - Zip van werkende eindstaat → op USB - Check OpenAI usage dashboard — key werkt + credits aanwezig --- # HET SCRIPT — Lees mee tijdens de les ## BLOK 1 — Welkom + Terugblik (10 min) `[SLIDE 1]` `[SCHERM: slides]` **Vertel:** "Welkom bij les 11. Vandaag de Vercel AI SDK — eerste keer dat we échte AI features gaan bouwen IN onze apps. Geen ChatGPT openen meer — AI in onze eigen code." `[SLIDE 2 — Terugblik]` **Vertel:** "Even kort terug: vorige lessen hebben we Supabase geïntegreerd. Tabellen en relaties opgezet. RLS-policies bekeken — wie mag wat lezen en schrijven. Vandaag iets anders. We gaan **niet** voortbouwen op QuickPoll. We beginnen een nieuwe demo from scratch. Nieuwe Next.js app, nieuwe Supabase, en dan koppelen we daar de AI SDK aan." `*[Wacht 2 sec, laat het landen]*` `[SLIDE 3 — Planning]` **Vertel:** "Dit is de planning. Drie uur. Eerst theorie — 30 minuten — wat is de AI SDK, welke modellen, welke functies. Daarna vier demo's. Eén: nieuwe app opzetten. Twee: 500 records in Supabase via een seed-script. Pauze. Drie: AI SDK installeren en chatten met de data. Vier: vragen stellen aan die data." `*[Wijs naar de gele rij]*` **Vertel:** "Belangrijk — dit is een **kijk-les**. Jullie typen vandaag niet mee. Pak je notitieboek of laptop voor aantekeningen. Thuis bouw je zelf een versie, met je eigen thema. Daar gaan de lesopdracht en huiswerk over." 💬 Verwachte vraag: *"Kunnen we niet meedoen?"* Antwoord: "Liever niet — als jullie ook typen, gaat 't te langzaam en haakt iedereen op een ander moment af. Vanavond is voor zien-en-snappen. Thuis is doen." --- ## BLOK 2 — Theorie AI SDK (30 min) `[SLIDE 4 — Wat is de AI SDK]` `[SCHERM: slides]` **Vertel:** "Wat is de Vercel AI SDK? Een TypeScript-library die één unified API biedt voor alle AI-providers. OpenAI vandaag, Anthropic morgen, Google overmorgen, lokaal Ollama als je dat wil — je code blijft hetzelfde. Open source. Gemaakt door Vercel — de makers van Next.js. Daarom: naadloze integratie met Server Components, Server Actions en streaming. Wat zit er in:" `*[Wijs naar de bullets]*` **Vertel:** "Unified API. Streaming out-of-the-box — geen WebSocket-gedoe. React hooks zoals `useChat`. Tool Calling — komt volgende les. En type-safe gestructureerde output via Zod." `*[Wijs naar het code-blok rechts]*` **Vertel:** "Kijk hier. Dit is alle code die je nodig hebt voor één AI-call. Vier regels. En zie je `openai('gpt-4o-mini')`? Als ik dat morgen wil veranderen naar Anthropic — verander ik dat in `anthropic('claude-sonnet-4')`. Eén regel. Rest van mijn code blijft hetzelfde. Dat is de waarde." `[SLIDE 5 — Modellen + kosten]` **Vertel:** "Het modellen-landschap. Loop ik even langs:" `*[Wijs per rij]*` - "**gpt-4o-mini** — je default. Snel, goedkoop, $0.15 input / $0.60 output per miljoen tokens. Goed voor 80% van de use cases." - "**gpt-4o** — multimodal, kan plaatjes lezen. 15× duurder dan mini. Pas pakken als nodig." - "**gpt-4.1** — beste reasoning. Voor agents. Volgende lessen relevant." - "**claude-sonnet-4** — Anthropic. Beter in coding, 200k context — dus lange documenten." - "**gemini-2.5-flash** — Google. Ultra goedkoop, multimodal." - "**llama-3.3-70b** op Groq — open-source model op snelste inference platform." **Vertel:** "Vuistregel: start met gpt-4o-mini. Werkt 't niet goed genoeg? Probeer gpt-4o. Pas daarna iets exotisch. Premature optimization is een valkuil — het is letterlijk één regel veranderen om te wisselen, dus geen reden om voorbarig te kiezen." `*[Wijs naar de blauwe callout]*` **Vertel:** "Onze hele les vandaag, inclusief Polderfest met 500 bands en 10 vragen? Ongeveer 1 tot 2 cent. Echt. Schaalt prima." `[SLIDE 6 — 4 kern-functies]` **Vertel:** "De vier kern-functies van de SDK. Deze tabel is je cheat-sheet." `*[Wijs per rij]*` - "**generateText** — wachten tot AI klaar is, dan krijg je een string. Voor korte server-only calls." - "**streamText** — streamt karakter voor karakter. Werkt met useChat. Dit gebruiken we vandaag." - "**useChat** — React hook. Complete chat UI in 10 regels. Ook vandaag." - "**generateObject** — type-safe data via Zod schema. Voor database-inserts of classificatie. Vandaag niet — komt later." **Vertel:** "Onthoud: streamText en useChat — onze combo voor vandaag. generateObject zien jullie volgende lessen terug. Tool Calling — onderaan — dat is volgende les." --- ## BLOK 3 — Live Demo 1: Next.js + Supabase scaffold (20 min) `[SLIDE 7 — Polderfest concept]` `[SCHERM: slides]` **Vertel:** "Voor we gaan coderen — wat bouwen we eigenlijk? We bouwen **Polderfest 2027**. Een fictief Nederlands muziekfestival. 500 verzonnen bands. Allemaal namen die niet bestaan. Geen Spotify, geen Pitchfork — pure fantasy." `*[Wijs naar de gele 'Waarom een fictief festival' callout]*` **Vertel:** "Waarom fictief? Omdat **geen enkele LLM** dit kan weten. Geen training data over Polderfest 2027 — bestaat niet. En dat is precies wat we willen demonstreren: AI alleen kan dit niet. AI mét onze data, wél." `*[Wijs naar het schema-blok]*` **Vertel:** "Onze tabel heeft deze velden: naam, genre, sub-genre, stage, dag, starttijd, stad, members, bio, tier, populariteit, ticket-impact. Genoeg variatie voor leuke vragen straks." `*[Wijs naar voorbeeld-vragen]*` **Vertel:** "Dingen die we straks aan onze AI gaan vragen: welke bands spelen vrijdagavond op de Main Stage? Vat de hip-hop scene samen. Welke acts komen uit Groningen? Allemaal vragen die ChatGPT niet kan beantwoorden — want hij weet niets van Polderfest. Maar onze chat straks wel." `*[Pauze, ademen]*` **Vertel:** "Goed — laten we 't gaan bouwen. Eerst Next.js en Supabase opzetten." --- `[SLIDE 8 — LIVE DEMO 1]` `[SCHERM: slides]` **Vertel:** "Dit zijn de 6 stappen die we nu gaan doorlopen. Ongeveer 20 minuten. Volg even mee — niet meetypen, kijk." `[SCHERM: terminal]` **Vertel:** "We beginnen in de terminal." #### Stap 1 — Next.js scaffolden ```bash cd ~ npx create-next-app@latest polderfest-demo \ --typescript --tailwind --app --eslint --no-src-dir --turbopack ``` `*[Druk enter, wacht ~30 sec]*` **Vertel terwijl het installeert:** "Standaard Next.js 15 met App Router, Tailwind, TypeScript. App Router omdat we Server Components willen. Tailwind voor styling. Niets bijzonders aan deze setup — dit kennen jullie al uit eerdere lessen." `*[Wacht tot install klaar is]*` ```bash cd polderfest-demo code . ``` `[SCHERM: editor]` **Vertel:** "Editor open. Niets in `app/page.tsx` — standaard Next.js welkomstpagina. Standaard `app/layout.tsx`. Tailwind config. Niks bijzonders." #### Stap 2 — Supabase project aanmaken `[SCHERM: browser → supabase.com/dashboard]` **Vertel:** "Nu Supabase. Ik heb nog géén project voor deze demo — we maken er een nieuwe." `*[Klik New Project]*` **Vertel:** "Naam: `polderfest-demo`. Database password — kies wat, hoeft niet kopiëren. Region: West Europe. Submit." `*[Wacht ~2 min — gebruik deze tijd voor de uitleg hieronder]*` **Vertel:** "Terwijl het deployt: waarom een nieuw project? Omdat we van scratch beginnen. Geen vermenging met je QuickPoll-data van eerdere lessen. Clean slate. Voor je eindopdracht en huiswerk geldt: één app = één Supabase project." #### Stap 3 — Schema runnen `*[Supabase deploy is klaar]*` `[SCHERM: supabase → SQL Editor]` **Vertel:** "Schema-tijd. Open SQL Editor. New Query." `*[Plak inhoud van schema.sql]*` **Vertel:** "Dit is mijn schema voor de bands-tabel. Naam, genre, stage, dag, tijd, members, bio. Een paar indexen voor performance. RLS aan en een policy: bands zijn publiek leesbaar. Voor onze chat hebben we read-access nodig, geen schrijfrechten." `*[Klik Run]*` `[SCHERM: supabase → Table Editor]` **Vertel:** "Check — tabel `bands` bestaat. Leeg. Klaar om gevuld te worden." #### Stap 4 — Env vars `[SCHERM: supabase → Settings → API]` **Vertel:** "Drie dingen pak ik hier op: de Project URL, de anon public key, en de service_role secret key. Die laatste is belangrijk — straks bij het seed-script." `*[Kopieer alle drie]*` `[SCHERM: editor → .env.local]` ``` NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=... SUPABASE_SERVICE_ROLE_KEY=... OPENAI_API_KEY=sk-proj-... ``` **Vertel:** "Belangrijke nuance — let goed op de namen: - `NEXT_PUBLIC_SUPABASE_URL` en `_ANON_KEY` — die staan in client-bundle. Mag — anon key heeft alleen leesrechten via RLS. - `SUPABASE_SERVICE_ROLE_KEY` — geen `NEXT_PUBLIC_` prefix. Server-only. Deze key bypasst RLS — daarmee kan alles. Lekken = ramp. Gebruiken we alleen lokaal voor het seed-script. - `OPENAI_API_KEY` — geen `NEXT_PUBLIC_` prefix. Anders zit-ie in je client-bundle en kan iedereen 'm gebruiken op jouw kosten. Server-only altijd." #### Stap 5 — Supabase client `[SCHERM: editor]` `*[Nieuwe file: 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!, ); ``` **Vertel:** "Standaard Supabase client. Voor onze chat — dat is alles wat we nodig hebben. Niets bijzonders. Kennen jullie." #### Stap 6 — Dev server check `[SCHERM: terminal]` ```bash npm run dev ``` `[SCHERM: browser → localhost:3000]` **Vertel:** "Standaard Next.js welkomstpagina. Werkt. Supabase staat. Schema staat. Klaar voor data." --- ## BLOK 4 — Live Demo 2: Seed script — 500 records (20 min) `[SLIDE 9 — LIVE DEMO 2]` `[SCHERM: slides]` **Vertel:** "Demo 2. We gaan onze bands-tabel vullen. Niet handmatig — met een seed-script. 500 records in ~30 seconden." `[SCHERM: editor]` `*[Maak folder: scripts/. Plaats seed-polderfest.ts erin]*` #### Stap 1 — Het seed-script bekijken `[SCHERM: editor → seed-polderfest.ts]` **Vertel:** "Dit is mijn seed-script. 200 regels. Laat me even door de structuur lopen — niet alle regels lezen, alleen de aanpak." `*[Scroll naar top]*` **Vertel:** "Bovenaan: Supabase client. Belangrijk — met de **service role key**. Niet anon. Want we gaan inserts doen. RLS blokkeert dat anders." `*[Scroll naar de bouwstenen arrays]*` **Vertel:** "Hier zijn mijn bouwstenen. Adjectives — 'Lost', 'Velvet', 'Iron', 'Neon'. Nouns — 'Tigers', 'Wolves', 'Mirrors'. Cities — Nederlandse steden. Genres — muziekgenres." `*[Scroll naar generateBandName]*` **Vertel:** "Hier de naam-generator. Vier patronen: - 'Lost Tigers' — adjective + noun - 'De Wolves' — Dutch prefix + noun - 'Sanne Van Dijk' — solo artist - 'Sanne & The Wolves' — solo + collectief Met 30 adjectives × 30 nouns = al 900 unieke combinaties mogelijk. Genoeg voor 500 records." `*[Scroll naar generateBio]*` **Vertel:** "Bio's. Drie blokken — opening, middle, ending — gecombineerd. 'Begonnen in een garage in [stad]', '[band] experimenteert met analoge synths', 'Debuut-EP eind 2027'. Compositioneel. Geen handmatig getypte bio's — 500× zou krankzinnig zijn." `*[Scroll naar bottom — async function seed]*` **Vertel:** "De main functie. Genereert 500 bands, dedupe op naam, insert in batches van 100 — Supabase trekt 500 in één keer niet altijd. Done." #### Stap 2 — Service role key uitleggen **Vertel:** "Even pauze voor één belangrijk ding — de service role key. Die zit boven aan dit script. Drie regels die jullie moeten onthouden: 1. **Alleen lokaal gebruiken.** Niet in productie code. Niet in client. Alleen scripts. 2. **Nooit committen** naar git. Service role key in `.env.local`, en `.env.local` in `.gitignore`. 3. **Lekt-ie?** Direct draaien in Supabase dashboard → Settings → API → Reset service role key. Vergelijk het met een root password. Behandel 'm zo." #### Stap 3 — Dependencies (was al klaar — kort tonen) `[SCHERM: terminal]` ```bash # We hebben deze al uit setup, maar voor je eigen project: npm i @supabase/supabase-js dotenv npm i tsx --save-dev ``` #### Stap 4 — Run het seed-script `[SCHERM: terminal]` ```bash npx tsx scripts/seed-polderfest.ts ``` **Vertel terwijl het runt:** "Daar gaat 'ie. tsx is een TypeScript-runner die geen build-stap nodig heeft. dotenv leest de `.env.local` automatisch. 500 bands genereren, vijf batches van 100, klaar." `*[Wacht ~10-30 sec, output verschijnt]*` ``` Genereren van 500 Polderfest bands... Schrijven naar Supabase in batches van 100... ✓ 100/500 ✓ 200/500 ✓ 300/500 ✓ 400/500 ✓ 500/500 Klaar! 500 Polderfest bands staan in Supabase. ``` **Vertel:** "Done." #### Stap 5 — Verificatie `[SCHERM: supabase → Table Editor → bands]` `*[Klik refresh — 500 records verschijnen]*` **Vertel:** "500 bands. Allemaal verzonnen. Laten we er even één openklikken." `*[Klik op willekeurige rij, toon bio]*` **Vertel:** "Kijk — 'Begonnen in een garage in Groningen, De Tigers experimenteert met analoge synths en gefluisterde lyrics. Polderfest is hun grootste festival tot nu toe.' Compleet verzonnen. Geen Wikipedia, geen Spotify — pure fantasy. Maar overtuigend genoeg voor onze AI om mee te werken." #### Stap 6 — Quick check met SQL `[SCHERM: supabase → SQL Editor]` ```sql select genre, count(*) from bands group by genre order by count desc; ``` `*[Run]*` **Vertel:** "Genre-verdeling. ~30 per genre. Mooi gespreid. Klaar om mee te chatten." --- ## BLOK 5 — Pauze (15 min) `[SLIDE 10 — Pauze]` `[SCHERM: slides]` **Vertel:** "Pauze. 15 minuten. Tot zo." `*[Coffee. Stretch. Check je OpenAI key nog even.]*` --- ## BLOK 6 — Live Demo 3: AI SDK + chat-route (30 min) `[SLIDE 11 — LIVE DEMO 3]` `[SCHERM: slides]` **Vertel:** "Welkom terug. Nu de echte AI-stap. We bouwen een chat-route in onze API en een chat-pagina in Next.js. Daarmee kunnen we vragen stellen aan onze Polderfest-data." `[SCHERM: terminal]` #### Stap 1 — Packages ```bash npm i ai @ai-sdk/openai zod ``` **Vertel:** "Drie packages. `ai` is de SDK zelf. `@ai-sdk/openai` is de provider — we gebruiken OpenAI vandaag. `zod` is voor schema validatie. Vandaag gebruiken we 'm niet, maar volgende les wel." #### Stap 2 — Chat API route `[SCHERM: editor]` `*[Maak file: app/api/chat/route.ts. Typ live, niet pasten — geeft studenten tijd om te volgen]*` ```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(); ``` **Vertel:** "API route. POST, want we ontvangen messages van de chat. We destructuren de messages-array." ```typescript // 1. Haal alle bands op uit Supabase const { data: bands, error } = await supabase.from("bands").select("*"); if (error) throw error; ``` **Vertel:** "Stap 1 — ALLE bands ophalen uit Supabase. Voor 500 records werkt dit prima. Voor 50.000 niet — volgende les lossen we dat op met Tool Calling. Vandaag: simpele aanpak, alles meesturen." ```typescript // 2. Format bands als context-string const context = bands! .map((b) => `- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time} ` + `op ${b.stage}, uit ${b.origin_city})` ) .join("\n"); ``` **Vertel:** "Stap 2 — we maken één grote tekst-context. Per band één regel met de belangrijkste velden. AI kan namelijk geen SQL, maar wel tekst lezen." ```typescript // 3. System prompt met context const system = `Je bent een festival-assistent voor Polderfest 2027. Hier zijn alle bands die op het festival spelen: ${context} Beantwoord vragen van bezoekers over de line-up. Verzin niets — gebruik alleen bovenstaande data. Antwoord in het Nederlands. Wees beknopt.`; ``` **Vertel:** "Stap 3 — de system prompt. Dit is de **rol** die AI krijgt. Drie belangrijke instructies: 1. 'Verzin niets' — voorkomt hallucinaties. 2. 'Gebruik alleen bovenstaande data' — niet uit training-kennis halen. 3. 'Antwoord in het Nederlands' — anders krijg je Engels. Een goede system prompt is je hefboom. 50% van de kwaliteit komt hier." ```typescript // 4. Stream naar OpenAI const result = streamText({ model: openai("gpt-4o-mini"), system, messages, }); return result.toDataStreamResponse(); } ``` **Vertel:** "Stap 4 — de AI-call zelf. `streamText` — onze keuze van vandaag. Model gpt-4o-mini. System message, plus de berichten van de user. En `result.toDataStreamResponse()` zet 't om naar het juiste streaming-format voor useChat aan de client-kant." **Vertel:** "API route klaar." #### Stap 3 — Chat pagina `*[Nieuwe file: 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}
))}
); } ``` **Vertel:** "Client component — `'use client'` bovenaan. `useChat` hook regelt alles: messages-state, input-state, submit-handler, streaming. Vijf properties, geen extra useState nodig. UI is bewust simpel. Tailwind classes. Berichten van user blauw rechts, AI grijs links. Input + verzenden onderaan. Disabled tijdens streaming via `status !== 'ready'`." #### Stap 4 — Testen `[SCHERM: browser → localhost:3000/chat]` **Vertel:** "Naar `/chat`. Eerste vraag." `*[Typ in chat-input]*` ``` Hallo, wie ben jij? ``` `*[Druk Enter. AI antwoordt streamend.]*` **Vertel terwijl AI antwoordt:** "Daar gaat 'ie. Karakter voor karakter. Streamt. Veel sneller voelend dan wachten op heel antwoord. UseChat regelt het, je hoeft niks zelf te doen voor streaming." **Vertel:** "Klaar. Werkt. Nu de leukste vraag: vragen aan onze data." --- ## BLOK 7 — Live Demo 4: Vragen aan onze data (15 min) `[SLIDE 12 — LIVE DEMO 4]` `[SCHERM: slides]` **Vertel:** "We gaan vijf vragen stellen. Eén voor één. Bij elke vraag — let op wat de AI doet, en hoe dat anders is dan een SQL query of een gewone chatbot." `[SCHERM: browser → /chat]` #### Vraag 1 — Filter `*[Type in chat]*` ``` Welke bands spelen zaterdag op de Beach Stage? ``` `*[AI antwoordt — geeft een lijst]*` **Vertel:** "Filter. AI heeft door de tekst-context gefilterd. Even bevestigen met SQL —" `[SCHERM: supabase → SQL Editor]` ```sql select name, start_time from bands where day = 'Zaterdag' and stage = 'Beach Stage'; ``` `*[Run]*` **Vertel:** "Zelfde resultaat. Maar het verschil — onze chat geeft natuurlijke taal, kan vervolgvragen aan, kan samenvatten. SQL doet alleen filter + select." #### Vraag 2 — Sort + Reasoning `[SCHERM: browser → /chat]` ``` Geef me 3 headliners met de meeste popularity, en hun bio's ``` `*[AI antwoordt]*` **Vertel:** "Sort. AI heeft op `popularity` gesorteerd en de top 3 gepakt. Plus de bio's erbij." `*[Vraag 2b in zelfde chat:]*` ``` En welke daarvan zou je aanraden voor iemand die houdt van techno? ``` `*[AI antwoordt — beargumenteerd]*` **Vertel:** "Dit is **reasoning**. AI redeneert over genre + sub-genre + bio-tekst om een aanbeveling te doen. Een SQL query kan dit niet. Dit is waar AI waarde toevoegt bovenop pure data." #### Vraag 3 — Aggregate ``` Hoeveel jazz fusion acts spelen er totaal op Polderfest? ``` `*[AI antwoordt met een getal]*` **Vertel:** "Aggregate. AI heeft geteld. Even SQL-bevestigen —" `[SCHERM: supabase → SQL Editor]` ```sql select count(*) from bands where genre = 'Jazz Fusion'; ``` **Vertel:** "Klopt. Goed." #### Vraag 4 — Samenvatting (waar AI uitblinkt) `[SCHERM: browser → /chat]` ``` Vat de electronic-scene op Polderfest samen in 3 zinnen ``` `*[AI antwoordt met een echte samenvatting]*` **Vertel:** "Hier is geen SQL voor. Dit is **samenvatten**. AI heeft alle electronic acts gelezen, gewogen, en in 3 zinnen samengevat. Dit is de unieke kracht van AI bovenop data." #### Vraag 5 — De mist in (bewust) ``` Wie was de hoofdact van Polderfest 2025? ``` `*[AI antwoordt eerlijk dat hij dit niet weet]*` **Vertel:** "Goed. Geen verzinsels. AI zegt: 'mijn data is alleen 2027, dat zit er niet in'. Dat is de kracht van onze system prompt. Zonder die prompt zou-ie waarschijnlijk wat hallucineren." `*[Pauze, kijk de klas in]*` **Vertel:** "Zien jullie wat hier gebeurt? Geen LLM ter wereld kent Polderfest 2027. Geen Wikipedia, geen training-data. Maar onze chat beantwoordt alles — omdat **wij** de data leveren. AI + data = product." --- ## BLOK 8 — Data + AI = kracht (5 min) `[SLIDE 13 — Data + AI = kracht]` `[SCHERM: slides]` **Vertel:** "Reflectie-moment. Drie scenario's:" `*[Wijs naar de grijze box]*` **Vertel:** "**Data alleen.** Wat heb je? SQL queries. Filter, sort, select. Geen taal, geen interpretatie. Gebruiker moet zelf SQL kunnen." `*[Wijs naar de roze box]*` **Vertel:** "**AI alleen.** ChatGPT zonder context. Generieke kennis uit training. Hallucineert. Geen privé data, geen live data." `*[Wijs naar de blauwe box]*` **Vertel:** "**Data + AI.** Wat we vandaag bouwden. Filter via reasoning. Antwoorden in natuurlijke taal. Samenvattingen, vergelijkingen, aanbevelingen. Schaalbaar — voeg data toe en je hebt nieuwe antwoorden mogelijk." `*[Pauze]*` **Vertel:** "Quote om mee weg te lopen:" `*[Wijs naar de gele callout]*` **Vertel:** "*Een LLM zonder jouw data is een gewone chatbot. Een LLM mét jouw data is een product.* Onthoud dit. Dit is de fundering van alle volgende lessen." --- ## BLOK 9 — Lesopdracht + Huiswerk uitleg (20 min) `[SLIDE 14 — Lesopdracht]` `[SCHERM: slides]` **Vertel:** "Lesopdracht. Voor thuis — niet vanavond per se, maar vóór volgende les. Je bouwt **een eigen versie** van wat we vandaag deden. Met **je eigen thema**." `*[Loop checklist langs op slide]*` **Vertel:** "Eisen op een rij. Bedenk een eigen thema — moet fictief zijn. Nieuw Next.js project, nieuw Supabase. Eigen seed-script. Minstens 100 records. Chat-route en chat-pagina werkend. Drie vragen stellen die alleen werken dankzij jouw data." `*[Wijs naar pink callout]*` **Vertel:** "Inspiratie: fictief restaurant-aggregator in een verzonnen stad. Scriptie-archief van NOVI met 1000 nep-titels. Museumcollectie met verzonnen kunstenaars. D&D NPCs. Cryptid-sightings in Nederland. Belangrijk: **moet fictief zijn**. Als je echte restaurants in Amsterdam pakt, weet ChatGPT die al — dan zie je niet wat we vandaag demonstreerden. Het hele punt is: data die geen LLM kent." 💬 Verwachte vraag: *"Mag ik echt elk thema?"* Antwoord: "Ja, zolang het fictief is en minstens 100 records heeft. Twijfel? Stuur 'm op Brightspace, dan kijk ik even mee." `[SLIDE 15 — Huiswerk]` **Vertel:** "Het huiswerk bouwt voort op de lesopdracht. Drie onderdelen — alle drie verplicht." `*[Loop A, B, C langs op slide]*` **Vertel:** "**Onderdeel A.** Pas het Polderfest seed-script aan voor jouw thema. Het script staat klaar als bijlage. Open 't, lees 't, en pas 't aan. AI mag je helpen — letterlijk: open OpenCode, plak m'n script erin, vraag 'pas dit aan voor [mijn thema]'. Klaar in een paar minuten. Daarna jij review. Minstens 200 records. **Onderdeel B.** Voeg minstens 1 extra veld toe aan je schema. Iets dat een **nieuwe vraag** mogelijk maakt. Niet zomaar een extra string-kolom. Concreet voorbeeld: een museumcollectie met `acquisition_story` veld — dan kun je vragen 'welke kunstwerken zijn op een veiling gekocht?'. Update seed-script, re-seed, test in chat. **Onderdeel C.** Schrijf een `AI-CHAT.md` in je repo-root. Drie secties: - Mijn thema — wat is het, waarom kan een gewone LLM dit niet? - 3 leuke vragen die werken - 1 vraag waar AI moeite mee had + hoe je je prompt aanpaste." `*[Wijs naar gele callout]*` **Vertel:** "Bonus, geen verplichting: deploy op Vercel, loading skeleton, vergelijk gpt-4o-mini en gpt-4o." 💬 Verwachte vraag: *"Hoe lang gaat dit duren?"* Antwoord: "Lesopdracht ~2,5 uur. Huiswerk ~1,5 tot 2 uur. Samen een lange middag. Loop je vast — Brightspace of plan een korte 1-op-1 met me." --- ## BLOK 10 — Vragen + Afsluiting (15 min) `[SLIDE 16 — Volgende les: Tool Calling]` `[SCHERM: slides]` **Vertel:** "Eén ding voor we afronden — wat komt hierna." `*[Wijs naar pink callout: Het schaal-probleem]*` **Vertel:** "Wat we vandaag deden: ALLE 500 bands sturen we mee als context bij elke vraag. Dat is ~30.000 tokens per call. Werkt prima voor 500. Werkt **niet** voor 5.000 records. En al helemaal niet voor 50.000." `*[Wijs naar blauwe callout: De oplossing]*` **Vertel:** "Volgende les — **Tool Calling**. In plaats van alle data meesturen, geef je AI **functies** die hij zelf kan aanroepen. Hij hoort vraag 'welke bands op vrijdag?' en besluit: ik roep `searchBands({ day: 'Vrijdag' })` aan. Krijgt 60 bands terug. Antwoordt. Schaalbaar tot duizenden records." **Vertel:** "Daarna in deze leerlijn: Agents met maxSteps. RAG met embeddings — semantic search op heel grote datasets. Testing, deployment, performance. En de laatste twee lessen: eindopdracht-werkdagen en je pitch." `[SLIDE 17 — Afsluiting]` **Vertel:** "Vragen?" `*[Open de vloer. Verwachte vragen + antwoorden:]*` 💬 *"Wat als ik geen schoolkey heb?"* → "Eigen OpenAI account — $5 starter credit zit erin gratis. Of Groq — gratis tier. Of Anthropic — $5 gratis credits." 💬 *"Hoe weet ik welk model het beste is?"* → "Start met gpt-4o-mini. Upgrade alleen als het écht niet werkt. Premature optimization is een valkuil." 💬 *"Kan dit lokaal zonder OpenAI?"* → "Ja, via Ollama. Niet vereist voor deze les. Komt eventueel in latere lessen." 💬 *"Moet ik de Polderfest demo zelf ook namaken?"* → "Nee. Wat we vandaag deden is jullie laten zien. Voor jezelf bouwen = eigen thema, in lesopdracht en huiswerk." 💬 *"Hoe duur is dit nou echt?"* → "Onze hele les vandaag met 500 bands en 10 vragen — onder de 2 cent. Met gpt-4o (de grote) zou hetzelfde ~30 cent zijn. Met mini blijft het peanuts." `*[Sluit af]*` **Vertel:** "Zorg dat je vóór volgende les minstens je seed-script werkend hebt voor jouw thema. Dan kunnen we volgende les meteen Tool Calling toepassen. Tot dan!" --- ## Backup-onderwerpen (als tijd over is) 1. **Andere provider tonen** — Open `route.ts`, vervang `openai("gpt-4o-mini")` door `anthropic("claude-sonnet-4")`. Werkt direct. Eén regel. 2. **System prompt fine-tuning** — Verzwak de prompt ("Help bij vragen"). Vraag iets. Verzin de fout. Versterk weer ("Verzin niets, gebruik alleen onze data"). Toon verschil. 3. **Token-kosten dashboard** — Open platform.openai.com/usage. Toon je verbruik van vandaag — letterlijk een paar cent. 4. **Privacy / data retention** — Wat gaat naar OpenAI? Zero-data-retention via EU-endpoints. Belangrijk voor productie. 5. **Hallucinatie-test** — Probeer met zwakke prompt of de AI iets verzint over Polderfest 2025. Toon dat sterkere prompt dit fixt.