j8>@f=+hZV?b6i>Ti3$W:*>R`LIuTVHd_R_;iR7O"0I<#p[$OV8>N_P4+doF3.74,&24Zl@N5G1?60h6Bes^D4SGf'#bh(LqlV)o#ZW%3#6KNDN[&~>endstream
+endobj
+28 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
+>>
+stream
+GarW5;/_pX'SYH?($@F/2M!@#d>Kl*k3QM,?IPRU>R`)Ak#Ea_jN65JVZ@h\V;?YV2Ei"Z&qC/$SiY>-]l!hkd42Ttd4r*`Wbp5r-Y&fgJAp7Urhr9i:a=l/rmdMef>-7sV$DI"h(*Dd3455sGLN8%Lm75oNR%n>Lemh-Ng90=1T\.V[>eJf(_[fCEfH-7=A2ILAn5s#Sj6ct\407/g\:CK#1N5:.ed"7d3f>%`7$k%CB[?154G=1@_3=ar=%DT15lSVF*5&1tKQl"BqiBSUqWb'N1j9%N52bQV39HiWDisD:/05ELIf]28`/+~>endstream
+endobj
+xref
+0 29
+0000000000 65535 f
+0000000061 00000 n
+0000000143 00000 n
+0000000250 00000 n
+0000000362 00000 n
+0000000439 00000 n
+0000000544 00000 n
+0000000749 00000 n
+0000000954 00000 n
+0000001159 00000 n
+0000001274 00000 n
+0000001480 00000 n
+0000001686 00000 n
+0000001892 00000 n
+0000001976 00000 n
+0000002182 00000 n
+0000002388 00000 n
+0000002594 00000 n
+0000002664 00000 n
+0000002956 00000 n
+0000003070 00000 n
+0000005458 00000 n
+0000007122 00000 n
+0000008886 00000 n
+0000010759 00000 n
+0000012497 00000 n
+0000014371 00000 n
+0000016380 00000 n
+0000018389 00000 n
+trailer
+<<
+/ID
+[<3079c5437762c8adeb291f8ff4fab280><3079c5437762c8adeb291f8ff4fab280>]
+% ReportLab generated PDF document -- digest (opensource)
+
+/Info 18 0 R
+/Root 17 0 R
+/Size 29
+>>
+startxref
+19015
+%%EOF
diff --git a/Les13-Agents/lesbestanden/polderfest-original.zip b/Les13-Agents/lesbestanden/polderfest-original.zip
new file mode 100644
index 0000000..a51ffa3
Binary files /dev/null and b/Les13-Agents/lesbestanden/polderfest-original.zip differ
diff --git a/Les13-Agents/lesbestanden/polderfest-with-agent.zip b/Les13-Agents/lesbestanden/polderfest-with-agent.zip
new file mode 100644
index 0000000..07be892
Binary files /dev/null and b/Les13-Agents/lesbestanden/polderfest-with-agent.zip differ
diff --git a/Les14-Agents/Les14-Docenttekst.md b/Les14-Agents/Les14-Docenttekst.md
deleted file mode 100644
index c920f63..0000000
--- a/Les14-Agents/Les14-Docenttekst.md
+++ /dev/null
@@ -1,785 +0,0 @@
-# Les 14 — Agents
-## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
-
-**Les:** 13 van 18
-**Onderwerp:** Agents — LLM in een loop met tools
-**Duur:** 180 minuten
-**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk.
-**Demo-app:** Nieuwe research-agent (from scratch, los van Polderfest)
-
----
-
-## Hoe deze tekst werkt
-
-Dit document is een **lopend script**. Lees mee tijdens de les op je laptop.
-
-- `[SLIDE X]` — Klik naar slide X
-- `[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 (45 min)
-
-### 1. Tavily account
-
-- Maak account op https://tavily.com/
-- Kopieer API key (begint met `tvly-...`)
-- Gratis tier = 1000 calls/maand, ruim genoeg voor deze les
-
-### 2. Nieuwe lege Next.js demo-folder
-
-`*[Doe dit thuis vooraf, niet in de klas. Klas kijkt naar de setup-stap als je 'm doet.]*`
-
-```bash
-cd ~/novi/novi-lessons/Les14-Agents
-pnpm create next-app@latest research-agent \
- --typescript --tailwind --app --no-src-dir --import-alias "@/*"
-cd research-agent
-pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
-```
-
-### 3. Environment vars
-
-`.env.local`:
-```
-OPENAI_API_KEY=sk-...
-TAVILY_API_KEY=tvly-...
-NEXT_PUBLIC_SUPABASE_URL=...
-NEXT_PUBLIC_SUPABASE_ANON_KEY=...
-```
-
-### 4. Supabase tabel
-
-```sql
-create table research_reports (
- id bigserial primary key,
- query text not null,
- summary text not null,
- sources jsonb default '[]'::jsonb,
- created_at timestamp default now()
-);
-alter table research_reports enable row level security;
-create policy "demo open" on research_reports
- for all to anon using (true) with check (true);
-```
-
-### 5. Backup eindstaat
-
-- Werkende `lib/tools.ts`, `lib/agent.ts`, `app/api/research/route.ts` ergens als backup
-- Verwacht: 1-2 typos tijdens live coding — geen ramp
-
-### 6. Browser tabs
-
-- `localhost:3000` (dev server, maar gaan we voornamelijk via terminal testen)
-- Supabase dashboard
-- https://ai-sdk.dev/docs/agents/overview (referentie)
-- https://tavily.com/ (dashboard voor usage)
-
----
-
-# HET SCRIPT — Lees mee tijdens de les
-
-## BLOK 1 — Welkom + Terugblik (10 min)
-
-`[SLIDE 1 — Title]` `[SCHERM: slides]`
-
-**Vertel:** "Welkom bij les 13. Vandaag stappen we van tool-calling naar agents. Eén stap verder qua autonomie van AI."
-
-`[SLIDE 2 — Terugblik]`
-
-**Vertel:** "Vorige les bouwden we Tool Calling in onze Polderfest-app. Zes tools. `stopWhen: stepCountIs(5)`. AI roept zelf functies aan om aan data te komen. Goed werk.
-
-Maar er waren dingen die een tool-call bot **nog niet kon**. Lange ketens — 30, 40, 50 stappen — daar is `stepCountIs(5)` te krap voor. Een eigen plan maken en bijstellen. Tools die andere tools triggeren. Per stap een ander model kiezen.
-
-Daarvoor heb je een **agent** nodig. Een LLM die tools gebruikt in een loop, en zelf bepaalt hoe lang die loop duurt en welke kant het op gaat."
-
-`[SLIDE 3 — Planning]`
-
-**Vertel:** "Vandaag drie uur. Eerst een half uur theorie — wat is een agent precies, hoe ziet de loop eruit, wat is een `ToolLoopAgent`. Daarna vier live demos waarin we from scratch een research-agent bouwen. Geen polderfest vandaag — losse repo, omdat agents een nieuwe context verdienen.
-
-Aan het eind: wanneer wel/niet een agent, en wat doe je voor huiswerk."
-
----
-
-## BLOK 2 — Theorie (30 min)
-
-`[SLIDE 4 — Wat is een agent]`
-
-**Vertel:** "De definitie uit de AI SDK docs is kort en bruikbaar:
-
-> *Agents are LLMs that use tools in a loop to accomplish tasks.*
-
-Drie ingrediënten. Eén — een LLM die besluit wat de volgende actie is. Twee — tools die het LLM uitbreiden. Drie — een loop die deze twee orchestreert.
-
-In Les 12 hadden we deze drie ook al — `stopWhen: stepCountIs(5)` is in feite een mini-agent. Het verschil zit in **schaal**. Vandaag praten we over 20-50 stappen, en het model bepaalt zelf welke tools het wanneer gebruikt."
-
-💬 *Mogelijke vraag: "Wat is dan het verschil met Les 12 echt?"*
-
-**Antwoord:** "Schaal en autonomie. Les 12 was: vraag → 2-5 tool-calls → antwoord. Vandaag: vraag → agent maakt plan → voert plan uit (10-50 stappen) → schrijft rapport. Het model beslist veel meer zelf."
-
-`[SLIDE 5 — Anatomie van de loop]`
-
-**Vertel:** "Wat gebeurt er per loop-iteratie, één stap? Vijf dingen.
-
-Eén: we sturen de huidige messages naar het LLM. Twee: LLM antwoordt — óf met text, óf met een tool-call. Als het text is, stopt de loop. Dat is een natural finish — model heeft niks meer te doen. Als het een tool-call is, gaan we door. Drie: we voeren de tool execute uit. Vier: we voegen het resultaat toe aan de berichten. Vijf: we checken stop-conditie. Niet voldaan? Volgende iteratie.
-
-De loop stopt in vier gevallen: text-only antwoord, stopWhen geactiveerd, tool zonder execute aangeroepen — dat is het 'done-pattern', komen we straks op terug, en tool die approval nodig heeft. Die laatste skippen we vandaag.
-
-Default in v6: `stepCountIs(20)`. Dat is een veiligheidsgrens — voorkomt dat een rare bug in je systeem prompt resulteert in 200 API-calls en een rekening van 30 euro."
-
-`[SLIDE 6 — ToolLoopAgent]`
-
-**Vertel:** "In oudere AI SDK versies moest je de loop zelf schrijven — een while-loop rond `streamText` en `generateText`. In v6 is er een dedicated class: `ToolLoopAgent`.
-
-`*[Wijs naar code op slide]*`
-
-Je geeft model, system prompt, tools, en een stop-conditie. Eén keer definiëren. Dan `agent.generate({ prompt })` aanroepen, of `agent.stream` voor streaming.
-
-Wat je terugkrijgt: `result.text` is het eindantwoord, `result.steps` is een array met alle stappen die de agent zette — welke tool aangeroepen, met welke input, welk resultaat. Dat is je debug-goud."
-
-💬 *Vraag: "Kan dat nog steeds met streamText?"*
-
-**Antwoord:** "Ja, voor backwards compatibility. Maar voor nieuwe code is `ToolLoopAgent` de aanrader. Minder boilerplate, herbruikbaar."
-
-`[SLIDE 7 — Stop-condities]`
-
-**Vertel:** "Vier soorten stop-condities.
-
-Eén: `stepCountIs(N)` — stop na N stappen. Standaard veiligheidsmaatregel.
-
-Twee: `hasToolCall('saveReport')` — stop zodra een specifieke tool is aangeroepen. Logisch voor 'doe het werk en sla op, dan klaar'.
-
-Drie: `isLoopFinished()` — onbeperkt. Agent stopt alleen natuurlijk wanneer model klaar is. Dit is gevaarlijk zonder andere safeguards — agent kan in theorie oneindig doorgaan. Alleen gebruiken met cost limit erbij.
-
-Vier: combineer ze in een array. Loop stopt zodra één conditie waar is.
-
-En vijf — niet built-in maar zelf te schrijven: `StopCondition` callback. Daarmee kun je stoppen op kosten, op aantal tools, op wat dan ook dat je uit de step-historie kunt halen."
-
-`[SLIDE 8 — prepareStep]`
-
-**Vertel:** "`prepareStep` is misschien de krachtigste feature van `ToolLoopAgent`. Het is een async callback die VOOR elke stap draait. Je kunt het model dynamisch wisselen, tools beperken, messages trimmen.
-
-Voorbeeld: begin met `gpt-4o-mini` — snel en goedkoop. Maar als de stap-historie complex wordt, switch naar Claude Sonnet voor beter reasoning. Dat scheelt geld zonder kwaliteit te verliezen.
-
-Of: dwing een research-fase af. Stap 0-3 mag alleen `webSearch`. Stap 4-6 alleen `readPage`. Stap 7+ alleen `saveReport`. Agent kan niet 'cheaten' door te vroeg op te slaan.
-
-Of: trim oude tool-results uit de berichten. Een lange agent kan 30k tokens aan tool-output verzamelen. Niet meer relevant voor de huidige stap — eruit, scheelt budget."
-
-💬 *Vraag: "Hoe weet ik wanneer ik prepareStep nodig heb?"*
-
-**Antwoord:** "Niet bij elke agent. Begin zonder. Als je merkt dat je agent veel geld kost, of te vaak in een verkeerde fase blijft hangen, dan kom je terug om `prepareStep` toe te voegen. Premature optimization is ook in agents reëel."
-
----
-
-## BLOK 3 — Live Demo 1: Setup research-agent (25 min)
-
-`[SLIDE 9 — Wat we bouwen]`
-
-**Vertel:** "Genoeg theorie. We gaan een **research-agent** bouwen. Doel: gebruiker stelt een vraag — bijvoorbeeld 'wat zijn AI-trends in 2026?' — en de agent zoekt zelfstandig op het web, leest de relevante pagina's, schrijft een rapport, slaat op in Supabase.
-
-Vier tools: `webSearch` met Tavily, `readPage` om URLs te lezen, `saveReport` om in DB te zetten, en eventueel een `done`-tool.
-
-Stack: Next.js 16, AI SDK v6, Tavily, Supabase, gpt-4o-mini. Losse repo — niet de Polderfest-demo. Agents verdienen een schone context."
-
-`[SLIDE 10 — LIVE DEMO 1]` `[SCHERM: terminal + editor]`
-
-**Vertel:** "Klas, leun achterover. Vragen tussendoor mogen. Ik laat zien hoe je een agent from-scratch bouwt."
-
-`*[Open terminal in /Les14-Agents]*`
-
-```bash
-pnpm create next-app@latest research-agent \
- --typescript --tailwind --app --no-src-dir --import-alias "@/*"
-cd research-agent
-```
-
-`*[Wacht op next-app, ondertussen praat door over Tavily]*`
-
-**Vertel:** "Voor web search heb je een API nodig. OpenAI heeft sinds kort een eigen web-search tool, maar die werkt alleen met gpt-4o en is duur. Voor onafhankelijke search gebruiken we Tavily — gemaakt voor AI agents, gratis tier van 1000 calls per maand. Gewoon registreren met email, API key gepakt, klaar.
-
-Andere opties: Brave Search API, Serper, Google Custom Search. Tavily is het simpelst voor onze case omdat het naast search ook content-extractie biedt."
-
-```bash
-pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
-```
-
-`*[Open editor]*`
-
-**Vertel:** "Eerst environment vars."
-
-`*[Maak .env.local]*`
-
-```
-OPENAI_API_KEY=sk-...
-TAVILY_API_KEY=tvly-...
-NEXT_PUBLIC_SUPABASE_URL=...
-NEXT_PUBLIC_SUPABASE_ANON_KEY=...
-```
-
-**Vertel:** "Belangrijk: `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*`. Anders zit-ie in je client-bundle en kan iedereen 'm zien in DevTools. Server-only. Hetzelfde voor `TAVILY_API_KEY`."
-
-`*[Check .gitignore — moet .env.local in staan]*`
-
-**Vertel:** "Check je `.gitignore`. `.env.local` moet erin. Standaard create-next-app doet dit goed maar — altijd checken."
-
-`*[Supabase Dashboard openen]*` `[SCHERM: supabase]`
-
-```sql
-create table research_reports (
- id bigserial primary key,
- query text not null,
- summary text not null,
- sources jsonb default '[]'::jsonb,
- created_at timestamp default now()
-);
-```
-
-**Vertel:** "Tabel voor onze rapporten. `query` is de vraag, `summary` is het rapport zelf, `sources` is een JSON array van URLs die werden gebruikt."
-
-```sql
-alter table research_reports enable row level security;
-create policy "demo open" on research_reports
- for all to anon using (true) with check (true);
-```
-
-**Vertel:** "RLS aan, en een open policy voor demo. Productie: koppelen aan auth.uid()."
-
-`*[Terug naar editor]*` `[SCHERM: editor]`
-
-**Vertel:** "Nu de tools. Ik maak een `lib/tools.ts` met onze drie tools."
-
-`*[Maak lib/tools.ts]*`
-
-```typescript
-import { tool } from "ai";
-import { z } from "zod";
-import { createClient } from "@supabase/supabase-js";
-
-const supabase = createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
-);
-
-export const webSearch = tool({
- description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
- inputSchema: z.object({
- query: z.string().describe("Zoekterm"),
- }),
- execute: async ({ query }) => {
- const res = await fetch("https://api.tavily.com/search", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: process.env.TAVILY_API_KEY,
- query,
- max_results: 5,
- }),
- });
- const data = await res.json();
- return data.results?.map((r: any) => ({
- url: r.url,
- title: r.title,
- snippet: r.content,
- })) ?? [];
- },
-});
-```
-
-**Vertel:** "Beschrijving van `webSearch` is kort en specifiek. AI leest description om te beslissen welke tool nodig is — vaag = verkeerde keuze. `max_results: 5` houdt het beheersbaar — Tavily kan ook 10 of 20, maar 5 is goed startpunt."
-
-`*[readPage tool toevoegen]*`
-
-```typescript
-export const readPage = tool({
- description: "Haal de volledige tekst van een URL op.",
- inputSchema: z.object({ url: z.string().url() }),
- execute: async ({ url }) => {
- const res = await fetch("https://api.tavily.com/extract", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: process.env.TAVILY_API_KEY,
- urls: [url],
- }),
- });
- const data = await res.json();
- const content = data.results?.[0]?.raw_content ?? "";
- return { url, content: content.slice(0, 5000) };
- },
-});
-```
-
-**Vertel:** "`readPage` haalt de fulltext van een URL — Tavily heeft een extract-endpoint dat ruis verwijdert. Ik knip op 5000 chars om token-budget niet te exploderen. Voor productie: chunken + samenvatten."
-
-`*[saveReport tool]*`
-
-```typescript
-export const saveReport = tool({
- description:
- "Sla een onderzoeksrapport op in de database. " +
- "Alleen aanroepen als het rapport compleet is.",
- inputSchema: z.object({
- query: z.string(),
- summary: z.string().min(100),
- sources: z.array(z.object({
- url: z.string().url(),
- title: z.string(),
- })),
- }),
- execute: async ({ query, summary, sources }) => {
- const { data, error } = await supabase
- .from("research_reports")
- .insert({ query, summary, sources })
- .select()
- .single();
- if (error) return { error: error.message };
- return { reportId: data.id, success: true };
- },
-});
-```
-
-**Vertel:** "`saveReport` is onze enige write-tool. Description benadrukt: alleen aanroepen als compleet. `summary.min(100)` — zod schema dwingt minimum lengte af, AI mag geen 'TODO' insturen."
-
-`*[lib/agent.ts maken]*`
-
-**Vertel:** "Nu de agent zelf."
-
-```typescript
-import { ToolLoopAgent, stepCountIs } from "ai";
-import { openai } from "@ai-sdk/openai";
-import { webSearch, readPage, saveReport } from "./tools";
-
-export const researchAgent = new ToolLoopAgent({
- model: openai("gpt-4o-mini"),
- system: `Je bent een research-assistent. Werk als volgt:
-1. Begin met een kort plan (3-5 bullets).
-2. Gebruik webSearch voor oriëntatie.
-3. Gebruik readPage voor de 2-3 meest relevante URLs.
-4. Schrijf een samenvatting van min 200 woorden.
-5. Sla op met saveReport. Inclusief alle gebruikte sources.
-
-Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
- tools: { webSearch, readPage, saveReport },
- stopWhen: stepCountIs(10),
-});
-```
-
-**Vertel:** "System prompt is **het** kritieke onderdeel. Ik geef expliciete fasen — plan, search, read, samenvat, save. Hoe duidelijker, hoe minder de agent drifft. 'Verzin niets' is belangrijk — anders maakt AI bronnen op."
-
-`*[app/api/research/route.ts]*`
-
-```typescript
-import { researchAgent } from "@/lib/agent";
-
-export async function POST(req: Request) {
- const { query } = await req.json();
- const result = await researchAgent.generate({ prompt: query });
-
- return Response.json({
- text: result.text,
- steps: result.steps.map((s, i) => ({
- step: i,
- toolCalls: s.toolCalls?.map((tc) => ({
- tool: tc.toolName,
- input: tc.input,
- })),
- hasText: !!s.text,
- })),
- });
-}
-```
-
-**Vertel:** "Endpoint stuurt query naar de agent, returnt text + step-samenvatting. Voor productie zou je dit streamen, voor demo houden we het simpel — ��én POST, JSON response."
-
-`*[Run]*` `[SCHERM: terminal]`
-
-```bash
-pnpm dev
-```
-
-`*[In tweede terminal:]*`
-
-```bash
-curl -X POST http://localhost:3000/api/research \
- -H "Content-Type: application/json" \
- -d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \
- | jq
-```
-
-**Vertel:** "We zien... `*[wachten 20-40s]*` ...de agent draait. JSON komt terug. Kijk — `steps` array. webSearch, dan readPage, readPage, saveReport. Vier stappen. `text` veld is een samenvatting."
-
-`*[Open Supabase, check tabel]*` `[SCHERM: supabase]`
-
-**Vertel:** "Rapport staat in Supabase. Werkend agent. From scratch in 20 minuten."
-
-💬 *Vraag: "Hoeveel tokens kost zo'n call?"*
-
-**Antwoord:** "Goeie vraag. `result.usage` heeft `inputTokens` en `outputTokens`. Voor zo'n research van 4 stappen: ongeveer 8.000 input + 1.500 output tokens. Op gpt-4o-mini: ~0.001 dollar. Bijna gratis. Op gpt-4o: ~0.05 dollar. Houdbaar."
-
----
-
-## BLOK 4 — Live Demo 2: stopWhen variants (25 min)
-
-`[SLIDE 11 — DEMO 2]` `[SCHERM: editor]`
-
-**Vertel:** "We hebben nu een agent met `stepCountIs(10)`. Laten we de andere stop-condities uitproberen en zien wat het verschil maakt."
-
-`*[Wijzig stopWhen]*`
-
-```typescript
-import { ToolLoopAgent, stepCountIs, hasToolCall } from "ai";
-
-// ...
-stopWhen: hasToolCall("saveReport"),
-```
-
-**Vertel:** "Nu stopt agent **zodra** saveReport is aangeroepen. Geen veiligheidscap. Laten we draaien."
-
-`*[Curl draaien]*`
-
-**Vertel:** "Werkt. Agent doet zijn werk, roept saveReport, stopt. Voordeel: geen onnodige extra stappen. Nadeel: als saveReport faalt en agent retried, kan-ie alsnog lang doorgaan."
-
-`*[Beter: combineren]*`
-
-```typescript
-stopWhen: [
- stepCountIs(20),
- hasToolCall("saveReport"),
-],
-```
-
-**Vertel:** "Best practice: array. Veiligheidscap én task-completion. Loop stopt op de eerste van de twee."
-
-`*[Probeer isLoopFinished]*`
-
-```typescript
-import { isLoopFinished } from "ai";
-// ...
-stopWhen: isLoopFinished(),
-```
-
-**Vertel:** "`isLoopFinished` is letterlijk: geen limit. Alleen natuurlijke finish stopt het. Dit is **gevaarlijk** zonder andere safeguards. Een misverstand in je system prompt en agent draait 50 stappen door."
-
-`*[Curl draaien — agent doet nu 8-10 stappen]*`
-
-**Vertel:** "Zie je? Agent gaat door tot het zelf besluit te stoppen. Eindigt met text — geen tool-call. Werkt, maar — wat als prompt slecht is, of als API een rare fout gooit en de agent retried?"
-
-`*[Custom StopCondition]*`
-
-**Vertel:** "Eigen stop. Bijvoorbeeld stoppen als we genoeg pagina's hebben gelezen."
-
-```typescript
-import type { StopCondition } from "ai";
-import type { researchAgent } from "./agent";
-
-type Tools = typeof researchAgent extends ToolLoopAgent ? T : never;
-
-const enoughReading: StopCondition = ({ steps }) => {
- const readCount = steps.flatMap((s) =>
- s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
- ).length;
- return readCount >= 3;
-};
-
-stopWhen: [
- stepCountIs(20),
- enoughReading,
-],
-```
-
-**Vertel:** "Drie pagina's lezen is genoeg, stoppen. Custom logic die uit step-historie wordt gehaald."
-
-`*[Demo run]*`
-
-**Vertel:** "Zie: agent stopt op stap waar readPage voor de derde keer wordt aangeroepen. Conditie werkt. Voor productie gebruik je dit vaak voor kostenlimieten — daar komen we morgen op terug, eh, in het huiswerk."
-
-💬 *Vraag: "Waarom `: StopCondition` en niet typed?"*
-
-**Antwoord:** "Excuus, dat is een shortcut. In productie zou je een `ToolSet` type extracten zodat je `StopCondition` krijgt — dan kun je tool-names typesafe checken. Voor demo houd ik 't simpel."
-
----
-
-## BLOK 5 — Pauze (15 min)
-
-`[SLIDE 12 — Pauze]`
-
-**Vertel:** "Pauze. Vijftien minuten. Loop een ronde."
-
-`*[Reset agent.ts naar stepCountIs(15) + hasToolCall("saveReport") combo voor volgende demo]*`
-
----
-
-## BLOK 6 — Live Demo 3: prepareStep (25 min)
-
-`[SLIDE 13 — DEMO 3]` `[SCHERM: editor]`
-
-**Vertel:** "Nu het krachtigste stuk: `prepareStep`. Voor elke stap kunnen we model, tools, messages aanpassen. Drie use cases vandaag."
-
-### Use case 1 — Dynamic model
-
-```typescript
-prepareStep: async ({ stepNumber, messages }) => {
- console.log(`Stap ${stepNumber}, ${messages.length} messages`);
- if (stepNumber > 3) {
- console.log("→ switch naar gpt-4o");
- return { model: openai("gpt-4o") };
- }
- return {};
-},
-```
-
-**Vertel:** "Stap 0-3 doen we met gpt-4o-mini — goedkoop en snel. Vanaf stap 4 switchen we naar de grote gpt-4o. Reden: vroege stappen zijn 'zoek even', late stappen zijn 'vat samen en redeneer'. Dat laatste verdient een sterker model."
-
-`*[Curl draaien — kijk in console naar logs]*`
-
-**Vertel:** "Zien jullie: 'Stap 0 — mini'. 'Stap 1 — mini'. 'Stap 4 — switch naar gpt-4o'. Dynamic. Effect: lagere kosten, betere eindkwaliteit."
-
-### Use case 2 — Fase-gebaseerde tools
-
-`*[Pas aan]*`
-
-```typescript
-prepareStep: async ({ stepNumber }) => {
- if (stepNumber <= 2) {
- return { activeTools: ["webSearch"] };
- }
- if (stepNumber <= 5) {
- return { activeTools: ["readPage"] };
- }
- return {
- activeTools: ["saveReport"],
- toolChoice: "required",
- };
-},
-```
-
-**Vertel:** "Nu dwingen we een fase af. Stap 0-2 mag alleen searchen. Stap 3-5 alleen lezen. Stap 6+ alleen opslaan, en moet die ook gebruiken want `toolChoice: required`."
-
-`*[Curl draaien]*`
-
-**Vertel:** "Agent kan niet 'cheaten'. Geen `saveReport` op stap 2 met een vage samenvatting. Eerst werken, dan opslaan."
-
-💬 *Vraag: "Is dat niet een beetje vechten tegen het model?"*
-
-**Antwoord:** "Het is een trade-off. Meer controle = minder flexibel. Voor research is een gefaseerde aanpak vaak prima. Voor open-ended problem solving — bijv. een coding agent — zou ik 't niet doen. Daar moet het model vrij kunnen kiezen."
-
-### Use case 3 — Context-trimming
-
-```typescript
-prepareStep: async ({ messages, stepNumber }) => {
- if (messages.length > 12) {
- return {
- messages: [
- messages[0], // system
- ...messages.slice(-8) // laatste 8
- ],
- };
- }
- return {};
-},
-```
-
-**Vertel:** "Bij lange agents groeit `messages` snel — 30+ items, elk met tool-results van duizenden tokens. Hier zeggen we: na 12 messages, gooi de oudste weg, hou alleen system + laatste 8.
-
-Voor research specifiek niet zo nodig — meestal eindigt agent na 8-10 stappen. Maar voor coding agents of debugging-agents die 30+ stappen doen — essentieel."
-
-`*[Test]*`
-
-**Vertel:** "Voor onze research-agent niet zichtbaar effect. Maar nu je het weet: bij langere agents — gebruik je dit."
-
----
-
-## BLOK 7 — Live Demo 4: Done-tool + custom stop (20 min)
-
-`[SLIDE 14 — DEMO 4]` `[SCHERM: editor]`
-
-**Vertel:** "Nog twee patronen. Eerst de **done-tool**. Een tool **zonder execute**."
-
-```typescript
-export const done = tool({
- description:
- "Roep aan wanneer onderzoek klaar is en rapport is opgeslagen. " +
- "Geef het reportId en een korte samenvatting.",
- inputSchema: z.object({
- reportId: z.number(),
- summary: z.string(),
- }),
- // GEEN execute — dat stopt de loop
-});
-```
-
-**Vertel:** "Geen execute betekent: als agent deze tool aanroept, stopt de loop automatisch. Geen tool-result toegevoegd, geen volgende stap.
-
-Combineren met `toolChoice: 'required'` — dwingen dat agent altijd via een tool stopt, nooit met losse text."
-
-```typescript
-export const researchAgent = new ToolLoopAgent({
- model: openai("gpt-4o-mini"),
- system: `... [zelfde] ...
-6. Wanneer alles opgeslagen is, roep dan 'done' aan met het reportId.`,
- tools: { webSearch, readPage, saveReport, done },
- toolChoice: "required",
- stopWhen: stepCountIs(15),
-});
-```
-
-`*[Run]*`
-
-**Vertel:** "Agent doet zijn werk, eindigt met `done({ reportId: 42, summary: '...' })`. We pakken het eindresultaat uit `result.staticToolCalls`:"
-
-```typescript
-const result = await researchAgent.generate({ prompt: query });
-const doneCall = result.staticToolCalls[0];
-if (doneCall?.toolName === "done") {
- console.log("Final:", doneCall.input.summary);
-}
-```
-
-**Vertel:** "Voordeel: gestructureerd eindantwoord. Geen vage text. Voor pipelines waar je het resultaat ergens anders moet injecten — heel handig."
-
-### Custom stop op kosten
-
-`*[Voeg toe]*`
-
-```typescript
-const costLimit: StopCondition = ({ steps }) => {
- const tokens = steps.reduce(
- (sum, s) => sum + (s.usage?.totalTokens ?? 0),
- 0
- );
- const usd = (tokens * 0.00015) / 1000; // gpt-4o-mini rate
- console.log(`Total tokens: ${tokens}, ~$${usd.toFixed(4)}`);
- return usd > 0.05;
-};
-
-stopWhen: [stepCountIs(30), costLimit],
-```
-
-**Vertel:** "Stop als kosten boven 5 dollarcent uitkomen. Voor productie: kritiek. Eén bug in je prompt en je hebt 100 calls — zonder cost cap is dat 20 euro. Mét cap: maximaal 5 cent per run, klaar."
-
-### Plan-Act-Reflect
-
-`[SLIDE — terug naar slide 14 visual]`
-
-**Vertel:** "Laatste pattern — alleen kort toelichten. ReAct: Reason + Act. Idee: in de system prompt vraag je expliciet om eerst een plan, dan acties, dan reflectie.
-
-System prompt:
-```
-1. Begin met een plan in 3-5 bullets.
-2. Voer plan uit met tools.
-3. Eindig met korte reflectie — wat zou je anders doen?
-```
-
-Geen extra code nodig — het model doet dit vanzelf binnen de loop. Effect: tracebaarder gedrag, en zelf-correctie in latere stappen.
-
-Voor sub-agents — een agent die een andere agent als tool gebruikt — schuiven we naar Les 14, want we hebben tijdsdruk."
-
----
-
-## BLOK 8 — Wanneer wel, wanneer niet (10 min)
-
-`[SLIDE 15 — Agent vs tool-call vs workflow]`
-
-**Vertel:** "Drie niveaus van AI-autonomie. Plain tool-call: 1 tool, 1 antwoord. Multi-step uit Les 12: 2-5 tools, 1 doel. Agent uit Les 14: 10-50 stappen, open-ended. En het hoogste niveau van **controle** — een explicit workflow. Code die exact bepaalt welke tool wanneer.
-
-`*[Wijs tabel op slide]*`
-
-Wanneer GEEN agent. Determinisme nodig — finance, juridisch, medische data. Geen agent. Doe het in code. Latency belangrijk — agent kost al snel 30 seconden. Voor user-facing chat: te traag. Voorspelbare kosten — agent met 50 stappen kan duur zijn.
-
-Wanneer WEL agent. Open-ended — research, planning, debugging. Volgorde onbekend — model moet zelf bepalen. Async gebruik — backend job, niet realtime UI.
-
-Quote uit de docs: *Agents are flexible and powerful, but non-deterministic.*"
-
-💬 *Vraag: "Wat is een workflow dan precies in code?"*
-
-**Antwoord:** "Gewone JavaScript. If-statements, switches, expliciete functies. Bijvoorbeeld:
-
-```typescript
-async function processOrder(order) {
- const validated = await validate(order);
- if (!validated.ok) return reject(validated.errors);
- const enriched = await enrichWithAI(validated.data);
- await save(enriched);
- await notify(enriched);
-}
-```
-
-Dat is een workflow. Reproduceerbaar, debuggable, testbaar. Een agent zou dit ook kunnen, maar je weet niet exact wat-ie doet — non-deterministisch."
-
----
-
-## BLOK 9 — Lesopdracht + Huiswerk (15 min)
-
-`[SLIDE 16 — Lesopdracht + Huiswerk]`
-
-**Vertel:** "Lesopdracht. Vandaag in de les: setup van de research-agent zoals ik 'm vandaag liet zien. Tavily account, Supabase tabel, 3 tools, agent, één test. Half uur — kun je halen voor we klaar zijn.
-
-Voor huiswerk: uitbreiden. Vier dingen.
-
-Eén: 4e tool `listReports` — om eerder opgeslagen rapporten op te halen. System prompt aanpassen zodat agent eerst checkt of er al onderzoek is.
-
-Twee: `prepareStep` toevoegen. Kies één use case — dynamic model, fase-tools, of context-trimming. Documenteer wat je koos en waarom.
-
-Drie: custom `StopCondition`. Token-budget, of aantal pagina's gelezen, of iets eigens.
-
-Vier: schrijf een `AGENT.md` in repo-root. Tool-lijst, stop-strategie, prepareStep-keuze, één voorbeeld-run met logs, één observatie.
-
-Bonus voor wie wil: UI met live step-rendering, of een sub-agent voor samenvatting.
-
-Beoordeling: 10 punten totaal, voldoende is 6+. Inleveren via Brightspace met repo URL voor Les 14."
-
----
-
-## BLOK 10 — Afsluiting (5 min)
-
-`[SLIDE 17 — Afsluiting]`
-
-**Vertel:** "Wat hebben we vandaag gedaan. Een agent is: LLM in een loop met tools. We gebruikten `ToolLoopAgent` om de loop te managen. Vier stop-condities — stepCountIs, hasToolCall, isLoopFinished, custom. `prepareStep` voor dynamic model, tools, context. Done-tool pattern voor gestructureerde eindantwoorden. En we wisten wanneer een agent wel/niet de juiste tool is.
-
-Volgende les: RAG. Embeddings. Vector search in pgvector. Combo met agents wordt heel sterk — een 'searchKnowledgeBase' tool die semantic search doet, in een research-agent.
-
-Daarna: testing, deployment, performance, eindopdracht.
-
-Vragen?"
-
-`*[Vragenronde — minstens 5 minuten over laten]*`
-
----
-
-## NA DE LES — Wrap-up
-
-- Push working `research-agent` repo naar GitHub als referentie
-- Brightspace: link naar deze repo + huiswerk-instructie
-- Verzamel vragen die niet beantwoord — neem mee in Les 14 opening
-- Note voor Klas B (later): Klas B doet dit waarschijnlijk in week na RAG, niet ervoor — orde aanpassen indien nodig
-
----
-
-## Veelvoorkomende fouten tijdens live coding
-
-| Fout | Oplossing |
-|------|-----------|
-| `TAVILY_API_KEY undefined` | `pnpm dev` opnieuw starten — env wordt bij start ingelezen |
-| Tavily 401 | Verkeerde key — check dashboard tavily.com |
-| `result.steps` is undefined | Je gebruikt oude v5 API — check je `ai` versie is `^6` |
-| `ToolLoopAgent` import faalt | `pnpm update ai` — moet 6.0+ zijn |
-| Agent stopt na 1 stap | `stopWhen` te restrictief, of system prompt zegt niet duidelijk wat te doen |
-| Tool returnt `[Object]` | `JSON.stringify` voor logs — Tavily resultaat is genest |
-| Supabase RLS error | Open policy nog niet aangemaakt, of `enable rls` ontbreekt |
-| Cost-cap stop werkt niet | Check of `step.usage` echt populated is — sommige providers geven het niet altijd terug |
-
----
-
-## Mentale model voor de klas
-
-Als studenten verward zijn over "wanneer is iets een agent vs tool-call":
-
-> **Tool-calling** (Les 12) = "AI mag tot 5 keer een tool aanroepen om je vraag te beantwoorden."
->
-> **Agent** (Les 14) = "AI krijgt een DOEL, mag autonoom plannen en 20-50 stappen doen, en levert pas op als-ie zelf vindt dat het klaar is."
-
-Het verschil is dus **doelgericht autonoom werken**, niet zomaar 'meer stappen'.
diff --git a/Les14-Agents/Les14-Huiswerk.md b/Les14-Agents/Les14-Huiswerk.md
deleted file mode 100644
index 3f7285a..0000000
--- a/Les14-Agents/Les14-Huiswerk.md
+++ /dev/null
@@ -1,322 +0,0 @@
-# Les 14 — Huiswerk
-## Agent uitbreiden + prepareStep + custom stop + AGENT.md
-
-**Vak:** AI-Assisted Development
-**Opleiding:** NOVI Hogeschool Utrecht
-**Deadline:** Voor Les 15 — RAG + embeddings
-**Inleveren:** GitHub repo + `AGENT.md` in root
-
----
-
-## Doel
-
-Bouwt voort op de **lesopdracht** (research-agent met 3 tools + `stepCountIs(10)`). In dit huiswerk maak je de agent slimmer: extra tools, dynamic step-control, een eigen stop-conditie, en documenteer je het geheel.
-
-> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt nodig.
-
----
-
-## Onderdeel A — Extra tool (verplicht)
-
-Voeg een 4e tool toe: **`listReports`** om opgeslagen rapporten op te halen.
-
-```typescript
-export const listReports = tool({
- description:
- "Toon eerder opgeslagen onderzoeksrapporten. " +
- "Gebruik dit als de gebruiker vraagt naar bestaand onderzoek " +
- "voordat je nieuw onderzoek doet.",
- inputSchema: z.object({
- limit: z.number().int().min(1).max(20).default(5),
- }),
- execute: async ({ limit }) => {
- const { data, error } = await supabase
- .from("research_reports")
- .select("id, query, created_at")
- .order("created_at", { ascending: false })
- .limit(limit);
- if (error) return { error: error.message };
- return data;
- },
-});
-```
-
-Voeg toe aan `agent.tools` en test:
-
-> "Heb ik eerder iets over AI in de bouw onderzocht? Zo nee, doe het nu."
-
-Verwacht: agent roept eerst `listReports`, dan eventueel `webSearch` → `readPage` → `saveReport`.
-
-### Eisen
-
-- [ ] `listReports` werkt
-- [ ] System prompt aangepast — agent weet wanneer te checken
-- [ ] Minstens 1 test-query gedraaid die `listReports` triggert
-
----
-
-## Onderdeel B — prepareStep toevoegen (verplicht)
-
-Voeg een `prepareStep` callback toe aan je `ToolLoopAgent`. Kies **minimaal één** use case:
-
-### Optie B1 — Dynamic model
-
-Begin met `gpt-4o-mini`, switch naar betere model bij complexe stappen.
-
-```typescript
-import { openai } from "@ai-sdk/openai";
-
-new ToolLoopAgent({
- model: openai("gpt-4o-mini"),
- tools: { webSearch, readPage, saveReport, listReports },
- prepareStep: async ({ stepNumber, messages }) => {
- if (stepNumber > 4 || messages.length > 12) {
- return { model: openai("gpt-4o") };
- }
- return {};
- },
-});
-```
-
-### Optie B2 — Fase-gebaseerde tools
-
-```typescript
-prepareStep: async ({ stepNumber }) => {
- if (stepNumber <= 2) {
- return { activeTools: ["listReports", "webSearch"] };
- }
- if (stepNumber <= 5) {
- return { activeTools: ["readPage"] };
- }
- return { activeTools: ["saveReport"], toolChoice: "required" };
-}
-```
-
-### Optie B3 — Context-trimming
-
-```typescript
-prepareStep: async ({ messages }) => {
- if (messages.length > 15) {
- return {
- messages: [messages[0], ...messages.slice(-10)],
- };
- }
- return {};
-}
-```
-
-### Eisen
-
-- [ ] Minimaal 1 `prepareStep` use-case geïmplementeerd
-- [ ] Effect zichtbaar in console-logs (welk model / welke tools per stap)
-- [ ] In `AGENT.md` uitleggen welke optie gekozen + waarom
-
----
-
-## Onderdeel C — Custom stop-conditie (verplicht)
-
-Schrijf een eigen `StopCondition`. Twee voorbeelden — kies één of bedenk eigen:
-
-### Optie C1 — Token-budget
-
-```typescript
-import type { StopCondition } from "ai";
-
-const tokenBudget: StopCondition = ({ steps }) => {
- const total = steps.reduce(
- (sum, s) => sum + (s.usage?.totalTokens ?? 0),
- 0
- );
- return total > 30_000;
-};
-```
-
-### Optie C2 — Genoeg sources verzameld
-
-```typescript
-const enoughSources: StopCondition = ({ steps }) => {
- const readPages = steps.flatMap((s) =>
- s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
- );
- return readPages.length >= 5; // 5 pagina's gelezen = genoeg
-};
-```
-
-Combineren met andere condities:
-
-```typescript
-stopWhen: [
- stepCountIs(30),
- tokenBudget,
- hasToolCall("saveReport"),
-]
-```
-
-### Eisen
-
-- [ ] Custom `StopCondition` werkt
-- [ ] Gecombineerd met `stepCountIs(N)` voor veiligheid
-- [ ] In `AGENT.md` uitleggen wat de conditie doet
-
----
-
-## Onderdeel D — `AGENT.md` documentatie (verplicht)
-
-Schrijf in repo-root.
-
-### Sectie 1 — Tools
-
-| Tool | Wat doet 't | Read / Write |
-|------|-------------|--------------|
-| webSearch | Tavily query → 5 resultaten | Read |
-| readPage | Tavily extract → fulltext | Read |
-| saveReport | Insert in research_reports | Write |
-| listReports | Bestaande rapporten ophalen | Read |
-| (eigen tool?) | ... | ... |
-
-### Sectie 2 — Stop-strategie
-
-- `stepCountIs(N)` — safety cap, N = ...
-- Custom: ... (uitleg)
-- Combinatie waarom: ...
-
-### Sectie 3 — prepareStep keuze
-
-- Welke optie gekozen (B1/B2/B3)
-- Waarom
-- Wat zie je in de console-logs verschillen tussen "met" en "zonder" prepareStep
-
-### Sectie 4 — Voorbeeld-run
-
-Pak een echte query, draai 'm, en log alle steps. Plak in `AGENT.md`:
-
-```
-Query: "Wat zijn de belangrijkste AI-trends in 2026?"
-
-Step 0: webSearch({ query: "AI trends 2026" })
- → 5 resultaten
-Step 1: readPage({ url: "..." })
- → 4500 char tekst
-Step 2: readPage({ url: "..." })
-Step 3: webSearch({ query: "AI regulation EU 2026" })
-Step 4: readPage({ url: "..." })
-Step 5: saveReport({ ... })
- → reportId: 42
-
-Finale rapport: (200-300 woorden)
-```
-
-### Sectie 5 — Eén observatie
-
-Iets wat opviel:
-- Onverwacht goed gedrag?
-- Loop die te lang duurde?
-- Tool die niet werd gekozen?
-- Effect van `prepareStep` zichtbaar?
-
-### Vorm
-
-- Max 700 woorden totaal
-- Concrete logs, geen abstracte tekst
-- Mag wat informeel
-
----
-
-## Bonus (optioneel)
-
-### Bonus 1 — UI met live steps
-
-`app/page.tsx` — een form + lijst die elke step toont terwijl agent draait.
-
-Gebruik `agent.stream()` ipv `agent.generate()`:
-
-```typescript
-const result = researchAgent.stream({ prompt: query });
-return result.toUIMessageStreamResponse();
-```
-
-Client: tonen welke tool aangeroepen wordt + resultaat collapsed.
-
-### Bonus 2 — Sub-agent voor samenvatten
-
-Maak een 2e `ToolLoopAgent` (kleiner model) die per URL samenvat. Hoofdagent gebruikt deze als tool:
-
-```typescript
-const summarizer = new ToolLoopAgent({
- model: openai("gpt-4o-mini"),
- system: "Vat één pagina samen in 5 bullets.",
- tools: { readPage },
- stopWhen: stepCountIs(3),
-});
-
-const summarizePage = tool({
- description: "Vat een URL samen",
- inputSchema: z.object({ url: z.string().url() }),
- execute: async ({ url }) => {
- const r = await summarizer.generate({ prompt: `Samenvat: ${url}` });
- return { summary: r.text };
- },
-});
-```
-
-### Bonus 3 — Done-tool pattern
-
-Vervang `hasToolCall("saveReport")` door een expliciete `done`-tool zonder execute, met `toolChoice: "required"`. Documenteer in `AGENT.md` wat het verschil was.
-
----
-
-## Inleveren
-
-1. **GitHub repo URL** in Brightspace
-2. **`AGENT.md`** in repo-root
-3. **Updated `lib/tools.ts`** met `listReports` (+ optionele extra tool)
-4. **Updated `lib/agent.ts`** met `prepareStep` + custom stop
-5. **Schema-update** in `schema.sql` (mocht je nieuwe tabel hebben)
-
----
-
-## Beoordeling
-
-| Criterium | Punten |
-|-----------|--------|
-| A — listReports werkt + agent gebruikt 'm correct | 2 |
-| B — prepareStep geïmplementeerd + zichtbaar effect | 2 |
-| C — Custom StopCondition werkt | 2 |
-| D — AGENT.md compleet met 5 secties | 3 |
-| Agent draait end-to-end (geen broken runs) | 1 |
-| **Totaal** | **10** |
-
-Voldoende = 6+. Bonus telt mee bij twijfelgevallen.
-
----
-
-## Tijd-indicatie
-
-| Onderdeel | Tijd |
-|-----------|------|
-| A — listReports + test | 20 min |
-| B — prepareStep | 30 min |
-| C — Custom StopCondition + tests | 30 min |
-| D — AGENT.md schrijven met logs | 30 min |
-| **Totaal** | **~2 uur** |
-
----
-
-## Veelvoorkomende valkuilen
-
-- **`prepareStep` returnt niks** → je moet `{}` returnen voor "geen wijziging", niet `undefined`
-- **Custom stop returnt nooit true** → log `steps.length` en je conditie-waarde per stap
-- **Agent stopt te vroeg** → check of je `stopWhen` array niet te restrictief is
-- **Sub-agent recursie** → een sub-agent gebruikt zelf weer een sub-agent? Stack-overflow risico — limit op depth
-- **Token-cost out of control** → log per stap usage en zet kostencap
-
----
-
-## Tips
-
-- **Log per stap** — `console.log({ step, toolCalls, usage })` is de beste debug-tool
-- **Test isolated** — test je `StopCondition` met fake steps eerst
-- **Houd `AGENT.md` bij tijdens werken** — niet aan eind
-- **Eindrapport kwaliteit** — als agent rapport een 4 is van 10, ligt het meestal aan system prompt — niet aan tools
-
-Volgende les: RAG + embeddings. Combo met agents: een RAG-tool in een agent. Tot dan!
diff --git a/Les14-Agents/Les14-Huiswerk.pdf b/Les14-Agents/Les14-Huiswerk.pdf
deleted file mode 100644
index 2165c32..0000000
--- a/Les14-Agents/Les14-Huiswerk.pdf
+++ /dev/null
@@ -1,194 +0,0 @@
-%PDF-1.4
-% ReportLab Generated PDF document (opensource)
-1 0 obj
-<<
-/F1 2 0 R /F2 3 0 R /F3 4 0 R
->>
-endobj
-2 0 obj
-<<
-/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
->>
-endobj
-3 0 obj
-<<
-/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
->>
-endobj
-4 0 obj
-<<
-/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
->>
-endobj
-5 0 obj
-<<
-/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-6 0 obj
-<<
-/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-7 0 obj
-<<
-/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-8 0 obj
-<<
-/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-9 0 obj
-<<
-/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-10 0 obj
-<<
-/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-11 0 obj
-<<
-/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-12 0 obj
-<<
-/PageMode /UseNone /Pages 14 0 R /Type /Catalog
->>
-endobj
-13 0 obj
-<<
-/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607091327+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607091327+00'00') /Producer (ReportLab PDF Library - \(opensource\))
- /Subject (\(unspecified\)) /Title (Les 14 Huiswerk) /Trapped /False
->>
-endobj
-14 0 obj
-<<
-/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
->>
-endobj
-15 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 2436
->>
-stream
-Gatm:e#n!9PLY=8M!p9O/d!!THIcR/7Uu77ncE:q-#JS@ndU%U.@[O)7k.Q$"?cQ,OH/n+p`>d@(g$CPS!5g+:R<-l[/T;joOe^-Y*Ls@VZT%.sa2eGWGR1CVt2eu>Ka#&9Ao+tHY"Q*8]o+d=0nUE8Ne>LpQXM-L@9o&VLj>ne(&egqqYc._&.!nZU-!1id$I3msnIRLu6i1K$rLm9?4bo8Jj+@o#nK8BdXW%Y9MF:,_RLjHtCD8[eUu(0Tme+H:&an#U':*qaC?50XkP#cSMBMOl*rO68el'>-M-*Vle7e3gs3Gkf!3%/*[9Z7\CE@/%]8ZViUTeaVg-.]Z[Z;ZgTlDIHJ%"0b"Y:3O$YPjQA0>>,bE-6-e"sp*5=7nRZNYn+qfJ6Z0N).D8h,^k,3Mhi6&_b%+.Dd'pB]bU0=5gW"(!P]#fa>SFj5F7S=*+hdm*?`38rHKmH\2-TIOm)OCP++eoMsU9423&^KY%VCXt,Ma#ust/naX9S'rCSVcq%F/\<0%`>6)9'=F'oEbdZQl'Ueo%DXSM?J83FBlMf4.DX)8KXHJm7bplY]Ue4]API>n@9gadV0Xhr1R4cI*L*AQfjDd'$,tASle"j,fB@]R[@Eh:a>p9EdG`11R3-Y0G+VS39#pHkipm'_&/;Ugi*I@ssF_.R]S'IcuPp9?=g7FrUTs!CPh[Sr?7sca)q"Pgbb&R$2jV(iDT,>3\$V+`.t$+0iXDg,Yo0_a"P4L\oj?GUqQB4p(7OMBu&Le"/WIqo,(2CcO-`NOQH%*1:i:3)h1CWtk&aYRLXta\D:Z`ZOCji)Hqg[`C/=YmKUrj42[8cZbo1')h#8caB:6gP'OdZ$aQ>*_f/s0P?1?qb4sYg7ePf]TE#6AHsiQmO!o3N$Ejn%lS_YXXC<_"63.B%:JR>i(hmQ]sG`7mL)*]i8J6-u;=YC=Kmju`:7U]UbG;CS^W=XmDM#;J9uDud%NkuSfq_!ZMTrSTS$'sr3MerBH2/N+s.FRhO$M(/WJ/'P2&;k;JkfC*);8P#2!oFV'kim+-Mb0=8b>]b1fqdWa6R,QHq7UDj4I7t:=PWJ$NrLV`!>,B8!@g0F%T%&WCTn;5'P.Q,l6KgFN@?$0G+N7hd&X"-MS]L013SGprHa$=?<8aMf9X&8q[pD2>Q7$dd2:\T<8h6+`%/5+s*/"G.p3j()'XG+1"2kMi$$IJk;t*6rSkeMW_/J\C4q8W%bc31ji2o?^ZgMl3rSmn9i9a;Z_tr^,:2Tq0LK:;*Vl=>FW2ncI8*^cq:Q6fg\)3pMe^8#F8!3FSiu7&>f\aFS[N,QX,gL2_m#*7Ws-/%dZ9pFmPm*h6U0crRc[C4RJdH6)9>9iia3Q*WNjr'q]Hn>_jg)WfCJ(h+pqp4)%IeOX9?)ccI23-L\^sr]MA~>endstream
-endobj
-16 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
->>
-stream
-GauI192jS!'SZ;\'k`piK90WsP*^8o#%m'$l?:.dj'3Rjpjk!=nF(^64)*,.QaJ[(aS4KYS1=cb99!T;!:P!!>pq+\r#WudKDZW0oB7nA3n;e,&@%F=VBN6io>+L*M^0$OVDAB'X`2k,3$k*<*&Q8MX"6BlrZkAQ2O2i2#cEj8V+:3`8@@U27O2]:rEVYZ\IDjld[?+MVp/]KLRLs)<+@/K8o0*l_;b'Q+'5u=.N6H0f<+R+GUcbqG?S4@?_*c!6TkQUiWK.RFk8S%L3L^g,!XcQg=&!#,"sQTU&HnYN/u/[H?^e:0^4$9IK)glgK$C3YA1+*m8%lJ[Te1oChO*PO&+hD:sm]-7D6XF-ER$Vh,+0Hg/%WlTr?S==LAq@,A,h?3~>endstream
-endobj
-17 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 1697
->>
-stream
-Gb!;dgN)%,&:N_Cm-%#9/Hc%S&p0VejC0L.VFf78I>RM[e$.&N(R_)cVN1\i$XQ(j[Of&83)dq/JkP%g!n_RM'a'>#*^&XiD;(>DXELcJR*srH)NuSABpSb7u&B1HC]PHP5ic1!]"d,AmXH.;[;^T/-G\qO>B(LdcfRpSqZDaoKPY#6!1kkJSGCLULl[5kYkVUU!pq8/!FBZM>F!$to:7d_^>ZU\oar7_-AL*i':4UhO":ic(Md.b)8Rs!$C5%X[[C4W>V&im=[,/G3;A38F=@kdMq3HaV_1>c^4+!6m2WO(2$Wm-U@ScsePJ9(l&EEskj]k>Xrqa+Dkbj3j]^/s>"KO;b9S4FE'ii_2m$G?]QrLqXIW,dUncdePrZm920XJ>k;%@@Jk2*8QgD=j0(P'dl<970MGm:8le.rpT:Oah8,(Z=_`c0;X^_UH?61?\i!O=`7f(Yjs>fQ]1!HMGg73K^&4%IBcKF-+7!Ha+;,tc8lRT6-Ur)c:k7o?TiFf7<%PnUNCt;Nqi,i'>N,oWc+r1H(!\%':k*.U?(]3C[X\t3IHf9YS]rIpX*c1'$QdBtPE!@]&?>aO0p"cc_bX;81GF-3p5bPS-h79*i:=t67KX?np:q6?a9t/r=Vq0(N5Z)OY1f#eR]Vm%`?h5#G+&3bF<(OK9nca_KAuj%hP?U?k3K#:CGCgN[,R;mo;Jh88<>[OncJ8^ol=_)L),ha=#$_<"ACcn.>WeneeLj8UQm?"L(&>M&kk1/k43Tc!?e$ZkNs4a&c=#9,VRtO:lB%mH!d4IQ/F(O8lCUKn^BkbAYL/>CV@Vlb&G&Z;aQgnHYc/bpJVASkJ%%Zg.1W:)EuD^$[srlae92lf"9Zs.VG`G$A`H4M^:ZXmD*j3cH!m\N=S$nUq`OV53a'"uAm9)hmcWZk4.J2?(86JO9$5#$KM&Rs1Y)->)hN1!?rIQA(XF(a=.(ST,PMbnWHMbn&[4W*R74g53okf!`<]'$V39g*6ohJ`h3"(Berq2ll3#U)+5&l8LQ2P)T`g\J4'5:QF.*BlGoQ=ac6MQAZm(tdC^410tUC-uYQ1Iii((b*ij]S!!HBR@\bpQ;Wh#gF??d7slK4hC]hbHah%*Q.Qo\B@DE1rKA)QOG@WqufLE6``T/V<`i(S#sT#6K=r~>endstream
-endobj
-18 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
->>
-stream
-Gau`SgN)%,&:N_Cm.a.I'*MAL9PSne2]'f9:=H0/6^6gh'I5Ep73O*mg._LZ$XQ*@[]I'(E@d/A/V155fKIC]",\2OI)d,?dj[D,c@Bo%/q?M;@JU>&9ta=DiB@dJ![9H9+U8VeqZl62bA][BrZDX'Iir\QR6%h0]P9G-YEs]<)liHAe%%n$^2@lbiJF?9L$DdT(OtXQU295BX!Krb#CnR?>bqkZRPohEIWh28KNF&!PQ07Co(Y"3p*CoPGW6E/HZ)qWmo8QapBYsn=q@aE`sL2"XYPV\"Q/l^c5"`).Nat*%/Ru0YfNWVq;V=hbqhNL7nhf%c'A_cj-tXW2/EC%<6X6=MDIRcMM@3k[2IF1YnGNPU,lB.+j@$6a'6bCF;q4;@ppGP^(\p1m'*GS_KdkVfK#or5gm8k,86o\,0gbcPSaa.)em5%Yg+(`1S@jh_Ab'gA`oLNj-%>BI"qKQdNlX!CfRq*R,bD_"LfYO-e/Z;rL)i\-K[pop/>9td0Sij;:47PTk]hIDObf2ank@^%6mhSpo6A.Z-@ak9QfC2,++-Y%.4_J7Q$SE`@TU[SZ;CMW03("97K,au+6)`at'ht@okV4*nkhsYL7Ls:-"4qGiYaXPE>29GiS!9Vub9l;/UDTsSbZ@Ml-V:a!ar9Q[f*8s*V\FN47>Vn3&1%RBYHON\A13O?W#Nh6UMrSSP*doR0hSB2^G\lZ2JM_qg\[%t;R+"L79U_L&PDEuMU/@Spc:KiULX(>1FF8'>&);$DZIJc3_C!c!@"A.DdkMlg^Kt[@Tg#I#`qWcamMOHd_L5)_:lu-MO*CX1d2fY<,cjZFEVp/Ad;3G?Nf*Ae!l+#5ltKKstXTKuj_/KW<[h0tsBepAP_:=RGgK(npJJV\K%-,pm.jnNcp1&eiu\lG.Ti-4pk"2cN72j]@8*eh2e_8s<:6u^#"AbdLX\Qq]8S_7.W&efVeoCX!XgfQh:]GO$J+lGmfRK0;Vm^0rIJ6+j!H8rC^a;M"3R_6f1[>s.O/cW/.nZrM%g!RPg@?]3?=kZ7/kGX.2;".n">]W/XdsSt.(XYuJmf~>endstream
-endobj
-19 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 1837
->>
-stream
-Gb!;d>>O9K'Roe[380^R$AOu=gl0acJ7Z>`/+u>TkF=6!pgWpRBEGCsK^JLB(l.&f0L5gM5Jk7`qsG'!\/#^5Dl=as+G'!1TGW"n84?Ln!sXKp@^H,D(bm?k_"!rG9LE7IlrT5Kd>DZ`J/BVu;0U=_6ue6]"d)C\\'NCr)+Q>&CFtNBs-Bnkd>N9>09+[s8n#sgOT#b^!n:;Is*)sNai4_HCMs[i_:J5*#&0\m/F6YLXP]\(r(5TCs,*-mu;Z*F=3Bu#$<']3ZE7kNAmrH@Afpo]9KkZafNoJtN_u,qb>=G,bRrFWtS3^j6jA>7qa^$J9T/t)fo"Pt!S\B:^RAQ=DK*@LPe->bK$ZHn3T+"q*0eXa0lHZ@nmD\`Pi\#ug<-XX=FLhV8j8^).q0V;QP.h!^B6S581g^+(a]@6_+Q;r_%BQ,f^BIm,;B=@X;;)mBb,PV]R\_gh?j3XErk!IW4e_Hd^5=3P,LjL&bs$Y#?02,q2BNO78#/ARZ3c)-dm]7Nl85H^j6`q2Hl*>n0S\ZXY.3m`oQr-YD:pUIN0p*$Bj6ru=t6#C-Z3Pt(8[=^-XGrtNX>?J^9EfiY(!F)f_)]-W8,s'B^><$t@E=#3k-%N&*DcRBP/\XL?L'8':bq>M8!j*@Vo`[U)i7abDgTZLZ7t\RG\3arBLG1+6Uq_J1Q3uV$B]a,)CB=TgXkuER@pRAXD!5a0^f`,n;mJoOmGQqW`nA^"">aJk4gp[fRd8,!o0YU?>qcPst#A5E5d.97bdQEoG@-U&lNl0t[V[QgB\[%m``\*$*TrLd%G$+:(Q'Y>#6EGHc"uZ*#nZ]E:;$.fX4#krr5Kbd^h3SlBUN>tn"MDd`$&_pu:7[=a]U:dV>IP>8*1*uPZ0m9U@)PTl"9dViNRbO\o3JG\W($6nQHjG?.426K!t/YSkgM!T49a%Po8T7%-)5r%J##rR\JshR^pLCpmBLs*LqoE-?sB$CO&^!.arH@=YpL+.]P4GsR4iX7j&:#%YHg\qU)YbSio`e4"=dB*sg~>endstream
-endobj
-20 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 2009
->>
-stream
-Gb"/'gMYb*&:N/3%"cIH=B\nY%,%p!tj%+KQCe=73'&A)(ZLr)0=d+FT8Qn_Eu@)]TaoaV-,>_[0MmgQorc;Xl4?E-JUB4+7[ZfJ`ak>1+]S>rjg&Ia'XLV&'c*?4'^8Na^L1*Xso1'+bTRH'fR[K).@N?3tN@T-M;6-()b=BVTHe\)\Yq!:8mK_obP1,',Y!ohG?M;L?b3im-!'K@9^,,di$Lj"DD1i*uPcbdf,rn!L_apJRS7Q7/3N5Z5MHF=CWBh9;?MWrMfPji']Y&jh2-9NIS%sR7D(W1F@VrXsIm]mFePJI3Gt(k1eKFI&FjU#eQ#@)6p/rNr&qX;4<^Q9Lifo_DIB1RdLW1Jg8Lb'<]o!^noGGZm+,Nh/7Bt4fr#[iY5k$+:;0_d*K&X#>6u0iXFJVCKk`sbfU^Pr9MI3b?8/+T:q_9i5BC,N;Nh`(+Cs6r-18;(8Stf6'snQq7\=djX5SlAaV(J]+R)`66$Fa:%;FWT5,pLA:>P*7SmO24SSeNOU(B^fH[RWX4R3\SF+q:XO8q\_FH=`q+Ab>fI2[h@XFtN8HC]rWDG^hCFojX'VO.m<]aT'E7_4G[qgm1Nin$;qkd,Z*Fa'#L9YJS2%m7?klYm`mIOW.t%lPQrA4o/Eg7KWrJdC^_L>GZ.XbtrENM;7*i^9D/I;eD#2nGJma.2`N-OP%m39[mc2*Eg:53UW3+[37KX^#oLR0o^F\;b_U708b%u28)_2>d8WQ0co1SdjESL>EX'/,kA0;p1?rCY5?38Z[)k2f%dUYHi(JcPH7aH_,A;P(qu,SFt!O\Cf'8VDEu#HP=;="fV5Qg1Urql[?f'7U$=Z!(TC\O%"@_($(sT93:Po\(gufWKNAah@fb(0WIr.(7_N)c)q'Z'XoTVQc5N/VZZ%\(OWo/NR0rltqq0cn]t78*J^>Ktif>AX;InG=-p8f/ZjORf&=;@1Z$WZ)o>C/PY@Uh2SU&dm[&*C2H/jolumZ!CjtE0!EDOQh0%f;\<9-T[Y=iM3'-bLTb'uSWJ(+f,3pK/>lN(a7D\9@70R8MM$aTMZ0gNIoi0o4QfJ1J+B)pr6lq.6Xu-/q%Si7\gQWf<9p6^7fDegiG-04>7nWY%b3'$-B,E[@endstream
-endobj
-21 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 1570
->>
-stream
-Gb!ksbBDVu&DcY&As:e;/X-7mmE0EH-TsJ('!',D)i0.q=3bP&@\B7>pEPM'P99XHfhj3n=qZa;&H,'n_XV)m_c)3RQI4p"7A6Wad,<*#P9'iqS:Q>@jHtb#WLdCb]-sPm/fF^d5K#4[BYR_a)"<7JfhkgFYepZ-^"#]?N@"j,]<8sLK1A:O0^=/h:>$P:Z+n6WelXI]@#-hG4%DEGB-soH!#]Jr[HXg%Xf2d$\W\('4WZ@CRJnd\7aqUb&r5Fudip,X[)/2\O?.7=j=D71;K>D+j@8>1/E8`N4/4mU9I\TN[\[3L5Eba,=0/,VkKP4efeJ^*PDpA4E$-!R>*b-15G,?hRoa.S5PQlJH:3_6,ccq-i/7ThQ$q!`!]fKf)3Ul!B6=RIZ8k?9=06^X<2!l01c5d6O$[L33i!kc*8l>6@0SA#nu_u;%1F8Y6,3u0YCP+'&3?=+LK@ctfgd4N>G^sqiPAS&\E3CpQh;bHcABV0Yjm]o>ZU7)4.F\U]r*,ZfNr'!ZuU#GJ)NsDVUBa\qtm&U9cB>X5LsCWc!Msf':Pe$f/mnEfBQ!F64`X=t#"'4ar]7;-%2]no2R]6$AK0B0@hkGH]V;Km9t6UR2X^L?rTmL=hN'%/sYc]Iq%26%DpdqX@*'aI-N%MkPp94$64I*Ag5o;<]?Ahu!B('OV=o[VHZ*@?2ebi)b1WQqN4;rDlY>elscL6J"!UD7IBTTG^^$EBp3H%4)p/!hWiZl]0f2]Z`Nn;qA@6rJMZM=I>'0jZf6#RZ@/Sksa!MWed3Iq%+lH7:[/^.#[iSk41:,E>UQEmV$JP4NEPTTCG!A#&=!dJu57g]nVIfcJoAB4~>endstream
-endobj
-xref
-0 22
-0000000000 65535 f
-0000000061 00000 n
-0000000112 00000 n
-0000000219 00000 n
-0000000331 00000 n
-0000000436 00000 n
-0000000641 00000 n
-0000000846 00000 n
-0000001051 00000 n
-0000001256 00000 n
-0000001461 00000 n
-0000001667 00000 n
-0000001873 00000 n
-0000001943 00000 n
-0000002236 00000 n
-0000002334 00000 n
-0000004862 00000 n
-0000005488 00000 n
-0000007277 00000 n
-0000009068 00000 n
-0000010997 00000 n
-0000013098 00000 n
-trailer
-<<
-/ID
-[<6b9084b4bccf0b543324f6b07f7d5576><6b9084b4bccf0b543324f6b07f7d5576>]
-% ReportLab generated PDF document -- digest (opensource)
-
-/Info 13 0 R
-/Root 12 0 R
-/Size 22
->>
-startxref
-14760
-%%EOF
diff --git a/Les14-Agents/Les14-Lesopdracht.md b/Les14-Agents/Les14-Lesopdracht.md
deleted file mode 100644
index 0f01c27..0000000
--- a/Les14-Agents/Les14-Lesopdracht.md
+++ /dev/null
@@ -1,282 +0,0 @@
-# Les 14 — Lesopdracht
-## Setup research-agent + eerste run
-
-**Vak:** AI-Assisted Development
-**Opleiding:** NOVI Hogeschool Utrecht
-**Duur:** 30 min in-class
-**Status:** Tijdens de les afronden — daarna huiswerk uitbreiden
-
----
-
-## Doel
-
-Aan het einde van deze opdracht heb je een **werkende research-agent** in een eigen Next.js repo, draait er één test-query, en kun je in de console alle stappen zien die de agent zette.
-
----
-
-## Vereisten
-
-- Node 20+, pnpm, git
-- Supabase account (mag dezelfde zijn als Les 11/12)
-- OpenAI API key (zit waarschijnlijk al in je `.env`)
-- **Tavily API key** — gratis tier op https://tavily.com/ (1000 calls/maand)
-- VS Code / Cursor
-
----
-
-## Stap 1 — Nieuwe Next.js app
-
-```bash
-pnpm create next-app@latest research-agent \
- --typescript --tailwind --app --no-src-dir --import-alias "@/*"
-cd research-agent
-pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
-pnpm add -D @types/node
-```
-
-Open in editor.
-
----
-
-## Stap 2 — Environment variabelen
-
-`.env.local` (in `.gitignore`!):
-
-```
-OPENAI_API_KEY=sk-...
-TAVILY_API_KEY=tvly-...
-NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
-NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
-```
-
-> **Let op:** `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*` — moet server-side blijven.
-
----
-
-## Stap 3 — Supabase tabel
-
-In Supabase SQL editor:
-
-```sql
-create table research_reports (
- id bigserial primary key,
- query text not null,
- summary text not null,
- sources jsonb default '[]'::jsonb,
- created_at timestamp default now()
-);
-
-alter table research_reports enable row level security;
-
-create policy "demo open" on research_reports
- for all to anon using (true) with check (true);
-```
-
----
-
-## Stap 4 — Tools schrijven
-
-`lib/tools.ts`:
-
-```typescript
-import { tool } from "ai";
-import { z } from "zod";
-import { createClient } from "@supabase/supabase-js";
-
-const supabase = createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
-);
-
-export const webSearch = tool({
- description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
- inputSchema: z.object({
- query: z.string().describe("Zoekterm"),
- }),
- execute: async ({ query }) => {
- const res = await fetch("https://api.tavily.com/search", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: process.env.TAVILY_API_KEY,
- query,
- max_results: 5,
- }),
- });
- const data = await res.json();
- return data.results?.map((r: any) => ({
- url: r.url,
- title: r.title,
- snippet: r.content,
- })) ?? [];
- },
-});
-
-export const readPage = tool({
- description: "Haal de volledige tekst van een URL op.",
- inputSchema: z.object({
- url: z.string().url(),
- }),
- execute: async ({ url }) => {
- const res = await fetch("https://api.tavily.com/extract", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: process.env.TAVILY_API_KEY,
- urls: [url],
- }),
- });
- const data = await res.json();
- const content = data.results?.[0]?.raw_content ?? "";
- return { url, content: content.slice(0, 5000) };
- },
-});
-
-export const saveReport = tool({
- description:
- "Sla een onderzoeksrapport op in de database. " +
- "Alleen aanroepen als het rapport compleet is.",
- inputSchema: z.object({
- query: z.string(),
- summary: z.string().min(100),
- sources: z.array(z.object({
- url: z.string().url(),
- title: z.string(),
- })),
- }),
- execute: async ({ query, summary, sources }) => {
- const { data, error } = await supabase
- .from("research_reports")
- .insert({ query, summary, sources })
- .select()
- .single();
- if (error) return { error: error.message };
- return { reportId: data.id, success: true };
- },
-});
-```
-
----
-
-## Stap 5 — Agent definiëren
-
-`lib/agent.ts`:
-
-```typescript
-import { ToolLoopAgent, stepCountIs } from "ai";
-import { openai } from "@ai-sdk/openai";
-import { webSearch, readPage, saveReport } from "./tools";
-
-export const researchAgent = new ToolLoopAgent({
- model: openai("gpt-4o-mini"),
- system: `Je bent een research-assistent. Werk als volgt:
-1. Begin met een kort plan (3-5 bullets).
-2. Gebruik webSearch voor oriëntatie.
-3. Gebruik readPage voor de 2-3 meest relevante URLs.
-4. Schrijf een samenvatting van min 200 woorden.
-5. Sla op met saveReport. Inclusief alle gebruikte sources.
-
-Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
- tools: { webSearch, readPage, saveReport },
- stopWhen: stepCountIs(10),
-});
-```
-
----
-
-## Stap 6 — API route
-
-`app/api/research/route.ts`:
-
-```typescript
-import { researchAgent } from "@/lib/agent";
-
-export async function POST(req: Request) {
- const { query } = await req.json();
-
- const result = await researchAgent.generate({ prompt: query });
-
- return Response.json({
- text: result.text,
- steps: result.steps.map((s, i) => ({
- step: i,
- toolCalls: s.toolCalls?.map((tc) => ({
- tool: tc.toolName,
- input: tc.input,
- })),
- hasText: !!s.text,
- })),
- });
-}
-```
-
----
-
-## Stap 7 — Test in terminal
-
-In één terminal:
-
-```bash
-pnpm dev
-```
-
-In een tweede terminal:
-
-```bash
-curl -X POST http://localhost:3000/api/research \
- -H "Content-Type: application/json" \
- -d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \
- | jq
-```
-
-Verwacht: JSON met `text` (eindrapport) en `steps` (welke tools werden aangeroepen in welke volgorde).
-
----
-
-## Eisen
-
-- [ ] Repo werkt — `pnpm dev` zonder errors
-- [ ] `.env.local` met alle 4 keys + in `.gitignore`
-- [ ] Supabase tabel aangemaakt
-- [ ] Alle 3 tools werken
-- [ ] Agent draait 1 succesvolle query
-- [ ] Je kunt in steps zien welke tools werden aangeroepen
-- [ ] Eindrapport staat in Supabase (`research_reports` tabel)
-
----
-
-## Tijdsindeling (30 min)
-
-| Stap | Tijd |
-|------|------|
-| 1 — Next.js setup | 5 min |
-| 2-3 — Env + Supabase | 5 min |
-| 4 — Tools | 10 min |
-| 5-6 — Agent + route | 5 min |
-| 7 — Test + debug | 5 min |
-
----
-
-## Veelvoorkomende problemen
-
-| Symptoom | Oplossing |
-|----------|-----------|
-| `TAVILY_API_KEY undefined` | Restart `pnpm dev` na env-edit |
-| Tavily 401 | Key fout — kopieer opnieuw |
-| Supabase insert faalt | RLS policy check (`alter table ... enable rls; create policy "demo open" ...`) |
-| Agent stopt te vroeg | `stepCountIs(10)` → `stepCountIs(20)` |
-| Steps array leeg | Check je `result.steps`-mapping — niet `result.text.steps` |
-| `gpt-4o-mini` does not exist | Check OpenAI key heeft GPT-4 access — anders `gpt-4o` |
-
----
-
-## Klaar? Verder met huiswerk
-
-Het huiswerk bouwt voort op deze setup:
-
-- Extra tools (listReports, eigen tool)
-- `prepareStep` toevoegen
-- Custom stop-conditie
-- `AGENT.md` documentatie
-
-Zie `Les14-Huiswerk.md`.
diff --git a/Les14-Agents/Les14-Lesopdracht.pdf b/Les14-Agents/Les14-Lesopdracht.pdf
deleted file mode 100644
index 1d90b9f..0000000
--- a/Les14-Agents/Les14-Lesopdracht.pdf
+++ /dev/null
@@ -1,194 +0,0 @@
-%PDF-1.4
-% ReportLab Generated PDF document (opensource)
-1 0 obj
-<<
-/F1 2 0 R /F2 3 0 R /F3 4 0 R
->>
-endobj
-2 0 obj
-<<
-/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
->>
-endobj
-3 0 obj
-<<
-/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
->>
-endobj
-4 0 obj
-<<
-/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
->>
-endobj
-5 0 obj
-<<
-/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-6 0 obj
-<<
-/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-7 0 obj
-<<
-/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-8 0 obj
-<<
-/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-9 0 obj
-<<
-/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-10 0 obj
-<<
-/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-11 0 obj
-<<
-/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
-/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
->> /Rotate 0 /Trans <<
-
->>
- /Type /Page
->>
-endobj
-12 0 obj
-<<
-/PageMode /UseNone /Pages 14 0 R /Type /Catalog
->>
-endobj
-13 0 obj
-<<
-/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607091327+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607091327+00'00') /Producer (ReportLab PDF Library - \(opensource\))
- /Subject (\(unspecified\)) /Title (Les 14 Lesopdracht) /Trapped /False
->>
-endobj
-14 0 obj
-<<
-/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
->>
-endobj
-15 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 2309
->>
-stream
-Gb"/'D/\/u')p0`0it#uP=DV/jkI(+JLIMPo@7fM>4Gl!!sVJ:Ct]T&OHa&JfAEn[`MMYigr5mp)'\hBR@2m!B1=D-j!1#]A5;c79&ne3#t`Ul,Wt5:FI0G6c528X5_bk+bZ3GX,dGEJIoN"#3V`[`+rL\b4Cfr6aA[[>'e,;J(PX-0;Uag\NZ])A4B*@V-2^"3\(8/BO:n9Njp+2Z\\?Ng)>,0hFP9%*p)d\0+ZcBE<7bVbpn:L43Gn/338C3W(8n`9cAEg@:N`Y#Z^QoM$u*"'RT*9jeTs^"l:[Z_Mn..!=#<(1oLZsL73bJq_RL,gd$elu_`GHSb@5n18[S%EuDQ&;ubE-__I:[8^;)bNlf(U$9qaPLNhnq#a\&/,WgC9OrHm+`YL;=IM&KtI]@:h]?1lQp:N5,Q)*(tGe1[njA\HlL*;@"!t%jUDZDX4h8='cDjL+5SY(%-_$&W59nO5[)eg7&Nb%:QcJ,hJa,_H(7:?]UO%sU=tMo2%CeQtKihC'q%Am;D#jJmA0Ch`q$q#l^h/2/$sJMd#/FVnK`]Vbl\OOhCe3VZYnn`t3u5&S><_)JH7&Zoc]XE@9P5"`,jWZJJ(Wsgtdj*Fik_1WU&2I_SSFW\]&X_E)Fds4A<9c68Br`g4!3naS%`Gl3!5?k#8PqF_'RFWa^:,.2jRu\Lloj7%_89G7o^Z!^dK'2h@DPtlHIn/[%_[_Rdj98CR$m6lX\3!Bs0^JhT3'X!Jj!0)#Yp&aW#rbB]*^5=(2kg34NLgHIdD'$emej$Tfe$E1`$<7+B-8E@>fi]uX*1Q_%+HAmd8K4mkl^>umd_sVpV==M8!s$`G(a=uZc7!D4E&[`NB:93[O,uhB=]F\_B8;k6K]7tRZm80D>H/h(5H^\Fc$t%l+_"W?=D-RGXLEa3!E8Q8$:_b.U2TUcCq:boOY#FlSg?7h?(d;1,hN#$u&:q%kl#B:$pDR+nf/Shd[o$2KO0JeOd1^o4Ji]EdH0'N70i-$nUk)MTqk\1p2Q'9\kiA3N,4dEspp;$71X&h8@s3M0>@IT*,"gkWgG=7qLUMf%)?epkKchM,\\+$"+-ef:N!u1;'>];=K+b4<`5pM(lS$cf"TPiloPk+S#mNHER`D>aMAr+7L93E!Gn.EaW2Ver=NI7=%p3WUWGc_F]SAC1V!(B<6Hc$ed_#1VORk)5h4;?VI?A#rs:psg1,#i;2bUj[@ZA`QICBM&WG3c7fp@V24E%,NsNo/q9X"+P'B?`T!3B*.P.4KCECW^X),Fm?>/2h)/AlX8$FS?g7$J%XR]2H)I7&*V-^!KbWVMj.bi\\86!KU<<^is+DW\PI:dLqO]^*lSZcdK=t+5W#bs7!6DM5)f7VN%Zf[\endstream
-endobj
-16 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 626
->>
-stream
-Gar>C?#SFN'Re<2\1a+.,[B*ok9Jg2%1Q/aV;3V$64dBm'bZg#r;Kt`J^BQ5k9dp`a6+EH'MfL9fcK'8Kt@QoN'mFO9Fq6h%$EID(D3g[((s/SBP][?)uBa&D,c*d6s4CF(oiG?!eb"#(i+Y3T@Q\'mGNC,0d'XsD$1EfUb_585:28/>Zq]oThK2-koOa*O"XH,Gc0(k:U:eJf`!gsi@PsoK,!$_5hNpdkZi_]9'pe"=6e0ANa8Gl3Z$/LdLSf-nQcNq\W`g*$o]jN1gk#,=*K]03&1IVCu'>#t7>m+gPXE-F5Q%lC2F&GekU'^TPW-D_hhV.kF6F?2&0Pel7&D#t=lWA)=V(GZ%@%:S$K_`9qE.YQ?TQ9q>8EQ-#EjD&bIj_F]2t/UpjtKU1VTZsMrG;=Y3U/B)Y7S6ap!M=M-Jpdb(\u,!Q/kk7Hq^-K$.45*R+\)~>endstream
-endobj
-17 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 2004
->>
-stream
-Gb"/&=``=U&:W67f]jc+lG*B*n5(K,SMZbGof-ikAD0Kj%8FYS*F9n_3(bOt]aKpR.-G"0P.m/E8?ID\Qb2,r?pO7;g&FVRn8J_i:Dbbqdn=)##ah4!j6>e#LiAsk63?[Dl9u[`jI$D@X&/oG-[ruY"sFFH(^LZr,@MFC^i<]mYQ/DAQZkq.8Dqr>'L4u3rQtRX^IN[s;7.OppS6L?48V?S?:=_:3]+2A75.u_\sIH9Sfj9>5f,@5^!in*O5ZGr8i=_r/2MOJCR$G.bW$-kkkO"3e"ht($i!l<;-@&`WmLVs]nYcWUT9/'\0*mH_(P_DC=%@D>?NQCfKl.K=(r8!f>IG*rON%$H"UqY6fNAJ^j(7Skp$_0'7.M353*q@8c.^^fn]ld;A&V54R?)T_?-A(-$$==)*&QuY`LRAj[9]`M^18uO0rbn!?4=V#&^$=&]8P?p.O\$W7-ogkgkV"5<'n"c0dH8Dk;oR,KhV/dt[nlM;aLAYYW,r,/(388pJFe#7PUc:kglEU)+W_=uY?Qp[ETh*teK,^>MDb#e@R"J$.f=%C]:r?WHVbgaLP>L!#,U+[ndIpO#U>7M-QFaPC'0=@-EL&`0P57j85m%!8dk,XQ6(DFH0PI6n)HqN>g(-5ef]PDi#Z:%pA1EYQ8B7[YpXZ:4c;3Y=AS;ZnhgoZ^mIkJUB)D?p>'LV3W6nDG+sIV3*4^fGMumA=TI%"'[1-'3YQ?[3Y*7t[<`N;g#%i4SXpY_s@?LaMZ;:Pc!o.p\?8\`Xis_qKc/X*ZYLNDV!00[gHmq+df.2R0g.SKU)N#1-<`q<\';:=q'Q.HX%^F%(GG5N-<<#)ksn-t0]X6blhn^aenA3TX%(-Tj^=#;B!sYQs"^;WLp<(F`7b,#?X><0n\)`8lfT(0*XOsSgSBicXm46[6PZ`9]Re;6&nk7MW`-SP0STob*Fa8mXO2%tfRnqF"dQqS.*"e'98S7HY;#kF1DKe,W6=FaF!U'gmqqCj(b^h:I<41>'grQZhAa`[isHrN38202!r.[F%KrR[(u=KR`XUEbDHVQ1m\IBB9^j)s+Q3E-en#?O3#rOZ5b*=\PS)XRH05aqQ&?dk*aq'qT%o$5lfk/q`Mq^frR>okg#;)PhK:W1+/5gT5e#YP7To63LnlR?%3$%$J&dqM@01d6f$L(jhGmij[r$pNhN64,Ino0>cQ8Yt5FS3/4$.b5FT-Oam#RA!#21XF+e]s@rtb1ho]bW<\&Wt-id*fAoU3OebKUo^;`Wc*`lZ4up""l'Flp+Vgp[uemCaUg7k#$,g.X^=49a;MV:2`;_"d@r]u'1E94a4lQFdL"BsTd=@&+-6G?/hjha:><pS?I%2.Ir&I+cCHsoZ[LBa<5nbpkKnP,Ng\5YLlcM#7NXfH&SGSjL2dC'UMUs'A(H";:VW(D$24b7R@^pnPj/hdWF:X>qk?56Rp65K7f03,Y]FYgiZ+$V4_N0_1:>utd"OE":Zr"PVS+.>cC$m13endstream
-endobj
-18 0 obj
-<<
-/Filter [ /ASCII85Decode /FlateDecode ] /Length 1601
->>
-stream
-Gb!#ZD/\/e&H8h>EUY?RE#Fh5hE5mG[^BeWTZnCa5Z&@Udb:[Nfk^ADE>bS0[)FKlca_DGgDK_),P^eqE)t&I[S%21BrfUlk5[.!bSA"FQRso#krTe4l+SNjlG@q`/f/]+J]#E!s'TV,RgFZ3;NdZ$U?5PI,lt(JPUnp!"#(VL]2Ms0(CsBd"6MV+78p.o*]D]&b4K+@@GFG_/KT_0<.B^"ej-J4IB\6Ie!!R#s9lD<#%'E,)+Wdc?TE1-]a=h.]JA1^i/LAV+$fOg`4,r,Kq4^=<%$e$c4,#Cu_.f$I]5"MK5D%#CRr&P$EUk_"n`1g4K-JSS[6\,kS$#53fF@o@[Fg"_!&6>g\9V>DoaeEe/cU(gPd:/(D8K4N[t?PhTT!;&8uK:cC:+>D=,nP].0#&S-n-Jqi+,Q9$4cf!./V/`79lBWu1WgGuOf2[\_9KH[Om3h*oI,MPs&39P`HWQUs@+B8B&3fF2M\FDNF_.Xo@G%dLAM@X!qC]Ai@rQj,!3cut-G=0R41DcdS#"laT;f8fSObbg]"/h@h)V*>seP01["*'H)#!1V4eBH1XhsPEn(5[_gnW9:[$"/>/XHV'X76ZFQF"($E_0(m;C6@.d:nKG_[ELq9*+$B@Kq]ifaZtg#-XILEFc7S!=6;VOW_?1H.k*Z0<3a8-@AQnSd:=qq5b:=YFeJdjAX:::X+0j6Ho>)1*g.r@`J4C*4$`%F$K#7`CQTPuFWSkH9/T%,=dEb792"a4a/[3NE&SJ]"b[)%1VjU@icY[DT>,cYVCK-h*UY`K&a"G0M#q)8/j:]e.s2H)WZiq--F;_k-`:l9NmCHQrcn9_+MGk8c.'4'o0T