fix: lessons
This commit is contained in:
1
Les14-Agents/.~lock.Les13-Slides.pdf#
Normal file
1
Les14-Agents/.~lock.Les13-Slides.pdf#
Normal file
@@ -0,0 +1 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 07:13,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
785
Les14-Agents/Les14-Docenttekst.md
Normal file
785
Les14-Agents/Les14-Docenttekst.md
Normal file
@@ -0,0 +1,785 @@
|
||||
# Les 14 — Agents
|
||||
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
|
||||
|
||||
**Les:** 13 van 18
|
||||
**Onderwerp:** Agents — LLM in een loop met tools
|
||||
**Duur:** 180 minuten
|
||||
**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk.
|
||||
**Demo-app:** Nieuwe research-agent (from scratch, los van Polderfest)
|
||||
|
||||
---
|
||||
|
||||
## Hoe deze tekst werkt
|
||||
|
||||
Dit document is een **lopend script**. Lees mee tijdens de les op je laptop.
|
||||
|
||||
- `[SLIDE X]` — Klik naar slide X
|
||||
- `[SCHERM: slides | terminal | editor | browser | supabase]` — Welk scherm op de beamer
|
||||
- **Vertel:** "..." — Letterlijk wat je zegt (mag in eigen woorden)
|
||||
- `*[stage direction]*` — Korte instructie voor jezelf, niet uitspreken
|
||||
- Code blocks = wat je typt
|
||||
- 💬 = verwachte studentenvraag
|
||||
|
||||
---
|
||||
|
||||
## VÓÓR DE LES — Setup (45 min)
|
||||
|
||||
### 1. Tavily account
|
||||
|
||||
- Maak account op https://tavily.com/
|
||||
- Kopieer API key (begint met `tvly-...`)
|
||||
- Gratis tier = 1000 calls/maand, ruim genoeg voor deze les
|
||||
|
||||
### 2. Nieuwe lege Next.js demo-folder
|
||||
|
||||
`*[Doe dit thuis vooraf, niet in de klas. Klas kijkt naar de setup-stap als je 'm doet.]*`
|
||||
|
||||
```bash
|
||||
cd ~/novi/novi-lessons/Les14-Agents
|
||||
pnpm create next-app@latest research-agent \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd research-agent
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
|
||||
```
|
||||
|
||||
### 3. Environment vars
|
||||
|
||||
`.env.local`:
|
||||
```
|
||||
OPENAI_API_KEY=sk-...
|
||||
TAVILY_API_KEY=tvly-...
|
||||
NEXT_PUBLIC_SUPABASE_URL=...
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
```
|
||||
|
||||
### 4. Supabase tabel
|
||||
|
||||
```sql
|
||||
create table research_reports (
|
||||
id bigserial primary key,
|
||||
query text not null,
|
||||
summary text not null,
|
||||
sources jsonb default '[]'::jsonb,
|
||||
created_at timestamp default now()
|
||||
);
|
||||
alter table research_reports enable row level security;
|
||||
create policy "demo open" on research_reports
|
||||
for all to anon using (true) with check (true);
|
||||
```
|
||||
|
||||
### 5. Backup eindstaat
|
||||
|
||||
- Werkende `lib/tools.ts`, `lib/agent.ts`, `app/api/research/route.ts` ergens als backup
|
||||
- Verwacht: 1-2 typos tijdens live coding — geen ramp
|
||||
|
||||
### 6. Browser tabs
|
||||
|
||||
- `localhost:3000` (dev server, maar gaan we voornamelijk via terminal testen)
|
||||
- Supabase dashboard
|
||||
- https://ai-sdk.dev/docs/agents/overview (referentie)
|
||||
- https://tavily.com/ (dashboard voor usage)
|
||||
|
||||
---
|
||||
|
||||
# HET SCRIPT — Lees mee tijdens de les
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik (10 min)
|
||||
|
||||
`[SLIDE 1 — Title]` `[SCHERM: slides]`
|
||||
|
||||
**Vertel:** "Welkom bij les 13. Vandaag stappen we van tool-calling naar agents. Eén stap verder qua autonomie van AI."
|
||||
|
||||
`[SLIDE 2 — Terugblik]`
|
||||
|
||||
**Vertel:** "Vorige les bouwden we Tool Calling in onze Polderfest-app. Zes tools. `stopWhen: stepCountIs(5)`. AI roept zelf functies aan om aan data te komen. Goed werk.
|
||||
|
||||
Maar er waren dingen die een tool-call bot **nog niet kon**. Lange ketens — 30, 40, 50 stappen — daar is `stepCountIs(5)` te krap voor. Een eigen plan maken en bijstellen. Tools die andere tools triggeren. Per stap een ander model kiezen.
|
||||
|
||||
Daarvoor heb je een **agent** nodig. Een LLM die tools gebruikt in een loop, en zelf bepaalt hoe lang die loop duurt en welke kant het op gaat."
|
||||
|
||||
`[SLIDE 3 — Planning]`
|
||||
|
||||
**Vertel:** "Vandaag drie uur. Eerst een half uur theorie — wat is een agent precies, hoe ziet de loop eruit, wat is een `ToolLoopAgent`. Daarna vier live demos waarin we from scratch een research-agent bouwen. Geen polderfest vandaag — losse repo, omdat agents een nieuwe context verdienen.
|
||||
|
||||
Aan het eind: wanneer wel/niet een agent, en wat doe je voor huiswerk."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Theorie (30 min)
|
||||
|
||||
`[SLIDE 4 — Wat is een agent]`
|
||||
|
||||
**Vertel:** "De definitie uit de AI SDK docs is kort en bruikbaar:
|
||||
|
||||
> *Agents are LLMs that use tools in a loop to accomplish tasks.*
|
||||
|
||||
Drie ingrediënten. Eén — een LLM die besluit wat de volgende actie is. Twee — tools die het LLM uitbreiden. Drie — een loop die deze twee orchestreert.
|
||||
|
||||
In Les 12 hadden we deze drie ook al — `stopWhen: stepCountIs(5)` is in feite een mini-agent. Het verschil zit in **schaal**. Vandaag praten we over 20-50 stappen, en het model bepaalt zelf welke tools het wanneer gebruikt."
|
||||
|
||||
💬 *Mogelijke vraag: "Wat is dan het verschil met Les 12 echt?"*
|
||||
|
||||
**Antwoord:** "Schaal en autonomie. Les 12 was: vraag → 2-5 tool-calls → antwoord. Vandaag: vraag → agent maakt plan → voert plan uit (10-50 stappen) → schrijft rapport. Het model beslist veel meer zelf."
|
||||
|
||||
`[SLIDE 5 — Anatomie van de loop]`
|
||||
|
||||
**Vertel:** "Wat gebeurt er per loop-iteratie, één stap? Vijf dingen.
|
||||
|
||||
Eén: we sturen de huidige messages naar het LLM. Twee: LLM antwoordt — óf met text, óf met een tool-call. Als het text is, stopt de loop. Dat is een natural finish — model heeft niks meer te doen. Als het een tool-call is, gaan we door. Drie: we voeren de tool execute uit. Vier: we voegen het resultaat toe aan de berichten. Vijf: we checken stop-conditie. Niet voldaan? Volgende iteratie.
|
||||
|
||||
De loop stopt in vier gevallen: text-only antwoord, stopWhen geactiveerd, tool zonder execute aangeroepen — dat is het 'done-pattern', komen we straks op terug, en tool die approval nodig heeft. Die laatste skippen we vandaag.
|
||||
|
||||
Default in v6: `stepCountIs(20)`. Dat is een veiligheidsgrens — voorkomt dat een rare bug in je systeem prompt resulteert in 200 API-calls en een rekening van 30 euro."
|
||||
|
||||
`[SLIDE 6 — ToolLoopAgent]`
|
||||
|
||||
**Vertel:** "In oudere AI SDK versies moest je de loop zelf schrijven — een while-loop rond `streamText` en `generateText`. In v6 is er een dedicated class: `ToolLoopAgent`.
|
||||
|
||||
`*[Wijs naar code op slide]*`
|
||||
|
||||
Je geeft model, system prompt, tools, en een stop-conditie. Eén keer definiëren. Dan `agent.generate({ prompt })` aanroepen, of `agent.stream` voor streaming.
|
||||
|
||||
Wat je terugkrijgt: `result.text` is het eindantwoord, `result.steps` is een array met alle stappen die de agent zette — welke tool aangeroepen, met welke input, welk resultaat. Dat is je debug-goud."
|
||||
|
||||
💬 *Vraag: "Kan dat nog steeds met streamText?"*
|
||||
|
||||
**Antwoord:** "Ja, voor backwards compatibility. Maar voor nieuwe code is `ToolLoopAgent` de aanrader. Minder boilerplate, herbruikbaar."
|
||||
|
||||
`[SLIDE 7 — Stop-condities]`
|
||||
|
||||
**Vertel:** "Vier soorten stop-condities.
|
||||
|
||||
Eén: `stepCountIs(N)` — stop na N stappen. Standaard veiligheidsmaatregel.
|
||||
|
||||
Twee: `hasToolCall('saveReport')` — stop zodra een specifieke tool is aangeroepen. Logisch voor 'doe het werk en sla op, dan klaar'.
|
||||
|
||||
Drie: `isLoopFinished()` — onbeperkt. Agent stopt alleen natuurlijk wanneer model klaar is. Dit is gevaarlijk zonder andere safeguards — agent kan in theorie oneindig doorgaan. Alleen gebruiken met cost limit erbij.
|
||||
|
||||
Vier: combineer ze in een array. Loop stopt zodra één conditie waar is.
|
||||
|
||||
En vijf — niet built-in maar zelf te schrijven: `StopCondition` callback. Daarmee kun je stoppen op kosten, op aantal tools, op wat dan ook dat je uit de step-historie kunt halen."
|
||||
|
||||
`[SLIDE 8 — prepareStep]`
|
||||
|
||||
**Vertel:** "`prepareStep` is misschien de krachtigste feature van `ToolLoopAgent`. Het is een async callback die VOOR elke stap draait. Je kunt het model dynamisch wisselen, tools beperken, messages trimmen.
|
||||
|
||||
Voorbeeld: begin met `gpt-4o-mini` — snel en goedkoop. Maar als de stap-historie complex wordt, switch naar Claude Sonnet voor beter reasoning. Dat scheelt geld zonder kwaliteit te verliezen.
|
||||
|
||||
Of: dwing een research-fase af. Stap 0-3 mag alleen `webSearch`. Stap 4-6 alleen `readPage`. Stap 7+ alleen `saveReport`. Agent kan niet 'cheaten' door te vroeg op te slaan.
|
||||
|
||||
Of: trim oude tool-results uit de berichten. Een lange agent kan 30k tokens aan tool-output verzamelen. Niet meer relevant voor de huidige stap — eruit, scheelt budget."
|
||||
|
||||
💬 *Vraag: "Hoe weet ik wanneer ik prepareStep nodig heb?"*
|
||||
|
||||
**Antwoord:** "Niet bij elke agent. Begin zonder. Als je merkt dat je agent veel geld kost, of te vaak in een verkeerde fase blijft hangen, dan kom je terug om `prepareStep` toe te voegen. Premature optimization is ook in agents reëel."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Live Demo 1: Setup research-agent (25 min)
|
||||
|
||||
`[SLIDE 9 — Wat we bouwen]`
|
||||
|
||||
**Vertel:** "Genoeg theorie. We gaan een **research-agent** bouwen. Doel: gebruiker stelt een vraag — bijvoorbeeld 'wat zijn AI-trends in 2026?' — en de agent zoekt zelfstandig op het web, leest de relevante pagina's, schrijft een rapport, slaat op in Supabase.
|
||||
|
||||
Vier tools: `webSearch` met Tavily, `readPage` om URLs te lezen, `saveReport` om in DB te zetten, en eventueel een `done`-tool.
|
||||
|
||||
Stack: Next.js 16, AI SDK v6, Tavily, Supabase, gpt-4o-mini. Losse repo — niet de Polderfest-demo. Agents verdienen een schone context."
|
||||
|
||||
`[SLIDE 10 — LIVE DEMO 1]` `[SCHERM: terminal + editor]`
|
||||
|
||||
**Vertel:** "Klas, leun achterover. Vragen tussendoor mogen. Ik laat zien hoe je een agent from-scratch bouwt."
|
||||
|
||||
`*[Open terminal in /Les14-Agents]*`
|
||||
|
||||
```bash
|
||||
pnpm create next-app@latest research-agent \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd research-agent
|
||||
```
|
||||
|
||||
`*[Wacht op next-app, ondertussen praat door over Tavily]*`
|
||||
|
||||
**Vertel:** "Voor web search heb je een API nodig. OpenAI heeft sinds kort een eigen web-search tool, maar die werkt alleen met gpt-4o en is duur. Voor onafhankelijke search gebruiken we Tavily — gemaakt voor AI agents, gratis tier van 1000 calls per maand. Gewoon registreren met email, API key gepakt, klaar.
|
||||
|
||||
Andere opties: Brave Search API, Serper, Google Custom Search. Tavily is het simpelst voor onze case omdat het naast search ook content-extractie biedt."
|
||||
|
||||
```bash
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
|
||||
```
|
||||
|
||||
`*[Open editor]*`
|
||||
|
||||
**Vertel:** "Eerst environment vars."
|
||||
|
||||
`*[Maak .env.local]*`
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-...
|
||||
TAVILY_API_KEY=tvly-...
|
||||
NEXT_PUBLIC_SUPABASE_URL=...
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
```
|
||||
|
||||
**Vertel:** "Belangrijk: `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*`. Anders zit-ie in je client-bundle en kan iedereen 'm zien in DevTools. Server-only. Hetzelfde voor `TAVILY_API_KEY`."
|
||||
|
||||
`*[Check .gitignore — moet .env.local in staan]*`
|
||||
|
||||
**Vertel:** "Check je `.gitignore`. `.env.local` moet erin. Standaard create-next-app doet dit goed maar — altijd checken."
|
||||
|
||||
`*[Supabase Dashboard openen]*` `[SCHERM: supabase]`
|
||||
|
||||
```sql
|
||||
create table research_reports (
|
||||
id bigserial primary key,
|
||||
query text not null,
|
||||
summary text not null,
|
||||
sources jsonb default '[]'::jsonb,
|
||||
created_at timestamp default now()
|
||||
);
|
||||
```
|
||||
|
||||
**Vertel:** "Tabel voor onze rapporten. `query` is de vraag, `summary` is het rapport zelf, `sources` is een JSON array van URLs die werden gebruikt."
|
||||
|
||||
```sql
|
||||
alter table research_reports enable row level security;
|
||||
create policy "demo open" on research_reports
|
||||
for all to anon using (true) with check (true);
|
||||
```
|
||||
|
||||
**Vertel:** "RLS aan, en een open policy voor demo. Productie: koppelen aan auth.uid()."
|
||||
|
||||
`*[Terug naar editor]*` `[SCHERM: editor]`
|
||||
|
||||
**Vertel:** "Nu de tools. Ik maak een `lib/tools.ts` met onze drie tools."
|
||||
|
||||
`*[Maak lib/tools.ts]*`
|
||||
|
||||
```typescript
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
export const webSearch = tool({
|
||||
description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe("Zoekterm"),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const res = await fetch("https://api.tavily.com/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: process.env.TAVILY_API_KEY,
|
||||
query,
|
||||
max_results: 5,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.results?.map((r: any) => ({
|
||||
url: r.url,
|
||||
title: r.title,
|
||||
snippet: r.content,
|
||||
})) ?? [];
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "Beschrijving van `webSearch` is kort en specifiek. AI leest description om te beslissen welke tool nodig is — vaag = verkeerde keuze. `max_results: 5` houdt het beheersbaar — Tavily kan ook 10 of 20, maar 5 is goed startpunt."
|
||||
|
||||
`*[readPage tool toevoegen]*`
|
||||
|
||||
```typescript
|
||||
export const readPage = tool({
|
||||
description: "Haal de volledige tekst van een URL op.",
|
||||
inputSchema: z.object({ url: z.string().url() }),
|
||||
execute: async ({ url }) => {
|
||||
const res = await fetch("https://api.tavily.com/extract", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: process.env.TAVILY_API_KEY,
|
||||
urls: [url],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const content = data.results?.[0]?.raw_content ?? "";
|
||||
return { url, content: content.slice(0, 5000) };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "`readPage` haalt de fulltext van een URL — Tavily heeft een extract-endpoint dat ruis verwijdert. Ik knip op 5000 chars om token-budget niet te exploderen. Voor productie: chunken + samenvatten."
|
||||
|
||||
`*[saveReport tool]*`
|
||||
|
||||
```typescript
|
||||
export const saveReport = tool({
|
||||
description:
|
||||
"Sla een onderzoeksrapport op in de database. " +
|
||||
"Alleen aanroepen als het rapport compleet is.",
|
||||
inputSchema: z.object({
|
||||
query: z.string(),
|
||||
summary: z.string().min(100),
|
||||
sources: z.array(z.object({
|
||||
url: z.string().url(),
|
||||
title: z.string(),
|
||||
})),
|
||||
}),
|
||||
execute: async ({ query, summary, sources }) => {
|
||||
const { data, error } = await supabase
|
||||
.from("research_reports")
|
||||
.insert({ query, summary, sources })
|
||||
.select()
|
||||
.single();
|
||||
if (error) return { error: error.message };
|
||||
return { reportId: data.id, success: true };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "`saveReport` is onze enige write-tool. Description benadrukt: alleen aanroepen als compleet. `summary.min(100)` — zod schema dwingt minimum lengte af, AI mag geen 'TODO' insturen."
|
||||
|
||||
`*[lib/agent.ts maken]*`
|
||||
|
||||
**Vertel:** "Nu de agent zelf."
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, stepCountIs } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { webSearch, readPage, saveReport } from "./tools";
|
||||
|
||||
export const researchAgent = new ToolLoopAgent({
|
||||
model: openai("gpt-4o-mini"),
|
||||
system: `Je bent een research-assistent. Werk als volgt:
|
||||
1. Begin met een kort plan (3-5 bullets).
|
||||
2. Gebruik webSearch voor oriëntatie.
|
||||
3. Gebruik readPage voor de 2-3 meest relevante URLs.
|
||||
4. Schrijf een samenvatting van min 200 woorden.
|
||||
5. Sla op met saveReport. Inclusief alle gebruikte sources.
|
||||
|
||||
Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
|
||||
tools: { webSearch, readPage, saveReport },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "System prompt is **het** kritieke onderdeel. Ik geef expliciete fasen — plan, search, read, samenvat, save. Hoe duidelijker, hoe minder de agent drifft. 'Verzin niets' is belangrijk — anders maakt AI bronnen op."
|
||||
|
||||
`*[app/api/research/route.ts]*`
|
||||
|
||||
```typescript
|
||||
import { researchAgent } from "@/lib/agent";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { query } = await req.json();
|
||||
const result = await researchAgent.generate({ prompt: query });
|
||||
|
||||
return Response.json({
|
||||
text: result.text,
|
||||
steps: result.steps.map((s, i) => ({
|
||||
step: i,
|
||||
toolCalls: s.toolCalls?.map((tc) => ({
|
||||
tool: tc.toolName,
|
||||
input: tc.input,
|
||||
})),
|
||||
hasText: !!s.text,
|
||||
})),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Endpoint stuurt query naar de agent, returnt text + step-samenvatting. Voor productie zou je dit streamen, voor demo houden we het simpel — <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 14: 10-50 stappen, open-ended. En het hoogste niveau van **controle** — een explicit workflow. Code die exact bepaalt welke tool wanneer.
|
||||
|
||||
`*[Wijs tabel op slide]*`
|
||||
|
||||
Wanneer GEEN agent. Determinisme nodig — finance, juridisch, medische data. Geen agent. Doe het in code. Latency belangrijk — agent kost al snel 30 seconden. Voor user-facing chat: te traag. Voorspelbare kosten — agent met 50 stappen kan duur zijn.
|
||||
|
||||
Wanneer WEL agent. Open-ended — research, planning, debugging. Volgorde onbekend — model moet zelf bepalen. Async gebruik — backend job, niet realtime UI.
|
||||
|
||||
Quote uit de docs: *Agents are flexible and powerful, but non-deterministic.*"
|
||||
|
||||
💬 *Vraag: "Wat is een workflow dan precies in code?"*
|
||||
|
||||
**Antwoord:** "Gewone JavaScript. If-statements, switches, expliciete functies. Bijvoorbeeld:
|
||||
|
||||
```typescript
|
||||
async function processOrder(order) {
|
||||
const validated = await validate(order);
|
||||
if (!validated.ok) return reject(validated.errors);
|
||||
const enriched = await enrichWithAI(validated.data);
|
||||
await save(enriched);
|
||||
await notify(enriched);
|
||||
}
|
||||
```
|
||||
|
||||
Dat is een workflow. Reproduceerbaar, debuggable, testbaar. Een agent zou dit ook kunnen, maar je weet niet exact wat-ie doet — non-deterministisch."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 9 — Lesopdracht + Huiswerk (15 min)
|
||||
|
||||
`[SLIDE 16 — Lesopdracht + Huiswerk]`
|
||||
|
||||
**Vertel:** "Lesopdracht. Vandaag in de les: setup van de research-agent zoals ik 'm vandaag liet zien. Tavily account, Supabase tabel, 3 tools, agent, één test. Half uur — kun je halen voor we klaar zijn.
|
||||
|
||||
Voor huiswerk: uitbreiden. Vier dingen.
|
||||
|
||||
Eén: 4e tool `listReports` — om eerder opgeslagen rapporten op te halen. System prompt aanpassen zodat agent eerst checkt of er al onderzoek is.
|
||||
|
||||
Twee: `prepareStep` toevoegen. Kies één use case — dynamic model, fase-tools, of context-trimming. Documenteer wat je koos en waarom.
|
||||
|
||||
Drie: custom `StopCondition`. Token-budget, of aantal pagina's gelezen, of iets eigens.
|
||||
|
||||
Vier: schrijf een `AGENT.md` in repo-root. Tool-lijst, stop-strategie, prepareStep-keuze, één voorbeeld-run met logs, één observatie.
|
||||
|
||||
Bonus voor wie wil: UI met live step-rendering, of een sub-agent voor samenvatting.
|
||||
|
||||
Beoordeling: 10 punten totaal, voldoende is 6+. Inleveren via Brightspace met repo URL voor Les 14."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 10 — Afsluiting (5 min)
|
||||
|
||||
`[SLIDE 17 — Afsluiting]`
|
||||
|
||||
**Vertel:** "Wat hebben we vandaag gedaan. Een agent is: LLM in een loop met tools. We gebruikten `ToolLoopAgent` om de loop te managen. Vier stop-condities — stepCountIs, hasToolCall, isLoopFinished, custom. `prepareStep` voor dynamic model, tools, context. Done-tool pattern voor gestructureerde eindantwoorden. En we wisten wanneer een agent wel/niet de juiste tool is.
|
||||
|
||||
Volgende les: RAG. Embeddings. Vector search in pgvector. Combo met agents wordt heel sterk — een 'searchKnowledgeBase' tool die semantic search doet, in een research-agent.
|
||||
|
||||
Daarna: testing, deployment, performance, eindopdracht.
|
||||
|
||||
Vragen?"
|
||||
|
||||
`*[Vragenronde — minstens 5 minuten over laten]*`
|
||||
|
||||
---
|
||||
|
||||
## NA DE LES — Wrap-up
|
||||
|
||||
- Push working `research-agent` repo naar GitHub als referentie
|
||||
- Brightspace: link naar deze repo + huiswerk-instructie
|
||||
- Verzamel vragen die niet beantwoord — neem mee in Les 14 opening
|
||||
- Note voor Klas B (later): Klas B doet dit waarschijnlijk in week na RAG, niet ervoor — orde aanpassen indien nodig
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende fouten tijdens live coding
|
||||
|
||||
| Fout | Oplossing |
|
||||
|------|-----------|
|
||||
| `TAVILY_API_KEY undefined` | `pnpm dev` opnieuw starten — env wordt bij start ingelezen |
|
||||
| Tavily 401 | Verkeerde key — check dashboard tavily.com |
|
||||
| `result.steps` is undefined | Je gebruikt oude v5 API — check je `ai` versie is `^6` |
|
||||
| `ToolLoopAgent` import faalt | `pnpm update ai` — moet 6.0+ zijn |
|
||||
| Agent stopt na 1 stap | `stopWhen` te restrictief, of system prompt zegt niet duidelijk wat te doen |
|
||||
| Tool returnt `[Object]` | `JSON.stringify` voor logs — Tavily resultaat is genest |
|
||||
| Supabase RLS error | Open policy nog niet aangemaakt, of `enable rls` ontbreekt |
|
||||
| Cost-cap stop werkt niet | Check of `step.usage` echt populated is — sommige providers geven het niet altijd terug |
|
||||
|
||||
---
|
||||
|
||||
## Mentale model voor de klas
|
||||
|
||||
Als studenten verward zijn over "wanneer is iets een agent vs tool-call":
|
||||
|
||||
> **Tool-calling** (Les 12) = "AI mag tot 5 keer een tool aanroepen om je vraag te beantwoorden."
|
||||
>
|
||||
> **Agent** (Les 14) = "AI krijgt een DOEL, mag autonoom plannen en 20-50 stappen doen, en levert pas op als-ie zelf vindt dat het klaar is."
|
||||
|
||||
Het verschil is dus **doelgericht autonoom werken**, niet zomaar 'meer stappen'.
|
||||
322
Les14-Agents/Les14-Huiswerk.md
Normal file
322
Les14-Agents/Les14-Huiswerk.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Les 14 — Huiswerk
|
||||
## Agent uitbreiden + prepareStep + custom stop + AGENT.md
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Deadline:** Voor Les 15 — RAG + embeddings
|
||||
**Inleveren:** GitHub repo + `AGENT.md` in root
|
||||
|
||||
---
|
||||
|
||||
## Doel
|
||||
|
||||
Bouwt voort op de **lesopdracht** (research-agent met 3 tools + `stepCountIs(10)`). In dit huiswerk maak je de agent slimmer: extra tools, dynamic step-control, een eigen stop-conditie, en documenteer je het geheel.
|
||||
|
||||
> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt nodig.
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel A — Extra tool (verplicht)
|
||||
|
||||
Voeg een 4e tool toe: **`listReports`** om opgeslagen rapporten op te halen.
|
||||
|
||||
```typescript
|
||||
export const listReports = tool({
|
||||
description:
|
||||
"Toon eerder opgeslagen onderzoeksrapporten. " +
|
||||
"Gebruik dit als de gebruiker vraagt naar bestaand onderzoek " +
|
||||
"voordat je nieuw onderzoek doet.",
|
||||
inputSchema: z.object({
|
||||
limit: z.number().int().min(1).max(20).default(5),
|
||||
}),
|
||||
execute: async ({ limit }) => {
|
||||
const { data, error } = await supabase
|
||||
.from("research_reports")
|
||||
.select("id, query, created_at")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(limit);
|
||||
if (error) return { error: error.message };
|
||||
return data;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Voeg toe aan `agent.tools` en test:
|
||||
|
||||
> "Heb ik eerder iets over AI in de bouw onderzocht? Zo nee, doe het nu."
|
||||
|
||||
Verwacht: agent roept eerst `listReports`, dan eventueel `webSearch` → `readPage` → `saveReport`.
|
||||
|
||||
### Eisen
|
||||
|
||||
- [ ] `listReports` werkt
|
||||
- [ ] System prompt aangepast — agent weet wanneer te checken
|
||||
- [ ] Minstens 1 test-query gedraaid die `listReports` triggert
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel B — prepareStep toevoegen (verplicht)
|
||||
|
||||
Voeg een `prepareStep` callback toe aan je `ToolLoopAgent`. Kies **minimaal één** use case:
|
||||
|
||||
### Optie B1 — Dynamic model
|
||||
|
||||
Begin met `gpt-4o-mini`, switch naar betere model bij complexe stappen.
|
||||
|
||||
```typescript
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
new ToolLoopAgent({
|
||||
model: openai("gpt-4o-mini"),
|
||||
tools: { webSearch, readPage, saveReport, listReports },
|
||||
prepareStep: async ({ stepNumber, messages }) => {
|
||||
if (stepNumber > 4 || messages.length > 12) {
|
||||
return { model: openai("gpt-4o") };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Optie B2 — Fase-gebaseerde tools
|
||||
|
||||
```typescript
|
||||
prepareStep: async ({ stepNumber }) => {
|
||||
if (stepNumber <= 2) {
|
||||
return { activeTools: ["listReports", "webSearch"] };
|
||||
}
|
||||
if (stepNumber <= 5) {
|
||||
return { activeTools: ["readPage"] };
|
||||
}
|
||||
return { activeTools: ["saveReport"], toolChoice: "required" };
|
||||
}
|
||||
```
|
||||
|
||||
### Optie B3 — Context-trimming
|
||||
|
||||
```typescript
|
||||
prepareStep: async ({ messages }) => {
|
||||
if (messages.length > 15) {
|
||||
return {
|
||||
messages: [messages[0], ...messages.slice(-10)],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
### Eisen
|
||||
|
||||
- [ ] Minimaal 1 `prepareStep` use-case geïmplementeerd
|
||||
- [ ] Effect zichtbaar in console-logs (welk model / welke tools per stap)
|
||||
- [ ] In `AGENT.md` uitleggen welke optie gekozen + waarom
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel C — Custom stop-conditie (verplicht)
|
||||
|
||||
Schrijf een eigen `StopCondition`. Twee voorbeelden — kies één of bedenk eigen:
|
||||
|
||||
### Optie C1 — Token-budget
|
||||
|
||||
```typescript
|
||||
import type { StopCondition } from "ai";
|
||||
|
||||
const tokenBudget: StopCondition<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!
|
||||
194
Les14-Agents/Les14-Huiswerk.pdf
Normal file
194
Les14-Agents/Les14-Huiswerk.pdf
Normal 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:20260607091327+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607091327+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Huiswerk) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2436
|
||||
>>
|
||||
stream
|
||||
Gatm<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=0nUE8Ne>LpQXM-L@9o&VLj>ne(&egqqYc._&.!nZU-!1id$I3m<a1W&Met*reh)9>snIRLu6i1K$rLm9?4bo8Jj+@o#nK8BdXW%Y9MF:,_RLjHtCD8[eUu(0Tme+H:&an#U':*qaC?50XkP#cSMBMOl*rO68el'>-M-*Vle7e3gs3Gkf!3%/*[9Z7\CE@/%]8ZViUTeaVg-.]Z[Z;ZgTlDIHJ%"0b"Y:3O$YPjQA0>>,bE-6-e"sp*5=7nRZNYn+qfJ6Z0N).D8h,^k,3Mhi6<JAm42gZ,$FE5gZpd;EMArm($EF28c`WC&[*>&_b%+.Dd'pB]bU0=5gW"(!P]#fa>SFj<e(OPt?fu7=4I@o9(>5F7S=*+hdm*?`38rHKmH\2-TIOm)OCP++eoMsU9423&^KY%VCXt,Ma#ust/naX9S'rCSVcq%F/\<0%`>6)<LC;V73)3(1bD;caZHTYl*4BAhn.;sZE]-Tj^b1m`7=DFW,ONb/3P97\QY*ePMX1*NRj<KL9c$6Y/ZVQbfVk]?\EBITXEhLm6m?Nk8H_sm:fWgBGtC3CTR$6[q#jonA]gX.[bL'Jpi#n[N+j-%$VHDiFY=3["B;O4_F#C6jrB"tD2$HG-9mI$ieHGbDtiG&mlS<hn!ct&*_T4JR2\+.D(*Z8N?oCL?(ce)A%QC^Hl+9FgFf9/h'5bP"'U%8H&A9-mm&^RkdXEn<a.!nU:+8"9=RD_D.UP!hG<@LH)kRM/,IR"._K[nZ$]peMg;Bap'`f95eUfM`shgiTI8='`'`,dSm:aBG9Q^WE\PaLg"62+ZP1iKdc>9'=F'oEbdZQl'Ueo%DXSM?J83FBlMf4.DX)8KXHJm7bplY]Ue4]API>n@9gadV0Xhr<L,uj?8%C4?Mg@)kHuZeYAopns12CP\0X,\_gsCC(K^oNq3ikBgen\3TP53a08I'Q.b>1R4cI*L*AQfjD<TYZ>d'$,tASle"j,fB@]R[@Eh:a>p9EdG`11R3-Y0G+VS39#pHkipm'_&/;Ugi*I@ssF_.R]S'IcuPp9?=g7FrUTs!CPh[Sr?7sca)q"Pgbb&R$2jV(iDT,>3\$V+`.t$+0iXDg,Yo0_a"P4L\oj?GUqQB4p(7OMBu&Le"/WIqo,<J'?b_tFe9>(2CcO<ogA-[1GDo[/^J!#Be[&_]M)SO7(:G8Y7pi"F+FVciC73"*F7F9Il2s'rGB5AlIGWnUWpP>-`NOQH%<oBaYW"riqO'0;Q*&PM#?AGs/n31D7'2;;l;&RJX)K>*1:i:3)h1CWtk&aYRLXta\D:Z`ZOCji)Hqg[`C/=YmKUrj42[8cZbo1')h#8caB:6gP'OdZ$aQ>*_f/s0P?1?qb4sYg7ePf]TE#6AHsiQmO!o3N$Ejn%lS_YXXC<_"63.B%:JR>i(hmQ]sG`7mL<RnlIKHmJ,FJS)dc9qmSIX^>)*]i<o*,\eISdC(UMHbO16DMkKN+/)_j-8Ofm._gGE'D6`'K-`*%fuE,<:Nk\+:gW2-:oU0(0"$MDnJJ!B)m*i"Y@&Uf!+*g)IJpDs1B\ZE6m:"o[]oEZ0.lY["!5#b*oh,'4Z'j[9>8J6-u;=YC=Kmju`:7U]UbG;CS^W=XmDM#;J9uDud%NkuSfq_!ZMTrSTS$'sr3MerBH2/N+s.FRhO$M(/WJ/'P2&;k;JkfC*);8P#2!oFV'kim+-Mb0=8b>]b1fqdWa6R,QHq7UDj4I7t:=PWJ$NrLV`!>,B8!@g0F%T%&WCTn;5'P.Q,l6KgFN@?$0G+N7hd&X"-MS]L0<QcE._[%*PIgsjolcO(-FJ+=1feJ$FJ)%gW[cVYg!N&KI]p>13SGprHa$=?<8aMf9X&8q[pD2>Q7$dd2:\T<8h6+`%/5+s*/"G.p3j()'XG+1"2kMi$$IJk;t*6rSkeMW_/J\C4q8W%bc31ji2o?^ZgMl3rSmn9i9a;Z_tr^,:2Tq0LK:;*<Ws1Q#>Vl=>FW2ncI8*^cq:Q6fg\)3pMe^8#F8!3FSiu7&>f\aFS[N,QX,gL2_m#*7Ws-/%dZ9pFmPm*h6U0crRc[C4RJdH6)9>9iia3Q*WNjr'q]Hn>_jg)WfCJ(h+pqp4)%IeOX9?)ccI23-L\^sr]MA~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
|
||||
>>
|
||||
stream
|
||||
GauI192jS!'SZ;\'k`piK90WsP*^8o#%m'$l?:.dj'3Rjpjk!=nF(^64)*,.QaJ[(aS4KYS1=cb99!T;!:P!!>pq+\r#WudKDZW0oB7nA3n;e,&@%F=VBN6io>+L*M^0$OVDAB'X`2k,3$k*<*&Q8MX"6BlrZkAQ2O2i2#cEj8V+:3`8@@U27O2]:rEVYZ\IDjld[?+MVp/]KLRLs)<+@/K8o0*l_;b'Q+'5u=.N6H0f<+R+GUcbqG?S4@?_*c!6TkQUiWK.RFk8S%L3L^g,!XcQg=&!#,"sQTU&HnYN/u/[H?^e:0^4$<Wk'dTpOmEDe0@l7RCbj**X5upnTlrJN^\t<dCZ)glT&T<Q2G,cHfPfO_g6L"\!l?M(O+$U"2oW/=G]EpVfob6"Xf^&W'\hb='0isiE.&k^2*RqU1c(BMd[+0W&SBO'Cer,/WiOGPUe4*W-o>9IK)glgK$C3YA1+*m8%lJ[Te1oChO*PO&+hD:sm]-7D6XF-ER$Vh,</+0Hg/%WlTr?S==LAq@,A,h?3~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1697
|
||||
>>
|
||||
stream
|
||||
Gb!;dgN)%,&:N_Cm-%#9/Hc%S&p0VejC0L.VFf78I>RM[e$.&N(R_)cVN1\i$XQ(j[Of&83)dq/JkP%g!n_RM'a'>#<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`:2aY1+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#6K=r~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
|
||||
>>
|
||||
stream
|
||||
Gau`SgN)%,&:N_Cm.a.I'*MAL9PSne2]'f9:=H0/6^6gh'I5Ep73O*mg._LZ$XQ*@[]I'(E@d/A/V155fKIC]",\2OI)d,?dj[D,c@Bo%/q?M;@JU>&9ta=DiB@dJ![9H9+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#CnR?>bqkZRPohEIWh28KNF&!PQ<Dm?)eQ0b('._Eu8qNB?P7g_h]>07Co(Y"3p*CoPGW6E/HZ)qWmo8QapBYsn=q@aE`sL2"XYPV\"Q/l^c5"`).<!kfaHK)L,_0mq,uLXZ?%k9]ic^`p`>Nat*%/Ru0YfNWVq;V=hbqhNL7nhf%c'A_cj-tXW2/EC%<6X6=MDIRcMM@3k[2IF1YnGNPU,lB.+j@$6a'6bCF;q4;@ppGP^(\p1m'*GS_KdkVfK#or5gm8k,86o\,0gbcPSaa.)em5%Yg+(`1S@jh_Ab'gA`oLNj-%<Tt-DrGX+t#)CC^aP_7JXceFDgY>>BI"qKQdNlX!CfRq*R,bD_"LfYO-e/Z;rL)i\-K[pop/>9td0Sij;:47PTk]hIDObf2ank@^%6mhSpo6A.Z-@ak9QfC2,++-<J-567!)\(>Y%.4_J7Q$SE`@TU[SZ;CMW03("97K,au+6)`at'ht@okV4*nkhsYL7Ls:-"4qGiYaXPE>29GiS!9Vub9l;/UDTsSbZ@Ml-V:a!ar9Q[f*8s*V\FN47>Vn3&1%RBYHON\A13O?W#Nh6UMrSSP*doR0hSB2^G\lZ2JM_qg\[%t;R+"L79U_L&PDEuMU/@Spc:KiULX(>1FF8'>&);$DZIJc3_C!c!@"A.DdkMlg^Kt[@Tg#I#`qWcamMOHd_L5)_:lu-MO*CX1d2fY<,cjZFEVp/Ad;3G?Nf*Ae!l+#5ltK<?TW^[\0j6Uo@Ud-]CD_s2eCk5`%_Po-`(olI(QFJq"f5BSWb4@ErJO>KstXTKuj_/KW<[h0tsBepAP_:=RGgK(npJJV\K%-,pm.jnNcp1&eiu\lG.Ti-</eDa1thoM/babM/a:lJC23b*LE-FmBt*SaDI6D[r(3?[FR!&[.ROiBT>4pk"2cN72j]@8*eh2e_8s<:6u^#"AbdLX\Qq]8S_7.W&efVeoC<Z*&A4fNfN%j-ZFU^Yn4QpIi#\>X!XgfQh:]GO$J+lGmfRK0;Vm^0rIJ6+j!H8rC^a;M"3R_6f1[>s.O/cW/.n<r8($U6@7"OC91rm'nRs6A1bAYNe+N=-+b6oaj*3X0KT!?@jQ%BbP&Z"XK]*cGILG0(#N`1am69J2A,iDLR3ZJl4N_`]VL?`<)#Hjc6`crFCBVU[bF)2hETWp23u2XI>Zr<F=I$"cf*oA5hoM?M8J=V;Vfj7]CR`*A"U,Rgdfq0Nk;'95@<H?YSZ`QUZi8W@i`lt&F:[!mB0nhDWL,,7A;q[D0McK+MR_(Tn1\)>M%g!<pP'?ijScQEo*%q/S1I4CVl\#YcULFA@XJ;m_/mlH\\[#?].(jiQ7!jk>RPg@?]3?=kZ7/kGX.2;".n">]W/XdsSt.(XYuJmf~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1837
|
||||
>>
|
||||
stream
|
||||
Gb!;d>>O9K'Roe[380^R$AOu=gl0acJ7Z>`/+u<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[-'+$Ro3o%FkG%IWCuS7Eea*mb][l2jQI3E&W]nJu-s=_i$'8';`V.%t&.EClV>FtNBs-Bnkd>N9>09+[s8n#sgOT#b^!n:;Is*)sNai4_HCMs[i_:J5*#&0\m/F6YLXP]\(r(5TCs,*-mu;Z*F=3Bu<!dF15KcFl9;goKuDp3RT<Fn@.mk4-jj=[3f33&eVaf/M.-5'toQ$Xo$_Dnl&9<K/(3IMZ@1,$BGI]7drra2-/b9O`hLB]]@GX:bo9_3HULBnF_kg3>#$<']3ZE7kNAmrH@Afpo]9KkZafNoJtN_u,qb>=G,bRrFWtS3^j6jA>7qa^$J9T/t)<V6SauS`0#r+:O9GWHbksMBV+hq$7%R5@=(lG\U0EfF#J0/hBss-CXq7%HHho78hgd#'g=V/;>fo"Pt!S\B:^RAQ=DK*@LPe->bK$ZHn3T+"q*0eXa0lHZ@nmD\`Pi\#ug<-XX=FLhV8j8^).q0V;QP.h!^B6S581g^+(a]@6_+Q;r_%BQ,f^BIm,;B=@X;;)mBb,PV]R\_gh?j3XErk!IW4e_Hd^<Bb,]RjD'La6WBnM<[2R_tN]D.:-mEUp%\?a<u07ICV#3,ZcXi<ISH/.Fu,8Yf@SWE+rrDO%:gqeE]]--i3%r9eS*Ze)oB;A(u?XHdBHLlhpL&oMhqnLYF?%SJ,48)k*Q6j]S"LFt:hXV7Wk<Dt5Hf`T'lgpu\DO8MmDgP39f`[;!9Rbd*"B9#8aCDsPa.-!6Xflc:Z*T@r5E-H&=i-Lf2h'@8dbNMdQI7Icc%Kqj,Wj/VOF)9'l0-]n&8M2&U6`fYuEVK85O6L/60C"H6Y)!VTS9C7P1#Sg>5=3P,LjL&bs$Y#?02,q2BNO78#/ARZ3c)-dm]7Nl85H^j6`q2Hl*>n0S\ZXY.3m`oQr-YD:pUIN0p*$Bj6ru=t6#C-Z3Pt(8[=^-XGrtNX>?J^9EfiY(!F)f_)]-W8,s'B^><$t@E=#3k-%N&*DcRBP/\XL?L'8':bq>M8!j*@Vo`[U)i7abDgTZLZ7t\RG\3arBLG1+6Uq_J1Q3uV$B]a,)CB=TgXkuER@pRAXD!5a0^f`,n;mJoOmGQqW`nA^"<jNu=:>">aJk4gp[fRd8,!o0YU?>qcPst#A5E5d.97bdQEoG@-U&lNl0t[V[QgB\[%m``\*$*TrLd%G$+:(Q'Y>#6EGHc"uZ<qV<.4V5.Jip,mmh>*#nZ]E:;$.fX4#krr5Kbd^h3SlBUN>tn"MDd`$&_pu:7[=a]U:dV><Y+=)Ja5,Z\QRf:AqW:]hmCK:"_`Jq3#<NT9cpqEo#Qh*>IP>8*1*uPZ0m9U@)PTl"9dViNRbO\o3JG\W($6nQHjG?.426K!t/YSkgM!T49a%Po8T7%-)5r<j//JFC-QJ>%J##rR\JshR^pLCpmBLs*LqoE-?sB$<LZE1u6rsmuL5K'>CO&^!.arH@=<TD:Q-X]q4<(UhRM'M?B6!It@#;(K>YpL+.]P4GsR4iX7j&:#%YHg\qU)YbSio`e4"=dB*sg~>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[ZfJ`ak>1+]S>rjg&Ia'XLV&'c*?4'^8Na^L1*Xso1'+bTRH'fR[K).@N?3tN@T-M;6-()b=BVTHe\)\Yq!:8mK_obP1,',Y!ohG?M;L?b3im-!'K@9^,,di$Lj"DD1i*uPcbdf,rn!L_apJRS7Q7/3N5Z5MHF=CWBh9;?MWrMfPji']Y&jh2-9NIS%sR7D(W1F@VrXsIm]mFePJI3Gt(k1eKFI&FjU#eQ#@)6p/rNr&qX;4<^Q9Lifo_DIB1RdLW1Jg8Lb'<]o!^noGGZm+,Nh/7Bt4fr#[iY5k$+:;0_d*K&X#>6u0iXFJVCKk`sbfU^Pr9MI3b?8/+T:q_9i5BC,N;Nh`(+Cs6r-18;(8Stf6'snQq7\=djX5SlAaV(J]+R)`66$Fa:%;FWT5,pLA:>P*7SmO24SSeNOU(B^fH[RWX4R3\SF+q:XO8q\_FH=`q+Ab>fI2[h@XFtN8HC]rWDG^hCFojX'VO.m<]aT'E7_4G[qgm1Nin$;qkd,Z*Fa'<?#L<lT>9YJS2%m7?klYm`mIOW.t%lPQrA4o/Eg7KWrJdC^_L>GZ.XbtrENM;7*i^9D/I;eD#2nGJma.2`N-OP%m3<!b)9sHDL0iVO+K2;'GP*uqg%s2k3rEbGmtE\(M@HN*U4Z9NkTGW$8!G:!k$&=.R\eHS]b[TBdOKXS;qb.RG-igIN0]nc?/O.!1)rD0*pEB'joACj"]nJn.kjMrLd-\H$dN/WKU'<D*!PjR$CMo<C(e_k&GY-IEakk?;WG3n!JM$3MnJ:2a[R'_qf;o6WKTR]9LFI$I@b>9[mc2*Eg:53UW3+[37KX^#oLR0o^F\;b_U708b%u28)_2>d8WQ0co1SdjESL>EX'/,kA0;p1?rCY5?38Z[)k2f%dUYHi(JcPH7aH_,A;P(qu,SFt!O\Cf'8VDEu#HP=;="fV5Qg1Urql[?f'7U$=Z!(TC\O%"@_($(sT93:Po\(gufWKNAah@fb(0WIr.(7_N)c)q'Z<!3#8tD'/.1!O=hI1GsoJkDa5:iK*G@k0:pIZJ6dejbaO_fR5</\<\`Sg*+"+AY,RTf3H%M+X]m2;/e=A`)%Lt_:g"@KhHdKi8pGLK&_#7iGfKq<@=f5=I&5gk=>'XoTVQc5N/VZZ%\(OWo/NR0rltq<!>q0cn]t78*J^>Ktif>AX;InG=-p8f/ZjORf&=;@1Z$WZ)o>C/PY@Uh2SU&dm[&*C2H/jolumZ!CjtE0!EDOQh0%f;\<9-T[Y=iM3'-bLTb'uSWJ(+f,3pK/>lN(a7D\9@70R8MM$aTMZ0gNIoi0o4QfJ1J+B)pr6lq.6Xu-/q%Si7\gQWf<9p6^7fDegiG-04>7nWY%b3'$-B,E[@<X?$[^A#s=ZpZe\%YX@pBmQ6O&_q)R(D*r\!U?/<m!+jgB.W.Isn'ZBBmEF,;f?2qK8anPV^hZ/?LZ^L?qbY/s`NYr'5.j(+\B7@E'A(bkX82SammL9lE6l;frTor[<H$#8#lZ8*D.dqtbG.'[@2gJs5,LY<JWqWYYX%7oVT?4:4=P!.O:dq:0-7L0o\ikNDD@+'bF):h$[L<u(<iC`eA4r;a,fa0i'SpWJH"p<]OK+N7?6<G%cdpM?0/P#.Ir&sPVE&SBAB9I@5TLhN:7D+gH7dnh]%^K:[4.ZG2fRB1ZI\4WNA.^=X,kNR*W[[;[:+%7.<^]~>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_XV)m_c)3RQI4p"7A6Wad,<*#P9'iqS:Q>@jHtb#WLdCb]-sPm/fF^d5K#4[BYR_a)"<7JfhkgFYepZ-^"#]?N@"j,]<8<U/'XP7p!7?okB%](&5.JA(p9dAHF]#Ietb*ZpMsM3O7ha\3V)2GC=6q]4W+.b7,nSr%C$4mP-?P];FY$oeY*bH65PQZo-dIa2/A&/'L8W`'X22++2s2TetouZ)l`,323b"m>sLK1A:O0^=/h:>$P:Z+n6WelXI]@#-hG4%DEGB-soH!#]Jr[HXg%Xf2d$\W\('4WZ@CRJnd\7aqUb&r5Fudip,X[)/2\O?.7=j=D71;K>D+j@8>1/E8`N4/4mU9I\TN[\[3L5Eba,=0/,VkKP4efeJ^*PDpA4E$-!R>*b-15G,?hRoa.S5PQlJH:3_6,ccq-i/7ThQ$q!`!]fKf)3Ul!B6=RIZ8k?9=06^X<2<rfjHQ&3SNJ=9o`0=k/_]BQ'-jd?eT-mDM!jrUk?do-^>!l01c5d6O$[L33i!kc*8l>6@0SA<?#nu_u;%1F8Y<hAam;?E=@'kQ[6+/"W2[.KNf6W*m^Otpd^]P?sNR8peD2.MpqcgiC5!=OAo#,DCiL2I7H-5+pa4&@gIAaQj^?E,>6,3u<j?UM(Z)+VYLNr7K!:$]4,O1>0YCP+'&3?=+LK@ctfgd4N>G^sqiPAS&\E3CpQh;bHcABV0Yjm]o>ZU7)4.F\U]r*,ZfNr'!ZuU#<j7#N'UTaa5IN&M\H]H#=Udr_,CdmUQ*:eV=8d0\2i@0J7Ap#a2e65l!$&qEeEtlTd.:RUY)HM^t`1^?T+Ca7i[1_#?;liBlp*621=Kl%GrakTE]%o2D9;`]O"2O5O0cdV>GJ)NsDVUBa\qtm&U9cB>X5LsCWc!Msf':Pe$f/mnEfBQ!F64`X=t#"'4ar]7;-%2]no2R]6$AK0B0@hkGH]V;Km9t6UR2X^L?rTmL=hN'%/s<C9Ca4O:ddn#Y[,5Zc%f'\5H<J5r?.[8Z8;N^7,R>Yc]Iq%26%DpdqX@*'aI-N%MkPp94$64I*Ag5o;<]?Ahu!B('OV=o[VHZ*@<EL]/mTS^:T#Aak+DI)\>?2ebi)b1WQqN4;rDlY>elscL6J"!UD7IBTTG^^$EBp3H%4)p/!hWiZl]0f2]Z`Nn;qA@6rJMZM=I>'0jZf6#RZ@/Sksa!MWed3Iq%+lH7:[/^.#[iSk41:,E>UQEmV$JP4NEPTTCG!A#&=!dJu57g]nVIfcJoAB4~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 22
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001873 00000 n
|
||||
0000001943 00000 n
|
||||
0000002236 00000 n
|
||||
0000002334 00000 n
|
||||
0000004862 00000 n
|
||||
0000005488 00000 n
|
||||
0000007277 00000 n
|
||||
0000009068 00000 n
|
||||
0000010997 00000 n
|
||||
0000013098 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<6b9084b4bccf0b543324f6b07f7d5576><6b9084b4bccf0b543324f6b07f7d5576>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 13 0 R
|
||||
/Root 12 0 R
|
||||
/Size 22
|
||||
>>
|
||||
startxref
|
||||
14760
|
||||
%%EOF
|
||||
282
Les14-Agents/Les14-Lesopdracht.md
Normal file
282
Les14-Agents/Les14-Lesopdracht.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Les 14 — Lesopdracht
|
||||
## Setup research-agent + eerste run
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Duur:** 30 min in-class
|
||||
**Status:** Tijdens de les afronden — daarna huiswerk uitbreiden
|
||||
|
||||
---
|
||||
|
||||
## Doel
|
||||
|
||||
Aan het einde van deze opdracht heb je een **werkende research-agent** in een eigen Next.js repo, draait er één test-query, en kun je in de console alle stappen zien die de agent zette.
|
||||
|
||||
---
|
||||
|
||||
## Vereisten
|
||||
|
||||
- Node 20+, pnpm, git
|
||||
- Supabase account (mag dezelfde zijn als Les 11/12)
|
||||
- OpenAI API key (zit waarschijnlijk al in je `.env`)
|
||||
- **Tavily API key** — gratis tier op https://tavily.com/ (1000 calls/maand)
|
||||
- VS Code / Cursor
|
||||
|
||||
---
|
||||
|
||||
## Stap 1 — Nieuwe Next.js app
|
||||
|
||||
```bash
|
||||
pnpm create next-app@latest research-agent \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd research-agent
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
|
||||
pnpm add -D @types/node
|
||||
```
|
||||
|
||||
Open in editor.
|
||||
|
||||
---
|
||||
|
||||
## Stap 2 — Environment variabelen
|
||||
|
||||
`.env.local` (in `.gitignore`!):
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-...
|
||||
TAVILY_API_KEY=tvly-...
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||
```
|
||||
|
||||
> **Let op:** `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*` — moet server-side blijven.
|
||||
|
||||
---
|
||||
|
||||
## Stap 3 — Supabase tabel
|
||||
|
||||
In Supabase SQL editor:
|
||||
|
||||
```sql
|
||||
create table research_reports (
|
||||
id bigserial primary key,
|
||||
query text not null,
|
||||
summary text not null,
|
||||
sources jsonb default '[]'::jsonb,
|
||||
created_at timestamp default now()
|
||||
);
|
||||
|
||||
alter table research_reports enable row level security;
|
||||
|
||||
create policy "demo open" on research_reports
|
||||
for all to anon using (true) with check (true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 4 — Tools schrijven
|
||||
|
||||
`lib/tools.ts`:
|
||||
|
||||
```typescript
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
export const webSearch = tool({
|
||||
description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe("Zoekterm"),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const res = await fetch("https://api.tavily.com/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: process.env.TAVILY_API_KEY,
|
||||
query,
|
||||
max_results: 5,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.results?.map((r: any) => ({
|
||||
url: r.url,
|
||||
title: r.title,
|
||||
snippet: r.content,
|
||||
})) ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
export const readPage = tool({
|
||||
description: "Haal de volledige tekst van een URL op.",
|
||||
inputSchema: z.object({
|
||||
url: z.string().url(),
|
||||
}),
|
||||
execute: async ({ url }) => {
|
||||
const res = await fetch("https://api.tavily.com/extract", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: process.env.TAVILY_API_KEY,
|
||||
urls: [url],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const content = data.results?.[0]?.raw_content ?? "";
|
||||
return { url, content: content.slice(0, 5000) };
|
||||
},
|
||||
});
|
||||
|
||||
export const saveReport = tool({
|
||||
description:
|
||||
"Sla een onderzoeksrapport op in de database. " +
|
||||
"Alleen aanroepen als het rapport compleet is.",
|
||||
inputSchema: z.object({
|
||||
query: z.string(),
|
||||
summary: z.string().min(100),
|
||||
sources: z.array(z.object({
|
||||
url: z.string().url(),
|
||||
title: z.string(),
|
||||
})),
|
||||
}),
|
||||
execute: async ({ query, summary, sources }) => {
|
||||
const { data, error } = await supabase
|
||||
.from("research_reports")
|
||||
.insert({ query, summary, sources })
|
||||
.select()
|
||||
.single();
|
||||
if (error) return { error: error.message };
|
||||
return { reportId: data.id, success: true };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 5 — Agent definiëren
|
||||
|
||||
`lib/agent.ts`:
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, stepCountIs } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { webSearch, readPage, saveReport } from "./tools";
|
||||
|
||||
export const researchAgent = new ToolLoopAgent({
|
||||
model: openai("gpt-4o-mini"),
|
||||
system: `Je bent een research-assistent. Werk als volgt:
|
||||
1. Begin met een kort plan (3-5 bullets).
|
||||
2. Gebruik webSearch voor oriëntatie.
|
||||
3. Gebruik readPage voor de 2-3 meest relevante URLs.
|
||||
4. Schrijf een samenvatting van min 200 woorden.
|
||||
5. Sla op met saveReport. Inclusief alle gebruikte sources.
|
||||
|
||||
Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
|
||||
tools: { webSearch, readPage, saveReport },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 6 — API route
|
||||
|
||||
`app/api/research/route.ts`:
|
||||
|
||||
```typescript
|
||||
import { researchAgent } from "@/lib/agent";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { query } = await req.json();
|
||||
|
||||
const result = await researchAgent.generate({ prompt: query });
|
||||
|
||||
return Response.json({
|
||||
text: result.text,
|
||||
steps: result.steps.map((s, i) => ({
|
||||
step: i,
|
||||
toolCalls: s.toolCalls?.map((tc) => ({
|
||||
tool: tc.toolName,
|
||||
input: tc.input,
|
||||
})),
|
||||
hasText: !!s.text,
|
||||
})),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 7 — Test in terminal
|
||||
|
||||
In één terminal:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
In een tweede terminal:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/research \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
Verwacht: JSON met `text` (eindrapport) en `steps` (welke tools werden aangeroepen in welke volgorde).
|
||||
|
||||
---
|
||||
|
||||
## Eisen
|
||||
|
||||
- [ ] Repo werkt — `pnpm dev` zonder errors
|
||||
- [ ] `.env.local` met alle 4 keys + in `.gitignore`
|
||||
- [ ] Supabase tabel aangemaakt
|
||||
- [ ] Alle 3 tools werken
|
||||
- [ ] Agent draait 1 succesvolle query
|
||||
- [ ] Je kunt in steps zien welke tools werden aangeroepen
|
||||
- [ ] Eindrapport staat in Supabase (`research_reports` tabel)
|
||||
|
||||
---
|
||||
|
||||
## Tijdsindeling (30 min)
|
||||
|
||||
| Stap | Tijd |
|
||||
|------|------|
|
||||
| 1 — Next.js setup | 5 min |
|
||||
| 2-3 — Env + Supabase | 5 min |
|
||||
| 4 — Tools | 10 min |
|
||||
| 5-6 — Agent + route | 5 min |
|
||||
| 7 — Test + debug | 5 min |
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende problemen
|
||||
|
||||
| Symptoom | Oplossing |
|
||||
|----------|-----------|
|
||||
| `TAVILY_API_KEY undefined` | Restart `pnpm dev` na env-edit |
|
||||
| Tavily 401 | Key fout — kopieer opnieuw |
|
||||
| Supabase insert faalt | RLS policy check (`alter table ... enable rls; create policy "demo open" ...`) |
|
||||
| Agent stopt te vroeg | `stepCountIs(10)` → `stepCountIs(20)` |
|
||||
| Steps array leeg | Check je `result.steps`-mapping — niet `result.text.steps` |
|
||||
| `gpt-4o-mini` does not exist | Check OpenAI key heeft GPT-4 access — anders `gpt-4o` |
|
||||
|
||||
---
|
||||
|
||||
## Klaar? Verder met huiswerk
|
||||
|
||||
Het huiswerk bouwt voort op deze setup:
|
||||
|
||||
- Extra tools (listReports, eigen tool)
|
||||
- `prepareStep` toevoegen
|
||||
- Custom stop-conditie
|
||||
- `AGENT.md` documentatie
|
||||
|
||||
Zie `Les14-Huiswerk.md`.
|
||||
194
Les14-Agents/Les14-Lesopdracht.pdf
Normal file
194
Les14-Agents/Les14-Lesopdracht.pdf
Normal 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:20260607091327+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607091327+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesopdracht) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2309
|
||||
>>
|
||||
stream
|
||||
Gb"/'D/\/u')p0`0it#uP=DV/jkI(+JLIMPo@7fM>4Gl!!sVJ:Ct]T&OHa&JfAEn[`MMYigr5mp)'\hBR@2m!B1=D-j!1#]A5;c79&ne3#t`Ul,Wt5:FI0G6c528X5_bk+bZ3GX,dGEJIoN"#3V`[`+rL\b4Cfr6aA[[>'e,;J(PX-0;Uag\NZ])A4B*@V-2^"3\(8/BO:n9Njp+2Z\\?Ng)>,0hFP9%*p)d\0+ZcBE<7bVbpn:L43Gn/338C3W(8n`9cAEg@:N`Y#Z^QoM$u*"'RT*9jeTs^"l:[Z_Mn..!=#<(<l"_MZJF?'jaiFkAnTT*Zoa-[g0e;9*H9XSfo9dGrlQc+G"W#0Sft\=RN-lH)n_g9+!S5L90D+"ik*2PO1!'MR1,shM\^Ing8t<U=KZgmo$a\FX"j@EE6p6tM;*1fpf(UH8?oW3g@=8ZqB\Wqf*"Fh]I!>1oLZsL73bJq_RL,gd$elu_`GHSb@5n18[S%EuDQ&;ubE-__I:[8^;)bNlf(U$9qaPLNhnq#a\&/,WgC9OrHm+`YL;=IM&KtI]@:h]?1lQp:N5,Q)*(tGe1[njA\HlL*;@"!t%jUDZDX4h8='cDjL+5SY(%-_$&W<G=8c\ZPf?4?iXE7\oB;>59nO5[)eg7&Nb%:QcJ,hJa,_H(7:?]UO%sU=tMo2%CeQtKihC'q%Am;D#jJmA0Ch`q$q#l^h/2/$sJMd#/FVnK`]Vb<tWsF/b9ReP#=N>l\OOhCe3VZYnn`t3u5&S><_)JH7&Zoc]XE@9P5"`,jWZJJ(Wsgtdj*Fik_1WU&2I_SSFW\]&X_E)Fds4A<9c68Br`g4!3naS%`Gl3!5?k#8PqF_'RFWa^:,.2jRu\Lloj7%_89G7o^Z!^dK'2h@DPtlHIn/[%_[_Rdj98CR$m6lX\3!Bs0^JhT3'X!Jj<ptfP71<ePF@Xc?'sD`W"tSLSn]3b%")-Y;lt-DgB?elG(j)K[gX!t('Go;UZ4MR>!0)#Yp&aW#rbB]*^5=(2kg34NLgHIdD'$emej$Tfe$E1`$<7+B-8E@>fi]uX*1Q_%+HAmd8K4mkl^>umd_sVpV==M8!s$`G(a=uZc7!D4E&[`NB:93[O,uhB=]F\_B8;k6K]7tRZm80D>H/h(5H^\Fc$t%l+_"W?=D-RGXLEa3!E8Q8$:_b.U2TUcCq:boOY#FlSg?7h?(d;1,hN#$u&:q<s?#qcL=ianR@%e2lY*Udpp3%'FRb?h&?`n%$9s&ok9?G_EF.L"^Z_"!GSlgmJE!POeTCa#(9BlZV9osjKQ[3bt4G>%kl#B:$pDR+nf/Shd[o$2KO0J<ZMJ\>eOd1^o4Ji]EdH0'N70i<dS_Llk]>-$nUk)MTqk\1p2Q'9\kiA3N,4dEspp;$71X&h8@s3M0>@IT*,"gkWgG=7qLUMf%)?epkKchM,\\+$"+-ef:N!u1;'>];=K+b4<`5pM(lS$cf"TPiloPk<j(4.&4Mc^oS+.W@fD*We,-I7GW'e,K<2.e)0E)f`$]O.8k>+S#mNHER`D>aMAr+7L93E!Gn.EaW2Ver=NI7=%p3WUWGc_F<u,Wp^j2+aRdBLiKt`;T2dk/d'FFQV$"L=6quR'A_?=^2SR4QoYcYIM]tl4%pK'lUcKZgl9XF&M_0tb:;H-jGSX+8d<`Gtf0?""7fdpQ'0us8!c64#RUDhJ=31i@H+"t,E?Zl:%#P?jUnX//dbGlt_T(liL6HA-WHX;'H]I(C5?GfR&"U:GMfLto.9)"#UlD)CKmn+1tjpK<lL#@JOq;4DR$0uj0)Sk-fKH!tpk*gplKso1UjG\FJ*;6>]SAC1V!(B<6Hc$ed_#1VORk)5h4;?VI?A#rs:psg1,#i;2bUj[@ZA`QICBM&WG3c7fp@V24E%,NsNo/q9X"+P'B?`T!3B*.P.4KCECW^X),Fm?>/2h)/AlX8$FS?g7$J%XR]2H)I7&*V-^!KbWVMj.bi\\86!KU<<^is+DW\PI:dLqO]^*lSZcdK=t+5W#bs7!6DM5)f7VN%Zf[\<!(CPOi+NVW*\7WD%=j)e8F0#e&m)k&_)N8@T@@NVd[7d&<3c7D:(BCqbQ?C'O)QQS9@'&7YZR?g9ZDQ2hT0%9S`BD5I!eh?r?jVQ7g\#+ad=b6DMV$)Z*L6=P[c&5L"j65%l(IGh%p)_-[^(]j+'C:MQmCgWe1$Zd=5IUUgV7IKu`h#Jle&gRomU,qhqD,[3*+=gJpU:)X4l652DKk:Bn\M.%ah,[2p@5%FGCMWRlUCL<GRV(s]qJPV3dKQg4PBV9f4_1fs0]#m7u*W7!XlsbMZ~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 626
|
||||
>>
|
||||
stream
|
||||
Gar>C?#SFN'Re<2\1a+.,[B*ok9Jg2%1Q/aV;3V$64dBm'bZg#r;Kt`J^BQ5k9dp`a6+EH'MfL9fcK'8Kt@QoN'mFO9Fq6h%$EID(D3g[((s/SBP][?)uBa&D,c*d6s4CF(oiG?!eb"#(i+Y3T@Q\'mGNC,0d'XsD$1EfUb_585:28/>Zq]<Pl9=no.uu0-)tU51qO.3E!GVeF[h>oThK2-koOa*O"XH,<Gc$*mAd1:kI,QeY(\g[2,'n-8^l`plSe7M*F2M_p9&5H62CWHaKK`;0=W.[$N1iljr1$+3)kZ-]fJM6<nk8l!PcEr6=$e9.Vjj&<1k?.qe#6"%GK@DE2Z(R6<<.P0,\s#6"ng59VK6'-'>Gc0(k:U:eJf`!gsi@PsoK,!$_5hNpdkZi_]9'pe"=6e0ANa8Gl3Z$/LdLSf-nQcNq\W`g*$o]jN1gk#,=*K]03&1IVCu'>#t7>m+gPXE-F5Q%lC2F&GekU'^TPW-D_hhV.kF6F?2&0Pel7&D#t=lWA)=V(GZ%@%:S$K_`9qE.YQ?TQ9q>8EQ-#EjD&bIj_F]2t/UpjtKU1VTZsMrG;=Y3U/B)Y7S6ap!M=M-Jpdb(\u,!Q/kk7Hq^-K$.45*R+\)~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2004
|
||||
>>
|
||||
stream
|
||||
Gb"/&=``=U&:W67f]jc+lG*B*n5(K,SMZbGof-ikAD0Kj%8FYS*F9n_3(bOt]aKpR.-G"0P.m/E8?ID\Qb2,r?pO7;g&FVRn8J_i:Dbbqdn=)##ah4!j6>e#LiAsk63?[Dl9u[`jI$D@X&/oG-[ruY"sFFH(^LZr,@MFC^i<]mYQ/DAQZkq.8Dqr>'L4u3rQtRX^IN[s;7.OppS6L?48V?S?:=_:3]+2A75.u_\sIH9Sfj9>5f,@5^!in*O5ZGr8i=_r/2MOJCR$G.bW$-kkkO"3e"ht($i!l<;-@&`WmLVs]nYcWUT9/'\0*mH_(P_DC=%@D>?NQCfKl.K=(r8!f>IG*rON%$H"UqY6fNAJ^j(7Skp$_0'7.M353*q@8c.^^fn]ld;A&V54R?)T_?-A(-$$==)*&QuY`LRAj[9]`M^18uO0rbn!?4=V#&^$=&]8P?p.O\$W7-ogkgkV"5<'n"c0dH8Dk;oR,KhV/dt[nlM;aLAYYW,r,/(388pJFe#7PUc:kglEU)+W_=uY?Qp[ETh*teK,^>MDb#e@R"J$.f=%C]:r?WHVbgaLP>L!#,U+[ndIpO#U>7M-QFaPC'0=@-EL&`0P57j85m%!8dk,XQ6(DFH0PI6n)HqN>g(-5ef]PDi<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!QPbWJc~>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$I]5"MK5D%#CRr&P$EUk_"n`1g4K-JSS[6\,kS$#53fF@o@[Fg"_!&6>g\9V>DoaeEe/cU(gPd:/(D8K4N[t?PhTT!;&8uK:cC:+>D=,nP].0#&S-n-Jqi+,Q9$4cf!./V/`79lBWu1WgGuOf2[\_9KH[Om3h*oI,MPs&39P`HWQUs@+B8B&3fF2M\FDNF_.Xo@G%dLAM@X!qC]Ai@rQj,!3cut-G=0R41DcdS#"laT;f8fSObbg]"/h@h)V*>seP01["*'H)#!1V4eBH1XhsPEn(5[_gnW9:[$"/>/XHV'X76ZFQF"($E_0(m;C6@.d:nKG_[ELq9*+$B@Kq]ifaZtg#-XILEFc7S!=6;VOW_?1H.k*Z0<3a8-@AQn<W/OLBX8.!h5gWt1\=4QK`UH=PM<c#D7`?mQVBDqo4GG`)`ITC$_ba&iNhchq^<i.R_Q(a31,3aI9Y?mV7n[@7C3k+QbCg^XSYW_?0![ie@rT>Sd:=qq5b:=YFeJdjAX:::X+0j6Ho>)1*g.r@`J4C*4$`%F$K#7`CQTPuFWSkH9/T%,=dEb792"a4a/[3NE&SJ]"b[)%1VjU@icY[DT>,cYVCK-h*UY`K&a"G0M#q)8/j:]e.s2H)WZiq--F;_k-`:l9NmCHQrcn9_+MGk8c.'4'o0T<oq<r9I5N%RbKBigXTAG"+ebk5ZHIK>ukjm3[]6F@]@G"=F/YnM794</ZbDC*2!BB$Mh[RFC[(%7u!A,oi]N&^H,P$l/59a9[\S*?1*PlDG&AiQ]i,[\pd)rG3%eW#S['?Lq%oQhCWVMoV:^JPX>BV+ha.q<;niFQ_+1@r8]i4BHogjb1e"aUqYEIN&Q)[X1#Ke-$GhqjomscI0*a1#L,rXl28%E#6eB8p\?;KkJYI2mNf08itf32hu7f-<f[J]".VJV&>p"LVWJbj#uS9*uOUOPZ0bDFETDL[Vj;d##fOlQ!/[6UG8Ghh"P:a4j8'bp\4Bn`Nu,sI\^h:tp(+7&&q!Y=dGV=a/`cL.Uf!7`RBj=U_[&j3dnh>I.a2<#?$NT\'qP?LUYPD:f0=0(<#f8---^=QhXQrNHSX'J$2_eRh<>@\gpgY#WVHYto&bEJ6k8R(0ZhLN>(eRj/VX-I5t9'\g]>piq.Wg"T9V=k[/EiFs*Y>M5u9'UYHU$Ks-NTLImC+s*CH)7kc=T"aONNe[?lUJfVWiu_+oa_0d\O9?\WaY]b-*7X*AfquJJ*ajs82l#LbY@J"nEO-AJ$Q9nM!_lj$:_4RU&RhY#3RY/]A6r~>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"k!YR8@)=!46Y:B*@2W:!S0IPf+q'NL9+/cEF%j>5/%nVa2Qsb=%FKClYqp7O54"+E1]rVe6n9:AsXXaQ[H=&[<k6<>Jg]C(b;;kV#^5n*d$$eX7p]=g4^(E[a24rn6=In8NS'K=U?,&Z\\.J,M=()g_b8W3ZFusX7E_ps6N0W5?._WU7)]4UlA6g(#po#`-%?CWjKl6?3mULI/@u80.ZsNn?;?c-QWZ"G5(K%Xj4^tWbdYP"Y4X8-.+AX[uDKk-5iil):5N=b'&I7Dnk?fBVi,:LM>S^-,E.mS]u7jQ.TQ[BB`/q_XiR":g)<kUZ`.FDYI;c@TA'5l%I[FDB7^5FjkPMYp0r7&,&ZPA<;HOl46rq]-h^sq8#''9(<7g>8,(6.&T`JcFL#8FUYhrK^)pe1A$hMHqerAg=n)-;>!-"SniD]pSNGQZP$OLW[uN/$8EY(B,p6+;BfdNASr,tZS]/\)&>3%:=:u"MWG+_,\><M?"/j&p]BW(\qL5,*B^o[1pt\(.H4dIp7]sZ,Wb3uIb2uS=C'5;j\\QU@Vr&JCi@PNWR6"$3Pn@i=#fOTEn8k)NN%*.[`H[e?cArU.B68sNI56&a"\Zb9uVL]%#&Cn=QE:XD!FcB%#MOJ&*S')H%^aaS1.K/%Ubd/9K[5;\""G>1HQ?9FGh/eYWP#d_86d#peiqN`@"@AFq,Vn2K2^f1FD?':N_*c@&q.kP@&p%$b/sl[@N?q@5sEu=qbKNSf57[?u'NPb;(tYX:n]2go1Nk]/V^W=<<V;<9b&B]p^u0n>A:iGL9uoDrd5HOQI-1ei#9.G_(eOdT2F`j0_<S_lX@:WJfh3Q-7fblbj]dqrX[k_8dGPH3.`%M(].)L7:<:33QdfBY;Nl[C&l;#"/U<?J0^FK/QdX)1e(S&_2+STfn'h2:b,0iP*YF*%,1-.jB%pR>VNUCBouMOm_eeN&fhlUu]k9?2;>^de2pqjdjNn77WLHo*Nms?tC@L^<m]K0W^CkWq0V.c7JNmRuQ"hML68??.UC:bIMA&\&+NiVr:-b3=bXYg9j(Pe*R%Q,kKuf>>?"UD6ET=`;-]$Ho-#-$Kfofnl&oo8:BV"#^n!<IJ*FRnoSNh==mp2\m"lH9r514VR'se:@;7rnpUXua^33i'NkBr^tRBGSJ[f<>5<N`eNNS',>rXR.1+s\Q\77h'6sK@FKq@sk%YBEbgd1V^#^Drl'$Zb_YI?XT$Mqbo2"SoA'd.q.PK*uE`&XSEj@nJa4(gq6>@=k!Sef0"TOlp>D$Al15oOOk:M7)CBK!-Drm3&]?:ir;NW+ads*bdWM)!Ir3>B/'!4;Ajia$=)eu#YTY."V%\\1n>Ff2]-cHZcR(UJS_C2TN.0S!!cQO4cZ_qg.KB9N=$u^N(<KQY1\!E9gn&]9>irJ`0F0K'Vr";F4qNteT>HLn$,D:i@VEr0&)84^Y,,[W4dD_hPhLc'PI56">]\Lj"/S-\Db'OJ+Dt\hT+((G$63~>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=-()RJoFdNX7VHPNoC.IP&uk4/a3pe)T3]L4^.=%Xpm+Tf>9RJ'%<WdqVsl:)^ZInR_fgu6jC+kFkac"K0+`a+aYmqlW&Nja6%f+eZA;#(5=bA18[XN+;`Y?(::GC<ip'>BE14'f\D5d_mc^o8R"Ngjl.5dP9Rr#<lc2i<MQJ&Dd4/k>j5&Grk4XZI`_Z!M6'(?k;Gd9Q].ELOKL#a=h7?$r'FLsk.B4dICl<D&q6Iei5B(W`pO34ZbfAOWBt$pncFC[$C+$s'B)bYfIh#.6>%KAhplC1HYGr\qU-jA[;<J8RL-4s'gCqD$(G=K<l)g)Wdu&B?E52"K+CnNn4LsH&<9\#HOC2j+fRrt5]'(X%Y?`iLhtZ;7LM"t8YgVu1(:3]6e0*dDO?kR$A?.W0QX$@6+"V)R[+F9fh"[q.;G)BcL,jmE1,XMetq\Ab9\cRghs=Lo4a^a0N!`o04Q1_[f:Y,oDd`[945msb5ZrEgCA4jD2H!1D/DD5Ca'U6LXh;d$!N..I/D:%^P1$p)!uQb3>R9:Ct#A`J3V@H)#Xaga(Jnl.Z/=tD_Fe30>'pJ@c*uGmWiNNQ`458R=>AV([G:$6eaGS@rblf#8k./S``RL@*9IJQ`8[D029@"pPL)MpTF*5hAIT\d1*\M2C'[>jKG,D\]U4"qlo[I=2:0\3lZ]):Fc"rBbjt"Y[?\HM4HJpjLVX:b3`PZrcp9?Xe.!AJ&Cs(:mtK5D1hRp9kmW6d3V=:O^RogA[f5sn&(X$j*)'6!_RpqQ.>H]6@CJ$=@6O6/p:5+@lI34,V)\;(#Qnr)l39J`*3u%'GNNAjD_0*$)2`Z2O,R<`HsFB[:58Odfa#b4=.l_/>PigQeL>PKWP^jmPc1"?3Ifr2^CXIc!!m.q\t0RY8a?U=5`K(hV_%;7@W/>0k/_A)SXp?/st[8N)9/;`R%O(ZU\WkWCdhp-aS,8^7/^bh6`eg?]oZ.1/M,>AGJ^j,e/5K7d,([WgOM801ab3apR6aTu*'N[ERT">qj>V."Z0'AEj1YZ0,suc]6VPoK8V@hBstslKDY0WQJ]2H*cK2[4HZOC3@01V:L4kQe@Z%fq"X><QLEZ;lJaL3/+4h:]Rpg0.:dB550JjA%t>fbJE*;mJ%Gi:<2Ztg:^?;S&aWFXR+n*hl*D$Dee#;BtQlg^ImB"h'@U[HX<[sPD]+%^>Cc!R*<G7$?a+E+Q$i8Z-f#CT']TK"@g/r%!&LM\4Dd!Q:*^W.nI#-;lHGL\V(9e%>]T,6H$3A[kf"Dd@Lm"p*nT!Z#1"jot_e$Hfs*j@2ToVm+Pf4f\qI)`EK)Bkjl,bhM[WK\ZR#%^arf_0eK.7.os$+bCr5cSM)hCO4,f)o,sXuf&%Jqh$:H1[j3an3'TP%nF>GtnDi7>+(1*pEuDo21*f;JI]BUX@4>pq4,?&T?7eW]&o9OO7Fnh,CE7b[+C'MkCku5Rn5_."178sJ6T"?(X.NdcUk-Cf0hLCjc#5Nq`*uph\/AD#3*.O"V<<OOKPXfCW2E-uO=%IR?\3d(r7NDl<jhJ#>&IXlB`Y`S?=;NU%PXn;FUrhj?pd$"0<eX<Ap*`%X8k6^2mLC3X\LBh,P8:c>=,7m=k?d%[:4*W0SK%C=HKhZN[!gIX$O22Z&4-I>)Yj)MLmg<8YU6]P607s+<NABgZ*[?Sb_2?&'FD,A3:L_ks;/\):2s\%K9[Dl'.@#=J*)JKj"C^6=RocdoQaYi3k4&;4!gL0ncmX/WlHe5)q@?"ak6=nLpB!Y\*~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 722
|
||||
>>
|
||||
stream
|
||||
GarnSgJ6NX'R\fAQkuP%Z,TNYa6?5c<@Ff%Q8YR;`Ci`3]4l>%iW[i8n&],o'K(a6SCfV8cPZo.[i38n"Q%*W3EK#uiWgk`1u^$U`O"GXi>F;u!JGBf+*eE[hd:&`ZO26q-Lr>a.mm`4%;hB72R;fh(PXD-96`Fb.>Z?lctWie!b9J*]J#6,nZT>+N/@PQ:\nl\0+qkpa"k\ja`rT0%^fm@[?)jD"2lMJf:Qq6ED\Bg9367Ai1"L2>rg\c?AR(R`G*rcSYHV^l&gk7.B.?CLjpU4lMn94Y<=JM8;AkoYGbt,i2LV"2VCuX:6p0j+Pc"g@:*Au2-()(8Bq+kjb%nh(gM)g&:9L"Uda/4Q8,Cm%aF4D7)9%2')6m[n%WdaAUnl((tF8IQ-ZS[3tc%Hm/3G1QD\2$jB&mCaIXjB.H"btD&>c'kY/7upENYD"0/Y.B+^8b/>$!3*_.F6GtckP$p!P(BO#L2#!ia-NVsn2.u2&hgUqEOj]:m&_99OC8bf=SQ5TgREmBM?4i..sNqp^>8_UY`V)G(a,rr'XH1s_oYG%f+$9`E'%tj^Jn)V_Hp-?-kf1um;Fko+.%5RcIkfBlq2kXg8-?'sN%X7MroD""XhUgR(*i!GK8QtuUW8T?#=OWG5A#/G9e`A,`F"WnM>$"kpE]OD)3;mf>*K)[?<i>7IeHrDbr5biJX7@&CP;:Nr&)p#Oqk2dS2eIO?;H+!-~>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
|
||||
0000005455 00000 n
|
||||
0000007551 00000 n
|
||||
0000009244 00000 n
|
||||
0000011145 00000 n
|
||||
0000013302 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<03e0fbda8ddfaf289b6c540c3b19f7f6><03e0fbda8ddfaf289b6c540c3b19f7f6>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 13 0 R
|
||||
/Root 12 0 R
|
||||
/Size 22
|
||||
>>
|
||||
startxref
|
||||
14115
|
||||
%%EOF
|
||||
474
Les14-Agents/Les14-Lesstof.md
Normal file
474
Les14-Agents/Les14-Lesstof.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Les 14 — 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 15 — RAG + Embeddings
|
||||
|
||||
---
|
||||
|
||||
## 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 14 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 14)** | 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
|
||||
315
Les14-Agents/Les14-Lesstof.pdf
Normal file
315
Les14-Agents/Les14-Lesstof.pdf
Normal 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:20260607091327+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607091327+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 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 1880
|
||||
>>
|
||||
stream
|
||||
Gatm;>BcPr&:WeDbY-8RgR.7-!rA3h2eTJOS%!gf$N\pm",I#%2Z)fa,VWUf+k5g;a0pZnc$V=BDe'1X?du_Y)8M_%]_XmE170_F3jo.^.sO:fITeWCb]@$n\PIIc^^dmRK5t\fi!NZk/>1o7ch9L,*1ts!+8EWjTBB&^8dUCl('WG+`:G"RIHh/H*:2js?joak[OXbM^VBHffBrgNBY?blRstRY'so:;TlS1#eFd(6B5mS-VsfuARp*ra=t,`R?Zdfm<=RQ=H#Ra`];Va*WauV-j@6#HaCJUAL\]g]J"^]gn/[;;0"SNMIjHUe+7Q&*eY7Wn*%mF;"?O?oJhc'_$._-e346H8jd:tRJ6N*LAX;Ae9p>s[3!a=iKRP\4^@4/K`r[Hc)c4[oijr+XmH9/I4W6S@7VU5NlATjAOYqBR>-5&IL@f/T2\oO!h+7(L?/L+p=fao)ARV$ElMHMbGD;s2:!r_QNRMl[*$MlU&%OLB-9cGJSi[>6<LpC47W\#&*VXH+oOt$Zp:n.R0\:Isok@4GZ'e5e4=f/2VI.S>GkD)S#^ogZYZQ6U"r3(ES9g]FnBaTj]O4jXc%/u+q,b3$D\RdAL#A`)JK6GEm3ChARD[S*+->P+nS>.(!L^$_`\U^S[_d0hEhtO]X'p_.DUU9j+fV%q2JCLeZ@g20\=aK8Qh:`8>/WP[7L8r=L(!=SDS)OVFpl`7rc](]b>"2L@;\Ai@%McWDdVrm4nL5>lI&OtMG8<"T9cE]Es0C"'T#US.[)j6QVUPJC131G,>pWjL&4A;q)o1fVcc[sVha2L!S:s(f'<qapJiDUcQSUBR`/jDOnn\A&PA!%=(8JjW,r?GGad8a'77ZV%e/Us^0R(j.cD\Hh+4EaY"7+A(p2h#+XNPs&lL_5@2!K?7&uT^Y88:CPK[oTX=LtF0_o;+FJh,Op,;'.:@Dml^AtH(gBEAaT!'5Uo$/Yp%iI>NBZ(m[$%.)%`3>GKJ3U/K2\PQn:kq!/7-0JELsi*GG:D>hZ"&Wn4dq*P>jh'h8&dt^7+?ZNZ0e@)Tl+75%n$$8XT:S@E52BG>?LipFSgjpN3j5V$IZ(khMLb]Jee3.!P.k3PCLBi%S5LkN#d'T(]0^C1cM/;,LR`rR3"5FE3d\t1!m@nG05RQWXGaS)cNc6krjJMe$ULF=NOP)VNS;hVqKOa.-UNmrAhKeSqfYH8FiQO,[bPL<*#q;2''GM_PU[p\du]\b]+]#7X*:ID=;jdRVAfW)SCWkj23Bp?og7O19;s-b+!FB+Gt'S"a8M/1)<o!*bSoP/>^2:6i*Cq'(@Y\bT`=mg<6Mhj>B]UiiQDIojr9+0U9*C2GDS@Ctm\?#GbA%A:+m7)BlCLkb=;1=NYQ<JhI!PCVs;Kd_tm*QmE>T.[GnA:c6)*\GT:oiOm'Qc1Do&6Gt;B?mnFGW\#4aR(?Nmot&Wj_D!?_*9Qo75)\0+ok9uH0[ET+0XRZ]Uf`G?(^)I6<q'^!G'Wj40;A:_WqFW\A>hO[_[hDePAL@[Q!lG+0LU5+Voma_WpVq=dfkQf2\1Bh4=WUdM/L/nn$ej^(S*1J+'G\mbfbC1f(#Megh@JZ9F!I'T0Q?fTDt@P&,-TL5O,"2g)k#2;npg@IY9QoFak;O[=iA78_C6rCrC=0hT:MkbhU-6%UrRr8NK>`3=.,uL"+]'+l67Z[rdAIHFBl"VShFshZsL+nG%e8*01\B-R%\I+io\eg:&(;*ofR^_Sr(_KcHG\U%XsTBk]6.KU[,u(*iHHUD@;?]-G;STHT;V3K7-c'?D/m'r1_HK\0uA_k03<3KDb2BN#s\FrI1_-87MZ-pEtI?L'_ue4,aJqR==^1fpSh^LRD@is<?7@XG'-Rem\!rrK#"5'm~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1564
|
||||
>>
|
||||
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\Gn9GKUd=SQ+3$51(A<_iW*00CiOm-sp-8rQ$r(G*_N:X^Mk^:`64?O'7S/i[3oD`,S&8>Cn<?`fAYUuOFgE:/dY*ZOidOW7$tIpt).O.k(s6i)*lELKZ?FN$5X/)<JUP-;(k0Z:?d\t2%_EDUh-*ZaB/$Vq4Ld-:]EjgSkEL>$WIO`dEQ=8%+^%<r;9B_%4C^JVL:CY-B0KmCPS_3j0L(#Hn.UkT'>BCn(r:7Rm0!7s_LaNcAIW,=OA9:(%"(dRe^NGTn9cRF4^c71Oskt"6;a_A;[om56^/q"e0n;K5ukjsl\m,,-JNDH`O6GVDUHhS;4Z)M,j&<QC_sSs+'")P:"h7o%]R66N*k%O6Mj?fSHUS'_[4i/F'4hk^6"lI)+=kp*32Ddc^+1:3)FN`DrV^Yg0-aif_W7jh"n\QTk"U(O5-[pjX=.(T=XAC?fQNCR0,FM4.'9o\Z1op'!<VFQgjG&23^gDY$^s_3[:@7U3ko=QfkJnP@s'=!!WT&E-:i/!P:a?g_^>5Mie$6[",/j2V873'[:r&.C6sKR?\rA*\bWSkABtK.4%Q=Q'`qeo6^K?Vo!>gUVK:X\FcYZSXNesN.IuF>*d=c1q=+VY\hokej&U?\jQ$JAY]4=3ggk=h3&udEn;8&h\O]<27[=5HI,26<ADD.2!f?E4FCL\0XCj+]#;&__+Yb@-WU=P8n<IW@KUgFJ5X7$;^"$&+UU^nAI_n!P,;"kJpsnA%)LH@H%-BCPauNf9&pL``*6+9&1jZ3F]6Ei<h_e?q\-R?IE>"PmWVpne5i[tPfen+d/-l:@Xb56Od5lkIE,2EP0`YsCL8pP6dd'=PP7u8AOoh*`Z4dN$U5JuXTA'%rF":p$clo8D1pCl0b/C?f&C#<KrFLRE4X][W/oZ4dFO"d+cE5+\uDLT-%R*\KS9sYTGBj=?-d!6".](&9Q9rr?o,@0I4Tc3eJoB+X1nGA.B;fjS0/c1Jm?Bb^/6CR^)Ri:9`8=$Bc9-uZ77qupoD<#'e=Q3*^b(0B_[#+2,9`<?i=<g.'UI_O[9l-d:I&_C7cHU_E?+=!U4'$j"K=TR8ZAFC\;ZW)@;#63`9X?J90l)`YHm2EU_g$aSJg`[1E\H1`!uJgIMcpFcZ9`7fV*gc'q>T[FG*Phr'k#idE?H=E+Qr?EIf@J)Y`Jh4jWoH)$Ekl3B:ghVEE/K@L=.YWi[f-1H]Wr*Q\.=@mjs<RWR_"0SNDHgBoN[s5;=S#/+7^d/+>ItDp_%f~>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%1D:*aH8*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(J&U$~>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@?^_VX#Q-$OHs+A$N`J^eJJd=X6(bSNOuJ7JWep1CKs>mLiLZ*jdCt]/_T(U:W@tlkeDlE_.eI,.lXlJ+9M%EQ$/.PIf<+!8du7G.DCNMP@/q=Vcg!Za!pr=5$F\Qca7(PD+4:XTG#De1p0+8:dj`rim`ZMM*d_\Kjck2FTg*sCF2oT1,e?R.6-'lh>VkdM6X3t'g4d`m`.bGLc*r0-psS,VK\sbX)8SY`gHGHPk1Rp?hk%#*Qf+dd+tD2??t%ts6?H@272/g[)+tI=A="9;6_X;<j=jZ;Y(!?M&J_B3`<ms@%.`5k?l>M2iSs+jE,CIf_-Fr7.#_<G#@fV`;,-h%dNr^'"fRlH2Das(,(KZlDGl'8^mGNh09S<#KbC:\=e<%d3u9pQqf]U7]lFdlBCA-E7Gngk9'49I;._KYhmt@D3FK`J]QX4m'2$fgVcueWTrC3-06I^r&h9guY3:%)m.G3('.;./am!c(',CB;A.?hh@Cf1(#2KVQof'<'Ph(K-Zj3A>/6`h"[t47a2WoF&qTFL[iBugLQ6*&_OG##&.nsdlTMJk9-C\jY<:C(b@X&:rhB$05R-h*Q/utiqI"c-oDr<$9Nh.6YPFV\Ii#otp^F.r\L[[<2N'^(oEl14+YU_tO=rO\ui]J_AUlWql7D,crn7Thdc+[o`ad3Ue!kkL7m2gdP^+edh*9CCg7EOe/>FgTJk]d44qd$.rBJ@TDd?,@Nq9?^Oe]ZNDX6E2T'2_em;-8Pn=j^8-)l)s4?$g'60-&OtU.,N$+bR%XY0C22SH!^?QNi,bTMr>B[stMSFuG<ZI,`WNUCj0Ga7O0ms)n<S,g'4tINYlZ4r@iK&g=XEqbc4H2S@F3PhZU,_(S_p_]"MVTSSlY)RHo?SD+CGFFfu<q66rq'Q'BKF)7V[UF*Dm_I^Hn.8=(/`E:2L5btUU2`enOM=N[Y%m,GSA$XQ]kE%e`,gBE^8aR<l@3\9FJ?RF#=Q2l7)Mk]a-I7Uh8-GF>qk.dqPVk?c7k"M+(&H!^A$dS%H>&fGeHl-IN9=)Q_"Oq7:#9)A9R9@q:i&?0b+Vr&*k1OBjXA-\n[CZb_:O[DpWmLHBNen,P+Kmc;YMYtP>]280'[!'"`1`@+6*,u8VTp"fpJS4fVY&-\&l*#934$8f29!/6rgNJE3mOd-[#X&=#D6TqW$p\AUUKT02-@gm#mEr;,eODHsn`+'8'%hT8q;0DS%L[o@MHT,2ZB[(nKZ"<$^5@?<usBcBJibjTdj.?hrb,a]>huo3DqI^jf%^MO2HMY^:+oWnG*KY.<uQomad'g^fc62sIM$fV[fk*Tc262qn)AqTl2@_G."C+'b/ZG5:[mP+tM^dbFdd?%s2"Xo^VR/jnCa[?B?f)$\?_:4mH':7'd22dLSWZj3A>X<0Q1m]eWI`&!ToU,6#$,QI+Mnj0RBDNfH8gb;L,"#Bj6%^lRDY(:g<<@@74B?bouR"GoB\jOk41RnJ'd!)HRjQ=<Y]-;lnnFLkQ=/IM\\h9/TVVInFFO*/9)\9T2!(mShJc~>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=jcJ;:#(/pl4mlo$@G`&!q8\>CL4iWE%P%2"2,N5Z#<4;,.X7S"g?'G6BtIi@&3Lhf>V1p])XL0g#sXn]OT*rDa1BYtKq)"URt.D^I)$isS:^/#k*2P`l_4O57,GM.)%"s=hUP"'P&OlJkQ3=HDHR*@aQG#>.ds6W1'p=sG]'<`TlON/n_N"U"F(VgS`R+QL(PI%b1eh?X>is]:;p^,ogn\$,KK"=+^s/E-o:XZ8uA8UIt]0r;#%3g$Y#KKj7E3,P\1t21TL'7p0IpO/<p1:R6>c]"?E\-RL=`Gk*g9S+H1p,U&?$XBS1c$e,D0gI_MA*sV%`_nQSNP:"#k&S(LnrV8(PK4ZOuPrO3&LlW:kVH@k:L<4P=D6?n.'!A65YHK251Elq=meFFX$e#$W.kl2:4+4H7U^61U3\gMda]FF#lH5564?R%XL`q#t[,:.NH,[cm8='G-:`MNa``%$l5A(J\M`P46XdGSVe%PIuKQbD<QJihDf2J1OIiF^$!$&VESNlCMR^iGluYNbHpT]AXd:9]<WSnMO4$u\+'&F?&j$Kh#`InR8`ujWnh,&1:pcD`QgA7o^?jjkGq%0B,A!qX+qc_6DA;+11+#Y3E1A?Q@"V)G!EQd<O_^?K]Z6A\-RZ]f>@2%$\)6Ig4FAlXPMaeI,i!m"1#pnh:C,A_ZV1Q_P.ibiOZ%(Gd+.els'qe>^Zc8[)jn!qo>qG-!]Q!$4f<efeSCsgcs&jZfY7fm@S$&f'M1([2<bm.WJK[ZMb%i1*Nog?Sqab1k$W6hQ">;p?VkOXN9,i."*R9N<nKg)r4j_/.\G^leGp>S$nF@g"r(733'at;Z-YoaRK>ThH9Q2H&_4CEP.5RSHXlG6/bFDq(3s*V'^:H:N7p,nc;V`eK9NBO-JWI`nPV!d/VRTB*p_Fo*EM&^+2.Ok(9U+1ll4"FhclZ*qg&M@kb38Jerj':V-cL,8Du)LYYM"kG3%0STBC*8LHmRZ+H1e%44.(#S-=/Enc;o5IXG=ab[_;FX"3f'l,j3G;2Si\O+3<>MLi=VRijMQjl+#(9NA-dldB=Zup0aL;gJ#7p&"26";S(p0d2\"04jo2/TOka.nS/D-"13d%5R(^4)[XLcCu?m*jh!k>ee48u+MpY&!pdh'U&tDW[l1s1\jh9f,eRS6M-H\ZNKWC(IK#cm^XS/`)i27<#0O674HJ4mP>@.N&mG[(0UF^Ra5mNe7IH\)=n@"h..Oa"Vthhf.=h\dU_&be"cdlN-cM<H)'8]^PO_#`RO!^1mS4r\4?,R")lOYi'tI"sJG9.NEDI$h38tH,$,TDXO#;q?Luim6C~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 944
|
||||
>>
|
||||
stream
|
||||
GasIe?#S1G'Re<2\CskCMJ##256fk!`[3$,^fZ*lMGTYND4jTqibkEgs.,X?-7tmfXd\=npY=eM:<\7JQ^.Tu!oR&a7op*9LN!;g&-:`!>__*2O)W4+#_;]=qM#),Idqu*l6R!4+q6I>1fZpo!CAT:[F)dp:FW.`ZS4E3(_A!mdK55:J^j7_I\00VEO6j>PihH6R\)4r)'Wj>8@/.9(p79>VA`E@&BY%h5e)@$gNT9miCuA@Gr/1C3IGq:NFiZTS1?S6IX[mOEUN01?J>]H:R;HY\3>T'+Zq;fD;bl=8V$GVnS`6(oDpVU9.(J@qp@5q7Nf0ZDGejlOQ8:H5kr9:k+`_6@?_XdJi2.Ppd-YF'dc&Y\q,P_PcDZ>C3*0a=u<[a`Xte@TPD*52esV@WA1.S$I;ADNL,mhN@s"g^cCN,U4ts46)aKcmP1H?Z`VRiN4-&:O1&T]ps<u^qqKr6[uiW@b%)7E.KDt?5+"+A32=d?Q>`YIQqLgZP/&2o_R=]o9JZoLE9G[27@K0F(T5@T6HmNAFg-TlQ7h*''Hb!_IiKA/.7uZ0fKH8:NdF[Sn0h"O#7a8cE.T&NmJWRrA^P=E\K?ec5'?r%keV_P--J/0\2^,!R'iAj(F`5&c+6Gc9oP7iV!_=EWHFQB,&>5BI(UuU9D<dI1#sYP;kuVD4TW8Zb8f*N5+J.[;[4nO#Z\7-eH=IfnA?T,&1%t2m!'8u@BTci#)()`e4>*ho$meAX*EPc4=b/WQ(sd\`2.?.&$&97B0_7N!RUciN/sC]j95e\4#G"P`lC$lYLdF<Xg2KD]2K"!2bIchL,;:(Xa,c.mT:GW;TrYJ9bmbl^="<:@NWbnh5'<+I#99l)Rt9Omh=f(Y3SfIc4BWXE'-@B;nR"a@r_<iAs6a";AaeDXqGrL%srci_/A,DG!KuKV)l5Cgj7oW!Vc8\Nr~>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]kpd)`n>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*rWA4b/,f~>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`[V%5DIe`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:Sm~>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>)K80@>!S+<k9r@M?R71@QLk:Q+9#8H`pW2,=G5*ZtC-TBiPEFY4d64lQPnUNI*rE/3%8LP<g1r`NW9+WF;4rt$A3Jl@Ki=/_E=f?ct_KeuY4gr\*3B_q2+@1V8IHE83b(b\Y#GV/HM+7dd)?/hk[`6!",L7%lb&a#Wmj@Zs0<KDfGXJ3/H&P)0.,g&'*uO#6.Sqa.i@$=32;A&jFbChtr.sjGnZ=!;KXuS^s.V(:Ts8(]`5_#@R*n8K_d3SW@sn2qON]#J93#`[+ID4hX^Uq;b\O^a1Uu#?M@IL>PVAV6bg0Y0J@dWBXUB)i:na@Zq9/F,Xt6WMf%CDQgtJItPr>j@ND>CscI2"t%<5hEESd^:@>9VLWbl4RQ.FL.Y%u)\ruUj[Q^#"EYtAXCpC'\^Ir,ngP`E/)n7GI;:Is0'Q$t:UKVI%:)!Ym48>r2%4JKDIM)SD4a<qR4^4Y^k6B!u:TEFA>CaSf`q@SB!ITWjJV+-O1!(MaOXPVLc"!+n[p4G<AmA#"%+`_V!O3'PjX)Z?)M2ho!'W@fYC?+;\_[684G\uau"2+6%1Bb5>bP+'#P,e\da70M[RQc6_^=2QCVQA"X"I;l;qCH<r<gXWBNlPTe`PTYI`+rk=VHCL3FCWb)3/5+jV;=HW(N:H4"c<j`<,;0+e2M?1`Gl&q%B0%OhH5s7=1LWIAi;<`h@s[aqp0!'YFQGuXa-ODkr_.a:S_6%lblJD^c33:\$&Fe&[H$+c;td\f]Gh4/S9q>.8#g;5pJ)cX1*?q=+=Ekbf7"lIfg*XM?K;[4j<)''<FhmUkY.qNo9*,Ze(j7l==Ze*($4!d4^H13HjKf@g-"<DC0m$i<hp[r!c=$Nenr!-D,dTQn0pQ'Q`J6beh7GP+`^=8Q.WFT[<Ee8"j7^,H#^$1>[uK+Xq-g:gRW07&cIk?,Z!TH=IFR3@-gX"2_pJgMN:^'.-ggpaADYjS;fD-CAInQ+l(OQ!Whu\@=OknEZRM3Q.=WY?/u2J9b+&53_(kKs4JOJ\Z?SmJgXF+p)1@KM[Oj*J$HgMoeF#W-#_tC^B%t3,Jaan4Qifd23&><+V)&fJEaDOO0>/KP_+Do/Oo)]ibP\-T5[J,AkKe5NuMgTY=3tcZt/3@^e;^5^t&&4($%=L6If7fX'UbMuCbDMhTl.]PRX#d5'/r/(Xk[E@$FJ3BubQGBK%:l9'I\@(LFTG+oimRM)O-;=EV,U=gW*3F6.Pg;XBKH<G+6)dVfH&c/Z$X"b@S*d=MgfCpuUs+AbQ/+?Q^IWl%1K7/C:HE5_N8\($1p9F_:c?6Kphgdisc5qSPd]QGfS_+_fPjPli-4BLR[SuUo'UphP5FTRj:/9G`d-p/mYh";EquaNQ;s'jhe84GS(FpJDgX/gbpkD`eHVgKhJ4Fap>;5:8GBE_3H2YD"QRUK:$tZTdNj=UL[gV_7>M`7p>OTR]gOk!h")m!7]RKs(B#]B~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1937
|
||||
>>
|
||||
stream
|
||||
GauHK>Ar7S(k(#>peVBOdkqP*8JL9\"1#$t"(YUhDYE(Fati?4O[W9:UeQsN^5oiQZ(kWW`doF7jPfp;4CUSG_lE0E"7X>>%ULkt*+g^/%WPm"$nl8E-];r[4i[_895=>DLi8bIaOXAnXrtUE4p+A>,!?/NabZ1q"HMe1O*Qg9=?X&/&5Q+u3^=s3KbX:4gI-"@><Bb<_S<^si?o'tT884Bd]EH\$tlfaW".@$#\C1%_JB=<J,VSkK%G=No'fWE(d8Y%$;V%sF7-m'dJdRG7sUeon<^^_Ytd5-.J@.m#.(4>Y5u@I97jTs\+,MZNq<?*H(e:sm$hB',lo\\!.'PuD34s>R!M^EgSQ"rj$A`12L7jHNc5,;/FJ\t[Mt,VmL`&33>ge^RH5"B#sK>KgGttXNSt>1N#AR3aN&S?$QZ\#l8]l9Fs'2)m<re*I*2;@Zid&Y/tk^\(,>gP82cAf1Bs/#eu;XqQ2/q75Bpb&Voi=3A<S*fQn_4CAD@BD\@-Kj'f0b>(W'oGhn9Nh7>:1DINRJ+`+;+ZmOe>qEKDGCZA.6jO&rRnMb$,P'U_]q16rS?dOln2NR20@4Zd99JN#&c<(-Ej_d`XO9`sOAG7&iGpMR7gc9pCHJI,KXn(?:&\o&_B`\1<nWD:VrLb`IcTs=gDW.:^r35T#g;3omW-]>-n?!e"Bd->TiRVkpX[B4Vc`+j.GmjtL[%NO3=aA.U&D7W!Eds\_EU^%o"Z&VN^hA\5LgIJKf*X(-!ZKQ+)J5TNf^<r^c"JQ#+osr_s$_-X\R*NI`FT-]c@g5,-q1VjP;&a:Kjrt/*^t,&@@.L'pCk%/Xhe-n,ILd<E*hAi<(tkuq5D+joiud2VoF05<&=8acpB1D<=67$1):Zk^W_<o5Z>Ej-2JK8:.2ZIER[S?9Ra&V*)7b;Te!h%mnT,U=@AF7W)glscB6B;GIU6R)(':]0a[Rp`$qlSh92kR%b_mUS;XpbEA?q"(m"\S6coS;"Yr6kKUd8$0#S@+N]o:OS"4J3+MaqQPn&r\!'p[T(fC-o$O1!(Vr90cqoOOptqp^6aOCVOkAiS):)VEk#4$n2K&_EqTdM.Z"l%COf]@M+%BaU)AK\*-R`c=2F<cHksCcQ_QiPL&BPBhaVLO-iMg!0o+?4:jL*%?.:eVuT1dl3Gp#3T7@P<&p(#X\2KN3)NE*'C)])"%&Qb!Pe[d%5$Z^:0uSh5W#Bn>fC5D8O.;5%bjX$Dk\7IML^2W"\OIH+DT2=`gJqGQEZ4KGb!^(c2*:"Eco=hC^Hc6ma4"0hN0)e9*U%-Q:GVL^r!MZ1$oT9pA%ASrE_q!Y[j\%8^?ILie6nj]<1MAYG^)f*!_V,->EleTadD]<E^D=;<h6h+BY,p,-&Sh0KF[W.'=EMI?ZI@fS.%k]e-"6DsjSWaq!2.fE;48TC!@-h_q+$jR`ZPcQ3P1Z*OC1*-%dmtkf6#A,\)^`N0X`Rn:*iesKg<f]b;\qCC!20=\Y-!?HJO\V@dQmV=]h/ONj-3ds'n81k]52&ksQGraj<,I49>S4Y/_t+,d6(,](+>5JaW:S,YO5Pi)_Tf$'Z&!8K_)>KcTfsuj4AuOV01cEY]"rq6^VbjPKMS,<?u%<Dg9IMeK$,8TQ4^sE0oAhfHIqhGiQ!uUBTB/CHou,he1(<9NBD,CN`bVW7+e*tYf>/6[Y\X<hCluM;53QnDN+BHKo?l="udbt*8@<8'5ioOb,;pjankC;KPPdE`/bt*-X(08U+faAnRJoSounL5p]b#BbK#]jF67a08`GZ/bO@BsgZog/hFsQBA:IH/f4p*%lT>>P7R`lg.S@piO#8VgmHMqQ^YS2pc$u@c0FHef2Ze$DI\V6.o+0DU/^NsWb8To\<f=&<cL8D$hsn`_FN8TNj);W4hL`cfJU4e%GhOGlL3"T)iDf-nZ?j#JqLeH*fG1N)~>endstream
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1859
|
||||
>>
|
||||
stream
|
||||
Gb!Sl=`Y6:&:U6Yd4\H&eeTLTV0U9#JVD>M!AA`?Ml`'.M8tdI,6<8l[cWU2GbMRE@WU0Xj6kLMf;Fo`\G/__=1JA,I0S#);P4G1$_T]R@<Mqn1Zu)3=ND[N!9/"MM]%W/ZTjO)*=Y;*_O/:V\/Q#$q;?<d30-'Re;GLh6*=Vcbb7:rd#GOGK[V()iL[bR6H3tYT*/R>n*7CW\:k`_YXO=$Rm/`a'!ue]\*Ee-V*Pg2C]e<ghgKe[J!d&^jN<hur[r7<=!cWU(;]h4l$6.%7B$oEU$bll[Mu'dV_G`:fe^7kaM.+_K"aV`m9cjpi<-e/V-,Wehhu'G"].68FG3$J/;:/o)S)A_Sf;)n*f[\i@:Zn2LOKLJLBXKam%7i!Mhp]qI.WBf<ludH$JG2Gd)ehVItec(;ef&"O(+?3"6aTK[#sSRF:TVum&i."ihpnnd-V:Qr@668#8A'4oH7B"hMmZ\dTJSQ;;'hRpWm4#ii/t;,eU7?;9l+5(oPTq>[X4M_!@N\;-jNF_S#b0Z6.ZkA&@s,qhtY7`km.3s*TYl"Et$6=E.7gZHV1\Rs_ojbl*ZukfYpC6W'@e7+>`7d!\jTpPmeEaNDH[+V>@AL.$f@hHQh4S[@_S+0`rd8*tISGO(V<LcAFklSkB*hC2;BmT&NVKMaW5?OO!$aq%.O&5*c(ZBtQ$9KlN-91JJi/_iXf&0FDuQE6AGR7R!I""02=9!hWdi$UjXpP:%SJ[s=C7dg>DC\dA%'"36g9E/"'ANE9:gVQZP@I3.@$%T--Rg(ag_I?2t3oX5%Y>ooo^4!=nO;k_7Be!C(a"UtF%pd]%o1m/<[AZ.r>UW4Lh'tPgK<J4M'6Pd`Xni9&.$;[fjCl?i8)kFF6#TsG;+d%NP8N>'NGWQr8n,g&6KAdk<K\D+WQ3Vh^<;^$c)"OCOb./,2u%l\FN-?4O=hC<\mE)ME[24knB)mTgin.M)4D-*1AHZ)FEMhh$6j#!(33&<LBRb5$[b.SVk#T+L\5:Jh_f\'D$mI!0&D1^52;("%eHM=H)(%VgN83B#(;LI:/^EjaGnGj=us9h;03KtmX[[Q"moLj"rf@N=kmVX^O-?#03"qA9rcZOUHk:o?.MXXNh&$@`rbE5/_'uAX#s*SS960_;>#Wp7$Q[Z64P(:H"#pfqT^d"MGe0aos!n-RD;_Fo."%4+>P@Pb.r(Vh]D<V\NeF+DQWC"g?q;H<b6?'S)i9G>O0<RYYc\Te-Bc_+fVI]WjKu$%6bPdT,<J.$H7_IG6V2jM:4dM6LBImU"AN1G;ZH;42sWk?s.0<Q@#irZCUFhs#HKQ?X0170e\Lb[k.7@i?l4(\@gX>-a:iLAuT*3+Aq`FM8uRnh#1-pouU`;SDb/!4]cEX_;PZ9E-fT5Y-pZ--%&YEin('19;/Ujf=61lieYY-)*#efR-+\.6r/Ob/VF#EI!&bCeInlKJJ<f!&q[ZU[NaY\b]0$$.Iq\c0`UB,SKt:f2r*rqojk4kp<4]BR6kDh"IZ)jqiqP(iQrBpc-YY5bie"o(C$JE)1R.rND>B4aPeJA<iNk*E4#6B,@NQ6a(pOCTq*_RMg4K0.^$0)nL_P;h,^1oF_dK[<cJ!G[.D4*B@dq4].q?I)Mo=@nDm='-Md')1)qBZnYS<RrbF^:XO9HbpZPAtBp8-u"UJaBPpmH<^P7e&rkrl@*/_Ph/Jjo@k:ce:3pM>n!I!(%AElg&r>-<-XgqO/U;Fh,]IIn>eC138&'Iob\6e,(psi>ogJ3?lB;pmH"L#7+1iY?P.Et?TB<SZ=Zs81YZHZ:8@YB^nF-G.Dm%gUf@pr>?QDr78eD`ob=0eoJ>bq6p"1Fpl/@lro8f+"I!-T0qo)~>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=Le0EFn3\Z[CW1auGu7HJCJW;)%-g;[kcQLsSCmNt-c4!81#eLSObl4coKu_aDIBr?h*#YdOmP,Wgq2ahl_f-"7%rAM'5X73XmR6:OI-S2Wk4PSF%HA&ClbsLF9ZQ!UhJ]KRTfoE-/0N4:6<q=8M+Rj"ii9eOmBiS)a2MGCMoQ9d][i^T>/puA$S&#j)ohCk);:.)\^g*Jo\OaLs;G9]m6kS)s/5,Hh'NK5I'$Mq7^VL,f.F4-N@R-Z6iQT)i>B20,\@hcH=n)+cMsE)8=p9([2:[f,B!E0L6QR,qu5iR'KArpd+M#h-j[bXo`(R?s)B$2&M8F?0<o/G_?nKGSR<`8eC*KAV0H2!>hJ^"YBaB;C]gLR`:e"r#oKW%(5%?=2dW%ih3,REJKS/Zd^]oDrteE*H=0$D8sWS`Tt;\Y-S4]D.T.A$mm6W%N:0A*tZZb:=nGiHM[c#Tr;:UVGq2K13c$/@ME\8)?Ak3,b6Z4"Y$P`g4:5e*m`NAY0N<G_T?rZY8[W,'$cb-T?F<4KtsL`5RGY:$-:OgE),^B?0[O,jQn,fqB/j0,1d*NMUeB_(aal.`YT&3(&S3s!gatJf:K1D]l1og6Y.3)r_Go$LghO)=1>L[#d,Vi?RS[SoNc*CU-K<J8l'_=WM(sfCRSFXk6s]_,YKj@1`="7;*uR24e0n6UNd#5pdt6B0Oc\Oc`qtn99VgX8kNki\Y^k=e/B,BZj]WUD.s2J#.lkJPA*NUOKh&[<40e_CqdKLP)<9fYcY*u^&7QN5r4K1oL^i?1'OP=V$b!%(%^jpnAG%!Z/WY8lZ#@k"Y\@GLuJ7$_his6m(dn%g@qbtkoR6ngR(LSKrqM)L#Q?9ZYYq+<o9"f625\bCj"RCE]J:s*8?lo:m=t$e+3Kf5kuT&+GC`:3NVMC9X.j/is.+n,e3R-6?:Qa4j_k%7&B(#&)Rhe8r:`/Tc:S.DI#"UD;+^0S.1iS=i*DB71!PAe1bBie]X[<UahJmp?aj4S=`=BY7);?cWRNbP3ejlopgf2oa$BGG?lUTeGY!e,i61#.cD#<j9[AO_6GH0]pE&DbnA77=1h'3e[D;CWS!4-P9U&Em_EF']qf6Rp>rC#gnLrk@oCKZar+$(Sfs/aB:g'dL8*a1\;;iEWVDL[N:j`u)&F$NmdoW^.\Wc+mG!@9&b7h-:ni>gD.5\!`#.s7_.>:IBqsgZ<G&MI4!*3n7kjt&5@]H#/+_NS6i>j$grJ6[A@rT?!_/r/46Jb=`D@Bq\7dfW'"F$X5r4DqS'@=-V]tToq0>O^3>]gt.JN_cE<EEXS?o<<RFqcQ<D?D8!XP]on`"$*DN"_AEjf@MSM;hRqR*o#H\U[)76/EnX7\__,l50J-*1k;;da>*ZY#ZO<LYI*ahBXbp[!HS>M3E_$S3OjDZ~>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+f6(I)&r`g1:B`,?cBr"AB5SDDA+2P2r`C75Y[/.P/SQ`Z9VudDD*=KT3ltdQI55WP]IEeC0%)Y1k@c0I?O6`:%/EV]We]VAZG-qOmoL!Zu`)_4$`t5W97L_!X4*(s]Gc,jg.rcOr<]Y9LrP?=^[X[kVhC4):a3Wp4>6mp9#=;g5Dj]'YPEbp6mWi*6@O>bQROf8S*C@0gZ>r:W;sO>T<hp`Y4/M9c7igfFUp\]lmn]i4pE;17^%jaV;a4l+M[7p=C4ISY.Qd1&.:f8MQ25U<59P#ZjC5jODYp:uo=V/`FHbNCZ4et24>Z,,+O`=+W]Hj+03);JMHKBW_e;unp>POB.Udd3&DR1tQLZ7n@a@H54al6+]e619]2$;"$,]d#VaqR*_8ph[8la+J$KGD^s8\-X/FN\4+>^#M0!q8IE</s:&$<I`6j]T&I=mj%i%TB9\`40IWAh`];\O.@(me;.aET$DDtFe%WARm/N(qZ>NN/5;^qH[fb)a1R<DSoWi#6S==@8brtcboH.FPfIR,rg5R_bBf&aFu*TiTC=\t7il8F\^'S#M&T[tei(^JM9BFBi56m*iOO*dZ1ZR>it<;TC\"oLo=rD-fR2dMo>9UZ;*out_1@-kq?#hE(&3rb~>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
|
||||
0000005714 00000 n
|
||||
0000007370 00000 n
|
||||
0000009099 00000 n
|
||||
0000011035 00000 n
|
||||
0000012767 00000 n
|
||||
0000013802 00000 n
|
||||
0000015509 00000 n
|
||||
0000016342 00000 n
|
||||
0000018215 00000 n
|
||||
0000020244 00000 n
|
||||
0000022195 00000 n
|
||||
0000024011 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<08c2f2200c35dce27e1015bb532ed2da><08c2f2200c35dce27e1015bb532ed2da>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
/Root 19 0 R
|
||||
/Size 35
|
||||
>>
|
||||
startxref
|
||||
25034
|
||||
%%EOF
|
||||
442
Les14-Agents/Les14-Slide-Overzicht.md
Normal file
442
Les14-Agents/Les14-Slide-Overzicht.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Les 14 — 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 13 — Cursor + Vercel deploy
|
||||
**Aansluit op:** Les 15 — RAG + Embeddings
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: Title
|
||||
### Les 14 — Agents
|
||||
|
||||
**Visual:**
|
||||
- Background: CREAM
|
||||
- "Les 14" 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 14: 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 14) |
|
||||
| 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 15): RAG + Embeddings**
|
||||
- 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 14-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
Les14-Agents/Les14-Slides.key
Executable file
BIN
Les14-Agents/Les14-Slides.key
Executable file
Binary file not shown.
BIN
Les14-Agents/Les14-Slides.pdf
Normal file
BIN
Les14-Agents/Les14-Slides.pdf
Normal file
Binary file not shown.
BIN
Les14-Agents/Les14-Slides.pptx
Normal file
BIN
Les14-Agents/Les14-Slides.pptx
Normal file
Binary file not shown.
BIN
Les14-Agents/lu41dctb9.tmp
Normal file
BIN
Les14-Agents/lu41dctb9.tmp
Normal file
Binary file not shown.
Reference in New Issue
Block a user