final lessons

This commit is contained in:
2026-06-07 10:44:05 +02:00
parent ceea2f206a
commit 39ec1bac72
105 changed files with 7789 additions and 1515 deletions

View File

@@ -0,0 +1 @@
,sharp-zealous-planck,claude,21.05.2026 07:13,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View File

@@ -0,0 +1,785 @@
# Les 15 — 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/Les15-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 /Les15-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 — <20><>é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<infer T> ? T : never;
const enoughReading: StopCondition<any> = ({ 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<any>` en niet typed?"*
**Antwoord:** "Excuus, dat is een shortcut. In productie zou je een `ToolSet` type extracten zodat je `StopCondition<typeof tools>` 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<any> = ({ 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 15: 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 15) = "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'.

View File

@@ -0,0 +1,322 @@
# Les 15 — Huiswerk
## Agent uitbreiden + prepareStep + custom stop + AGENT.md
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Deadline:** Voor Les 16 — 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<typeof tools> = ({ 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<typeof tools> = ({ 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!

View File

@@ -0,0 +1,194 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> 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:20260607084029+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607084029+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 15 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 2437
>>
stream
Gatm<CN%rs(B)NcEFGLJ3H_4ji+(uF$b4s@0i2u$M)P6"5XA=YM_RZC,S_[9dbhcOp_q:U/D%M"+d0a2F75[s3[6p1+45P8CSDp6o\IiFKV\DY$(q#Ms+Oe(]j$Q>: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;<A.rmY8u-ZrW.`kU,QgT<BfFeL]X$KC%gjWhd+Upo%MUXW[r5fG>joOe^-Y*Ls@VZT%.sa2eGWGR1CVt2eu>Ka#&9Ao+tHY"Q*8]o+d=0nUE8NM1AoNZhhsH2]_>$P3]2?85+P__kmo7U$OWGQ!dH/-og87*VVn%>C1f5FPG"ga$\&r&+,k.p4lgCpDd)rN=S?(`e-P/[;IO+.M=IqBL9YnXARV0P'W>D\^q3.8%:qH:0?e$W0-,PTAO#B."]fMH13U'>"]%R-"9<N;5:B8s$,87!i1_Ge92]aY'H\2m5"*BNP`[IC]!l\g-1O9LW5mhs4MQ'(9g%0/Y$65PC\OP/45HN:R;FGr<sR;bn^GJm*fMY_FSYY.B+_IMO[2A$XqRSmL[FN.F8h:d2!&]-uR7<.BE`(4hF05d5Gbm/!DPZ6/U+f17P'pD]?%!;nB*#E_Z>7:M8joI\!h<nmKa.=Cd="Eb7@@9`Mnpj+LWt\$(SSJZ?@?6]RFL=O2QF/V/W3s)*6NXBE1%*qpdo\X^?+DGGD?Sn\ub[-WN0'jL$6+"gC%i]&Cc/0fU!aYM0LF4cXA^dHHYb1SIG!P;<s't'1dGEMU]ll9=jlV"QhB#SokLeX$sbGL1Aa-K`&3,I)C-/<RfH.KXb?6Tt?jWoF,/#Xr@a1N[iA^EPd^O1VbcSFTX8_\+T!;s<Q6H1hjcip78h7Ar-D?;<>0]=/cG]Sn]]=,Rj>*C&J?#`5OV76?ng@lWGXnU):6'%ih\]bL&O>`?cfuB0_gFc==_m*LS"[3?ZhJF',L$fPOmK'FQT0,P=hKL*24*98U=KAQg%T-H4q^(#AV7*\f2=Do7hn7JqV%rsl"VolQVH?$dKuH1**.*F/)PGrn\75_kB@%<-?K.Yo#>_LHn@OeZ;E:-,ZL$oVf@^IF7qqNPE3)2%IcfC(Ef/(i/@Tg4I6]Z'#^(W2-oZ:Z=]S,;b%?p<9i@ggQV#pqetsTZ%@C@Z$pu&jr1i$_bsf=5$,/B**&G/SBUL`b)Aq*f8T*b*)+&TQ&KOoCBBut):<@m"Wh]_[U#i0r=_DH,rA#s_VCM0N1?_dd_m6^6fl>*S$sl2&R;QjV2Pu'G>+%+oB@ga,9[96$7_T44":FqS1fCUGpTZnQ&JZK_.mNYnYU/@:G<:h9Z[e`g?Ve\;mUt5H*QmV;0[*,%E==cK+t`I,LQJqOg5jnE*UT@oJRK[TK#<$)$+q#@,p.^5.)h@I^hM,cP5XCfN/?XZVn1326lVD!VtqQ\M&@/3Pa:JfV)0#o93rqtA>7i@qpu.'bIKlkpM_@]OcHU3i4g0tOT)[p-M?YNb0fA/TT`>LNBBJdg>j,l@W?dO&Lg\S+[l<Mh1&LplSdIFe#Cf\L;:sS%?=,m9cU0rNR\A[8D"+ITB)ko(ul/!&o]>Q>$GO)i7J2)Jar#?RHHY,CPdL^O69]lcL!qWjB+V+5*)QcHC2$+s*^bR(%'@tA7!?*]<VO*8p?Ig%E8uF\Ha(eOk,`JgjsGmb6G1o2K5R0^YW,RDYF>hG]i=,^-@'.b)J'O7!C%:+0EEd<NC"\ip)<Ma,AX6rC>WTMf\%P\;cjnTh*dLQ#2tkgn@Vm(?-m_!][Vq89?[A92nef/Q"&H0njpgW]s*s;if>Fk_Qr9!09Z@T_f5ugg!n7m&'.M$$l`3rH1%%Gos%3\Se?:P):kdh'<<<lmVZ9i^1BhALGdsX_#r5Z<q-SHT+5pQDS(\-O138rbU"+B#SB:]M2I>SXe+MXLrqAk't`>_FG_^iRC2$b8LabCdFu"`=-T</"61Cc3gpNH]rh^bTqK]>FMU'S)\Xq3'O,kWc;jS9IK,Ki<9:R+QA#kk+bMWNB*i2r`#[ACS*\"/JA$OG<07`&"!V)kO1$t58/F+TGLZE%M`=^e]D(ud!E;q/sG!O%N;CV=uoJ(G#agn`U%7\+2:l_^)ZMSh`Y'i-7\N4$RSu`a]gD>A]kauh4!WX$)NnYE/+6QBb!I]$2_HfEXGt'oE!qSjoi;Xe1#Ve%Ss!EL#=n5=!%YX,%f@?;t/?]Mck6arr-m;E:ES!NF@6Y"fJeXJ3tj9Co[Vp[0*>_Krn'T,Nn=7!GJ<3Uhh7sY^8@qlN9d+C:aB&E!Xnp5s"t0ileLHq\pCh@jATK5R7"RiB$\[f'_4lXFBM$3hCNAK-4&[J+6SKm>c['jbchnBeDdfI8reA[dGNBkFFH-rQ#WFLZd`Y/X,p9k>aYg~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 534
>>
stream
GauI192jS!'SZ;\'k`piK90WW]VO"30LHpY[js?XkMj"i%qJOlIp^0<d\MFHCMC/<n%%=9<rtK:0qMqY][(S@+Wr'$HO.S)nnWWsPK/>J,H]C%WT.\>_6S1S_,j29D4&FU-K)CFc714iQue[JN-3\Y$W"8s8HbD<gc>W$KJq*qeYgV^)-\VgNQM1&TD@7H04SUkMjVtR@dss`S(aj=MK,>l7c%PCq9+gL>]phkjf7Xtp`;P_X+38\/^COpc\J28!\NqnIaPam)f!8Pj?Jd!CjPL5X&Lm?EdOa79`p'gj\n<)PCJsA^D9i*(6El02_if,;B%E9R]AE,Gg4:paj`9c5)r)"L9dqB3V062+(O"Lk>3,KZO==o*!a0e'@n3ImR*k'Bd'#!of<kYAH&5W3H&85na.XBg7\1Gdg$RU>>\`9F7;U5A&^<%4GpQnnd'nh7HFWZD^%X-s3^PKe]uXEg>Ys+;r'ptD1!#Eck&qoT?+d?U89N#HAQ3-=NC/Z97?!@JDfan+s2haDT7Ck2`Z6A"T~>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'>#<r\4YJEKjUI)(8PR%=9k7)8F(lqa0.LN&=O63;-AFTW+!E6SWF\W_U:L_oQ2JYG>*^&XiD;(>DXELcJR*srH)NuSABpSb7u&B1HC]PHP5ic1!]"d,AmXH.;[;^T/-G\qO>B(LdcfRpSqZDaoKPY#6!1kkJSGCLULl[5kYkVUU!pq8/!FBZM>F!$to:7d_^>ZU\<?KuU&jGQuMeIG:QK]s`ACOe'C8VBb8<W"ee"=E+#1+k5%n[ZL724V]i#jq]V4YCK)jRG-+T.8h=a=+`E`>oar7_-AL*i':4UhO":ic(Md.b)8R<SL.Xk'MJhO^4U#Yr6DP`L)4^V?$Js&hLK7RQ`W[c4FE7>s!$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,d<Xlab;K2Eoa3,)J&OAina&_[EEfN3f3J>UncdePrZm920XJ>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!@]&?<K"N:N5P4Jk7Ym<ojMWI>>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<E=uWn>#:CGCgN[,R;mo;Jh88<>[OncJ8^ol=_)L),ha=#$_<"ACcn.>WeneeLj8UQm?"L(&>M&kk1/k43Tc!?e$ZkNs4a&c=#9,VRtO<HgPua#LmG]IjT4Y`A?M!_U5NN@)ODi5G'[W3&rI9LIsNktV/[[2OIcm<;F4"-dZ`VtXR9UDYSKT%HXl;S/[PbX]rq[<-_lhY:Pud3o8]V7g#4\_@KS'/H3/:u+0N+hW-t4c@8t5/H>: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"(Be<U1_]``56m_s%'>rq2ll3#U)+5&l8LQ2P)T`g\J4'5:QF.*BlGoQ=ac6MQAZm(t<YMArZQF;J2Kb&6nlb1%\''a'H,sKam)9-]W)W_LkE!k2m;(6^S<p?&O\TbkF%#M+7ZiYk5?;mTJ!:dT(2oL=ISNBbOFA_-G`XXpJR-/eW#MbV2PoNF=9kW/$jhn&s#C7W>dC^410tUC-uYQ1Iii((b*ij]S!!HBR@\bpQ;Wh#gF??d7slK4hC]hbHah%*Q.Qo\B@DE1rKA)QOG@WqufLE6``T/V<`i(S#sT)1R(X~>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+U8VeqZl62b<bsB%sc5]dt$XIK%,`m*GXLl0*kY[V1HTKNr]Q7)JBFF1Z`EPb'ZL&cI'$Fb4ifso/hETdL1ohYsXuI5D%i"eh,%<$:uXEJV%S?POmD@T>A][BrZDX'Iir\QR6%h0]P9G-YEs]<)liHAe%%n$^2@lbiJF?9L$DdT(OtXQU295BX!Krb#H.j]\Fd_B2ef[ir0QBP<8n,!d]Kgg*?F+@iA0<KS>Ak)gC'N\2.;Z?i#k0>`na2fi!k9KMnd>2QjYhP-Dab@uKWm`1F<o%F^8=-t`m,>c>GH$eC>X"F<Fo"T/)GA,)8t&#fZ*c:dNOJOPG\*6E*(>K1@<Z+':mUp?UNp(*eMjCtpO-b@K^;9VAD=NJ#WL:BQ#h8JZ%'GnX?D8(B<iVO(58GH24abaIM-LNekVQ,O_n8%tF0(O9c-O+1H$WF4W"I!bI\1]T7OLiB7@YFH)45k22XLgs<ZrO>A3-cIHb2hRaN6/m]9Dj6s9MSe978@7efbh4IMXlBSl.k?\[cgsl#k%sa=%TW.o7q;OhH$#V?JGE<AZ]"1`EssAn6n+[RX.1/`G(MGNFEaD]uM&OY,u^_`F)Oe]T#YJaWP9_PI%*WeC7559Wj3IKM!/A/\3#;Hd9W-(0jJ_5WN%>Uf%8?E/#QM"uun5Jl'>q/"/`hd:54fdD;1"N?6Y9#Hln\@5Ysi[CQq]2CV"sQVGO=7kmn1R'2qa:QfO"PVps?Y75m4:+>rFM\7iF+A*+b?XGsCaA="]:cFKJ8)]10,r&c.A!BfDIVkZ=D:7Lm[eCjqV.,!#MR5F!,F$mu'VAN0n9&m[8&t,\AkkO-[+(O'h>hkOFd+(P"%5a;ho'+b\J[pA`3Ntq&0R0OPMM"oV1JC1K=-h9'eRZ:Bm_`=W8Q_>kj7g<bmpNn^'M*`V`,,&JcrrV^NTPAE)1F4k-9=8GN2=pDp#`IM.7+h:J0lcpi]_snFc=c3[[;_jSY"[$?+,1$^2B=$!"5Z@sg^TmJSETYhJIt0gapt:+>o892*E_idp[@,pouBeUd0]9W5NgNe+Oh''"GN'&tNb!2G1M4"`6lg1ut/OkVEgDqi?]CTUs,B^;o[d2[Kkb`cB%M_qG`Nn1NCWjT[VSLuF%$(aV#>+LaDP1HM;:j(Q6XQ/I=3G'NV*?Ds^:Z)5F@j-*kr[r<[<_ZJU0C'>m++a9cneN(u?:q\E@nqsK6^!oOqj5OV&ERuGLVBC\r^5/O;%[ZWq7W%5K_Cp'eQBre.KigJa]`e=*!9iY8p3FhP+@::@!(m\a%G2dQL0,#=CGtOmr"m?/&'JAPLjHtCa/Sf%ke5te0F=JGY&LJW1%m]SP9Toked74Cq#%C]RR-jC+P59q\6kXkYh!#T#8S`J[i$]'8=Q7Uq3OLGNWD3a$+5/\<!`?*-XpQI_Nf\@5$E,8(/J8a#]fs+P96ugKhd[grS+7MF;g@g?qGt6%&E063F-0\$!R!Xk*0^`,5<+jh*$l>KSqGf7Z<%@9\qka`:sUh0f_bpC::%^D;0^_ikn_cDMqW_^;?]Za>M=cVY/CU#<i$\E8>:VYPc:0B1>Kn,~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1837
>>
stream
Gb!;d>>O9K'Roe[380^R$AOu=gl0acJ7Z>`/+u<P^?!8c6SLKCeom]Zec4J^G%*NE_F8-B#U3k9o:>>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>&C<d<'2STNP44mf;(=a4S18uQ.-SROVFE.X/o!'dAo/oh-c`44sGm,G<2=)B%m?V<]<]5\Vp<'9.FCMRk",7UGr7/pSi<s!*iD[FKs3[.J,^bt*jg?Zm)r/cu36=LK4eOHAe-2#pF/GBEi]G4pZI\$,O-VN7:dW":j,u6[m98dp9dgaR\&Q[?QQSsPhHuD'4d"@!i<(lp3N.*M^jUce(>EKHSsI0$+[Ud=kL=#:.I00n/J)cq6lt_tV"d_YEdkMuUSiCuSU5KU\Q@hgmIr5XlKD6dbi>J]Z[a_FE,L1LY&C,8IJ0i,(W%(HhgY%QWu=2Fr%>_A7'ZdpG6rcnO+X2NRD]["dEDbn=="_PK.C/"dfcFa[.-t'W.;==iNa&ahrTYaZT3;R$I^>U*Q>j'LA<XM[u*;O2W6-r3.oXKaJ%BlP.OmQ5'<%W8n58s42]ro5StKm;XnRo'gp3[ndu!-J%k3cnC4<hYn_p@>Zdqp9Jl]L)ogXiMPRPQ%Isc7=:AQh#eVn/EKs>/b,P^t4&5.U9\Nr&AY0:25@29?X#k7cp>W^ch^f1\Dd?LW:VLYk&_$S_P*k3k@6V0+<Z"FcL0@F@\J#0LGGk?6/?99(dGS]Fd9*=UcYW7VU2[[M8+7H/EkfN]`h>Umc%W5HWjt>EX*i>D2bU1#Nn<Oe'@hA/L@+/f;nUmj87.-\O\bBNqJoqD8ZlA\X8Ku=;lk1O@?3.9i6ong*g'PkWRm>9:\<$nRpK:>Vp2WUa0ta;p7?d"f(ne+kc%af%_#N(4"%JP2DduKb-Nj!m8f[:96XTWhrIpVMoV`YnBFV(P%[bX,gVBICX[O/Qt6fcP_,=dhq"F;9<^;Vf9';45-r4h9o+]]9]28Z-D5MN)G_rpMrHJ)$V159``:hj1Q%_@:*AqN'+K(KN?\hi:$4G)K\"E?d]TF<0[q-0QJ)p@&LsaIYF*;#aD9Cp(;qW?C8c:b)elF&=F`2ERS>AdFp4SNIp>XKNTkjb4##C1Ea?*;FJ'Z+q>!dTmVuf>m6a^^M6:`rKA+C?EeMuNCBn4:o60':\$1IRj<B6/!k)QH2D1/N96?gG[W(r_iY%C_9)r%3hlIm+>'tn\%1=0TRU%mO"BmZ6l3i.1^pQ<g[UBc=Ns:(mEJ;nc%q/5K8UgjB//?*'d*111ecZ0W>JGg/`k%\:f[/C>J[8;gVIY`'gVLd9N3f4#XCanYS?TPL"HgJiDZrUP7"h@<7*iaP.Z;k`IiJR;QNFM,jhdY970Q'b@sA7@0A.:A*eKKB3'+-n&:Ha'5S0),?^hNjmU4psA\[6W;cHL<"F4)ch^@-%j'c`TU'<T9GAsonJ!OSF].Y\c8*Amf$$_MJ',InuS2qNLGVaD5[sW;Y2:gR8Ae0rVSGI)RH_I_uS$IMtng2I%5:p_ljgr'Z4"/*[O3A7u.&h[P7GZ(3d`qD5_dWL)FPddlEuW$LijCUl];GCK"`<5=4J+s!50%F).Po#L)91JnX^4:tke:)r[E9+%qlakq]QKY"fPI[!s7]jii9Ujc'X#;hBtCiph]JCu-[]"+H^dGnp%k[3g8].:H9KB.8^i!.'F-;urrV"V/u\=j$r[6)Gnq.G`><^+S%*<o\,@12?q5KhN!JfYk2d.`~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2009
>>
stream
Gb"/'gMYb*&:N/3%"cI<BcBO;.#Dul"d66,!sIFsj)eZ+:g%CIV7K*iS6h;,hk@2j@Ri_R^24il1.<80+_T(/7DZnQXhKE)i+N/s-PRt]aX<aOE1!03X,1h-&EY[5gH[;u=rme*=o&["KopoQ&BA9%"_"u1\/`#O6jQQg_f;hP?-"=>H=B\nY%,%p!tj%+KQCe=73'<dC//TcnSXD+lkA)8O#W,ABP.4(@+^)iUcMJ5'9h.Q%?hVuDu]fU$?+@H/:qH%7W>&A)(ZLr)0=d+FT8Qn_Eu@)]TaoaV-,>_[0MmgQorc;Xl4?E-JUB4+7[Zf!Rui_AQW3\r,[kpO1tr6+.P3]GIXS'P/A22>uM;-6O*(o.rJJ!1;`#\G9Af2:$UK9/M`\d9TtCB2C=l!S5@gGlRd;7-SW+i]V0pT%Fm=]f[$mt_mX77V_au_#ggB\4u+NMV>Wcf"#@Jk!lOB,M"!oIA2C`jZ,Sm[Q:Bt8qGZk]_23?,b'P-Q)ZNpo1QU08B1r:o>YE_Dg9Xjrqa+u0bdXckpeHP3&U"t_1La8n*VTg:UGOD-R#]TgKkV`B1u0,A"@niM-X<e"J4lXlBgo2&]A;gsH<Rr?`@8`'5npIITV#l9&!h%?`>Pn6f='TqR$B8)q:ChFQF"445=7<Q_26_7)>OXK/5fmJq=/RV/4bgVJhVa,npONRb"i"abM6/sG9q2JKfBuMRc:f851`b!ao"34N1e%BH1(R'+qNUEYt&,9<j21A3SU`S=f#gBKo]]Kns,B[YYc5Z`:cm&OofBm;k\GZf3/e:.6t9eXEM2-i3$8lDq>aB**ijUp._)=42g3W]A=ac5(V*s3+IUL_()CdNME&9<r*`)0rOHh=jXQ!<<#=dJLe[mA$ZCrqmj'VLn97PgY.VTg%;`m"i18DMH=l**dEW!PS_doh"7W6*X-7U-n*4tp@IdCbi&^NnerjB0$_m%44H>Q*IZh8'j=!T"DWqX<JUWp3g\52dS)!:3$%B;1:>HWrA[KfSa#";!A)hg?P'ld.__Xda(*cti%+S$qE_+AoCe&=;^\'WgN4._0IrnhWe0RGc/\<9qjMda]V8jDh_Fs'F%g!TDMA.-L:cJhL8m*-G:A_q'qWH[RAeSD3jOJHF5/?5CI<!:Hd;r.@iFlBVjd=N?53!sCO2KD\753,@Q_<0VG4"![j:-=8`X<Vm*1he>ka0NBCu`CXgr)<p\&qQ-<[=nK;FP*0mk51lr"&Afq?P6h11(p-AtJ"Ysi$YAoKabC+YpM6Ig.u02fF))#WB/&jV)PET"fC0YlQ8#d5GZ`WE)?;?r&.NI&2P2l%8W!DqMsg-45A!b?QpB5.hscPtCS_]Rd`baXXqA@YDSb7u&IYkqN<E@kH1ZpSl5auhu1YIT$$6V`mDU>TV`M4m&sK!dl_$CCIt_94e#"NTnM_W)mkX%lVIYq"GYcB$t9kp_*QI_o+>@LEp'<P\p.@ocslW="r?Tk3rMO3k@[$\'J[b:LogmY1dOY&gV'2>JPV_B>(8@TuLd>FNc5\f5"+VMh%3eCo;]lgZ_>",quj>[E\(0'BjUV]rW96)dW]&g:+M%q!(t4!F,6Xte^u=AGu/Nou6Q_M7%N'aGD3'`DD%s/nCiGg;j@s5Z,jq56f;LW1<>ngPPMFAL-VW7F=ENV_OX_ps9G[j$9<*NE-'9c.gA_X:Z&D/3upZ[1ETDge7`mM.uK*ih]00m1+oDDAQ=XH[-][g*8;s8#.>cd[fl7VMWCo]o>e.;,U>=^#BG%,%C<>V&m;q17<^/QTfN_MgX.RIbLD3orY"RbiLbVWo3hr)!`&%j8f?NmL3Qp\!j<.%D;W"Xh,"?[Y6m<%\+(O/R;^GSGW)!W:TRo;g3M%DN=\cd1^`5.Ee1TYt;"Xt&R\fKKXFqZ15WNb_q0m["Z"m@cu!5`2TJX3<QRn(B<>,bZcm,q+6i,KuacR7qM3&CEDLgQjrNVP.>)I^'7F<>mGW10O2qE0Woa<+6.7cI;%7D(u4S5?RmR_#~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1570
>>
stream
Gb!ksbBDVu&DcY&As:e;/X-7mmE0EH-TsJ('!',D)i0.q=<t]_TRG7Yr-JYkorr74D)YbMH;GjkpC7B`(LmdQ^8V&a)M%!,q!f.-Peh/B1a3OUhS@=fjR\&/KV_NS,g:ioB0c%7rIM*u/]7reEgBYFJs9e["AqW[QK8u<S<p*0"YF-XCdN]K8W[NrOGq1NQ9:NVP(YQcN_#Zcq;M8!^Wa6TP@X:qh&P8WL)+l9e.sp#83l2lL04UkDh%ZZ)IBC8Qsumk%DQ+!MNrlU@jtZqSmE6YN>3bP&@\B7>pEPM'P99XHfhj3n=qZa;&H,'n_XTS1ko)F1j;9i#MaO9OtDH2&*Q.\oms#\`^gjM&T5UfR,bh)gBDoGU2>kFCL\#HO4fWM"[@[XlXgn?:*SlC^Aq$^8_iOW7@h5*O03M]ldH'E.e%/s`jFFRaoo24q<bfDdL%T'k/a,!is'Aeht=)dm@J=U/N9I>o"X*n\];41n!]E/e^%'%l?s57dairl*Xb14Y:p'P&e!MgI.2OdL!l?pa0KaOiJ>c#[mU""aOoZ[qG)s/CN42[LW"[5XG[8F)T6[l2u#kPUO3q",.sbam#>!S>[,`aWj\-mWchM<$*"U%-rZZ4nJ>E,MlW1_B;?t2FA5gE@A1M5$jN%EKfMFX_M-%oYU'0*qMcs^kL+rqbGeK?]YOHH&u()6<boA3^,>M[-Q#?AHAljrfpT.9f-d3q;]>"j3T9cK5;s6BZ]'U:0FTg!o]hs?k.lbQ$&RVY.j<0?]\I?5f;N'FmqGn@2B%lZ&lY5Z%=;(8U*SM4I%EH'#h2>JHr18jBamK_4*N+2/EBNhJN#jF-q2OE_[M-D-VV#t755L'^'::#[C_E<J^Fs^#2,$Ph%Vj+?0`*Rr=6:-!Y8Dk2+EL3Zl-R[mr&4Wq",!KaV2/RLMOd%jsc4`*)Ad8p7:%EqkE9A"p$g8Ooka>`Kn)U+6'ht"/r[mO/P:_]`M(:5j30I*+!akB-2i+DjAadKa1s6i0N!\?Ikmi3@?!]_8\FZEET^@mVGYIoU_4Z@=+!!dE8j7Nl)e:NPYbpqJ@2dm5Y[:P9)`NZ5'46F3"I?-l)NbLSkh%Rao(g:hs&s-Sub3c;>:.VMOIWC02np&;P"CJ;AdBe4N[B:9bGNg5gVb@DQudq3SoZlZU`Z/Y-W-%fig1`H9IAh!n\i\T(8#kU-.3MU&B?YC[SVWO`gj=fXX1/p&=Vat_%#baFZRBqYq:o_;S#79PUebJP(h#2)k^Sb9AJh7[.4(#mVuNFG@j)R?+Q)e(n;1&,*U/BNN,6P1V)_fcd\2BOubs/9grp4IT*aNj^i&q.*Y4@A,/eu2a`9WS!B<u_e-3EU*\06pimo.e.sc(BSER*DkN=qLI=d8s7ZG*op%m-=*>q<eaL-!q#jDa4Sf<qA,(bT,C*nS$^O]Ma0g422U&#->"mTg?QloWJ)^jLbX>gI(@QM2sL^>ZV*p4'lELLBUi(b2WF?:])d8"heVH[BMfu#Dd8/joFtIW[f``[4jh]LPm[a44,TI=j"0.">3]\:k$1b#-_?;$fL,t*%=RKr!BZ6AB=~>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
0000004863 00000 n
0000005488 00000 n
0000007277 00000 n
0000009068 00000 n
0000010997 00000 n
0000013098 00000 n
trailer
<<
/ID
[<323c7ae53850c32e02de0259196e5e57><323c7ae53850c32e02de0259196e5e57>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 22
>>
startxref
14760
%%EOF

View File

@@ -0,0 +1,282 @@
# Les 15 — 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 `Les15-Huiswerk.md`.

View File

@@ -0,0 +1,194 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> 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:20260607084029+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607084029+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 15 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..!=#<(<l"_MZJF?'jaiFkAnTT+E_C%I'`N5#Fkl(95c!q@gXH2#c'j)S?AHQ#<0_/M>a+KlL"B[U,][1S@RT"r1`$>l;aP[1&j\"8;-uo*<&2*"W0?dVS(GIc^&bTg(7.%'^=Po<%KAk&=K_7L`U2_\7EB)IhnD2@X*i9a&kaXXmC*o1./o6kn(UnB(L#)P(fe-Pq\tL%r/jmVnpQ`-l7bt.P=PmV)mi/t-I1Nf#g^<qND)Yido7^#X)?r_(7>,ngKphbDd4kP/1'/+CE%K[2beT,KjW\fC9-I9q4H-?X]<16+>^8ID)QF>X<O`Q+7kBh?-irB2>q[#@ZdNi]S;lN,`#&A?>*k%+.[Tt+!<Up#O:22$32$k,5N)Kn0+MoT=*jtEGb_M0R;K@'PH3C`Ybh;*k7]h@Z+t0h#L^uZemWsro:")pXWmP&1&\*(@NG>c6<Y01kMT]Y`37?qq7%/8"lHQ$7C8rfZIV%4q'aAEWd;a<Y;5%/NpC9L";=u2gP@GAeU8R4[\i<b9]m/81h?rPpiBU!k\('/(<7`"rG7m&=1Z&;AQk^j3lUkGD*DiNeU+^m,Ln6]r.\;-$(fmI\=H+hr#M>0%6Gu,P8?V=0n>YTi9i;h_QX:Cir"%qOe8S4:GP>3:23D,FCB3rTit,"HE(i'1%8JV::8LZC_N`Lg(e3sh_J0p=V-[2O,ir;C[oJ,`.lXN-/9UhGk`-<h1YKk2gQfm7fRp3\auTAB=D\c&W[XHSq2iJE$^FrXP0po0Mm7P7SBRRV@U1t\BD?HhZN&)*F[fsfb'*qc5O_Xo"7[u2""lhfp]i>S`uSb$@g"K$(YjnBhlY][HZG==ragcegXe0VuNpQH%e8:iXLJ!itP<9*P$W'W,1-G3/=[%d?g[^YMZE$HOCr4bM3@,0Vg!`?5;,d35\fta,:+5gnZuD:QGZ4:b5!DFDi^T1-i^7e>"ma$hmK#(O,u'"f0E:[I6K27Z-?")Y?VObqdBkOM`,h2(*D?5.i!O2mlH=L"LI?I?0B+gW\ms=EPlcE:oe_!i*UBnfEV];G?cA>OAt$Z<Q+R0sJ;@.Q31ddC9t71Nc4Mk+Dp0bUnA2.[2V6FUr7h-JE=nHk,Y8U'$\=*L21'=^/)2i?)g=-;rZJ-%IT2?$lRsaRG'f7DT($o7U(a,f9'-4G2[0M_cmL=h/RV5SQfhdi2KOMtn;Q:%-F%i7[&M%'m11A^J86&YHuV-m;0>,oGiZCEB+".rKJ&)7JCtk;hb#U8I&e@j-n>4^bNHUqXO]?Wjaa!9[("BX5B?(]#'DhLJV0:aFsH-]H<uo*ZCO#SXSeFFO%Y_7&M&p'.^2h-V2E40,.O1<lt'#5>[/92TPgFBZ\,>#Ch7]bCi"B=!7;`>nb"3/:n;N+tJ9j*^RiI_=T^I+@a/*mf6I`++,./?>LmHK(,#$6>2Nms4:ho:#TsGmuu4()Ul)?n^'R/);"JW/i"s]dlOmRO^)P'S6E.ja[<;-*6.[CB/E7&oLq`Qtm1L(>:BFP;DXsE7gpdEs=QM!>H,smgX2/!qY/1Cp*\>n5+4jH4B]l5hUn^MbpCh0XkHFatg,nX:<tMg9%j8f[i2j_kj:l3I[<-XKEs6T/H@"k1iW3VPM^]XXV;ANGr'>Ze1MYRoT./dG9#".p*U=m7Q'k&rfBRog2_OR4`7'Lst\t"[$*7!oJ8XWlej/91q+iq4]8\4A4^lIX!r'rPQOY,`]n"RPqI7fnQL;Y8uKL2!2ib)9"&<NRL`^],2>TD.qc?1O<8JM1*$_)4aii3O9!>SC)P4HWbrA?\73H95rY]AQfk[\YZ%D\4korS_[Mt>JK[FQ!ZX8hjOn0AHh?(PS,CE(eWr_1bVK"O/#eN?O6>1gNsJbpeJ%H9s>`5\+_42a/Br<s4I>:Qg/%u(jcIL:+(fY]79M>lc\YkEJ-2qgtVhSoQ^Db]A9%P`s871-grAegW,rch#6?9Xr#X7j"]ImoS-iJl/[+;n7;"*?DFL6rn"`Q+!#D$$RPWeMu~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 627
>>
stream
Gar>C?#SFN'Rf.GS<lhg;I/&I=!$UlDJ4i(.IB$-&q]%.W^'Z!nb[O$KTDEYZ$g->j5Y3A!2h0t52-,L.IJ8]KLGY@TpS/-6';`W%@B!#.Ld.R*lSaCNl$E`4kN%>AmuVc3,Y.j$H`\;>jNR^cWXaSH+XRk!mDR#SH<k!)eZ>E,A*aIUD;7t`1QXJIP0!*RH`B:c0cl3g*-;cSPUQM\Bn8$h;WVSoK(!QEp`1/;jISnR`]94#(NJV(lduRFe.-D?f@J=pnHP^rN>sJoH!0MS99CdUgc)[p*9RTf+8iWPl"Ig3'kk+l[60l]YJh??*D><fJ0;7U3b2Wc-fN/R`2T%Ud<QS/+S+MN3]lgeoWro'GQWS3XW^"rfK3r)VI+nk/Bb';ea`!7*@AIbqmM=U$*)&b)[u6+D!BC>3>d<Y3.C_$lf*_@Q1ge]@GG;a:khD>/fP/-R7GoMRO"Sm-Z:c/APK+\k9G<jfMVGaf.d!KaN<6'ebG6T+,oUXgRU>a_N(#0[]^lo2r+^8K3ok[W(1neaAe&9,g2XcW"eSB$#-OSr[LDq9S+4nf$DLA;Y*l4.X](*[]pNo3pg=D+VgVY7A:XX7MUsZH1o6I`FX>,hV1gD&E0U#4^B>~>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_DC4Jmk>?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<eR>#Z:%pA1EYQ8B7[YpXZ:4c<s%u(``)"t\@>;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.<I<GqJZ/I]YVP6-rBK.0lEJsFYef1fCQ^d;>HX%^F%(GG5N-<<#)ksn-t0]X6blhn^aenA3TX%(-Tj^=#;B!sYQs"^;WLp<(F`7b,#?X><0n\)`8lfT(0*XOsSgSBicXm46[6PZ`9]Re;6&nk7MW`-SP0S<kIRASSK?r^fQZA.m9jX$.V.`P:#+/p4cI!9MBHWeo2B,TnGiW?Ac9j1dFV00ILZ[K>Tob*Fa8mXO2%tfRnqF"dQqS.*"e'98S7HY;#kF1DKe,W6=FaF!U'gmqqCj(b^h:I<41>'grQZhAa`[isHrN38202!r.[F%K<XYngNeA\sE4QK,P1k(X#lcG.rT<s4/I"kR"nD?0$,_I[>r<DNnX=+][AHV@/*O:Tn"\rMFW;GKkW0>R[(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:><<M_Mi%o$o1'%dYE7CCS,fN@`i!q/[*ENKk4rVLc.1SR@jfSRjh"!;.P6g+QZh!HenhVs?beHFlD>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$m13<oT'%/b[)nU5eimT4mc^C?EOP/G3]R`?KeaVcMNfrS=:Q_5<emYIWTaLP3hM[!psPdI/nQjG2?+4SKYc91gmIpf%lenm?W;c^GXbaW3qG(m'qa=',-qiE7rk]lj[4%oVsMp*pI#1!Qj@/Z1_qVLLT(O^gOs(LOdYlR-t]e$/Q0]*256#51t^D9bf@FUugB^lZS:!Sa4Y.#7ob3bHEoB^`\:,j8OhgdlWNf!SOcaK)~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1601
>>
stream
Gb!#ZD/\/e&H8h>EUY?RE#Fh5hE5mG[^BeWTZnCa5Z&<u.l4G6]p5>@Udb:[Nfk^ADE>bS0[)FKlca_DGgDK_),P^eqE)t&I[S%21BrfUlk5[.!bSA"FQRso#krTe4<SN4h1u/i>l+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,)+W<X*A>dc?TE1-]a=h.]JA1^i/LAV+$fOg`4,r,Kq4^=<%$e$c4,#Cu_.f$CD&X']h[)%f/k*-+O/`Jb:DA[0?0t3n_=A8a'$&I+2__k-M`X$Gm"J\Y9O7[MEATj8oE40Y"LS<iC@tH'Au^.(@"uUFbtuTl+\6[L>/e.H)?%,KLi:":_u6/TjHPX^c;7>/2LbcsY<8[W>oUD\UKR#X_ocFZ+er7^ej+E6eDo;O9[^6)ac+FVkG%E8tljK@)k`m*Iqa'HankfDb_`qOb%!FQuo8mtR.GB.cS0%?uP3VVGQ0+q\JC#>Q][1odMoX.-EA#3$i1%!9-FWL96:^YRdf/J8FYiZUB@'#4R<=XU%;ML5c+k>J0jK'OUTeKV9STKZeGCRKfQ3P9f`$:RNUPCXR%:V8(ilPN3"Y0;1(<108n=&OG@W+(C9_Fg`W:`5hb=7bt[J=slAE&TuuMW'K)'A"tgNJ^e,9gMghGmnJ1MZZb(L7t&\*(^OlI%`'-KiOCFA7FIpR<U_6O-[eMeb&?-Q3f;:3_=6]>\&WT`o)S/UWHmlJNS]=l9YM^at/GS<r_OJq/!:B4=!e^M[f\4G(A#k'Y_EIf-3.ul9(ZoQ>2)7Z7OHMQBpCFO&_='i,0qC$O@4*AqD+^`U+@g5C`K<9iZ4Z45=Ju,L#m?'*Z1O?$oNU<pCl0<CKl99kMFa:/9\Q*1rd,r:0FH5^JQNS#L>.k(QLho&&EqI_ZuM#M0V;5/$i4X8*>>oqlYud,h1?GOY`D_m#\l>"BmMQGW>>Q4r$B")u+%^)M_eBQ6Bt!a8i\GcT=n8)t\=Img[AEm[WA3eGam+GB'D^u`4iTp8dF*9qr/Bk'll+/Hcf;Y)W6TFt(9\*Q?[N^pEUjE6!H5&<_NH`,`plB)?@Val)l?R;o+.oi4A&!T<(n@IShhZ#k?4LA#!95MbCO)i(LW0]_B]UmWr?YcY&YC5WrYI2^uN:sOUC\bl;:"p(\lF0(7"80ut3Tno(8,n>?QP5^2h>S7^Vm7)W,0/a<COngOn[Q!+TL?XO.Ob9FdgB$u8pi=F]"+[.5M")m"<QLm9'P)IS_[)V!NK.ca^#I@-$XSf]D;3MC;_W')UEnk-af8=-4aJ?Y#`H$Y7X-9IBUU:0r`m1<kEsBLXiWX[`CYj[^3*5pY9q,Q7"7_PIAC?]E*F.X3LA7<\)>rPh(PC]2#r;<@L/R9'n4=jAHd3?DC>tPgo6o6J%^9)U/fde6p3enk*UPYkUA()IY.]er>K6<+-<5l5p:REf$UA<Q"BO8m312bs4%ss4D\qODb&"R%)g#i75*`s(#Og&DMQ^'T??-6iWX=%*i4[FkpR~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1809
>>
stream
Gb"/%bAu>q']&@2DS52S)%-L2&-S7igH:qU*D_j<9!Zhg0Ek+V.?Z#$4)$&AhTIk>GX?&,DIRrD&d$?ApPHe<i\.A3LKf;,$^eV8^c3mjJI*N.E<:V@S1BA6aaN+KE><Tu%O?#:+3<&/!hLYmhuNpKIA2)i68>36TRc&_he,.6#0n;O%#?R9Y65EU4K7CCk?T8l8(3'7;?jVV2uHWH0-IQ/LG\<]"N)7]EN2BK"ej034]lN/o((>CA,%Q[Rj-X`BaeosDShBe-0IHS3M&D`MQ!ak"jbH$M:69V&pjnB$WoMBD%5t<(PE%1ho@r9"Y^JV,L'9"m\!qQFd-S/#$Cn5>[&o()mVY-`-&&?q=nRK*>a@'>TnfSXi!;"5B^hI_gOkN:kLX.RU(M!?h305fBrs9D@I\T@]Y7JQ3,hXGgKm2XPVD<BGWu\fKR=c_f&]X-1-4rB-hY5cQ^AqNb!4@F(=-Rk\QchY0,W_rNd;OrDWqOM/#XnO>]Q7=,ac,&D'pRWmW4!Gp07!oBjj)^9$f+_S=),S9QU#g>0Y2[5nDnWNnD4'WfN(PXN9Rh)9_HRq/TQA3dc<.,,@!^\s>9TU"*.*i=&jQNS[UF>hUE<FUm^Su"Mc$-si&5\3rJN1r8`\D:e'McC3qV\<B`]6/eqf1k/)`K!d%4P2<0P<fg2W'=Nam@M\hk6L0<.)d^8DG;8"TU[bp39RU(e1p/f&?:4/bKs=*n29ULBQ'rQ7+M0"H`kWhhOFN7c:W1%W3S\W.`?i?S8kWG8r93,Q+9Jnc-kCaB3=)23_@f%.[D<pOd&"%GDSI8i!Z(<knXbNFR\M^dF:K=V20okg4rEZPN\rtq-?M>@Wa]3QU`3IM7'$pZ+Vl)Vafk.jp3?B?K&+EaB;l>2l><Vg*/>2IMJVIUo6nj2"HW3*li['2&5_f1)-TW@?EoSZ)KhP0dPmn5G63Ak7In"F/E%[3IktX1@`#6gi@G>cKnN+dk,b4_=I)/":I_)i(MI*'q[1JgGsZVgV?t6c^:M<4Os-'K<5LL9n!C00&SdMf60>eK],JqBhas,G\Z`^JZYp10?`!YYte[eEMjkJmGcdN@X1L6:oQsPoP3Z]^n9a?haNl[^6)Wg6^gNd=PLoUhutb,93XPsO4P.A%C\+.V_4,k<A%d&XV5L.m@0;K"<V90lNs*3-,(L@)K,-0ik1.7U%'bMf%*=4)A!K7HY)fb%GG'TA-?'?6r`3?K_>s<elWG_K_n(`E1DUOY)(:^AhH!GXrTq(7CpZ01Zj5NPEJ0-G3'*m8Ct%`Q?O\U(EH'kbU6(gK9^S$qF4]!^p$9HWm66V3O?eRD)PS</+R,EG$UI00'B76h$RY@T8"H(jZ(IWC"N'39saR5PK\f7DE2-I[(4o:''II/nBolO.[XH7a\R?X,P@?%,P77;o[W8;aO?\;A$pgfl>#:i23lImREhO041T!hanlAq-@7KA;dfPg!c)-fFCJq7C$"dq=813;NC_I9V^-re>nYj>:#P]Fe&??jS#\Q^0ipHLoTk7dVG@C'$0PqQHThP'c9J!YO$V[eW4Y6ra@24<bK9`s*)fe_$`REJ#B5<_&c?9^D%2@Oa=qq.S[SaAXUXpQ]p2N6ln2'f84A:"9]Of.UkZ_lnAguY8@8)LPV6P;D5`"XJF\RI3eZdXCitIeSeO[+@IbK>$DY9-U_#d!4GRU'c^r.U&!ki<1VE7?<C+3_hca&=]I0U?N<@O[cbY>Nn4OHmk[TeCCpd;,N=[.GRKaK7AFsIUMOaOm7Mgt4Gn1r1pW;2BnT$k#\!5!Z.IS^I^"EqAIrV3J6N~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2065
>>
stream
Gb".=gJZcqoNbgDL51MZj(3GBP#c\%*Lc+W_?9omD.]0W..Ui#F[M<dS@8ISZaq)l<&,-]-.j@)B[?Xg$Nl8.<rVb:J7$@$pQ>R;0YRZ:Lk,"-]GMSH#2qIaKE];Ye9!\^f@3$H9oTu@S(K?;)+a$(km.i`&Lgul*:Y#(3KP]"HtG&TN_LNCJ4O;*d_:KT&t<rQ/VmummTo`o5,lMBM-Wc0@THh]L+?B.<(kaUKJL`U?l[#N]ArKMGb%N.mU%83#eq1DnuiD1i7rm9_pLcA#C3!in=*U[0M*K((JeIe%HD)i=-()RJoFdNX7VHP%a5!e,j?TG>1+eT1l+<!HaMY)>oR62YD%+t,c4-RoZ)SR2bZ&g1kdMtL^\0`lF)D##'U?L6M=eleuJp^Nm7G4X'+I%/IZQbA5&2%5q]<]/SJjfXBO'[ciAG-Ydk8RL2U5hP.$$WbJcDR-Uih%XcPD\X%#m+hmbGa\C."lrHg2?r0.4u'3U)]cY\UR/f?U!+^J#MZ?2T&q1Z&qb]g6RqK>Hf-2]rT_26-9N856FB8)\(;h`(jjTPcA'Io"p-GlI<Y[,t;K[)ua^8oYAp<noBoVGRaC>!dN2%sBp.t#oh&iRSuXFcL1<!+qb]iIF$"s9^%iK]jo+WRE&p(eD^6;ifqJ_H9;*!CE\&D=/TN>?-sP">1tA/J@DL8p(Rgb:U.'F9-7@HL*`JnU'02(]hRZBL;l;Ud+cSDESdi%i,#X\79bQ:kK.\D:K!kL2DM@%nEh?H,BHD#a07k5W9@QGA_pPlLbi[N4?]gCepAgY-mIfgI=L%]^DR'!r5;q"CD(Ig_qj1"u0OE\%KSfVVSI!Ip\p0_uGXNmG^a<>=]shHlTE??har`P+nmh=K'&02oFP1B.\60%RM'L:1e/a5jlW%PW2<43s,#^m6hr02o7h?CHV"mhJ,$mp5*J]JE*AUDn=$D+IF[a^7+gF-S>$p0m+pYCJ:BFc6>1T2b$ndk&##@DC<o'0Bnja`V1TPhWt>r:=H\><bsbreBa.Tes#Jg'7#jRae8KU.^SS,/N]Xb&2>phM4%&a6t-K"dJql/$%dDL&,'(Y_L(K>jSI5`bqBF8QMKV._^[n2G!EsLpnk(.4B/baPg02'1DN?Car(WMYCecC;hA'V[:)OGY3]H=@eWX0"&G)$!SD_gi#;#]EiNmDb,CrR^J[:pGW:.?T1[5Y/&f.]tp&VMDi.ZA&PHa212g^>VO4O(np.TMkR%0Ark0_;jA[j:M0:PHoK;N\nOCX^*N5:AY?=[amtG^8T=M!N6q*@<&5mO?BDHEPSM=K6\R%'C72u#\mUV6:^#6,aj^E=@aEetSfP")l$50_]h_spe^;9@;O,5Bo4GrDC0C9(eEV9A9!&3_0=*2)ZTC1\WfS[=VbkG"E=,E[TE&eX?;TRcI.$k]a*r_WQ@m!Ug@6b\S;u3r[!SQU2NQ#j=kU]4^/7U'hUJtUdWgZWJ!I`$\k*)@p:O;p-5Gu(ID9J!0q!aM($^5i6,(_P@\Yed4km0!#`O5m)<>&%E02F!/;\>7=-.%9Vbfh"EsWLT)\<,7KnsBbDJ(rgU-0U"m8L/"@bhs^l@VH'p<L(^_D3f5fsSSHYep_1MR?%cd,f&N]GE#tF'V"*J6BNG@p2>N<ig!5Q4&>P3aZQd+/`P1k<V5tXhQqm\drcADFnGfE.3'(iT%briQ&D\5/8.jjt_bCA4VRrrD[2;_G\klFqB#1]ipBE,hR)(MQCN7eiNL?6+@%`fatJ.iN6;#AMPpsLM5]/=$OMP8HXZVA!4i_Rbr!lLrHeZE&4^$ENW.#9@!u)#hcMd;,<7u+AHf.^'+I.q6NecXD6d$[G8DcdK4B/]th'4*FMiVl5oX]_1m-#?XU:Wc0FM*=93FFDISYE>+J^Z7d+EO[tIQeZECI(CW548@0l&fYTRL=*C\Ur<eG4B@hf0rZl"X1'ErIVP=4LD-O$Hp5sAgc\&\:]47g:^*gGX6a`o)Hdt:8B1SDqB)uRChdO?J$Ys*.t$FBZFKZ&`OVlpM<_/3D,U+SJ!@gH_:>9YjTI2lb^$M`HXj'T-aY\4~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 721
>>
stream
GarnSgMWN8'R\fAR$U@5.-oMn%.pAgeYJ$?V+jT]WQGU"P?h@0"`G%Hl['',-&i8Qk$ZLhShg_:D`*JV"h[KIEiu&t,S*^TBI$Y=94kqO_D>Ms!sh,SIdBA=^:V`DB/K`89^K_*OUOYmC?S3F.#K6oN!hufU*Dc?bnNQZA/K$:8-L=qYOK7[T#\Us+ttAC(I./BS@hsk8KGto+"$"gC-'k1e^m-f+:O?2QsY?S]u3@GUcr!?9o3esUMOL]kBS4G0,LO7?hBY)]Ve7LS$o'F*[:E:jN.:8O'd6;Gd.`cnm>6)'nE3Zc+A'jrAe.!Fc(.!l\9h9-.YIf7"NkC%phH0-\fI7pEd'LR71-lXg<s`'m++YT-[)^fTO$o`P2Adfp)TD?"&s"D(D5_UN\!'m+D+s@k:F;`5o-&"-JTmj!int3GfU'3U"\k_Y5;)ZNI6!=(`,MN$CNiLcpbdbk%4++W5=IZ-g)>@LRi.C`E77,ZYD3osT/%po:^u3dA3e3XK0(DQ+f;-g.!ZjTtjXEm-b6C"g8>/Tj'rW5rGU0)?s\ILRlO.eLe:4Uc@jhT;6om"'-5XIh$bS#3N*>3,=2d$%bEDa;XW949iP4mqGYkOAqf]W](34@\(&Rs+GS;Ti;.Z(WJoa2liFD5p=Ak$59OXJtg.(3BtbEVeY\3Y>AP]ZJ_WeL@[-r$]X?<V_Q28XX8`#Os"8/uU_HNS.Z=0+(q~>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
0000002239 00000 n
0000002337 00000 n
0000004738 00000 n
0000005456 00000 n
0000007552 00000 n
0000009245 00000 n
0000011146 00000 n
0000013303 00000 n
trailer
<<
/ID
[<6b5e377fd0647992621660653a695082><6b5e377fd0647992621660653a695082>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 22
>>
startxref
14115
%%EOF

View File

@@ -0,0 +1,474 @@
# Les 15 — Lesstof
## Agents — LLMs die tools gebruiken in een loop
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 12 — Tool Calling
**Volgende les:** Les 16 — MCP servers
---
## Inhoud
1. [Wat is een agent](#1-wat-is-een-agent)
2. [De agent-loop in detail](#2-de-agent-loop-in-detail)
3. [ToolLoopAgent — de v6 abstractie](#3-toolloopagent--de-v6-abstractie)
4. [Stop-condities](#4-stop-condities)
5. [prepareStep — control per stap](#5-preparestep--control-per-stap)
6. [Done-tool pattern](#6-done-tool-pattern)
7. [Planning + reflectie patronen](#7-planning--reflectie-patronen)
8. [Agent vs tool-call vs workflow](#8-agent-vs-tool-call-vs-workflow)
9. [Productie-overwegingen](#9-productie-overwegingen)
---
## 1. Wat is een agent
De definitie van Vercel AI SDK is kort en goed bruikbaar:
> Agents are LLMs that use tools in a loop to accomplish tasks.
Drie ingrediënten:
1. **LLM** — beslist iedere stap wat de volgende actie is. Tool aanroepen of antwoorden in text.
2. **Tools** — uitbreidingen van wat het model kan: zoeken, lezen, schrijven, rekenen, anything.
3. **Loop** — de runtime die LLM en tools afwisselt. Beheert berichten (context) en stop-condities.
Het verschil met Les 12 zit niet in *of* je een loop hebt, maar in **hoe lang die loop draait** en **hoeveel autonomie** het model krijgt. In Les 12 deden we maximaal 5 stappen — kort en gecontroleerd. In Les 15 gaan we naar 20-50 stappen, en het model bepaalt zelf welke tools het in welke volgorde gebruikt.
### Voorbeelden van agent-taken
- **Research agent** — gegeven een vraag, zoek op het web, lees relevante pagina's, schrijf een rapport
- **Coding agent** — gegeven een bugreport, lees code, draai tests, voer fix door
- **Planner** — gegeven beschikbaarheid + voorkeuren, plan een vakantie
- **Customer support triage** — gegeven een ticket, classify, gather context, draft response
In al deze gevallen is de *volgorde* van stappen niet vooraf bekend. Daarom heeft een agent meer ruimte nodig dan een simpele tool-call.
---
## 2. De agent-loop in detail
Wat gebeurt er per iteratie?
```
┌────────────────────────────────────────┐
│ STAP N: │
│ │
│ 1. Stuur huidige messages naar LLM │
│ 2. LLM antwoordt met: │
│ a) text-only? → loop stopt │
│ b) tool-call(s)? → ga door │
│ 3. Execute(s) de tools (parallel) │
│ 4. Voeg tool-result(s) toe aan msgs │
│ 5. Check stopWhen-conditie │
│ 6. Indien niet gestopt → STAP N+1 │
└────────────────────────────────────────┘
```
De loop stopt in vier gevallen:
- **Natural finish** — model genereert text in plaats van tool-call
- **stopWhen voldaan** — bijv. `stepCountIs(20)`
- **Tool zonder execute aangeroepen** — done-pattern
- **Tool needs approval** — agent vraagt user-confirmation (advanced)
> **Default:** `stepCountIs(20)`. Dit is een veiligheidsgrens om runaway loops te voorkomen.
---
## 3. ToolLoopAgent — de v6 abstractie
In AI SDK v3-v5 schreef je de loop vaak zelf met `streamText` + `stopWhen`. In v6 is er een dedicated class: `ToolLoopAgent`.
### Het basis-pattern
```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";
import { z } from "zod";
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
system: "Je bent een research-assistent. Plan je werk, zoek, lees, vat samen.",
tools: {
webSearch,
readPage,
saveReport,
},
stopWhen: stepCountIs(30),
});
const result = await researchAgent.generate({
prompt: "Schrijf een rapport over AI in de Nederlandse bouwsector",
});
console.log(result.text); // finale antwoord
console.log(result.steps); // array van alle stappen
console.log(result.steps[0].toolCalls); // welke tools werden aangeroepen
console.log(result.steps[0].toolResults); // resultaten daarvan
```
### Waarom ToolLoopAgent?
- **Minder boilerplate** — geen handmatige while-loop, geen message-array management
- **Herbruikbaar** — definieer agent 1x, gebruik in API-routes / scripts / queues
- **Single source of config** — system prompt, tools, model, stop-conditie op één plek
### streaming vs generate
```typescript
// Eenmalig antwoord
const result = await agent.generate({ prompt });
// Voor chat-UI: streaming
const result = agent.stream({ prompt });
return result.toUIMessageStreamResponse(); // streamt steps naar client
```
---
## 4. Stop-condities
De `stopWhen` parameter bepaalt wanneer de agent klaar is. Vier opties:
### 4.1 `stepCountIs(N)` — maximum aantal stappen
```typescript
stopWhen: stepCountIs(50) // stop na 50 stappen
```
Bruikbaar als veiligheidsgrens. Default is `stepCountIs(20)`.
### 4.2 `hasToolCall(toolName)` — stop bij specifieke tool
```typescript
stopWhen: hasToolCall("saveReport")
```
Stop zodra een bepaalde tool is aangeroepen. Handig voor "doe het werk en sla op, dan klaar".
### 4.3 `isLoopFinished()` — onbeperkt
```typescript
stopWhen: isLoopFinished() // GEEN limit — agent stopt alleen natuurlijk
```
> **Waarschuwing:** zonder limiet kan agent oneindig doorgaan. Alleen gebruiken met andere safeguards (cost limit, timeout).
### 4.4 Combineren — array van condities
```typescript
stopWhen: [
stepCountIs(50), // max 50 stappen, of
hasToolCall("submit"), // submit aangeroepen, of
]
```
Stop zodra één conditie waar is.
### 4.5 Custom stop-condition
```typescript
import { StopCondition, ToolSet } from "ai";
const tools = { webSearch, readPage, saveReport } satisfies ToolSet;
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const tokens = steps.reduce(
(sum, s) => sum + (s.usage?.totalTokens ?? 0),
0
);
return tokens > 50_000; // stop bij 50k tokens
};
new ToolLoopAgent({
tools,
stopWhen: [stepCountIs(30), budgetExceeded],
});
```
De callback krijgt alle `steps` tot nu toe. Je kunt op alles stoppen wat je kunt afleiden uit de step-historie.
---
## 5. prepareStep — control per stap
`prepareStep` is een async callback die VOOR elke stap draait. Je kunt dynamisch het model, de tools, de messages of de toolChoice aanpassen.
### 5.1 Signature
```typescript
prepareStep: async ({
model, // huidig model
stepNumber, // 0-indexed
steps, // alle vorige steps
messages, // berichten die naar model gaan
}) => {
// return iets — of {} voor "geen wijziging"
return {
model?: ...,
messages?: ...,
activeTools?: [...],
toolChoice?: ...,
};
}
```
### 5.2 Dynamic model selection
Begin goedkoop, switch naar duur model voor complex reasoning:
```typescript
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 2 && messages.length > 10) {
return { model: "anthropic/claude-sonnet-4.5" };
}
return {};
}
```
### 5.3 Tool-filtering per fase
```typescript
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 3) {
return { activeTools: ["webSearch"], toolChoice: "required" };
}
if (stepNumber <= 6) {
return { activeTools: ["readPage"] };
}
return { activeTools: ["saveReport", "done"], toolChoice: "required" };
}
```
Forceer een research → read → save flow zonder dat agent kan 'cheaten'.
### 5.4 Context-trimming
Lange agents genereren veel berichten. Token-budget bewaken:
```typescript
prepareStep: async ({ messages }) => {
if (messages.length > 20) {
return {
messages: [
messages[0], // system
...messages.slice(-10),// laatste 10
],
};
}
return {};
}
```
---
## 6. Done-tool pattern
Soms wil je dat agent ALTIJD via een tool stopt — bijvoorbeeld omdat je het uiteindelijke antwoord gestructureerd wilt.
```typescript
const tools = {
webSearch,
readPage,
saveReport,
done: tool({
description: "Roep aan wanneer onderzoek klaar is en rapport is opgeslagen.",
inputSchema: z.object({
finalReportId: z.number(),
summary: z.string(),
}),
// GEEN execute — dit signaleert: stop de loop
}),
};
const agent = new ToolLoopAgent({
model: "openai/gpt-4o",
tools,
toolChoice: "required", // model MOET altijd een tool aanroepen
});
const result = await agent.generate({ prompt });
// Het finale antwoord zit in de done-tool call:
const doneCall = result.staticToolCalls[0];
if (doneCall?.toolName === "done") {
console.log(doneCall.input.summary);
}
```
**Wanneer dit pattern gebruiken:**
- Je wilt een gestructureerd eindantwoord (geen vrije text)
- Je wilt voorkomen dat agent tussendoor stopt zonder iets op te slaan
- Combineren met `toolChoice: "required"` om text-generatie uit te sluiten
---
## 7. Planning + reflectie patronen
### 7.1 Plan-Act-Reflect (ReAct)
Een populair pattern uit de literatuur:
1. **Plan** — agent schrijft eerst een plan in text (stap 0)
2. **Act** — agent voert plan uit via tools (stap 1-N)
3. **Reflect** — agent kijkt terug, vat samen, evalueert (laatste stap)
In de praktijk komt dit door simpelweg in de system prompt te zetten:
```
1. Begin met een plan in 3-5 bullets.
2. Voer het plan uit met tools.
3. Aan het eind: schrijf een korte reflectie — wat ging goed, wat zou anders?
```
Het model doet dit dan vanzelf binnen de loop — geen aparte code nodig.
### 7.2 Sub-agents
Een agent kan een andere agent aanroepen als tool:
```typescript
const summarizer = new ToolLoopAgent({
model: "openai/gpt-4o-mini",
system: "Vat één webpagina samen in 5 bullets.",
tools: { readPage },
stopWhen: stepCountIs(3),
});
const summarizePageTool = tool({
description: "Vat een URL samen in bullets",
inputSchema: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const result = await summarizer.generate({
prompt: `Vat samen: ${url}`,
});
return { summary: result.text };
},
});
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
tools: { webSearch, summarizePage: summarizePageTool, saveReport },
});
```
Voordelen:
- Token-budget: sub-agent ziet kleinere context
- Specialisatie: sub-agent kan kleiner/goedkoper model gebruiken
- Modulair: sub-agent te testen los van hoofdagent
### 7.3 Self-correction
Geef de agent een tool om eigen tussenresultaat te valideren:
```typescript
const validate = tool({
description: "Check of een rapport feitelijk juist is. Returnt issues.",
inputSchema: z.object({ reportText: z.string() }),
execute: async ({ reportText }) => {
// bv. tweede LLM-call als fact-checker
return { issues: [...] };
},
});
```
Combineer met system prompt: "Als validate issues teruggeeft, herschrijf en check opnieuw."
---
## 8. Agent vs tool-call vs workflow
Niet elke taak hoort bij een agent. Drie niveaus:
| Niveau | Wanneer | Voorbeeld |
|--------|---------|-----------|
| **Plain tool-call** | 1 tool nodig, 1 antwoord | "Wat is het weer in Utrecht?" |
| **Multi-step (Les 12)** | 2-5 tools, vraag heeft 1 doel | "Hoeveel jazz acts vs rock?" |
| **Agent (Les 15)** | 10-50 stappen, open-ended | "Onderzoek X en schrijf rapport" |
| **Workflow (code)** | Reproduceerbaar, audit-trail | Order intake pipeline |
### Wanneer GEEN agent
- **Determinisme nodig** — finance, juridisch, medische data
- **Latency** — agent gebruikt al snel 30s+
- **Voorspelbare kosten** — agent met 50 stappen kan duur uitlopen
- **Eenvoudige flow** — een if-statement is genoeg
### Wanneer WEL agent
- **Open-ended** — research, planning, debugging
- **Volgorde onbekend** — model bepaalt zelf
- **Lange ketens** — meerdere bronnen, meerdere stappen
- **Latency + kosten acceptabel** — async use cases vooral
Quote uit AI SDK docs:
> Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes with explicit control flow, use core functions with structured workflow patterns.
---
## 9. Productie-overwegingen
### 9.1 Kosten bewaken
Eén onbedoelde loop kan tientallen euro's kosten. Altijd:
- `stepCountIs(N)` als safety cap
- Custom `StopCondition` op tokens of dollar-amount
- Logging per step (welke tool, hoeveel tokens)
```typescript
const costCap: StopCondition<typeof tools> = ({ steps }) => {
const usd = steps.reduce((s, step) =>
s + estimateCost(step.usage), 0);
return usd > 0.50;
};
```
### 9.2 Latency
Een 20-step agent op gpt-4o duurt 30-60 seconden. Voor user-facing:
- Stream steps naar client (`agent.stream()` + `toUIMessageStreamResponse()`)
- Toon progress (welke stap, welke tool)
- Loading state met geschatte tijd
### 9.3 Observability
Log per step:
- Step number
- Tool calls + input
- Tool result (truncated)
- Token usage
- Latency
Dit is essentieel voor debugging — agents zijn niet-deterministisch, dus een specifieke run is vaak niet exact reproduceerbaar.
### 9.4 Veiligheid
- Write-tools (DB inserts, mails, API calls) altijd met explicit user-intent
- Confirmation UI voor irreversible acties
- Read-only sandbox voor development (Supabase RLS policies open)
- Production: tighter RLS + write-tools authenticated
### 9.5 Testing
Agents zijn lastig te testen — non-determinisme. Strategieën:
- **Mock LLM** — test je tools los met fake LLM
- **Snapshot tests** — log volledige step-trace, vergelijk met goedgekeurde versie
- **Eval suites** — set van vragen + verwachte tool-sequences
---
## Bronnen
- **AI SDK Agents — Overview:** https://ai-sdk.dev/docs/agents/overview
- **AI SDK Agents — Loop Control:** https://ai-sdk.dev/docs/agents/loop-control
- **AI SDK Agents — Building Agents:** https://ai-sdk.dev/docs/agents/building-agents
- **AI SDK Agents — Subagents:** https://ai-sdk.dev/docs/agents/subagents
- **AI SDK Workflow patterns:** https://ai-sdk.dev/docs/agents/workflows
- **Tavily Web Search API:** https://tavily.com/
- **Anthropic — Building effective agents:** https://www.anthropic.com/research/building-effective-agents
- **ReAct paper:** https://arxiv.org/abs/2210.03629

View File

@@ -0,0 +1,315 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 15 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
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/PageMode /UseNone /Pages 21 0 R /Type /Catalog
>>
endobj
20 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607084029+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607084029+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 15 Lesstof) /Trapped /False
>>
endobj
21 0 obj
<<
/Count 13 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
16 0 R 17 0 R 18 0 R ] /Type /Pages
>>
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1860
>>
stream
Gatm;?$Dbt&:N_CbY-8RgK9^D#j>Lh\N-/8IX0\7]F^0A2Lm2`B=kp-7fie$CI5^pF)4K?GNR20m`5eb^6j2,14WGIGk$dpj"Og?F,0qi)gFHR^0?6umPum2gS]51JHWF\K5t\fi!LY0*\d)_7srV`a(N?b"!9Mjo/u/$.:Mbj@R*%A4l6;"1KXA2@)#D=k@iQmXq8GKrmj;@],)%+HA?Arrs4?0=A,Y][2hT%la\^qi8W&n?a&EI[?O[u;C[C8l;C*P)K%S0i9GW3<E2Ujb8B*WND*?7ah3'0o=n`59\UKD3t<Vj]!mTI,:d.Si($eJ70EEA>UF_%Ck2*`A%DgV]QI3+N-Knh@kL4QD@,*b=cVZ';5TH7+<[0]rF?IfiS2!e$](m%^mIiplS?/_T+f0:=9'?[@mRH$1Y0f=f&1O_[Docqg8!'P=Cb9N,K9D#IJ]^NYm.=qKkVmXpUQ:=3L5P>MT3\MS<#b@&^pCN+a<>dl;Q='\auJ"eJ[[b2"tn7EURLQO%+J+s-pV)F,A)-c['%1i@YN-XDErbIPiM3`B1FMoO5SQipCFt<%sn_k1SMo%pc!\$\fDO*/Go,J$iM\7Nn-r#^[7C+9?-/5(^M63S`Ef!D*b6^SXU<ONADU4G-XD.pU[U=";+u)HQY"4q5CBP3UG)K,-U%4*YRr$T\<-H,&:L7Esk0K[=(@\F<8Z[ehLi[iirXIog'_gb_#3jqbl@k/GsAroK0h6a$Qm/fg4VfPDB$EJ`C,7kkE"On_bK15V3Bq)FY=(A=[HOl?s&[XSpThp\fUSAeHHFpJRZE-l$2H<nDI8!V0m98I/VAoX5TA:FDtKG(2\Q'a*].U@WU*[`kr`#^Y."&<nq5@;*ZkO:CAPHc`sC5gMRI#s<2@gT-)OK+-q*[N(m'Df(K\l0Oa],LE@U%d'6?\"K9m=N4`cfb$^q-^SbLC>.71bt)."MXe=^9EB9JD[IVG7ng-:ks7o7*Ud-Lsi*G[j^&LYucdb0q),i>he.QN8,3I&5i<*_hO?bLUqUg4Ufe$[g)II_Ys<eD.3*]g?kX>0[^km.qlPlH)J[>#ZgH^)<B?^9`>&?'%"Fl(d%(//p92e1j>Y%(U>3FR3"4;E3d8h0sJ*NG05S;V@0=O)cNc6h&X7nl.V;3XCNf%;b>]Ee*Q<k'Qecrr`n56cU/"G,^E98'!NlU.P"JYC6%11@FHYmh1=NkjuADGUO24r[VJ&cD7!:Z%Gj@qn_jp@0YJD-)-.J'j^#Dl&4JP9!rH_<)%3K!O0c&cQI91.+am'q$$_F]As(5Gm:Fgone.j3nQN'dqRX7+QqCS2*N&'I2Jd7/*R^rg1;8ETN=abaFA^1)/EI[96'qS9[Ge2d$1X'=b`W5e'h^r1W1)XP>lL_sE8G!8ApOn#+_$[\0GG^4eJ8Y,93n]qH<oeE@@36@%eKMtT>bZQHF/`IR-,k&(tqk?;Ck40$j)bV.sNEj]0(J,=&FAPebX/q1/o9ii>Y+WaYn^>atT5P(a;+f;!%X"<=^,GklPDU2\1Bb*#0TcMf-Ap0=RLH(S*1JT3&\Qbj0YQf'uU'mPG:>U][*M:EZZnDuZ8u5OT\Q*;.^QmE`+iWF+p0hng:i\j)0E/Kl'Q(,gOI)fZdc2mQjH1O!f&K36]5P)bZFc9f[F6>C<$&9"Vh><5,_^"Cjte)BhuDZk`;Gl0pWNlMFrZOh5U&EKa#mE:LsO)^k+iSmVk6;L8*;#<te['Z0'_G.WKM\19u;%#+0h2,T\d#^`<S4>-C$"Qf\MU?l46L6gdiD1TYS4;j[Zmh%i4-*8B':>=='H]s_YB?FKC*Un,rEE\jR]H%e?TNbcBnTf$Z2LcXRem\!rrMW'42(~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1565
>>
stream
Gb!;cfkq`f'RcUW\-NXF0l2U*m!Ws@"uYDK8Y[UcI\C@%M:$!++mGj!qHe1XNNFK7$3RPD%7u\;G<+iL@R0J&r+E8=pgjdc?SPq.VK6,WM-tW5^J0>V!T=>\UDQOhd4j`Ko$I>d?3E.(#t]0/1WX6%Z8@.]@jK7g$okC_;jN$$0L#j\O>NYF('2NZS7KaeqN)tmj44g!P9bXj^9]J6k2dlQX@"eu2hKusei>U\L(U+1:1KZo@IiSgmk<YoAg'mRCmk_1Z!%o9$eLoHE.3(UDN$>OQ>e!cA^tVJZF2Doq@TSPDQaY$'QFb\Gn9GKU]D555)a@@/aOF]:pg9e_Kh%om=>o,)4K!4KHX&E(InQKKG^(-N05T?FhhM82M\Oei$k?Vb!ol'lXiS=V$S6)`;Q2M(rier1<(<`0U1V14GEkuAF5o'J:=.V"8d6V0`75R^SCrC*Hik5]!\9Lc<t1lG]70RGnDS0cRJU';ucERj,PL*6F)UmUQdK*GfFt6$us09c@!de-ng7]@=J+oi?o[2-[Z`f0nSN.gB[KqKEYpPar84X+e?V0)#0P-X/Ee3i;$%jHbbMA,uFl#Kq_HaV]&gJL+#c"WDTUuK;s_pf,7489X`aoMJX_5h4gU0UH5/%8BeR,fIh+p5,o))S$ZNh*E%HL(r)s(K_CUU3t#0-LD-V=kHZ^aHm(Mp1Pugj3EChRT.T5SEM20KhSqA<['bG\Yk<9]\b17,6cg70*k>,ib>G;/5BYXf^;fue1'_c#G;-RhF&a`k,[=-j0B(a+CFG[h>efbGF[ebN7/+\Y0$dbf-I9sX!=T8+iTf\="*TO^\1nUI(*`pKBE;,^Cp4GE.%9h+;J(at1Fkha4^a<1cJ.ct;bE2Y.ksgTkP+r^:5.MW7tHN:ETp1>4#ORq(^!bkZn.KOBlY66@G@faXFK(]FGJpsb!u8XFYY`Y]-TrSjK:F*^Fc>WCN@\JoUq=KWa^^:C"V`jGkf#B?tKX5FGXoHJseH_:94Z*PfNi7`<Pal!2YA'VF#$*6PPPgarIbu,u(!a"T;Za(kS`^oDTlf-p,mUQ,k&KLps,P+]q>El_]m]X@.K\pEt(]qi[$*h=%kfWNB>s.$YT5T_GSR`V`IK,:iZaqMq=i,bO+pf"Ph)LSR-Y-h!nObE/`3NB-M&(Oe%t>6F**qSBHj(6H_NgBacc@N=e]XMqkV$W5l.iH2?@;'32GUTFpS65E:4F]1l29*.7C#R_e<5V7XY\t6mJ#<;,,R,Ifm_.I_?qcNVEX#Vf6<djX`;cMQ^3C,PA"hB^NIA9i/HoWVSR/4S'dPQ7sAQ<mtnQ1K%.ou5F4GN,>dI@%5CRdMX^A>OW;.+lH,D@b9UW_/Ie35g3KmB2Z!n,''`a?N218fXjfBVB91_KqJFemC^!9_S0N@UaDio%L'OS,MJC*=9nBe5"t[ZMKjl5oCINW-1YRQ$M1CT7(*^VMT%`VW]oZ/Q6o]NWP_rl"Ds\jc'hnjX^`e.6NX]Y!]=#-/J:@=KDW9&L67q7g<;Z'+epX/9/H#@'ugpX[f'DXhIY2GJ&LJV+5[rrA0Z_[l~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1637
>>
stream
GauHK997gc&AII3bU%cuKc*'C.Ye4B*b^(@RNQKgnO4sJD'cLK9!jfSf6=.uV;8P'RiZ#X(.iM[H17:HaaiB;FoU,)i3sF`H-LkZ=Bu#m7)J4%?EKs:,JmrnV#hhL'aY,.#lfHf'31]7&goeM@&PDA99=l?@hJk`jg8V<)@P/X,6gQV<#Z.^0GU)PNk?JhkGVPs(!"F5q8#8TbtCSi=GLM^56SkACiCks$:uXE>/=qYMmn1bHiq.8)r"uZ\h;qpf@Xp*@*Q6:kAQ[4gU:ddc/GK^g9TT(Cum9Wobgek]ri6ePaj-YmjA%1D1O.O8*fI+*qimN.B4*#%-KdnF9IKs@&TrW#XL>N`Ou;5"9ujO)9Wk?&QbcHL9WMnRSYKYClFilQ6]/EHp4:E3E['9Ye^`X*\%RE3%n,?YQa@U"9ZYp$k432A]4'iRpOMC7"^dVU6?<<G(Pj/E,]=LGnt7lN`5r30E%*dSi[s<@q#Xgba+2b_@>N,(ubmt9XeP[[P!7+@D;]1iPusf6MOi6cNogZDf=RK[#f0e7a&*?pJ=hN>F-,e6YqPl)VEs]&Gk9YP%1H'N\5*]T=*OW`B2g>iG=^h_EPZ,l(fr.BG]6[H&SI.R&],u1U(@1ju223O1Rqth5>N8.jJc,(S$4H/Vj^u<6n(Y3%'X$(diUh.9Qlj`Y'J\&hnnn,Z?WN=:dq5C?g'7Bk(bYklaI'?(QOXnpsQE%e>fB;tD:7"=2eQcr9R08AZ67;H$"dXLa[$HrrO]#B-6+Xtc\>,#E&aH3lj;i.F*2)V?*g]\$?=@C\[a[8<]6".,@(g^f0k'hT[PHT4d5#c*a_>1ZL:&C*FH:lI])`pa#&qkWNcE;#c*elDPER3,T2$6A<7'^j:Hd1K2hi[nV\^afqO7=(AuA[u-Sl\fBsEg&hMG2A9-'o@W&".UM'lC6$.c?:n/JK8\;lbu&)&D`YV_bHgU5(?^BglEuhd=Us%=q^U4gJ)U"A(sB8H6'0.]MGT42YWtejLX77##n%L`R\X.]$PlZg1m&+<F0&k5<tIlfqZIghA4ZCCA07:K@eT>(\U\u;1aR\nAEc]/)IZ$^>^.@X@F4VoT?<%f;#Q]95I>VC%lpIZ(R#1e&a"V&7IEjm.P_IIX]cF2:ME'O]Y3i_egpf\dR0.9X(kmi<^cP#G`>E(KJk#4#N4#MuLZ!X)hesDAosg[W,SVq)Ifc1j(AiO]EV[c5LF_R%G1S;:c=JA)oQglS?M&3?=0]iq&9TSa?>EIAkqjG.li+7\\a_S^X4oqP`]b?!mF!k<G?bQlEK;:;1+PCfB*%4VN[bY3^2$3o&JjqqV4.hZ&)g.r4_ldf$nZYr%to\2uafYt<E(K0sDn?O<ooAqrmG]#"N?k*QXHbtl^FrRq$6jfH0Y]!?QG#FAb^(2k+ZK<-WtraLIW4$7&lr?6e[M&j6R.,d8Z>E4N;Y(CKT8F;*t0>S$XS3Rd\d'9.8/3_2W&Y!kVTcPZ#LVhYbR*bl^cHZj,nEDZ,d3DlWGC75:>b\cl$/W<'(M^IjdE[?k\g;OrMq3B\\@BQF'EOT8`;nIDi9]^^mhs_+m[\n3nht(Wb0JXZV\n>UCl>UOCi,<DS&SJ\Q;&V81l!RZRXX_6Um@Mo#t4dp,lMd=~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1844
>>
stream
Gau`S968iG&AIa;m.eq)1,]Pk+94IOKA.Xb[7Qk+8p\m=VBC^%7EQleVoe#7G6'm=b@]2eUlm9k+RncF^GQ+NDs"do%DXVQ3.9)&EL8>/#pNdN&t[@\/!"YkPmDp8Ree04')M;*`QH@HO@4gcA>#nG=pYb/&ZnD0*7$(Q'GLb;O$*\88ZK,tU0/VH]Q;pWP&h<h80DJgqg>02[d&Q72VOjno`r<P#A]:`:jkVX$WI,4%8?croDKA0j==0KlT@u%>ud8:?,<%P"m=?uo<M'4at2JM\Me09h3b9i"LAUmHYd7&n:fdG5kXj@?^_U0!;KJnq6Ga'*NbJU!\qK9K0F.',]=As<"n/e$=iV!_`](^UO=;>KT4"S;I=T_WQ,aI<9V.:f?Gp6R$)j,'"!!pY?SmPVAZ^:h,9(+_>lW5T@J9L"knYI'l:'PO5X$g5GT;2mA.ZAmBoLTVGjc\gmKn$4SI?tb:3=k6\"sfk)N)@8T^.;K9.f[\7XM$L:Fr-ZjW9dL^RY!RrA<:n>4#5$,;C:1P1=J[sRm+bd3X]^-,h30#:>Q79$F^^r!mpK]o_CMC>V?1R9"YaY#TVL-kFVa^M>U?2[[%+t?]EMA8g^)<AD`_(n'D_St!]iS"uWK"6bM:`-Nl&&r@KU71T)UeB@-$;`SnD._p/70!?bh4t0PJ2%pZ?6jN$$RJQCZ92pQG:mq,p[*.MHfZSbcea9iMng\aPg#BpUWd'<^NE\hEkmDsGjY9c-^:WX:UcX96;r68?g8Po,ZISt>g_r1f^%9/-;L5=P1/A.-S"cUaVp[Z_fVA/%D!:-l>LK-.'3`8Bb*[[=gf`#DZfBLD9hn,oTof@_M?M"/3QrG+UM",<gqSb5bGWP9fCb=W8J'M`UF]o]KO9I0\]t+>ujZmp^51ghnW*R*'H=;-T_?r^d3diIo!lC%cMKC(kn'gjbAJ6?rqm(Znu:s`HbKb80=[bMKiEni6RLQRsicJP:n/T"aWtNg-#G*HstGZ36J]WMiuQ>[Q5#rdI@JHp:F-md9r2gUE_]'nsb2'X-]rh<n!82-DIRcUTb.gZD+I92FcdG](X-K?9##r7#_u'63^p9?(8=C3WJC^/e&)M6)SUcDuX"1ltdO=q8B3&7jC9mO6P=erp0L08<gBrr'4Z=I5&f!,XQ1hpS5AoD0VhF.(L(7JmYCjL,Bn65S5N;2.gb]3O]_mklNqXnmPZk.G?g!k1E.?7nmeeKZeae;jt5=Mm8A#J4O&3Dfgg('BO;<+*Iq1`al!Dc67IJ8=HaEPM.[c_FBQk!+2Y%Yf)YL2%X?L9qE/ZOU*k[pHdPm-u3XPN`$!4/+etG`b8*)o[#MlWtG3q):,/-JF-WLS%Q4bR.Q]kU"Ff@PXD_*4`9"cb>F4Bj)99NK<H4gn"7qodBpo8,X%POV=%=r-`)@P>h$m,$eTJ_5K*5uOpma"ZS>$GYtZt9Dj,*&QEG$NYG5p=M5k)ti+@rR:@&7*YA-U3oZ,YBb5+m1?^TiYfHhUmUSp.gq8-P5-O--[4rpC?gico?kHCd27D>gA0KR2#W(=@^]t2pcSLGZOaq&U:^[fK8P-&Otk.;fqJH)!E'ebd$@JA9i<3q!u>]\`+li2L-\K<MLDUMm'YYEB_43G=KDm]+aoq,7_Kptuf5.E5=mIK;d,tF"GUppAR\dU=#>Qo.-?&#fLCF-UW1(9UGSHdo-SM%ODDS#18BFdRZ=[./Bggb,qLhA'h6trt'7fM*$jF^tbh'VrP\7(n6#A!aL*,H(g>QaLWW_VDFc^Olu0F&\cFGHTGAiKm-T^Pd.aN]F<G!uTeiTK[,Y=r(CFBp/19t<]lkam4P2]d5D!*[*-K)~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1640
>>
stream
Gb"/&gQ(#H&:N/3$j$QOJqV=:8?+osdV[D8#e!In2F-Rlcna3R<u4Ai2f6=D6X;*cf?'lqd_<XX/;YR3%QS0^L(#JZ=neO(!8f?dlo>0f@<i/Q&dc`_n8&uUN<iTjb8?$h<$WGS,5_]2D,`j'Le;p/^edaaQ66V]`ZtaJNmjG2W#],,MAQo,W&>9E@BhTQep'IpkP/^^@\o6Wis1%Qdn>K8fHJN=LLAbe%?h-t65%FM*Q,15`PqSd]S%*Oehu]pWcImFX^/MR@E#d3V_=Z)D47P?h$1T9f.O9#Ke(o5Yl"ThO8Q=rpel,F,J@Ub=0;Q>"!3o>]'NjTYhQ7d&C5<%j-kunM@W*42+._M0btj9n4;KS(a5/D:ddDmrForg,H_?Ld`gFT)1"lk[,U\CGjLV`Taaeb@E_)mV?HcB0a)=/qEBJGfT7;nnN;jLhIY/1)LuLG8fH*67?/18j=iPe@lEp8g-qR0r3m48flO?f::uIO5p!@o0hSra>Q@,r@:I]>9[;Wa=MN'=O#)16i?s<9`qP?u$0q3is/3<Z5=qnrPI6fomLk(+1l1)W*#r&!_m+)cdnJZF(`"O\rHWN6gR$0"ENsiBbH\:$B#UGDCWk;kd'Ru2GNBF?e',JO[HZep-r(KJ3?3C6G4J$'+M;';,*H.(?4bi]8E(V/i7&GL6P1fIT!t)l:E"pF_)mbK$=OcteqbePjPrU]f89,)019YNf0mLnlES#"bf#N:/;>;]cft(tque:93p8$c,p_H.X)R^b5a0j8ft$Z(3/U^00j&C<#6$f2n";&gF=Km2r@pq$\"WgBHfLcqcKo7`p7Ca4S,/4PXf\_AjpbS-/C1adQYi1.mE10V.:1^qhSRmbF?.bqG7,fWAkq`GWHO26aR!eU(H^A&dD+"ET!&n]SmC!cXWDoo$&PtHb'[(Vk>:\G=RJ'?gAU7/<ScND&XlXLi?AEe?OPJ4/GrdjC(&=PY]bA/o8NjU%*0@UF^%pK%MOZ6$]A?(L$Pb=iPDC3Y]DD/F7g:+cmH4um2s6cQ>i+!.QmJ3A]l?jEW7lEcC.g7[==d5=gN1=eRGoQWl/$`an?tCaFqK8IaRq'dJ/OuH)pr4fs;(-ZQa2BU\(N.2'R_:D019pYWRTiY&fA@DV@FGB7!u$j/<"q8G*?Y+iasAHXl"fju'SSa71p>F;?.a#DF[ZjgUaFPa]th4N4RMa9P,p=+;&R4]7/m(iRls6iTqDSgj$abp+X3ql\`0RL?,Jd6*^$f):>]Gh=t%Nkrg)$"!7854',%N)[l?*d/d%Ss-n]G0UEF-(PH9a6lR22jig=,<3?Zb&%qYs4fe<-R/[7e;;Q3=01Xkh8L?Cj9c@6DK3:=RHh*'@5d='>I5FQ:\3B=dakQ!)]$a,*+Aeb#E?E=gOlUe%BL=Ze[3#I*fi'[Zru@f6[S)=qtb^V+Q@XCYto_uSl`ll/!9*^]<:O.GFK/q\='*ar;S+>1so"9Ec+=gkI0gPW]dk(6`7^B[!5pb(VuY/$_>)soW8)FWanefd,p'_rH<MQ3Xudih1G6I'\k<,+2dk>J&?-@kq_l60^ia-XU!c'<P`+'o"N_n,;=#!qkj0nqSW)LA,cG0_LhZm)2.e,WGoYl/\m\lkP^FD]")m6kT>?)m6L~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 942
>>
stream
GasIe?$"FZ'Re<2\C2h(\D?!-B=7-Vg5_`gW3oJE`,THm+:8E%9<6P#^O,Q<!-J8YP2+`;mb)YVQ3"rXX8_,s^oIa$Xr&i&d0Q*^!hpXDN.RZ&iJMCoJV8c4Q34C#q13o5U`^CLQ<OQJC1=OM@2L`=)W98G2:IXK(``ehLfRIo!Q6[kN;BG:,`6X_/VmBKrnZK$1F51W<5T2[eis;T's-(fM])phRFDkD)0F`LqfE9cZEYq>8X<*0UX*4.n^-H(_tiuso:eb_mtU>c4h,2aT=NHq+o2\r@6&**?O0CPdbDdVE<S$&GU\"uWaXcq`^r2k[=NCWR]_/TDhq!t'$FF6?3GpYAeb[(?>JLm_#dT'E/A]FZTr20Mqus]JM=HI>aJVa'VU-Dc&,Z+M1`7VX<AuSYFnp&;MmE1FLMJ;Ph1gs#&OO;edp5[&WN@I$=/;3orL-GCGJh9BXp&"leSX\j'WL!aXY&J!043KYZ69p7[&rDhe]8H=2;0edNXCn[U_;BEXtrbkHjtQQ4=S#A8)PubfH@HY_5lFc/;uk*5)&r**QGj.Q^;U*NLI+4m@9;S>CoEO/!%A5n8tDrltuDGgaLb%u(oCf7RQ%i'+Z.jUrY_;N)74>!m6N.8e74p6"?h=KOJQh(OLmk:_>(c4/V!G[$c+rqG2Va=HDN8A3o+!do:U"YBOV3&u\j4g^S'<7?_*#@OCG&!rSK<(J9Y_aWqgSNTMC&uMI<O8u6oM]&Nqo>#]t<n>?d5W-lDchq/heW\;(JG-T<TY!_H&2Uf<$P)Cgd2_&5&Du]U=e@djaOA]-:-m<HPgV]3aq>kVi>'@QcXNkF3KNPc=\,8RD(<KR4]VTpag1a<(f*1LE;,;F,Wb#A1n1M8n*P#OG^bmTGgh8bir&DdV8r)%2#&8!V#IshGJ_LTmC[l'"229Hl>.D[)aNltrHt<#~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1615
>>
stream
Gau`SD0+/q&H8h>Z/Z?g@%>Erl.uPs%cq7oLES7ZHti@p8/GK!QAhW80HL.>=)TJ>A>1dBJF1;+lLqdEgO5YT0qe;RmJhD=nB2/6Shjao<p.7$&[6QfcW7m&J8iK8O?c-"_h\DO_WD$`3ljT:U*:X50N8^\"UN#%0p)KsITeN:V1\Ad!<]&2eOBNn9IcgOndgT:*2aQ?.MX!#p[OXu]SNLM3Xi@l+;g,HI8V+D'SJ6@-*4Ooo]l)9$i%,3].2YO70\7mV2I8iS8k^jnVcGJ<L$Um9Ale]FBPm']"G`bX1r(cYF00X0*EX8_qW4]kpg'Rn>iRIo+(J<]2!DTH@k_nY3>q^GTWl]L4g`#=Gpr24:#q89IQ-%YRS)fh+qt4!j2j+_G"e$oV2Ot>4JR\%V+M6g[GG(N&=Gg']HPmK#\@(2oZ(V(h*nmpmhQsn$;I1og%$Q&dNN;,/Xd1AFJm6Qt.6CS"T/Jk1u=%jE]39D=]5dqY$;@N)@G$iup+p%?32]alK6MKQYIcpcZGp/Ug+_fD&'1YR(,L%t@ZlSLrZeg?mhD2R1fp!Q7E5Qe6aK=$fpAh-=_DXaFn@CkV5_hspKncId0)FrYCO4F17\c=;ZIQG'ct#=q_tDbj/9K?HHgG'Z\OCX\`fD[ns9j>A3TF1Y//,Hra4QS"n#Z;aKL@UE=b%q'<7VC.iWNSP0EOgY#1<gCkM?:%_k,g8*ACDVHD:im$?3k&*QHBYLQ>@3V4ITdo"FH!8dA;k!.:mLs&8%+^o-t@pSjkL&,lV>j?/IR6*NT4^CZDRsC<)5)X(_[/2`j?jX.Fph2)mW-&&-d*N9fL-`i%53l?8Odn`_::!&WD`k2,lAA%WmZ;pE-^ZIpP^S&>HT7oI?_,,\e-`8WL#Y*"q3ofX<0RNrH^q>s3^'I)DZo!/>CX?RI5*\O_p+h:ZG&:i.DY7$!!F^O(+.IIG<iLI@3P4E6uu1'>F_7A\B7VO@^B,]dp-Pj-nE2eV'I,YZ<'We"O#!=n-_EIW"SbM(b*ZHF!6aL1?-UCc`-R1B):fj/jt#\I*VgG"inC"GdP3oj7(kG0:GKJC,`lAP<+I3=1oYBQ8#1t/_W[EnkBhEhhiC?(k%>c3p+eL21/s+LIAAe;h@k`GBJ@aqb(7DP%obmC6@s70Qp,5l^iO?(E!\76"8F^'9Ag<+]r[nc$?LJ(Y.o4f0&k2$m;LT>")n?f$gR]9rKPg=%)eM<<d+<Q0D1hu8.]NTUh*Ppr!f6:9=4XOI:Q@$4)GN50tqg`N4*`%hd3-YQGmbI?*,]idmhOp96KV-(EjPh:&?-D&ZL!ApX=mXT3,gPaFif:&`cWtsX-iM"8E=R6ijdT@4K^um`P=O=?j0n;j'^PNVc"bc)I,1A#cY[fWZOE$1T*t:P-RZfe!\VFj;$PAB6H[DIV@g1Id!1cm/F?;H\/:8^K8#=nFI_ZD+(_<2odi+A8BJP.qJ/_&oFSlbmN(ZJ'Cnk@7,/=is1%fAS1WdGin\X\eKI"Ip[KFZ>r#3pKAD!2MDLEJq5IS3b<^HNZO&g2]>_DN,:J?==8((UG.XKM(J@jU7IG&ucJPEe%4?HF\?-T2_'`1UWBRK.FLst%3`$t*rWAgP/,o~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 742
>>
stream
GasJPgMYJ*&;KZJ.HWCfd`J'k78tagMFnTg@CU#t@@:sFA1&NY86(q!BC,Cm5`$Lmj2q;XcMfV8!M"&THtNVuRfm=^!ec6f&VNU_aW8#efM@7M8uE,knlu>)(fYq"DSan3/D;5!!7)[3Va5Z\h?ORSFNt0t]*OJ\9E7,sS_&Bu)!gQRHA,Bl>$@]+2Z0[qoCI*N!H";HV0VhIN+Rlh;a",pjMEkY8HG"c+2?XKqop=NflFF@iG5iZfs64?PRaj70<WK=<EAWO]T'[9^=a9A?(F:P"-W8%XV"oUM2'H*ahN`[#QloJe`QCPrKJp8lP08Qo9h`.5ZY=MRNTsjE]#?a$8-oV_trn2Hu0]FLJ\tI#:Ps_mU12[bF/(:((R%P+r7`ODB/3&S;d+q@0CU\7"ac7MQ1)"FDt[4@8`^`+!K/'O>DI5qpF;KJFR'MN:A'ZS0%N3>Q(dqiFg/rXQch_4uC(A7R:]KR%nSHA5p$g5mh>*b<6&BF]5a9/oK-Dr)1N$-)BYO3eR7h)%%j'9"b<'q&-iZK'30C6B;fj\`9AL!j&@"P7E.h>VE*'>_1r=4%8-5Kf-Ff!?H^Uf5?"/(<BJhr#gW@$^Yg^>YB3?:%o+N6%.'$VD,Sn1FSmZS.s#32PAXXb83i?lqJr#<>Ob]$UL@XCMKH<'lh?Gj(.2=UEW1gLZrf82JABI]jh$,pjApb.4I?prqL;$a/"/e[BmJkSQ_g.CXrF'$Nes&^/.$d~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1781
>>
stream
GauHK9lo&I&A@[:m._i=C`!.<!d)K3E>,6m`_Dmi9b157'Eu[eq&tr`SYhVNh?E]=`%od_,0;#4@NXH9h_*Ig.)),H:3(lWk&BsU!TPlK"=AqTq$!;5]j$QeO@p/cnJ1^A%OCW4$`^U95S0)4iXl8],uIQj:lHe;.H"ke-'M/>\:!En+>T`j#p>*/TaJQ?pIWFd/hKU*L=)@]/O*h273l^&+jK/%?oWeH:A(R"+I!$bJ9_X2f5EjC__:+Tf?%#qB(3F3FE1r#_7>6Jo>B%g:2?V+n3Z$sXPDmJ0`*Q=LA<X;ed,Ug:'.6c&H>)K^]M;ED9C$+p:#:;(,83%S@?#+*Djg]U67"cqc%#UT%n;2a,Ns1#Xo$0`rjcFp1'N2-)";:d25sP/4#X3q041Kjs47tL,Vd\Atf'p#j4VWo[l?Ck4AefJHQ:&pR0(k.2s>V*f0Nj,T[t.AGDoIfc'U&N''nK/'hqP^73Ik]sQL5iXL`ZjY7)[UP(/;I:.lsX$F4WKql-henjrEf+EJqn0>fb`j^h5&aK&jren)/LW53g'-!^I@S-m!%!s#MNq)Yc5r+do/lel^K3Xj@[>*B319]Y#c/;dC.Q.jA;8R9u0gC0\!q+JRZnB/A6%s2[jY,*K]<\N(>%]?6F)-Qn=5(GJ2**<i3_QYp1rP*Ya*F`2Kc)5#WO@Z:<a&QT]<3:fr-Qp\?W,l]`[[NVgbUJjrO#M8;``JB_OE^54X"B8<r0)F'8fu2@Ap@m+eaM1nVHJm,i2@k,+h8nq$sDH$Sh'3J.A)>ZC02ukXQ2!pgM,oPo;f_!uN'0Z?fT)%s6a`g(3$K[u]h1LWIT"4sp$FY/W2C,W[8s;kT;ZW/Z^b%4CYkj0c"u%.MQ/c4Jo?0Al**9!/Z.*lfc^C\e%rpeJTQS$FdS'3IR5lEd,e>@CPP4!'=5'FQ"l&?h':RUYfidZ$iAiY_CGQ$NOP>ekbm'IZ9r;6ABH;W#8a(<7/c25&s0G[j>!?f_JlS(YFsHW-_#mQ[U8^NNds[cW!YV=XI!5*9l2XV3MX!TR^0h7bN37*(gF4'u-d@[f#n[?rQ>V^PD7"HC>*XPW)b>lAMJ0H8JMr=(GS.2$$_oe<#99s?5ROW%7a3eF!Mc<c/&W0=@2E!UTt7_^Vak1Gq8N"ol7\X<F-Ke!Q`n2E$-3$sN!R!emA@^<f9:s>Ut0aeMe8;:T<,YdA_JO883*//]iNh"h.a`oUrLSU`:6[=O]';\lMF:kE@l]!n=jcoBS%1!>oDRWok:7,F;iM-BUPNQ"YRUDlW<Vgm.<dM/ti8WlI_67e&jq)/P]j<Qd!p3@6q4rqH(W&f/#6N4?\,VBbM$SbH&OIrGF9DY:/1)<)T[NKkZQY.nj0T7$_(T*57Um-?;jBA7@FGoY5XJhX&[j8Vcf6QDoO\ubS_0`pNO0_0s.k-=JGMHl53EAiMV[-j"9"A7m<fr;)GX%&@*al%/bURX/J`*Un[8J*7*A8fXBZ:_`0DInk5;%8h:4r/Wq5_bKB^\CgPKNUBM+fO7_P-MMIq@Fj`lK1CE"?tlXdIuDfi7j7Hh@,Y/XG>Heb0<?/NPErsq_7YJ=3kqqW:b%e^@/l`Fb,-KS!cfZOI-44hk`I090k3-tr19"RR7F^e]7:q#!@ROlt;g.o2Z;Ja@3rb*)E4#p`u6,0&P_cm)^nIOj79pq(=;R<K@?*&"XD+?h$iu:m5lp6V;"!6"_DS`"'hpLikjn84#?`$r30U0++43B=%h(HX$D0\d^D7pngD%HX=%_Qq&nGWTCeAd(~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1937
>>
stream
GauHK>Ar7S'RnZ;3%0Ntko,;P,`6Wi!SQ%u!OAgD2gb'^AXR_+aJ,\-dO*%bhkBI0m&68DEF?m&q47CCT,:E&Yj+=*!;nR(K3&HuLY3$%6QDI6_onJ@aWau/cZ+NgP1i))inNT+ZF,sJ.u<]*cTkBh8C%<,1*WPJJ`Ac:@if.f<_k:O`"UXa%M6`e+iV.d2A)ISBSX)apik%CqFRHR:ILruW`f?H)"(dk;&u\(&B\8(L"QYWs874a"L%K%jl*2i0mkB)'qQ0pkM1^-U]&(mO7GUhi@f>I@\%C9;XD6d$t`:pX9$%F97jTs\+,MZ%a5*hjFppjY]#,9PQ\ec!9c!q[7(_@dF=qSfJ`CV(_:rJZI(JVG(_)J@r[jb\(t5(EK<r]aMAd]bq=c*:[m>#g[5BMD@LNOAB[;]90Ve8?0JM0<q#8eBGX!7Ye?)LL/1tPOENmrP7h'+f-83VSW_7VcA[>9Tlk*M8S().ljmEjjJfFd&D?-b+D-QF/fD-L+g*D&=iJT(`.bGLc$->nHr8l"K\sgo3,t@IM0fX19cPEMPcWh&bi;^)_BMm>#">C&Q&OgXOL32f:F#/0Y+?XYGi4S((R63ALLIIfY#YYePBR@`chD<kZ!@l[9+O_;l@4Rsj6F44<.\JW"B(eVE=c^$nj6moPYhHS"`]41Jtag&ktWhA>'><Kd"5;ZgX($tH.TLYo+J+3<IAF^m;V"1%RtWAkg617[$'jpeK@$=^km2M.e);V8_lR^2>/0.I[*?7WreO.Eq>83`?Pp?:oM^Z:;*k<5Dn_'.no-`B0'Y_6^D(mPng^3-=ng(!"*Puq#_s.MT,9j=T!RO2s'dqdHAp1H%D+&b4RR##Vc5YNhU0Z/qUUO'Y#7S\%=-A(!`IaDQ1e>:fk)"f:u%Y.l.>2lrm>ODHO7@(S$U,82IqN0c*'PE>1mB!564H=#d75*:rbG1U[j>NDDHYWFAtE]Npu;a'`3Vad%`bXW`lsP[H_W>(Y=>m>SHpo,nLJ6:$D=OQ^8_hAr!?BNmT&gO*?\p8b.Y::X[`'0k%pS+*2&.asTX6LIc*#!nDdQ($eoYA.Hha6XMWGt2,3MMb=dMUMl7=Egn5U>ckXgBb!\f>_hnY@_b0>&62YM==ofeBpBEW'e8WMJW$?<_io[-]XG(K'Lo,eC-@@Ym0Are=/K.\eTn`Ii)AJ8ND#hHSF0s1<`)Y2UGd^k'V)AJ%LtKf\fi)U>(KX:OLGIne=]Q6iALIP>EXZWUk/:$*rbeAafa6FU9i4gbEF9TaC"3EkS#M[YRULiaS-%7&]Eb$;WLU5nG'<dph4^qjT!jSidqmncU:kF;gg`KG!GOS'.ub#g5OtDm'IPW^U+?euJ8-\p%R[`j%"61+GmU<VA;EWbstRCLfZG'$7A0RkXo0@s#@Yf0ZD8SZ64XG:NPiQUu1]cR8F6D[Zji[?RanjCZ.#Ht5>0;;W%PfEZRS,etJo\&I!%BB"iC2HGl/Qb%hV[l&8aY9d>9(6M@\aI"=DiSAJqKdI"6+n0>Bk!%KrP*X)an5O7V.9mIu35bQEg-M.GAg&f_,cL=VI#m3C\\cUiGHls@f>0@Kb>=gr8Y.@6q3N@[#2S4QMr.s]&],nj<Nn=r5j_ZP1VmW6%EjQ8h&+/17_S,6`9>6&:hW>L'L&+q1Na&>>bdlL+%NDT>U!u[G$*ehAD>&/3U`#>W[O#)?8olOR6j@OH%VRu,`_9?]EpMfA*)0G[q,I2)<'<*N;1e/H(oG9/II\!+!6smY@mi2K5]]@[X8L`2eS_k;b;@VFW%ojG-\iEmEP:69(j(_EijL]^<TYrAQ[H-XJ3XlIB7uSDGPbRq5p`:2#i!(_C158g^qO]q[N".bWf3F[62JiQSmiDXVP%WS_n^(^>k9Hk`5-&`nu)F]`jHW!r6`-E7uU/L3#_IiDf-nZ?j$5qLeH*lA_o_~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1856
>>
stream
Gb!Sl=`Y6:&:U6Yd4\H&eeTLTV0U9#JVD>M!AA`?Ml`'.M8tdI,6<8l[cWU2GbMRE@Uj&rmT,re[nOWW&$:n5"7Z0rT"h'd)9`j]IH&02&h[eAB8<^Q7=G6Tj/s1S+L,SZ+C@/JJrkX?M^`U(,!>G4kEKp,F(!'*75TU$_;^kMJOH1NE?>Ia6--r,IZK/@iR0mr_D'j.DtW-k]pOJTG,T$E7:A_Q>GHMDSkiT06ikLiE;'"]Vq:En=fOfkN61#H108Kr_@9ueSA1Ye(I_Ml.Y/E$:1\%L;7_,^ZWie[ZYPQ]q]7+u9Cu'[V(.!"J>]+uMam&[YtVP*8k4K9d&ES3OL:NH/gm7R7Mq(S4mmRY)=I<o+2Z$+!GO1`B:6X]ijq;q4_).$nd1:B2LZ9\1t(W\i<A>->#G6n)!U#[HJ'?VBR0in#8.d<Xttjm><9E=HU2PaMKIW>M[XNM-!TNgN035M=ZLV&fhW:Yk:^]CeCbcRhWDj,B%/,c;s%L>d*Xu6Fj0V//qc-#hE?q\LUI&IY^Q=2,6<cYE?E7d?XaYa>lc6`gf0.^ZT%V*?S=0UIj?K>oF:SW'IO5"q3X6Chj!qbiO(kT1i[brKZ(;AdGUR:b7q4FEddohEgcs+gs5)#mILE$`sctdVR"Z4-*"kj9&5"Oap-j@Ts[eo@;6QeWa]<.e=V]Ak=T=-Lu,7j3u'3_a#-7V<m(;5UjC+5':Q@'Zt>]`)kkH%e0?uIF3Aceec@DTinIlg/H)NEri=2aFGdT;Kpr:`fZhkY$F[$P)Up5P<l`.W,BibV[;9d"[r8\^E[00Y9r6%mR#[Qi6P[#oGph-X\YqS!>UW4Lh'tPgK?'t>M)sb<QMlFcPfVqnEYMpPUOJ.@Vmdt3>"Y!WP<-_N)Va$mQ,JL'KubVaW['a5;QjHFqCYS/0UH7N7LU&$hVeDc_AVO\6b$OVFhPBdEWcsKnB)mTgin"I)2]!o1AHZ%FEP)`"VIIf$b:td_ZU#-#sOusduG<^_qHnumi&jN15/dG(N2Sj+84]p_g(E9hIulD1>P6ZJY/]jP7W(/j[NiF/Y1_E-p"@Rp;!?#Jp+8EJW6$_/@Lu/RkNaeKdp*:8Te3Q'PN]+bBg/(Yd7s$=Q$icV4,2:Q)@32Zd&\3WkM;kLf,5COFtT'Sf>8K^=*OXU/Y<F4`#J9-EpJ@]Lpd%L^XWm1@X)Y[n!N3ma&5N>O+AQ[U#iaWba;l8ZiMs[=6=%)ca`TIBXJ*P,cZu9$>Gqi+((-M8&a.='R/%B+QAU3%]4se=ITn>+I:MX'rHh0cnaEdm>"]gf*MegA-3eT7bhi`T:eJo>9^@Mdl,r2W;?bj_`J;^c"QPV0Y.Q7*VbYDPQo8pIc[]-^--%L/VLP>clBYLHVlXkok\ig'Wt-n4dJP?Q6R/KuR"rA,"Ej&W<nLE0a?TcOH1sSd\Cu"T+JSlFcnl,ufR".1149"`ki(7_l-oj"RT6`:$r>r6pWd$!7:I\Dui>a]pFbSjU^XKu.@,1/#+M-Ptj\Te?bBU!L]Gcc<jS$p8kmrr'hUm1oec_bEZR;h*uhB^$$o_JkTsf+N?r4'=HSn^?f,>UrD*.DXPQ9@+[7>#,GT\:+Y9H"+InCC?!TR,^Ai`/Fo1o-SGjr-!Q-N]4Rpe*Jaf24e8Vl(O2cF=64XM>=CZKX7`%PR>]C_oERCi]]pD8`9'7MiIB#Q[Y8Aou<m0g[uANqN>%Z"aXWnmT%!"E-""B8t6+Y2!N&u16i!F=4dPgRL6:d,\VIh>2N'DC9'b_7oP]$CfJ1D3kJ22'rWe?/<qZ1f]Y(L=l!U9:&Z'37\s[a)I5U&?Bf^]b::P$\fWe;.n\;[(Z\-PK(BM04Mp^~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1724
>>
stream
Gb!TW>Bed\&:i[:/*:\_)dLR1pT9eSCsLIEB_R2s@12*</D$%VZbLrZ1)QoSDVL@,%@ggb2)id-)Mrus04OpY97U)RGlF!/EFCs3inHYU7rUMt^0.l`\re(bmDY*P\6p\Ure!8@8dIaF9\b:dlpQ":q;B<3NoMAh(WSrVM4eH7HYO.sUX4j,+7,?t@3"]op[$7L;71<I@@6sPIViGnbtIDBD&o(u^/5_26.SeR;E`o\8.YqY?GbY!LTb@[VoY9"k<a._U2$">'hhjkJsU,fQVnSsYmfKn(#u#lZ;Ni:nHFlDC0a!u^),Pf96=J=Le/]ii.k9@er'Dso:i#tet/O1(t=P@cs9io3O.ss:5"gNA&Kr0,85EQk_HCLgq[e\]6i=R,MR59\9H8Yf2)3#M*e\$-eLPF>2;?S+>G$C<-Kk/kE5g+fcFk"kmZ5"8C>5t27Ekj9"$rFSKOiZP$6/^$BAIS,M.Q02gV'nf%`#PV-iWF5C]_ta(0+%a6WZebo(M;2^Y[4"QarL&=Y[QHLkU02U"C7p>guuIU]q$npT+"8:l\F:&`1:A50'22A@ZB?S]i[S<^Q06Pqmj146^Q0%(K?Y;Q$j@"L*-941M]0P-SmnUp!&]#28M>m80._6Cf(C+pFj]?Xh=nH^j!n1%OIPp"3ub66iC""ltF$X*UdUf<S"1lXB#pd;p8(i$r]Y)8/(`Bd//iXR!<B;oBihTXNi3oY<&gPh30Mq<MC>\=9CgWD>b(edI7*AeBb4s66MSZ]k]o_&G$6W(N49rZG!AFP'=`$j?N2#tcF8318G$;t%JZi`>T4J&l`?(EKmKp1i>?T/87-(HK:5*oEG$@tcIJJ+?T&s/rXi1/@c]$qn6ak92Vog,a@7'7+%'rsXH0h_e<N%Z%E.ej=o"YMrsY<?5gHf'cXLWVN2r1A_&&]I(1YARuA&6r.[^JLG0l+>6f7"HNrQ(IQZ;F=eVehjc9cP_HI8=!^_BJOrMU4u.CH8qZK7e$tIn<<?c@)QC(Sn%deQR7\;Pa'a\E_TQXWAQ:dBGm34g<pCs%!HWr-e"*5+Cfu@W+qIHfR.g!,o*OW?s?tsHhm!&K4Z#Bk`oV]A-u!X9,2p)/*>YjiJ6r!A&X3Pf&MW_$XTbn&B"8&LD'dKfn&`)[I7CrdlhFf[kWr0$X6p2$H9QQB%\`5XM,jUKCJCNg$6.ej`9]q34:WhTI?l'VWIdVJFZ)*5mfJSF'-seR:<^=`t)9g89!t8L#f0MHD%T)Le>u$+2/[TPnKG>68s!:h7@-5g9g7>3?'Z1Z@m^bM\=0aWF<d\X-Z7X86$ecmFuXG3BrSc?PtU]T<i!N-.tSblT#JClOa]mn%)>3W;?bS8\B>&<5KqVaV.e)Jn%`>HS<(hRO+>LYBZ0FWbkCe;R%28-V".jgk!_-H:c@.m)rP$\OBfa`h\p>PV]!/4@:/KcTX.R%7SCBE"cMh;t1l@)=)?t1G1.&h<30G<'jA4gU@VR,33T9TLASWgVeK"Lb[aLK?ISqdmhS>X3>$qG<EIgNaVi*J&Vo%=6@!0L\SX'\W=@@a`o2]"cPq>G0YHYMP2^lE7&Q8,]Fn9K4Yhl2k)P::.=$goCJ,GE\<Sr;XX:Ois&m;3+lBV1U@N-Wg]gO";"<gj2K!3h&$Haj_Vc%3b)R-okS\%pC,=2MK4df<pP=H8G%0r93B`UVnga3B?`9(X#<t4P'q,Mn(@g1[^*aG(I[!FDu~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 932
>>
stream
GauHI966RV&AI`dp6]I364]0rd:o&KVjXDrBQM9!A(d`qa8t;-s1Z\*,bC)8+)F%DUt,4l?-*L'?EF-c2ZOX;\5s(ULN!RD,9DNin0N1J*5X"'Se"5&.KU)*&Ch$VQ'6bPM%"XE5Z:+Yo"`_5BH:dE\5"KM+?-MeG8Ll;WR;h!c$ZMP_0SBmk=FtP?cq<oqGs5kj97r[l;2*a750[.Xf3@AG_@hZq%.H%m/,&r)18hB;oEHe;tsAT-=3&.^q#']CbdT"UaXIh0$V\;G.TJ3q$Fo&&=us."+3mXJmQrBr+f42o)WbuBpT:qO(aeh&MPp>]#CIg8<UGS("G\XWj!>8)0Z3q7LN"<&iL6o8b:SqV%-R[;GRhC\8J'(_Na-!&\j6\SULthQtEFOlA#G"$[h0C"`%Rn!Sp#"!k[XF@5uI(PI(Rh4\P.g]o!M2HBIUShT!?Vm&ZbhWN*"u]mEa::Yi^IkdE\[23XEPJ9)/.EJ=+/>r+qTKd!P>o@s.m5h6&=iJ.OW-U%pBF'fMaj>P1Tp.kG\7eNV0QiG1$olN]^*be?lq`WW77lTY1?8L"d"-Ju-8lk>U!Ne<Wf`P1<Q,]Li/>(7m=F7>@`!0j-(,OHfo+dciB5i(j%AkZ2;$GbA9YO0D9=%![A5Xs"ahmE"KoHI!We4R2#/)Oe.O70NooAAulds^*iNpeO*"SVqh^Jb)i[/W_41[PBooA>tk82H9\k',,=8alFo/:d?]T;j.J!uSsn$nDLJ+l'd5'C7S;'VZ\Grj7lf5[DND>q%=m0da*Z8?hbmeruA*qM!ZGfkm'%_4:I-0[Y(2flJb;')oLqIA(n0C(&"gWbOCI_sQm+*j5bjZ9m*,]7Gk>MC]q-8(LQJIe7FK]gp+a4=$BND"*CXi]>#c2T+P@e5j(bmTkZ7L9fm#6t<JlO06_+8_4p~>endstream
endobj
xref
0 35
0000000000 65535 f
0000000061 00000 n
0000000123 00000 n
0000000230 00000 n
0000000342 00000 n
0000000547 00000 n
0000000752 00000 n
0000000857 00000 n
0000001062 00000 n
0000001267 00000 n
0000001472 00000 n
0000001678 00000 n
0000001884 00000 n
0000002090 00000 n
0000002296 00000 n
0000002502 00000 n
0000002618 00000 n
0000002824 00000 n
0000003030 00000 n
0000003236 00000 n
0000003306 00000 n
0000003598 00000 n
0000003742 00000 n
0000005694 00000 n
0000007351 00000 n
0000009080 00000 n
0000011016 00000 n
0000012748 00000 n
0000013781 00000 n
0000015488 00000 n
0000016321 00000 n
0000018194 00000 n
0000020223 00000 n
0000022171 00000 n
0000023987 00000 n
trailer
<<
/ID
[<fb3f05f6608a2d955517a71787b2930a><fb3f05f6608a2d955517a71787b2930a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R
/Root 19 0 R
/Size 35
>>
startxref
25010
%%EOF

View File

@@ -0,0 +1,442 @@
# Les 15 — Agents
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Nieuwe research-agent (from scratch, los van Polderfest)
**Vervolg op:** Les 14 — RAG + Embeddings
**Aansluit op:** Les 16 — MCP servers
---
## Slide 1: Title
### Les 15 — Agents
**Visual:**
- Background: CREAM
- "Les 15" in BLUE
- "Agents" in BLACK
- Subtitle: "Een LLM in een loop met tools — autonoom tot het klaar is"
---
## Slide 2: Terugblik
### Waar staan we?
**Vorige les (Les 12):**
- Tool Calling — AI roept zelf functies aan
- `tool({ description, inputSchema, execute })`
- `stopWhen: stepCountIs(5)` — multi-step
- Polderfest chat met 6 tools (search, stats, schedule, favorites)
**Wat een tool-call bot nog NIET kan:**
- Lange ketens (30+ stappen)
- Eigen plan opstellen + bijstellen
- Tools die andere tools triggeren
- Dynamisch het model wisselen per stap
**Vandaag: agents. Eén stap verder.**
**Visual:** Pijltje van "5-step tool calling" naar "20-50 step agent loop"
---
## Slide 3: Planning
### Vandaag — 180 minuten
| Onderwerp | Duur |
|-----------|------|
| Welkom + Terugblik | 10 min |
| Theorie: wat is een agent? | 30 min |
| **Live Demo 1** — Setup nieuwe research-agent | 25 min |
| **Live Demo 2** — ToolLoopAgent + stopWhen | 25 min |
| **Pauze** | 15 min |
| **Live Demo 3** — prepareStep + dynamic control | 25 min |
| **Live Demo 4** — Planning patterns + done-tool | 20 min |
| Wanneer agent vs tool-call vs workflow? | 10 min |
| Lesopdracht + Huiswerk uitleg | 15 min |
| Vragen + Afsluiting | 5 min |
**Format:** Demo-driven. Code-fragmenten in slides + live in editor.
---
## Slide 4: Wat is een agent?
### LLM + Tools + Loop
**De definitie van Vercel AI SDK:**
> Agents are LLMs that use tools in a loop to accomplish tasks.
**Drie componenten:**
1. **LLM** — beslist wat de volgende actie is
2. **Tools** — uitbreiding (search, read, write, anything)
3. **Loop** — runtime die deze twee orchestreert
- *Context management:* wat ziet model in elke stap
- *Stopping condition:* wanneer is de taak klaar
**Het verschil met Les 12:**
- Les 12: tot 5 stappen, korte taken
- Les 15: 20-50+ stappen, langere autonome workflows
**Visual:** Loop-diagram — LLM → kies tool → execute → resultaat → LLM → ... → klaar
---
## Slide 5: De anatomie van de loop
### Wat gebeurt er per stap?
```
┌─────────────────────────────────────┐
│ Loop iteratie (1 step): │
│ │
│ 1. Stuur messages naar model │
│ 2. Model genereert response: │
│ - text? → loop stopt │
│ - tool-call? → ga door │
│ 3. Voer tool execute() uit │
│ 4. Voeg tool-result toe aan msgs │
│ 5. Check stop-conditie │
│ 6. Volgende iteratie │
└─────────────────────────────────────┘
```
**De loop stopt wanneer:**
- Model geeft text in plaats van tool-call (natural finish), OF
- `stopWhen`-conditie is voldaan, OF
- Tool zonder execute wordt aangeroepen (done-pattern), OF
- Tool-call needs approval
**Default in v6:** `stepCountIs(20)` — veiligheidsgrens.
---
## Slide 6: ToolLoopAgent
### De nieuwe agent-abstractie in v6
**In plaats van zelf streamText loopen, gebruik je ToolLoopAgent:**
```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
system: "You are a research assistant...",
tools: {
webSearch,
readPage,
saveReport,
},
stopWhen: stepCountIs(30),
});
const result = await researchAgent.generate({
prompt: "Onderzoek de impact van AI op de Nederlandse bouwsector",
});
console.log(result.text); // antwoord
console.log(result.steps); // alle stappen
```
**Wat krijg je:**
- Minder boilerplate (geen eigen loop schrijven)
- Herbruikbaar (definieer 1x, gebruik overal)
- Built-in loop, context, stopping
**Visual:** Side-by-side: handmatige while-loop vs ToolLoopAgent.
---
## Slide 7: Stop-condities
### Wanneer is een agent klaar?
**Built-in condities:**
| Conditie | Wanneer | Wanneer gebruiken |
|----------|---------|-------------------|
| `stepCountIs(N)` | Na N stappen | Default veiligheid |
| `hasToolCall("done")` | Specifieke tool gebruikt | Done-pattern |
| `isLoopFinished()` | Nooit — model bepaalt | Lange autonomie |
**Combineren met array:**
```typescript
stopWhen: [
stepCountIs(50), // max budget
hasToolCall("submit"), // OF submit-tool aangeroepen
]
```
**Custom conditie:**
```typescript
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const cost = computeCost(steps);
return cost > 0.50; // stop bij $0.50
};
```
**Visual:** Drie traffic lights — rood (step limit), oranje (tool reached), groen (kostgrens).
---
## Slide 8: prepareStep
### Per stap iets anders doen
**`prepareStep` runt VOOR elke stap. Je kunt aanpassen:**
- Model wisselen (mini → sonnet voor complex)
- Tools beperken (alleen search in fase 1, alleen write in fase 3)
- Messages trimmen (context-budget bewaken)
- `toolChoice` forceren
**Voorbeeld — dynamic model:**
```typescript
new ToolLoopAgent({
model: "openai/gpt-4o-mini", // default
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 2 && messages.length > 10) {
return { model: "anthropic/claude-sonnet-4.5" };
}
return {}; // niks aanpassen
},
});
```
**Voorbeeld — fase-gebaseerde tools:**
```typescript
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 3) return { activeTools: ["webSearch"] };
if (stepNumber <= 6) return { activeTools: ["readPage"] };
return { activeTools: ["saveReport"], toolChoice: "required" };
};
```
---
## Slide 9: Wat we vandaag bouwen
### Research-agent from scratch
**Doel:** Een agent die zelfstandig een onderzoeksvraag beantwoordt door web te zoeken, pagina's te lezen, en een rapport in Supabase op te slaan.
**Stack:**
- Next.js 16 + TypeScript + Tailwind + Shadcn
- AI SDK v6 (ToolLoopAgent)
- Tavily API (web search)
- Supabase (rapport opslag)
- OpenAI gpt-4o / Anthropic Sonnet
**Tools die we bouwen:**
| Tool | Wat | Gebruik in flow |
|------|-----|-----------------|
| `webSearch` | Tavily query → 5 resultaten | Fase 1: oriëntatie |
| `readPage` | URL → fulltext extractie | Fase 2: diepgang |
| `saveReport` | Insert in `research_reports` | Fase 3: opslaan |
| `done` | No-execute, signaal klaar | Eind: stoppen |
**Visual:** Diagram van research-flow: vraag → search → read → save → done.
---
## Slide 10: LIVE DEMO 1 — Setup research-agent
### ~25 min
**Wat ik laat zien:**
1. `pnpm create next-app research-agent --typescript --tailwind --app`
2. `pnpm add ai @ai-sdk/openai @ai-sdk/anthropic zod @supabase/supabase-js`
3. `.env.local` — TAVILY_API_KEY, OPENAI_API_KEY, SUPABASE_*
4. Supabase schema: `research_reports` (id, query, summary, sources jsonb, created_at)
5. `lib/tools.ts` — eerste tool `webSearch` met Tavily fetch
6. `lib/agent.ts``ToolLoopAgent` initialiseren
7. `app/api/research/route.ts` — POST endpoint die agent.generate() doet
8. Eerste test in terminal: `curl /api/research -d '{"query": "..."}'`
**Belangrijk:** Tavily API key is gratis tier (1000 calls/maand). Studenten krijgen instructie om zelf account te maken.
**Visual:** Folder-tree van de research-agent repo.
---
## Slide 11: LIVE DEMO 2 — ToolLoopAgent + stopWhen
### ~25 min
**Wat ik laat zien:**
1. Tweede en derde tool toevoegen: `readPage`, `saveReport`
2. `ToolLoopAgent` configuren met alle drie + `stopWhen: stepCountIs(30)`
3. System prompt schrijven: "plan eerst, search dan, lees relevante pagina's, vat samen, sla op"
4. Test met `"Schrijf een rapport over de Polderfest 2027 lineup"`
5. `result.steps` loggen — zien dat agent 8-15 stappen doet
6. Wisselen naar `isLoopFinished()` — zien dat agent zelf stopt
7. `hasToolCall("saveReport")` toevoegen — stoppen zodra opgeslagen
**Visual:** Console-log met alle stappen (toolName, input, output) — laat zien hoe agent zelf plant.
---
## Slide 12: Pauze
### 15 minuten
---
## Slide 13: LIVE DEMO 3 — prepareStep
### ~25 min
**Wat ik laat zien:**
**Use case 1 — Dynamic model:**
- Default: `gpt-4o-mini` (snel + goedkoop)
- Vanaf stap 5: `claude-sonnet-4.5` (beter reasoning)
- Console-log per stap welk model
**Use case 2 — Fase-gebaseerde tools:**
- Stap 0-3: alleen `webSearch` (oriëntatie)
- Stap 4-7: alleen `readPage` (diepgang)
- Stap 8+: alleen `saveReport` + `done` (afronden)
- Zien: agent kan niet 'cheaten' door te vroeg op te slaan
**Use case 3 — Context-trimming:**
- Na 15 messages: trim oude tool-results
- System prompt + laatste 10 berichten
- Voorkomt token-explosie bij lange agents
**Visual:** Timeline-strip met de drie fasen + model-switch.
---
## Slide 14: LIVE DEMO 4 — Done-tool + custom stop
### ~20 min
**Wat ik laat zien:**
**Pattern 1 — Done-tool (no-execute):**
```typescript
done: tool({
description: "Signaleer dat onderzoek klaar is.",
inputSchema: z.object({
finalReportId: z.number(),
summary: z.string(),
}),
// GEEN execute — stopt de loop
})
```
Met `toolChoice: "required"` forceer je dat agent ALTIJD een tool aanroept — en `done` is hoe-ie stopt.
**Pattern 2 — Custom stop op kosten:**
```typescript
const costLimit: StopCondition<typeof tools> = ({ steps }) => {
const tokens = steps.reduce((sum, s) =>
sum + (s.usage?.totalTokens ?? 0), 0);
return tokens > 50_000;
};
```
**Pattern 3 — Plan-act-reflect loop:**
- Stap 0: agent maakt plan (text)
- Stap 1-N: voert plan uit (tools)
- Eind: agent schrijft reflectie (text)
**Visual:** Plan-Act-Reflect ReAct-diagram.
---
## Slide 15: Wanneer agent vs tool-call vs workflow?
### Niet alles hoeft autonoom
| Scenario | Aanpak |
|----------|--------|
| Vraag → 1 tool → antwoord | Plain tool-call |
| Vraag → 2-5 tools → antwoord | `stopWhen: stepCountIs(5)` (Les 12) |
| Onderzoek → 10-50 tools → rapport | **Agent** (Les 15) |
| Productie pipeline met validatie | **Workflow** (explicit code) |
| Wettelijk: must be reproducible | **Workflow** — geen agent |
**Wanneer GEEN agent gebruiken:**
- Deterministisch nodig (audit, finance, juridisch)
- Voorspelbare kosten
- Latency-sensitive (< 1s antwoord)
**Wanneer WEL agent:**
- Open-ended taken (research, planning, debugging)
- Tot stand komen via meerdere stappen
- Cost en latency mag wat hoger
**Quote uit AI SDK docs:** *"Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes, use workflow patterns."*
---
## Slide 16: Lesopdracht + Huiswerk
### Bouw je eigen agent
**Lesopdracht (in-class, 30 min):**
- Setup research-agent in eigen repo
- Tavily account aanmaken + key in .env
- Agent draaien met `stepCountIs(10)` en eerste eigen query stellen
- Logs bekijken: hoeveel stappen, welke tools
**Huiswerk (voor Les 14):**
- 4e tool toevoegen — `listReports` om opgeslagen rapporten op te halen
- `prepareStep` toevoegen — minimaal 1 use case (model OF tools OF context)
- Custom `StopCondition` schrijven (kosten OF specifieke conditie)
- `AGENT.md` schrijven: tool-lijst + stop-strategie + 1 voorbeeld-run met logs
**Bonus:**
- UI met live-step-rendering (elke stap als card)
- Sub-agent: 2e agent die per pagina samenvat, hoofdagent gebruikt sub-agent als tool
---
## Slide 17: Volgende les + Afsluiting
### Vragen?
**Vandaag gezien:**
- Agent = LLM + tools + loop
- `ToolLoopAgent` met `stopWhen` en `prepareStep`
- 4 stop-condities: stepCountIs, hasToolCall, isLoopFinished, custom
- prepareStep voor dynamic model / tools / context
- Done-tool pattern voor forced tool calling
- Wanneer wel/niet een agent
**Volgende les (Les 16): MCP — Model Context Protocol**
- Semantic search op grote datasets
- pgvector in Supabase
- Embedding models — wat zijn vectors
- RAG-pipeline: chunk → embed → retrieve → generate
- Combo: RAG-tool in een agent — best of both worlds
**Daarna in deze leerlijn:**
- Les 15-16: Testing + Deployment + Performance
- Les 17-18: Eindopdracht-werkdagen + Pitch
**Vragen? Feedback?**
---
## Slide Summary
| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik | Recap |
| 3 | Planning | 180-min |
| 4 | Wat is een agent | Theorie |
| 5 | Anatomie van de loop | Theorie |
| 6 | ToolLoopAgent | Theorie |
| 7 | Stop-condities | Theorie |
| 8 | prepareStep | Theorie |
| 9 | Wat we vandaag bouwen | Intro demo |
| 10 | **LIVE DEMO 1** — Setup | Demo |
| 11 | **LIVE DEMO 2** — stopWhen | Demo |
| 12 | Pauze | Break |
| 13 | **LIVE DEMO 3** — prepareStep | Demo |
| 14 | **LIVE DEMO 4** — done + custom stop | Demo |
| 15 | Wanneer wel/niet | Reflectie |
| 16 | Lesopdracht + Huiswerk | Praktijk |
| 17 | Afsluiting + Les 14 preview | Closing |
---
## Bronnen
- AI SDK Agents — Overview: https://ai-sdk.dev/docs/agents/overview
- AI SDK Agents — Loop Control: https://ai-sdk.dev/docs/agents/loop-control
- AI SDK Agents — Building Agents: https://ai-sdk.dev/docs/agents/building-agents
- AI SDK Agents — Workflows: https://ai-sdk.dev/docs/agents/workflows
- Tavily API: https://tavily.com/
- Anthropic — Building effective agents: https://www.anthropic.com/research/building-effective-agents

BIN
Les15-Agents/Les15-Slides.key Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Les15-Agents/lu41dctb9.tmp Normal file

Binary file not shown.