fix: vercel
This commit is contained in:
1
Les13-Agents/.~lock.Les13-Slides.pdf#
Normal file
1
Les13-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;
|
||||
337
Les13-Agents/Les13-Docenttekst-Autocue.md
Normal file
337
Les13-Agents/Les13-Docenttekst-Autocue.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Les 13 — Docenttekst (Autocue)
|
||||
|
||||
**Fysieke les — letterlijk voorleesbaar. Slide-nummers tussen `[SLIDE N]`.**
|
||||
|
||||
> **Structuur:** ±1 uur presenteren → kwartier pauze → ±1,5 uur "Nu jullie"
|
||||
>
|
||||
> **Voorbereiding — accounts:**
|
||||
> - Supabase ✅ bestaat uit Les 11/12 (polderfest-database)
|
||||
> - OpenAI API key ✅ bestaat uit Les 11
|
||||
> - Vercel ✅ bestaat uit Les 11
|
||||
> - GitHub ✅ bestaat
|
||||
> - Open-Meteo (weer-API) — **GEEN account/key nodig**, gratis voor demos
|
||||
>
|
||||
> **Voorbereiding — Supabase:**
|
||||
> - Tabel `user_favorites` bestaat al uit Les 11/12 — geen nieuwe tabel nodig
|
||||
> - Eventueel tussen demos resetten via `supabase-reset-user-favorites.sql` in de zip
|
||||
>
|
||||
> **Voorbereiding — deploy:**
|
||||
> - polderfest-with-agent gepushed naar GitHub repo
|
||||
> - Vercel project aangemaakt + env vars gezet (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY)
|
||||
> - Productie-URL getest in browser (zowel `/` als `/agent`)
|
||||
> - Test prompt 3 ("zet in mijn favorieten") en check dat er een rij verschijnt in Supabase
|
||||
>
|
||||
> **Voorbereiding — presentatie:**
|
||||
> - Cursor open in Polderfest-with-agent map (voor code-walkthrough)
|
||||
> - Browser tabs: gedeployde agent-URL, Vercel dashboard, github.com
|
||||
> - Drie demo-prompts copy-paste klaar (onderaan dit document)
|
||||
> - Voor uitgebreide handleiding zie `Les13-Docenttekst.md` (normale versie)
|
||||
|
||||
---
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik tool calling
|
||||
|
||||
`[SLIDE 1]`
|
||||
|
||||
Welkom bij Les 13. Vandaag gaan we de Polderfest-app slimmer maken. Niet één tool die data ophaalt, maar een echte agent die meerdere tools achter elkaar kan gebruiken, een eigen plan opstelt, en zelfs een externe API gebruikt om het weer te checken.
|
||||
|
||||
We doen dit op de Polderfest-app die jullie al hebben draaien — geen nieuwe app, geen nieuwe context. Alleen één extra route en een bijbehorende UI-pagina.
|
||||
|
||||
`[SLIDE 2]`
|
||||
|
||||
Even kort terug naar vorige les. Tool calling. De AI kreeg toegang tot functies, in ons geval `searchBands`. Een gebruiker stelde een vraag, de AI riep één keer een functie aan, kreeg data terug, gaf antwoord.
|
||||
|
||||
Dat werkt prima voor simpele vragen. "Welke bands spelen er op zaterdag?" → searchBands met day=zaterdag → antwoord.
|
||||
|
||||
Maar wat als de vraag complex is? Bijvoorbeeld: "Plan een zaterdagavond met indie en techno, geen tijdoverlap, en als het regent alleen onder een dak."
|
||||
|
||||
Eén tool-call is dan niet genoeg. Je hebt nodig: bands per genre voor zaterdag, het dag-schedule om overlap te checken, én het weer. En dat moet allemaal worden gecombineerd tot één eindprogramma.
|
||||
|
||||
Dat is een agent. Eén stap verder dan tool calling.
|
||||
|
||||
`[SLIDE 3]`
|
||||
|
||||
Hier zien jullie het verschil. Maar belangrijk vooraf: er is **geen harde lijn** tussen tool calling en agents. In de Vercel AI SDK v6 gebruik je voor beide dezelfde syntax. Het verschil zit in hoe je het ontwerpt.
|
||||
|
||||
Tool calling. Typisch een vraag van het type "voer deze functie uit". Meestal één tot drie stappen — ook met stopWhen, want stopWhen kunnen we ook in tool calling gebruiken. Jouw prompt stuurt sterk welke volgorde de AI volgt. Per tool gebruik je typisch één bron.
|
||||
|
||||
Agent. Vraag van het type "bereik dit doel, kies zelf hoe". Vaak vier tot tien-plus stappen, en het aantal varieert per vraag. AI plant zelf de volgorde. Combineert meerdere bronnen door elkaar — database én externe API én meer — in één antwoord. En de AI reflecteert: hij ziet een tool-resultaat en beslist wat de volgende stap wordt.
|
||||
|
||||
Beide kunnen externe APIs aanroepen. Beide gebruiken stopWhen. Het verschil zit in mindset en in hoeveel ruimte je de AI geeft.
|
||||
|
||||
In code zie je dat aan twee dingen: een hogere stopWhen-waarde — acht of meer in plaats van twee of drie — en een bredere, opener system prompt waarin je niet stap-voor-stap voorschrijft.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Wat is een agent?
|
||||
|
||||
`[SLIDE 4]`
|
||||
|
||||
Wanneer voegt een agent écht waarde toe. Want zoals ik net zei: het is geen "wel/niet". Er is een schaal.
|
||||
|
||||
Tool calling is genoeg als je weet welke functie nodig is voor de vraag. Bijvoorbeeld een database-zoekopdracht of een API-call. Het antwoord ligt in één bron. Het pad is voorspelbaar — je kunt het in twee of drie tool-calls zelf beschrijven.
|
||||
|
||||
Een agent is écht zinvol als de vraag open is. "Plan een dag." "Vergelijk twee opties." "Vind het beste alternatief." "Concludeer hieruit." Dan moet de AI meerdere bronnen combineren, en het pad is onvoorspelbaar. Soms drie stappen, soms zeven. En soms triggert het eerste resultaat een vervolgvraag — "die band is uitverkocht, zoek een alternatief".
|
||||
|
||||
Risico's van agents — waarom niet altijd. Ze lopen langer. Meer stappen betekent meer tokens en hogere kosten en hogere latency. Ze zijn lastiger te debuggen door non-determinisme: twee dezelfde vragen kunnen verschillende paden bewandelen. En voor simpele vragen is een agent gewoon overkill — tool calling is sneller én goedkoper.
|
||||
|
||||
Pragmatisch advies dat ik vaak geef: start met tool calling. Pas als je merkt dat je system prompt steeds langer wordt om stappen voor te schrijven, schaal je naar agent-niveau. Want dan kan de AI dat beter zelf bepalen dan dat jij het in tekst probeert te beschrijven.
|
||||
|
||||
`[SLIDE 5]`
|
||||
|
||||
Hier zien jullie de agent-loop. Lees mee.
|
||||
|
||||
Gebruiker stelt een vraag. Die gaat naar de LLM. De LLM kiest: een tool aanroepen, of direct antwoorden? Bij een tool: voer 'm uit, resultaat terug naar de LLM, en de loop herhaalt.
|
||||
|
||||
Hij stopt wanneer de stopWhen-conditie waar is. In ons geval `stepCountIs(8)` — maximaal acht stappen. Of als de LLM zelf besluit "geen tool meer nodig" en gewoon een antwoord geeft.
|
||||
|
||||
Soms duurt het twee stappen. Soms zes. De agent beslist.
|
||||
|
||||
`[SLIDE 6]`
|
||||
|
||||
Welke stappen nemen we om bij een eindproduct te komen voor onze Polderfest-agent.
|
||||
|
||||
Eén: bedenk welke tools de agent nodig heeft. Voor ons: searchBands om bands op te zoeken in Supabase. getStageSchedule om een complete dag op te halen. En getWeather — een externe API call naar Open-Meteo voor het weer.
|
||||
|
||||
Twee: schrijf de system prompt. Vertel de agent wat hij is, welke tools beschikbaar zijn, en welke werkwijze hij moet volgen.
|
||||
|
||||
Drie: voeg stopWhen toe. Veiligheid — maximaal acht stappen.
|
||||
|
||||
Vier: bouw een UI die de tool-calls laat zien. Anders is het magic zonder zicht — voor onderwijs cruciaal.
|
||||
|
||||
Vijf: testen met echte vragen.
|
||||
|
||||
Dit gaan we zo live zien.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Nieuwe AI SDK functies voor agents
|
||||
|
||||
`[SLIDE 7]`
|
||||
|
||||
De eerste nieuwe functie kennen jullie al: `tool`. Dezelfde syntax als Les 12, maar essentieel.
|
||||
|
||||
Drie velden. `description` — wat de AI hierover leest om te beslissen of-ie deze tool wil gebruiken. `inputSchema` — een Zod-schema dat de AI automatisch invult. En `execute` — de functie die draait als de tool wordt aangeroepen.
|
||||
|
||||
Voor searchBands hebben we genre en day als optionele parameters. De AI bepaalt zelf welke combinatie hij nodig heeft.
|
||||
|
||||
`[SLIDE 8]`
|
||||
|
||||
Dit is de kern. `stopWhen` is wat van tool calling een agent maakt.
|
||||
|
||||
Bekijk het code-blok. We voegen één regel toe: `stopWhen: stepCountIs(8)`. Maximaal acht stappen.
|
||||
|
||||
Zonder die regel doet de AI één tool-call en stopt. Met die regel mag-ie blijven doorlopen tot acht stappen zijn gehaald of tot-ie zelf besluit klaar te zijn.
|
||||
|
||||
Andere stop-condities die je kunt combineren. `hasToolCall("finishPlan")` — stop als een specifieke tool is aangeroepen. Dat is handig als je de agent dwingt eerst een eindplan te maken voor-ie stopt. Of eigen functies die naar de output kijken.
|
||||
|
||||
`[SLIDE 9]`
|
||||
|
||||
Het mooiste van tools: ze hoeven niet alleen je eigen database te zijn. Externe APIs werken net zo.
|
||||
|
||||
Belangrijk om eerlijk te zijn: dit kon ook al in Les 12 met tool calling. De execute-functie is gewoon JavaScript — een fetch is altijd al mogelijk geweest. Dit is geen nieuwe agent-feature.
|
||||
|
||||
Wat agents wel échte meerwaarde geeft: omdat de loop meerdere stappen mag duren, kan de agent het resultaat van een externe API gebruiken om vervolgens een tweede tool aan te roepen. Dus eerst weer ophalen, dan op basis van dat weer bands zoeken. Die combinatie is wat agents onderscheidt.
|
||||
|
||||
Bekijk de getWeather-tool. Beschrijving: weer-forecast voor een festivaldag. Schema: één datum-string. Execute: een fetch naar Open-Meteo.
|
||||
|
||||
Open-Meteo is een gratis weer-API. Geen account nodig, geen API-key nodig. Perfect voor demos.
|
||||
|
||||
Vuistregel: wat een fetch kan, kan een tool. Stripe-saldo opvragen. GitHub-issues lezen. Wikipedia. Spotify. Alles via HTTP — alles kan een tool zijn.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 4 — Drie agent-extra's bovenop Les 12
|
||||
|
||||
`[SLIDE 10]`
|
||||
|
||||
Drie features die we vandaag bovenop Les 12 zetten. Dit is waar het echt agent-werk wordt.
|
||||
|
||||
**Eén: prepareStep.** Hiermee wissel je per stap van model. Stap nul — dat is de eerste stap, het begin van de loop — krijgt het krachtige model gpt-5.2. Dat doet de planning. Latere stappen, waarin de agent alleen tool-calls uitvoert, gebruiken het lichtere gpt-5-mini. Resultaat: zelfde kwaliteit, significant lagere kosten en latency.
|
||||
|
||||
**Twee: hasToolCall plus finishPlan.** We voegen een speciale tool toe genaamd `finishPlan`. Die heeft een Zod-schema voor een gestructureerd eindplan — een lijst van items met tijd, band, stage en reden. En we voegen aan onze stopWhen-conditie toe: stop zodra de agent finishPlan heeft aangeroepen. Voordeel: gegarandeerd een nette eind-structuur, geen vrije-vorm-tekst die je moet parsen.
|
||||
|
||||
**Drie: een write-tool. `addToFavorites`.** Tot nu toe lazen al onze tools alleen data uit Supabase. Met deze tool kan de agent een band toevoegen aan een `favorites`-tabel. Dat is een gigantische stap — de agent verandert nu de wereld, niet alleen leest hij. Concreet: gebruiker zegt "Zet Band 5 in mijn favorieten", agent roept addToFavorites aan, er staat een rij in Supabase.
|
||||
|
||||
Combinatie: agent plant met het krachtige model in stap 0, voert uit met het goedkope model in stap 1-plus, schrijft favorieten naar de DB als de gebruiker dat vraagt, en sluit af met een gestructureerd plan via finishPlan.
|
||||
|
||||
Dit is écht agent-werk — niet zomaar tool calling met meer stappen.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 5 — Roadmap + code-walkthrough
|
||||
|
||||
`[SLIDE 11]`
|
||||
|
||||
Dit is de roadmap. Twee kolommen. Links de dingen die we al hebben uit Les 11 en 12 — het tool-pattern, stopWhen, de Supabase-client, en de bestaande tabellen bands en user_favorites. Daar gaan we vandaag niet opnieuw doorheen.
|
||||
|
||||
Rechts wat nieuw is. Zes echte agent-features: getWeather voor de externe API, addToFavorites als write-tool, finishPlan voor gestructureerde output, hasToolCall als tweede stop-conditie, prepareStep voor model per stap, en de UI met tool-call rendering.
|
||||
|
||||
Voor de komende zes slides geldt: ik open de gedeployde polderfest-with-agent, en in mijn editor. Per slide zie je een zoekterm — STEP N. Daarmee springen we direct naar de juiste plek in de code. Ik leg uit wat er staat. Geen typen.
|
||||
|
||||
`[SLIDE 12]`
|
||||
|
||||
STAP 7. getWeather — externe API als tool. Zoek in de editor op STEP 7 in app/api/agent/route.ts.
|
||||
|
||||
Dit is een fetch naar Open-Meteo. Gratis, geen account, geen API-key. We sturen lat-lon van Utrecht, een datum, en krijgen temperatuur plus regen-kans terug.
|
||||
|
||||
Belangrijk: dit kon ook al in Les 12 met tool calling. Het is niet exclusief voor agents. Wat agents wel doen: ze gebruiken het resultaat van deze tool om een volgende tool te kiezen. Bijvoorbeeld: regenkans hoog → searchBands met stage="Tent".
|
||||
|
||||
`[SLIDE 13]`
|
||||
|
||||
STAP 8. addToFavorites — onze eerste write-tool. Zoek op STEP 8.
|
||||
|
||||
Hier zien jullie iets nieuws. Tot nu toe deden alle tools alleen reads uit Supabase. Deze schrijft. Twee stappen: eerst zoekt de tool de band op naam — we hebben de band_id nodig als FK. Daarna insert in de bestaande user_favorites tabel met een vaste demo-user.
|
||||
|
||||
In productie zou je hier echte auth gebruiken plus RLS uit Les 10 zodat users alleen hun eigen rijen kunnen toevoegen. Voor onze demo hardcoderen we de user.
|
||||
|
||||
Dit is écht agent-werk — de agent verandert de wereld.
|
||||
|
||||
`[SLIDE 14]`
|
||||
|
||||
STAP 9. finishPlan — structured output. Zoek op STEP 9.
|
||||
|
||||
Deze tool heeft een Zod-schema dat een complete plan-structuur dwingt. Een summary plus een lijst items met tijd, band, stage en reden. De agent moet hier zijn eindplan in plaatsen.
|
||||
|
||||
Voordeel: in de UI krijgen we geen vrije-vorm-tekst die we moeten parsen, maar een gestructureerd object dat we mooi kunnen renderen als een timeline-kaart. Komt straks bij STAP 13.
|
||||
|
||||
`[SLIDE 15]`
|
||||
|
||||
STAP 10. hasToolCall — stop-conditie op signaal. Zoek op STEP 10.
|
||||
|
||||
Hier zien jullie hoe we stopWhen combineren. Twee condities in een array. Eerst stepCountIs van acht — onze veiligheid. Tweede: hasToolCall finishPlan — zodra de agent finishPlan heeft aangeroepen stopt de loop direct.
|
||||
|
||||
Effect: gegarandeerde nette afsluiting. Geen agent die nog drie nutteloze tool-calls doet na zijn finale plan.
|
||||
|
||||
`[SLIDE 16]`
|
||||
|
||||
STAP 11. prepareStep — model per stap. Zoek op STEP 11.
|
||||
|
||||
Dit is een mooie. We geven een functie die per stap een model kiest. Stap nul, de eerste stap waarin de agent het probleem begrijpt en zijn plan maakt, krijgt gpt-5-punt-2. Het krachtige model. Stap één en verder gebruiken gpt-5-mini. Goedkoop en snel.
|
||||
|
||||
Resultaat: significant lagere kosten en latency, zelfde eindkwaliteit. Want het denkwerk zit in stap nul. De rest is uitvoering.
|
||||
|
||||
`[SLIDE 17]`
|
||||
|
||||
STAP 13. UI rendering. Zoek op STEP 13 in app/agent/page.tsx.
|
||||
|
||||
Hier zien jullie de truc die het magisch maakt. We lopen door m.parts en herkennen drie types. Een tool-finishPlan rendeer ik als nette plan-kaart met timeline. Een tool-addToFavorites als gele badge "toegevoegd aan favorieten". Andere tool-calls als expandable JSON details.
|
||||
|
||||
Zonder deze UI ziet de gebruiker alleen een eindbericht. Met deze UI ziet hij stap-voor-stap denken — en dat is wat het écht agent voelt.
|
||||
|
||||
`[SLIDE 18]`
|
||||
|
||||
Tot zo ver de code-walkthrough. Voor wie het visueel wil onthouden: dit is het stappenplan van Polderfest naar Polderfest-met-agent. Tien nummerblokken in twee kolommen. Voor de details: zie Les13-Stap-voor-stap.md.
|
||||
|
||||
Nu een live demo van wat dit allemaal samen doet.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 6 — LIVE DEMO
|
||||
|
||||
`[SLIDE 19]`
|
||||
|
||||
Tijd voor de demo. Ik open de gedeployde versie van Polderfest-with-agent in de browser. Jullie zien een chat-interface met de tekst "Polderfest 2027 — Agent" bovenaan. Onderaan een input-veld.
|
||||
|
||||
Ik typ de eerste prompt:
|
||||
|
||||
> Plan een avond met indie en techno voor zaterdag.
|
||||
|
||||
Versturen. Jullie zien onderaan een bericht verschijnen "Agent denkt na". En dan klap-voor-klap zien jullie de tool-calls. Eerst `searchBands` met genre=indie en day=zaterdag. Dan `searchBands` opnieuw, nu met genre=techno. Dan `getStageSchedule` voor zaterdag.
|
||||
|
||||
En aan het eind: een gepland avondprogramma. Compleet, met tijden, geen overlap, gesorteerd per stage. De agent heeft autonoom vier of vijf stappen gezet en het resultaat samengebracht.
|
||||
|
||||
Tweede prompt:
|
||||
|
||||
> Wat speelt er op zondag en hoe is het weer dan?
|
||||
|
||||
Hier zien jullie de externe weer-API in actie. De agent roept getWeather aan voor 2027-07-11. Krijgt temperatuur en regen-kans terug. En koppelt dat aan het schedule.
|
||||
|
||||
Derde prompt:
|
||||
|
||||
> Welke band moet ik NIET missen op vrijdag? Zet hem in mijn favorieten.
|
||||
|
||||
Hier wordt het interessant. Er is geen tool die "must-see"-bands teruggeeft. De agent moet creatief combineren — searchBands met day=vrijdag, kijken naar populariteit-velden, en een mening vormen.
|
||||
|
||||
Daarna kijken we in de code. Cursor open. `app/api/agent/route.ts`. Ik wijs aan: de drie tools, de system prompt, de stopWhen.
|
||||
|
||||
Dan `app/agent/page.tsx`. Vooral het stuk dat de tool-calls toont — die details-tags met JSON-output per part.
|
||||
|
||||
En als finale: vergelijking met `app/api/chat/route.ts` van Les 11. Twintig regels system-prompt + alle bands in context versus deze agent met drie tools. "Kijk hoeveel meer kan met agents."
|
||||
|
||||
---
|
||||
|
||||
## PAUZE
|
||||
|
||||
Tijd voor een kwartier pauze. Ik mute mijn microfoon, koffie halen. Tot zo.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 7 — Nu jullie
|
||||
|
||||
`[SLIDE 20]`
|
||||
|
||||
Welkom terug. Nu jullie beurt.
|
||||
|
||||
Doel: bouw zelf een Polderfest-agent. Vanaf de Polderfest-app die je al hebt uit Les 12, of vanaf de starter zip uit `lesbestanden/`.
|
||||
|
||||
In de lesbestanden-map staan drie bestanden. `polderfest-original.zip` — startpunt. `polderfest-with-agent.zip` — referentie als je vastloopt, mag je gerust openen. En `Les13-Stap-voor-stap.md` — elf stappen, volg ze één voor één.
|
||||
|
||||
Werk in jullie eigen tempo. Test met de drie voorbeeld-prompts in de browser. En als alles werkt: pushen naar Vercel — vergeet de environment variables niet, Vercel kent jullie keys nog niet.
|
||||
|
||||
Deel je productie-URL in de chat als je klaar bent. Ik kom bij iedereen langs.
|
||||
|
||||
Vragen? Hand op, of roep me.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 8 — Afsluiting
|
||||
|
||||
`[SLIDE 21]`
|
||||
|
||||
Bijna klaar. Korte samenvatting.
|
||||
|
||||
Het verschil tussen tool calling en een agent zit in één regel code: `stopWhen`. Zonder is het tool calling, met is het een agent.
|
||||
|
||||
Drie nieuwe AI SDK functies vandaag. Eén: tool om een tool te definiëren met description, inputSchema en execute. Twee: stepCountIs om een maximaal aantal stappen te stellen. Drie: multi-step loops via streamText.
|
||||
|
||||
Externe APIs als tool. Open-Meteo voor weer, geen key nodig. Vuistregel: wat een fetch kan, kan een tool.
|
||||
|
||||
En we hebben de Polderfest-app uitgebreid. Van een chat met alle bands in de context, naar een echte agent die zelfstandig data verzamelt, externe APIs gebruikt, en eigen plannen opstelt.
|
||||
|
||||
Volgende les is Les 14. Externe APIs in diepte, samen met Cursor agents en Vercel deploys met preview deployments en GitHub Actions. Belangrijk om je leerlijn af te ronden naar productie.
|
||||
|
||||
Vragen?
|
||||
|
||||
---
|
||||
|
||||
## DRIE DEMO-PROMPTS — copy-paste tijdens demo
|
||||
|
||||
**Prompt 1 — basis multi-step**
|
||||
```
|
||||
Plan een avond met indie en techno voor zaterdag.
|
||||
```
|
||||
|
||||
**Prompt 2 — externe API gebruiken**
|
||||
```
|
||||
Wat speelt er op zondag en hoe is het weer dan?
|
||||
```
|
||||
|
||||
**Prompt 3 — creatief combineren**
|
||||
```
|
||||
Welke band moet ik NIET missen op vrijdag? Zet hem in mijn favorieten.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VEELVOORKOMENDE FOUTEN — voor mezelf
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|-----------|
|
||||
| Vercel kent env vars niet | Add in Vercel → Settings → Environment Variables, redeploy |
|
||||
| Agent stopt na 1 tool-call | `stopWhen: stepCountIs(8)` toegevoegd? |
|
||||
| Tool wordt nooit aangeroepen | Description te vaag — herschrijf in heldere taal |
|
||||
| Open-Meteo geeft niks terug | Check ISO datum-format (YYYY-MM-DD) |
|
||||
| UI toont geen tool-calls | Check `m.parts.map` — zoek op `p.type.startsWith("tool-")` |
|
||||
| Supabase fout in agent-route | `SUPABASE_SERVICE_ROLE_KEY` in .env.local |
|
||||
| Hydration error op /agent | "use client" bovenaan page.tsx |
|
||||
236
Les13-Agents/Les13-Docenttekst.md
Normal file
236
Les13-Agents/Les13-Docenttekst.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Les 13 — Docenttekst (normale versie)
|
||||
|
||||
Dit document is de gewone docenthandleiding bij Les 13: Agents. Voor de **letterlijke voorleesversie** zie `Les13-Docenttekst-Autocue.md`.
|
||||
|
||||
**Vorige les:** Les 12 — Tool Calling
|
||||
**Volgende les:** Les 14 — Externe APIs + Cursor + Vercel deploy
|
||||
|
||||
**Lesvorm:** fysiek, demo-driven, ±1 uur presenteren → pauze → ±1,5 uur "Nu jullie"
|
||||
|
||||
---
|
||||
|
||||
## Voorbereiding (de dag voor de les)
|
||||
|
||||
Loop dit door voor je morgen begint. Drie groepen: accounts, code-deployment, en presentatie-omgeving.
|
||||
|
||||
### 0. Supabase — geen nieuwe tabel nodig
|
||||
|
||||
De bestaande `user_favorites` tabel uit Les 11/12 gebruiken we (kolommen `id`, `user_email`, `band_id`, `created_at`). De `addToFavorites` tool doet eerst een band-lookup en plaatst dan een rij met `user_email = "demo@polderwave.app"` en de juiste `band_id`.
|
||||
|
||||
Tussen demos resetten kan met `supabase-reset-user-favorites.sql` (zit in de zip).
|
||||
|
||||
### 1. Accounts en API-keys
|
||||
|
||||
| Service | Account nodig? | Voor wat |
|
||||
|---------|---------------|---------|
|
||||
| **Supabase** | Ja — bestaat al uit Les 11/12 | Polderfest-database met `bands`-tabel |
|
||||
| **OpenAI** | Ja — bestaat al uit Les 11 | API key voor `gpt-5.2` in `.env.local` en Vercel |
|
||||
| **Vercel** | Ja — bestaat al uit Les 11 | Hosting van Polderfest-with-agent |
|
||||
| **GitHub** | Ja | Repo voor Polderfest-with-agent |
|
||||
| **Open-Meteo (weer-API)** | **NEE** — geen account, geen API key | Externe API als demo voor de derde tool |
|
||||
|
||||
> Open-Meteo expliciet noemen tijdens de les — studenten merken vaak dat het gratis kan zonder registratie en dat is een heerlijk moment om aan te kondigen.
|
||||
|
||||
### 2. Code en deploy klaarzetten
|
||||
|
||||
- [ ] Pak `polderfest-with-agent.zip` uit (uit `lesbestanden/`)
|
||||
- [ ] `npm install`, `npm run dev` — check dat de app lokaal draait
|
||||
- [ ] Open `localhost:3000` — de gewone chat moet werken
|
||||
- [ ] Klik door naar `localhost:3000/agent` — de agent-pagina moet werken
|
||||
- [ ] Push naar een nieuwe GitHub-repo `polderfest-with-agent`
|
||||
- [ ] Importeer op Vercel, vul de env vars in:
|
||||
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||
- `SUPABASE_SERVICE_ROLE_KEY`
|
||||
- `OPENAI_API_KEY`
|
||||
- [ ] Deploy. Wacht tot live. Check beide URLs werken in productie.
|
||||
- [ ] **Bewaar de productie-URL** — die ga je tijdens de demo gebruiken.
|
||||
|
||||
### 3. Presentatie klaarzetten
|
||||
|
||||
- [ ] Cursor open in de Polderfest-with-agent-map (voor code-walkthrough)
|
||||
- [ ] Browser tabs vooraf openen:
|
||||
- de gedeployde agent-URL (productie)
|
||||
- Vercel dashboard van de polderfest-with-agent
|
||||
- github.com — de repo
|
||||
- `Les13-Stap-voor-stap.md` open in een tweede editor-window
|
||||
- [ ] Beamer-test — slides goed leesbaar, code-grootte oké
|
||||
- [ ] `Les13-Slides.pdf` of `.pptx` klaargezet
|
||||
- [ ] De drie demo-prompts copy-paste klaar (zie eind van dit document)
|
||||
|
||||
### 4. Mentaal klaarzetten
|
||||
|
||||
Twee dingen om scherp te houden tijdens deze les:
|
||||
|
||||
1. **Tool calling vs Agent is een continuüm.** Wees eerlijk hierover — er is geen harde lijn. Studenten waarderen die nuance meer dan een "deze is goed, die is fout"-uitleg.
|
||||
2. **Externe APIs konden ook in Les 12.** Niet doen alsof dit een agent-feature is. Het verschil bij agents zit in *meerdere bronnen door elkaar combineren*.
|
||||
|
||||
---
|
||||
|
||||
## Lesopzet
|
||||
|
||||
| Blok | Onderwerp | Slides |
|
||||
|------|-----------|--------|
|
||||
| 1 | Welkom + Terugblik tool calling | 1–3 |
|
||||
| 2 | Wat is een agent? Wanneer waarde? | 4–6 |
|
||||
| 3 | AI SDK basics: tool, stopWhen, externe API | 7–9 |
|
||||
| 4 | Drie agent-extra's: prepareStep + hasToolCall/finishPlan + write-tool | 10 |
|
||||
| 5 | Roadmap + code-walkthrough (6 STEP-slides) | 11–17 |
|
||||
| 6 | Stappenplan visueel (recap) | 18 |
|
||||
| 7 | LIVE DEMO — gedeployde agent | 19 |
|
||||
| – | Pauze | – |
|
||||
| 8 | Nu jullie — zelf bouwen | 20 |
|
||||
| 9 | Afsluiting | 21 |
|
||||
|
||||
---
|
||||
|
||||
## Blok 1: Welkom + Terugblik tool calling (slides 1-3)
|
||||
|
||||
**Doel:** studenten warmlopen + de stap zetten van "tool calling" (vorige les) naar "agent" (vandaag).
|
||||
|
||||
Open met de vraag *"Wie heeft thuis nog gespeeld met de Polderfest-app?"* om aan te haken bij de vorige les.
|
||||
|
||||
Belangrijke punten:
|
||||
- Tool calling werkt prima voor enkelvoudige vragen ("welke bands op zaterdag?")
|
||||
- Complexe vragen vragen meerdere stappen ("plan een avond met indie + techno + geen overlap")
|
||||
- Vandaag voegen we één route toe — `/api/agent` — die dat aan kan
|
||||
|
||||
**Cruciaal op slide 3:** vermeld expliciet dat er **geen harde lijn** is tussen tool calling en agents. In AI SDK v6 is het dezelfde syntax. Het verschil zit in mindset en in hoeveel stappen je toelaat.
|
||||
|
||||
**Wat veel docenten verkeerd doen:** zeggen dat "tool calling maar 1 stap kan". Niet waar — ook met tool calling kun je `stopWhen` gebruiken. Het verschil is subtieler: bij tool calling is het pad voorspelbaar (jij beschrijft de stappen), bij een agent is het pad open (AI plant zelf).
|
||||
|
||||
---
|
||||
|
||||
## Blok 2: Wat is een agent? (slides 4-6)
|
||||
|
||||
**Doel:** wanneer is een agent echt zinvol, wanneer overkill.
|
||||
|
||||
Goede voorbeelden om te noemen:
|
||||
- Plan-vragen ("plan een dag", "vergelijk twee opties", "vind het beste")
|
||||
- Onderzoek-vragen met meerdere bronnen
|
||||
- Vragen waarbij het eerste antwoord een vervolgvraag triggert
|
||||
|
||||
Geef ook **slechte** voorbeelden om te illustreren dat agent niet altijd het antwoord is:
|
||||
- "Wat speelt er op zaterdag?" → één DB-call, geen agent nodig
|
||||
- "Wie maakt account X aan?" → één auth-call, geen agent nodig
|
||||
|
||||
Slide 5 toont de agent-loop visueel. Loop dit langs en benadruk dat de LLM in elk loopje opnieuw beslist: "ben ik klaar, of moet ik nog een tool?".
|
||||
|
||||
Slide 6 noemt de 5 stappen om een agent te bouwen — dit is de teaser voor het stappenplan op slide 10.
|
||||
|
||||
---
|
||||
|
||||
## Blok 3: De drie AI SDK basics (slides 7-9)
|
||||
|
||||
**Doel:** de bekende AI SDK functies opnieuw uitleggen + bredere context.
|
||||
|
||||
- Slide 7: `tool()` — bekend uit Les 12, herhalen voor zekerheid
|
||||
- Slide 8: `stopWhen` — DIT is de agent-knop. Zonder `stopWhen` doet de AI één tool-call. Met `stopWhen` mag-ie blijven doorlopen.
|
||||
- Slide 9: externe API als tool — Open-Meteo voorbeeld. Benoem expliciet:
|
||||
- **Geen account, geen API key** (perfect voor onderwijs)
|
||||
- **Dit kon ook al in Les 12** — niet doen alsof dit een agent-feature is
|
||||
- **Echte verschil bij agents:** meerdere bronnen door elkaar combineren
|
||||
|
||||
---
|
||||
|
||||
## Blok 4: Drie agent-extra's bovenop Les 12 (slide 10)
|
||||
|
||||
**Doel:** hier wordt het écht agent-werk. Drie features die NIET in Les 12 zaten.
|
||||
|
||||
- **`prepareStep`** — wissel model per stap. Stap 0 = krachtig (planning), stap 1+ = goedkoop (uitvoering). Lagere kosten zonder kwaliteitsverlies. Verkoop dit als production-feature.
|
||||
- **`hasToolCall("finishPlan")`** + `finishPlan` tool — speciale tool met Zod-schema die de agent **moet** aanroepen als laatste stap. `hasToolCall` zorgt dat de loop direct stopt. Resultaat: gegarandeerd gestructureerd eindplan dat je in de UI mooi kunt renderen.
|
||||
- **`addToFavorites`** — eerste write-tool. Schrijft naar Supabase-tabel. Tot nu toe lazen alle tools alleen. Dit is een grote stap — de agent verandert nu de wereld. Benoem productie-risico's kort (veiligheid: RLS, audit-logging).
|
||||
|
||||
Combinatie maakt het tot een échte agent: plant met krachtig model → voert uit met goedkoop → schrijft favoriet als gebruiker dat vraagt → sluit af met gestructureerd plan.
|
||||
|
||||
---
|
||||
|
||||
## Blok 5: Stappenplan Polderfest → Polderfest-met-agent (slide 11)
|
||||
|
||||
Korte intro op het stappenplan — nu 15 stappen ipv 11 omdat we de drie agent-features hebben toegevoegd. Wijs naar `lesbestanden/Les13-Stap-voor-stap.md` voor het zelf-bouwen straks.
|
||||
|
||||
---
|
||||
|
||||
## Blok 6: LIVE DEMO (slide 12)
|
||||
|
||||
**Doel:** studenten laten zien hoe een werkende agent zich gedraagt, voor ze zelf gaan bouwen.
|
||||
|
||||
Volgorde:
|
||||
|
||||
1. **Open de gedeployde agent-URL** in de browser (productie).
|
||||
2. **Prompt 1**: `Plan een avond met indie en techno voor zaterdag.`
|
||||
- Studenten zien meerdere tool-calls verschijnen (searchBands × 2 + getStageSchedule)
|
||||
- Wijs naar de details-tags in de UI — die expanden om te zien wat de tool deed
|
||||
3. **Prompt 2**: `Wat speelt er op zondag en hoe is het weer dan?`
|
||||
- Hier zien ze de externe weer-API in actie
|
||||
4. **Prompt 3**: `Welke band moet ik NIET missen op vrijdag?`
|
||||
- Open vraag, AI moet creatief combineren
|
||||
|
||||
5. **Code-walkthrough** in Cursor:
|
||||
- `app/api/agent/route.ts` — wijs naar de drie tools + `stopWhen`
|
||||
- `app/agent/page.tsx` — wijs naar `m.parts.map` met de tool-call-weergave
|
||||
- Vergelijk kort met `app/api/chat/route.ts` van Les 11 — "kijk hoe weinig code het is voor zoveel functionaliteit"
|
||||
|
||||
---
|
||||
|
||||
## Blok 6: Nu jullie (slide 12)
|
||||
|
||||
**Doel:** zelfstandig werken, ±1,5 uur.
|
||||
|
||||
Studenten kiezen tussen:
|
||||
- Eigen Polderfest-app uit Les 12 als startpunt
|
||||
- `polderfest-original.zip` uit `lesbestanden/`
|
||||
|
||||
Met `Les13-Stap-voor-stap.md` als 11-stappen-gids. `polderfest-with-agent.zip` is referentie als ze vastlopen.
|
||||
|
||||
Loop rond, laat ze in eigen tempo werken. Geef ruimte voor vragen via chat én fysiek.
|
||||
|
||||
Doel-deliverable: live productie-URL in de groeps-chat plakken.
|
||||
|
||||
---
|
||||
|
||||
## Blok 7: Afsluiting (slide 13)
|
||||
|
||||
Korte samenvatting + vooruitblik Les 14. Vragen-ronde.
|
||||
|
||||
---
|
||||
|
||||
## De drie demo-prompts — copy-paste-klaar
|
||||
|
||||
**Prompt 1 — basis multi-step**
|
||||
```
|
||||
Plan een avond met indie en techno voor zaterdag.
|
||||
```
|
||||
|
||||
**Prompt 2 — externe API gebruiken**
|
||||
```
|
||||
Wat speelt er op zondag en hoe is het weer dan?
|
||||
```
|
||||
|
||||
**Prompt 3 — write-tool + finishPlan**
|
||||
```
|
||||
Welke band moet ik NIET missen op vrijdag? Zet hem in mijn favorieten.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende fouten — wat tegen studenten te zeggen
|
||||
|
||||
| Probleem student | Wat te zeggen |
|
||||
|------------------|---------------|
|
||||
| "Mijn agent stopt na 1 tool-call" | "Heb je `stopWhen: stepCountIs(8)` toegevoegd?" |
|
||||
| "Tool wordt nooit aangeroepen" | "Description is te vaag. Maak 'm heel concreet." |
|
||||
| "Open-Meteo geeft niets terug" | "ISO datum-format checken: `YYYY-MM-DD`" |
|
||||
| "Vercel deploy faalt" | "Env vars in Vercel toegevoegd?" |
|
||||
| "Hydration error" | "`use client` bovenaan page.tsx vergeten?" |
|
||||
|
||||
---
|
||||
|
||||
## Voor jezelf: 4-zinnen kernverhaal
|
||||
|
||||
Voor de afsluiting, samenvattend in vier zinnen:
|
||||
|
||||
1. Tool calling en agents zijn geen verschillende technologieën — het is een schaal.
|
||||
2. `stopWhen` is wat een agent mogelijk maakt: zonder doet de AI één stap, met mag-ie doorlopen.
|
||||
3. Wanneer een agent echt zinvol is: open vragen, meerdere bronnen, onvoorspelbaar pad.
|
||||
4. Externe APIs als tool is geen nieuwe functionaliteit — wel iets dat agents vaker echt benutten.
|
||||
78
Les13-Agents/Les13-Lesopdracht.md
Normal file
78
Les13-Agents/Les13-Lesopdracht.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Les 13 — Lesopdracht
|
||||
## Bouw je eigen Polderfest-agent
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Werkvorm:** in-class, na de pauze (±1,5 uur)
|
||||
**Status:** klaar = productie-URL in de chat delen
|
||||
|
||||
---
|
||||
|
||||
## Doel
|
||||
|
||||
Aan het einde van deze opdracht heb je een werkende Polderfest-agent
|
||||
met zes nieuwe features bovenop Les 12: getWeather, addToFavorites,
|
||||
finishPlan, hasToolCall, prepareStep, en een UI die tool-calls toont.
|
||||
|
||||
Live op je eigen Vercel-deployment.
|
||||
|
||||
---
|
||||
|
||||
## Wat je nodig hebt (zit allemaal al klaar uit Les 11/12)
|
||||
|
||||
- Je Polderfest-app uit Les 12, of `polderfest-original.zip` uit lesbestanden
|
||||
- Supabase project met `bands` en `user_favorites` tabel
|
||||
- OpenAI API key in `.env.local`
|
||||
- Vercel account
|
||||
- `Les13-Stap-voor-stap.md` als gids
|
||||
|
||||
---
|
||||
|
||||
## Stappenplan
|
||||
|
||||
Werk in jouw eigen tempo. Hierbij de hoofdlijn — voor de exacte code
|
||||
per stap zie `Les13-Stap-voor-stap.md` en `polderfest-with-agent.zip`.
|
||||
|
||||
1. **Project klaarzetten** — Polderfest-original uitpakken of eigen versie gebruiken, `npm install && npm run dev`
|
||||
2. **Nieuwe route** — `app/api/agent/route.ts` aanmaken (kopie van chat-route)
|
||||
3. **`addToFavorites`** — write-tool toevoegen (insert in `user_favorites`)
|
||||
4. **`getWeather`** — externe API (Open-Meteo, geen key)
|
||||
5. **`finishPlan`** — Zod-schema structured-output tool
|
||||
6. **`hasToolCall`** — extra stop-conditie naast `stepCountIs(8)`
|
||||
7. **`prepareStep`** — model switching (krachtig → goedkoop)
|
||||
8. **UI** — `app/agent/page.tsx` met tool-call-rendering
|
||||
9. **Testen** — drie demo-prompts in de browser
|
||||
10. **Deploy** — Vercel + env vars
|
||||
|
||||
---
|
||||
|
||||
## Op te leveren
|
||||
|
||||
Tijdens de les in de chat plaatsen:
|
||||
|
||||
- Productie-URL van je `/agent` pagina
|
||||
- Screenshot van je Supabase Table Editor met minimaal één nieuwe
|
||||
rij in `user_favorites` (gemaakt door de agent)
|
||||
|
||||
---
|
||||
|
||||
## Test-prompts (gebruik deze om te valideren)
|
||||
|
||||
| Prompt | Verwachte tools |
|
||||
|--------|-----------------|
|
||||
| "Plan een avond met indie en techno voor zaterdag." | searchBands × 2 + getDaySchedule + finishPlan |
|
||||
| "Wat speelt er op zondag en hoe is het weer?" | getDaySchedule + getWeather + finishPlan |
|
||||
| "Welke band moet ik NIET missen op vrijdag? Zet hem in mijn favorieten." | searchBands + addToFavorites + finishPlan |
|
||||
|
||||
Werkt prompt 3 niet (geen rij in `user_favorites`)? Check of de service-role-key
|
||||
in `.env.local` staat en of de tool de `band_id` correct ophaalt.
|
||||
|
||||
---
|
||||
|
||||
## Vastgelopen?
|
||||
|
||||
- Open `polderfest-with-agent.zip` als spiekbriefje
|
||||
- Zoek in de code op `// STEP N` voor de exacte plek per feature
|
||||
- Vraag in de chat — ik loop rond
|
||||
|
||||
Veel succes!
|
||||
105
Les13-Agents/Les13-Lesopdracht.pdf
Normal file
105
Les13-Agents/Les13-Lesopdracht.pdf
Normal file
@@ -0,0 +1,105 @@
|
||||
%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 /F4 6 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 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 10 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260609144911+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260609144911+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Lesopdracht) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Count 2 /Kids [ 5 0 R 7 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2066
|
||||
>>
|
||||
stream
|
||||
GauHLgN)"=&:N^lqC+j20\23NWS.SiZe9TTRlE,o?DuQr6j'`KUdtP"J,T*)%LA"H=keXmeC2lpqXe@;6).eYIsOb<2^:;/a+4U8c%$bWJC,\@1$X2U0jtCC0S/JKVRlnsX/d=5k.M="Mcm3%3B3JP*8:J2iQQ:H#6_08D/M^m0RM@1^hcZM*<V\H:!9g8f'CNmK!mS)Bj'llqU.PtRHH?+[#W?]r^[nunC":>X)9VNS$iWu-1jELr-AD4UH,4.,)QHsOsT<D1)LC-_taJK`AIisN6+^0h8&6>g7Y:l0K!J-L:s_]'%88#-F-Zo-0K_*J[^kk+quOZQ7,J6FO"^Se-Z)K&GI_Z\*P!@=l?;G5Sl$dR\cU_n?s<B1tNLf^&!>%SS=tCe_9/W`H7T]:i#+H/Yq[Vl8r=Uof20#S(2_i7[qtFk4LPd,G<@\^AOns''jk%*X.jL`t:B./r1Kn:(rE0GWt#=:+CD@C&9N!VE(+01qj@,Pc?[CmjR)ZMq[q%Nn.mmRa%Gj]2E5rPB2L<NYtNXgnjA9P"PZggcp-UHWVa\LfgCqNu!-mVn&o;*GdK.dqlLf06"U4DMuZl)3>X=SehV0W+q[0Jh&':o:'F$JK"aak8;'9'ML6L@@@WO?"`nD#O/Tq4e_4U$e1S]>$<NI]r0tQ.j2nW/[nd17BB[rJaO4FaH>J#]6"UbPkb[2;Ft1$qd8IPlRemd<k1UBjnGacq6o__MrArMqIt]5WX(2#1&A9=K[:e%`&CA-f28idhigttf^ZljLL)+PF9#8AV[0P4h'%aS@ps@@0O_@]`\3DKodI<K%\ucnnEEe&`CVmlH's`20:so6&!^AU6LIA,e'>BZQVEZiR6t]&_rXA'cte4255G"M]",ojJceY"/@VP]</]]2J3qOsL#9A1jGrf:n>u_sKh73f%2O@O^elK`.n0DtEIe&<&*Ng>%e\^b(#(9VCZqQq'o;i!c1^d9B/3\fqc[nQIGd%D'P[Cc@!Ic>2<oq^0Ul8B_)ND+XV*TS=)CCMW]?[^5GFh4gB`EF9G5q-NX(qYOCFRQ6]GfGco;oITctdM=C=Vk4="1_;Sb`2>n9.-SE/e'MZ&82Le;EklGJc!gel6$I9^WfKcrl1U$G:W`\<9#LU;HL]Jk]!0ZWN!#qdnLJ=KA)d5sqln1u=2bl#g`(KZ.r#I'<SLj8U)%"\K`'4[HgFb]Dda<kKMQOB2W#T3pX4d&Fff.^\^i2AZ6i7M=$0)`2pWG.)o1%?q7:,*6QT.5aoYX!CaRM&<[+LuT/Lm>bSan:Wa:Y(9Je=WeWdV.CMo.qN5E%,?`?coHQdG6o[r/?OYV7l<OA66suPP_cj<NA@7ZgLjKk-f]b8/e"UH)oY7?XZqoGuru&CRaj6gqY-=&C%54UfXB?DPW-9lJ)rT4$VT&:eFH4l%B0cjji<njM4+hn._7i]cII.odZ7@RA)1@2s:uV@2G@=3L]@:6-6G`\4SLOG?.^s]bQs**%@+ICLm>8GZB)R&?h-LL3,O@^Uer"r2CU`/d;a%2%(V.Bh(:&7_2sqkp+6hU+h2k]P)FurcOZ,mXK3;^6n.j0r(Q-qN*%aGdrd.A4VOrZfQ:iSs#Phgb5(EdPNCH3kqZg,)7(fi-$^Ae4-)N+SNe(?gho1KN;mK,?&5&bLP+?\:(-tNCBJ`E#D.ShYW[6r#7MRdG-^N#6%>!S6-N5l8JX'?:Mh9Uh2qe5bSX2IQ\UX;WZEDFB"]0[GL]3U)7UUlLj+ZhRr:!K4pY`DhARZCH(q^@1s*PI(NtSO#oO1PuTsh&FM6qb'ql1[s1i[hu-NPMrcNbO/cJ$/6HDFdaU1q8[@pOa^?bU@D!lJ:aCYn?K;Xo0BuJeHC1e`[(8G!:^m:_?kk')C37:,gl(HQ@U`*,<JK=Km\3%9HR<8&/>q`8^5uq'%8t>f.2Ce$rbWAH`FuH84i;ZhOdStX>'gb/oFk%7pS-5AL:<3Oh7.=OZ_9DocuB';&^GGOLesIL?/>5L`,QKqB>MbN!O'P[eXX"XTu%lh-6_NHa>febLAtY9d=ANd5bmS>_u$0l617$>JA,I~>endstream
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1871
|
||||
>>
|
||||
stream
|
||||
Gb!;dD,8nW&H9tYR-UZW.#Ycf^G"g)_B#PSV'Vs9'rbC'8aOCcqUV._o':5FEC,KQ#"@&_&0gW3lncu34>Roq/Gcg@hsgj-FUq6mD6Q%m#%5'eL#+mNlQ,qF`44+qf)t/gQ4n7GO^l*j%?CkcI-*p]Z6?RV('ZD$U!:G.$A$[?+KtrE#_Q2K2R(!+K$XZ:g7BD_Xo/b$3N&WXk4sJ5^;%K#)L)kc)fu4(r^4\=L#"_r[-G0FZG"&+h?5n.5?qR2?ZLs&S4]3eB<<j_2=rm>i/\7jM_iRgM<ZDZ7aQKn%TkINH5?)l2gB$.*EKI>PZSj8'Z7!`^fQJB,SjV2I)f]iB=YrE4gsXrF"cf<=EPVY1(&s<+['l.HZfO("h"T;!IAe3(Im`;G!,Hui@7=KC)gJ"e[@EGEcUS)<Z,h)e.=ZnXN7F61)EB-`k=a13CdFfd\S)E6pRU!1tGTZ8QSZ03RscO2Hul/&3c$HrW1l2:NJ]7<KCC0,P$5gp\8#Ej1$YPYq2&3B*a2&Q%ioL_JH-u=MBpH6=3^j:=t',K,4JiBF,0A`IAdXV8?+Y]HW\#dk[$lk"slG3_m"j#lW&Y/urfui5L*M[a.*dbuF9;L(]]TPiYZ_gh3<34Ff2204fMEAY1V61UWf$U+B*Y&.<nadMpZcpX[%bc3pZl;d+>Z('uH7Gm?G/=kJQ@@#$t+SK:[SC_<%&;,&>rOkipW8Z=5]$Mk]ZPe=+>#lM9gcf2?L*T6Y?]RGcM/q7^2qAN@--$0Ptb[;bseH58\%?/ULh2$.7\@uT0D-Z"Qrc5j6o_;(*lGh*3oR&&Q,NR?l<>3/B7,8sP]+9p.ZEZ+uCJrO&7L9iq$%@nm1l,)(U!/5;'c+Q&7Bl./*a_A*LgJ?.k=DMb*<]S43]?SG41(>eD,2R,;aOb>R8=.6Yl"7F43Eun7bCV+\\,;TY,l9,>pAhIh1&=K>f@%!4]86qY$XmrE?crdT")lScDWs#<U8h$VOE>Q3V4?<2Qm1>%&^RZGaWj.C)≤CWWRr*>B.PQ^C.5g6%H1feI)kR;1N&n;B-,Y'9[lWET=N(^NqjKR"mgI8sOK&qTV'a5iF9=6[NDq$T_'AbeR3AH+?'7#l\ak21CIe*6'_k7aO0CnT#/R?6"\?_LjAkBcs/RadPauHX)hJ@5M]lpruF%Ir<HsL2^kk+-%<*r$GC[*fb?+B4,aE=8s>&"b\B!fF$-<P*ule3+k5M$9X>;+3f%obq^.9Ij<V.;F5,'>&d>+??@Ag[-;KLV5_+a^I!A7N,9U#P^P-kaZ-LY<]Fc8J)h<rBJ%]0e?Ml9'g/X%(:4%i".Ek82SEW^P&gTE895*PiIX"#H2J=Tp6?^I;,"q[bY4>@?Y:?VosNllUg1C4K_VVU7dKnt]]4%.>jpHDEJ7[3pTK7\rmW:Z3feVJDBXJOqaL?s`OtF3n5oUDBiL+HiNJcPP$7%C<%:^ZdU:@WiT/LWYG%7Gb#IoH=qbI/<GjH#UADPf\>GdpG+?D&5U\%@J4rI&k1F[_H(OVO0h:p.4<^<$Zj4<M=u%jBC0h6F8pJ:&?S>iD#4")!&J'.4q-&0m8i+YrfrIRk-=```Oj-n8;r]V.g'80\nIgY-".e2"0ar9T"5@P*VM<.YY.)9M)3Hl0Yh?4fd_o(-ciJda%f!\_q+crL0mt_toR^icMqSF3@cJq%9s!'E5RO@r^'E489&TTG?!Tk1%ocVZ"ou2so2`RGDHACU2#3;QpSM2cTC$;1%a+?O`T6-&rf53*lKBcF([;'=d=u?,HIS'shM<_=&SF2HJsNX!H@=OShl(:1jLk,O)TKe<O&e.eVVXRR/W`,"55ln9NIm+-uHWJ=u^#AfA]lrO-kp*;p7X,s\m~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 13
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000122 00000 n
|
||||
0000000229 00000 n
|
||||
0000000341 00000 n
|
||||
0000000446 00000 n
|
||||
0000000651 00000 n
|
||||
0000000728 00000 n
|
||||
0000000933 00000 n
|
||||
0000001002 00000 n
|
||||
0000001290 00000 n
|
||||
0000001356 00000 n
|
||||
0000003514 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<3c3ecab7e0474d3949798c7db62413af><3c3ecab7e0474d3949798c7db62413af>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 9 0 R
|
||||
/Root 8 0 R
|
||||
/Size 13
|
||||
>>
|
||||
startxref
|
||||
5477
|
||||
%%EOF
|
||||
358
Les13-Agents/Les13-Lesstof.md
Normal file
358
Les13-Agents/Les13-Lesstof.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Les 13 — Lesstof
|
||||
## Agents — een Polderfest-app die zelf nadenkt
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Vorige les:** Les 12 — Tool Calling
|
||||
**Volgende les:** Les 14 — Externe APIs + Cursor + Vercel deploy
|
||||
|
||||
---
|
||||
|
||||
## Inhoud
|
||||
|
||||
1. [Wat is een agent?](#1-wat-is-een-agent)
|
||||
2. [Tool calling vs Agent — een continuüm](#2-tool-calling-vs-agent--een-continuüm)
|
||||
3. [Wanneer voegt een agent écht waarde toe?](#3-wanneer-voegt-een-agent-écht-waarde-toe)
|
||||
4. [De drie AI SDK functies die we gebruiken](#4-de-drie-ai-sdk-functies-die-we-gebruiken)
|
||||
5. [Externe APIs als tool](#5-externe-apis-als-tool)
|
||||
6. [De agent-loop](#6-de-agent-loop)
|
||||
7. [Stappenplan: Polderfest → Polderfest-met-agent](#7-stappenplan-polderfest--polderfest-met-agent)
|
||||
8. [Veelvoorkomende fouten en debug-tips](#8-veelvoorkomende-fouten-en-debug-tips)
|
||||
|
||||
---
|
||||
|
||||
## 1. Wat is een agent?
|
||||
|
||||
Een **agent** is een Large Language Model dat in een loop draait, tools gebruikt, en zelfstandig kiest welke volgende stap nodig is om een doel te bereiken.
|
||||
|
||||
Een voorbeeld maakt het concreet. Stel een gebruiker stelt deze vraag aan onze Polderfest-app:
|
||||
|
||||
> "Plan een zaterdagavond met indie en techno, geen tijdoverlap, alleen onder een dak als het regent."
|
||||
|
||||
Hierin zitten meerdere vragen verstopt:
|
||||
- Welke indie-bands spelen er zaterdag?
|
||||
- Welke techno-bands spelen er zaterdag?
|
||||
- Wat is het schema (om overlap te checken)?
|
||||
- Hoe is het weer op zaterdag?
|
||||
- Kunnen we al die info combineren tot één avondprogramma?
|
||||
|
||||
Een gewone chatbot beantwoordt één vraag tegelijk. Een agent pakt alle vijf de sub-vragen autonoom op, in de juiste volgorde, en levert een eindantwoord.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tool calling vs Agent — een continuüm
|
||||
|
||||
Belangrijk om vooraf te zeggen: **er is geen harde lijn tussen tool calling en agents.** In de Vercel AI SDK v6 gebruik je voor beide dezelfde syntax: `streamText`, `tool()`, `stopWhen`.
|
||||
|
||||
Het verschil zit in **hoe je het ontwerpt en gebruikt**.
|
||||
|
||||
| Aspect | Tool calling (Les 12) | Agent (Les 13) |
|
||||
|--------|----------------------|----------------|
|
||||
| Vraag-type | "Voer deze functie uit" | "Bereik dit doel — kies zelf hoe" |
|
||||
| Aantal stappen | Meestal 1-3 (ook met `stopWhen`) | Vaak 4-10+, variabel |
|
||||
| Wie kiest de volgorde | Jouw prompt stuurt sterk | AI plant zelf, autonomer |
|
||||
| Bronnen combineren | Per tool één bron | DB + externe API + ... door elkaar |
|
||||
| Reflectie / bijsturen | Nee — direct antwoord | AI checkt resultaat, beslist vervolg |
|
||||
|
||||
Belangrijk: tool calling kón in Les 12 al externe APIs aanroepen. De `execute()` van een tool is gewoon JavaScript — een `fetch` is prima. Het is niet alsof tool calling daar beperkt in is.
|
||||
|
||||
Wat agents onderscheidt is dat ze **meerdere bronnen door elkaar combineren** in één antwoord, omdat ze meer stappen mogen zetten en zelf de volgorde bepalen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Wanneer voegt een agent écht waarde toe?
|
||||
|
||||
**Tool calling is genoeg als:**
|
||||
- Je weet welke functie nodig is voor de vraag (een DB-zoekopdracht, een API-call)
|
||||
- Het antwoord ligt in één bron
|
||||
- Het pad is voorspelbaar — je kunt in je eigen prompt al beschrijven "doe eerst X, dan Y"
|
||||
|
||||
**Een agent is écht zinvol als:**
|
||||
- De vraag is open: "plan", "vergelijk", "vind het beste", "concludeer"
|
||||
- De AI moet meerdere bronnen combineren in één antwoord
|
||||
- Het pad is onvoorspelbaar — soms drie stappen, soms zeven
|
||||
- Het eerste resultaat triggert vervolgvragen ("die band is uitverkocht — zoek alternatief")
|
||||
|
||||
**Waarom niet altijd een agent?**
|
||||
|
||||
Agents lopen langer. Meer stappen = meer tokens = hogere kosten en hogere latency. Ook lastiger te debuggen door non-determinisme — twee dezelfde vragen kunnen verschillende paden bewandelen.
|
||||
|
||||
**Pragmatisch advies:** start altijd met tool calling. Schaal pas naar agent-niveau als je merkt dat je system prompt steeds langer wordt om "doe eerst dit, dan dat" voor te schrijven — dan kan de AI dat beter zelf bepalen.
|
||||
|
||||
---
|
||||
|
||||
## 4. De drie AI SDK functies die we gebruiken
|
||||
|
||||
### 4.1 `tool({ description, inputSchema, execute })`
|
||||
|
||||
Dezelfde functie als in Les 12. Drie velden:
|
||||
|
||||
```typescript
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
searchBands: tool({
|
||||
description: "Zoek bands op genre en/of dag.",
|
||||
inputSchema: z.object({
|
||||
genre: z.string().optional(),
|
||||
day: z.enum(["vrijdag", "zaterdag", "zondag"]).optional(),
|
||||
}),
|
||||
execute: async ({ genre, day }) => {
|
||||
// Supabase query
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
- `description` — wat de AI hierover leest om te beslissen of-ie de tool wil gebruiken
|
||||
- `inputSchema` — Zod-schema, AI vult de parameters automatisch in
|
||||
- `execute` — wat draait als de tool wordt aangeroepen. Gewoon JavaScript: DB-query, fetch, computatie
|
||||
|
||||
### 4.2 `stopWhen` — de "knop" om de loop te bouwen
|
||||
|
||||
```typescript
|
||||
import { stepCountIs } from "ai";
|
||||
|
||||
streamText({
|
||||
model: openai("gpt-5.2"),
|
||||
messages,
|
||||
tools: { ... },
|
||||
stopWhen: stepCountIs(8),
|
||||
});
|
||||
```
|
||||
|
||||
`stopWhen` is een conditie waarop de loop stopt. `stepCountIs(N)` is de meest gebruikte: stop na maximaal N stappen.
|
||||
|
||||
In Les 12 hebben we dit ook gebruikt, vaak met `stepCountIs(3)`. Voor agents zetten we 'm typisch hoger — 8 of meer — omdat we de AI ruimte willen geven om zelf het juiste pad te kiezen.
|
||||
|
||||
Andere stop-condities (combineerbaar):
|
||||
|
||||
```typescript
|
||||
stopWhen: [
|
||||
stepCountIs(8),
|
||||
hasToolCall("finishPlan"), // stop als specifieke tool is aangeroepen
|
||||
],
|
||||
```
|
||||
|
||||
`hasToolCall` is handig als je de AI dwingt een "klaar"-tool aan te roepen voor-ie stopt.
|
||||
|
||||
### 4.3 `hasToolCall` — stop op specifieke tool (NIEUW)
|
||||
|
||||
Combineer `stepCountIs` met een **stop-zodra-tool-aangeroepen** conditie:
|
||||
|
||||
```typescript
|
||||
import { stepCountIs, hasToolCall } from "ai";
|
||||
|
||||
stopWhen: [stepCountIs(8), hasToolCall("finishPlan")],
|
||||
```
|
||||
|
||||
Effect: zodra de agent een tool genaamd `finishPlan` heeft aangeroepen,
|
||||
stopt de loop direct — ongeacht hoeveel stappen er nog over zijn. Dit
|
||||
forceert een nette afsluiting.
|
||||
|
||||
Combineer dit met een tool die **structured output** vraagt:
|
||||
|
||||
```typescript
|
||||
finishPlan: tool({
|
||||
description: "Roep dit ALS LAATSTE aan met een gestructureerd plan.",
|
||||
inputSchema: z.object({
|
||||
summary: z.string(),
|
||||
items: z.array(z.object({
|
||||
time: z.string(),
|
||||
band: z.string(),
|
||||
stage: z.string(),
|
||||
reason: z.string(),
|
||||
})),
|
||||
}),
|
||||
execute: async ({ summary, items }) => ({ summary, items, ok: true }),
|
||||
}),
|
||||
```
|
||||
|
||||
De UI kan dit object renderen als een mooie timeline of kaartenset.
|
||||
Geen risico op vrije-vorm-tekst die je moet parsen.
|
||||
|
||||
### 4.4 `prepareStep` — dynamisch model per stap (NIEUW)
|
||||
|
||||
Geef per stap ander gedrag:
|
||||
|
||||
```typescript
|
||||
streamText({
|
||||
model: openai("gpt-5.2"), // default
|
||||
|
||||
prepareStep: ({ stepNumber }) => {
|
||||
if (stepNumber === 0) {
|
||||
return { model: openai("gpt-5.2") }; // krachtig — planning
|
||||
}
|
||||
return { model: openai("gpt-5-mini") }; // goedkoper — uitvoering
|
||||
},
|
||||
|
||||
// ...tools, stopWhen, etc.
|
||||
});
|
||||
```
|
||||
|
||||
Eerste stap (begrip + planning) = duur, krachtig model. Latere stappen
|
||||
(simpele tool-calls uitvoeren) = goedkoop, snel model. Zelfde resultaat,
|
||||
significant lagere kosten en latency.
|
||||
|
||||
`prepareStep` kan ook andere dingen aanpassen per stap: `activeTools`
|
||||
(welke tools beschikbaar zijn), `system` prompt, etc. Voor vandaag
|
||||
gebruiken we het alleen voor model-switching.
|
||||
|
||||
---
|
||||
|
||||
## 5. Externe APIs als tool
|
||||
|
||||
Tools hoeven niet alleen je eigen Supabase-database te zijn. Externe APIs werken net zo. Een tool is gewoon een functie — wat erin gebeurt is aan jou.
|
||||
|
||||
Voor onze Polderfest-agent gebruiken we **Open-Meteo** — een gratis weer-API die geen account of API key nodig heeft. Perfect voor demos en onderwijs.
|
||||
|
||||
```typescript
|
||||
getWeather: tool({
|
||||
description: "Weer-forecast voor een festivaldag.",
|
||||
inputSchema: z.object({ date: z.string() }),
|
||||
execute: async ({ date }) => {
|
||||
const url = new URL("https://api.open-meteo.com/v1/forecast");
|
||||
url.searchParams.set("latitude", "52.0907");
|
||||
url.searchParams.set("longitude", "5.1214");
|
||||
url.searchParams.set("daily", "temperature_2m_max,precipitation_probability_max");
|
||||
url.searchParams.set("start_date", date);
|
||||
url.searchParams.set("end_date", date);
|
||||
url.searchParams.set("timezone", "Europe/Amsterdam");
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
return {
|
||||
temperatureMaxC: data.daily.temperature_2m_max[0],
|
||||
rainChancePercent: data.daily.precipitation_probability_max[0],
|
||||
};
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
**Vuistregel:** wat een fetch kan, kan een tool. Stripe-saldo opvragen. GitHub-issues lezen. Wikipedia. Spotify. Alles via HTTP — alles kan een tool zijn.
|
||||
|
||||
> Dit is een mogelijkheid die ook in tool calling van Les 12 al bestond. Het verschil bij agents: omdat de loop meerdere stappen mag duren, kan de agent het *resultaat* van een externe API gebruiken om vervolgens een tweede tool aan te roepen op basis daarvan. Dat is de échte meerwaarde.
|
||||
|
||||
### 5.1 Write-tools — de wereld veranderen, niet alleen lezen
|
||||
|
||||
Tot nu toe lazen al onze tools data uit Supabase of externe APIs. Tools
|
||||
kunnen óók schrijven. Dat opent een hele nieuwe categorie: agents die
|
||||
**iets doen** ipv. alleen iets opzoeken.
|
||||
|
||||
```typescript
|
||||
addToFavorites: tool({
|
||||
description: "Voeg een band toe aan de favorieten van de gebruiker.",
|
||||
inputSchema: z.object({
|
||||
bandName: z.string(),
|
||||
}),
|
||||
execute: async ({ bandName }) => {
|
||||
// 1. Zoek band op naam (we hebben band_id nodig — FK in user_favorites)
|
||||
const { data: band } = await supabase
|
||||
.from("bands").select("id, name")
|
||||
.ilike("name", `%${bandName}%`).limit(1).single();
|
||||
if (!band) return { error: `Band niet gevonden` };
|
||||
|
||||
// 2. Insert in bestaande user_favorites tabel
|
||||
const { data, error } = await supabase
|
||||
.from("user_favorites")
|
||||
.insert({ user_email: "demo@polderwave.app", band_id: band.id })
|
||||
.select().single();
|
||||
if (error) return { error: error.message };
|
||||
return { added: true, band: band.name, favoriteId: data.id };
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
De gebruiker zegt *"Zet Band 5 in mijn favorieten."* — de agent roept
|
||||
`addToFavorites` aan — er staat een rij in `user_favorites` met
|
||||
`band_id` als foreign-key naar `bands`. We gebruiken de bestaande
|
||||
tabel uit Les 11/12, geen nieuwe tabel nodig. Voor demos hardcoderen
|
||||
we `user_email` op `"demo@polderwave.app"`; in productie gebruik je
|
||||
echte auth + RLS (zie Les 10).
|
||||
|
||||
Belangrijke veiligheidsoverweging: write-tools zijn machtig én riskant.
|
||||
Een agent die kan schrijven kan ook fout schrijven. In productie:
|
||||
- Beperk wat een tool mag (alleen INSERT op specifieke tabellen)
|
||||
- Beperk per user (Supabase RLS — Les 10)
|
||||
- Log alle writes voor auditability
|
||||
|
||||
---
|
||||
|
||||
## 6. De agent-loop
|
||||
|
||||
Visueel:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────┐
|
||||
│ Gebruiker stelt een vraag │
|
||||
└─────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
┌──▶│ LLM │ → kies een tool of geef antwoord
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Voer tool │ → bv. searchBands of getWeather
|
||||
│ │ uit │
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Resultaat │ → terug naar LLM
|
||||
│ └──────┬───────┘
|
||||
└──────────┘
|
||||
|
||||
Stopt wanneer: stopWhen-conditie waar wordt
|
||||
```
|
||||
|
||||
Soms duurt het twee stappen. Soms zes. Soms acht — dan kicked `stepCountIs(8)` in. De agent beslist.
|
||||
|
||||
---
|
||||
|
||||
## 7. Stappenplan: Polderfest → Polderfest-met-agent
|
||||
|
||||
Compleet stap-voor-stap met code-blokken: zie `lesbestanden/Les13-Stap-voor-stap.md`.
|
||||
|
||||
Samenvatting van de 11 stappen:
|
||||
|
||||
1. Polderfest-original uitpakken (Les 12 versie) of eigen versie gebruiken
|
||||
2. Zod check — al geïnstalleerd
|
||||
3. Maak nieuwe route `app/api/agent/route.ts`
|
||||
4. Voeg eerste tool toe: `searchBands` (Supabase)
|
||||
5. Voeg `stopWhen: stepCountIs(8)` toe
|
||||
6. Voeg tweede tool toe: `getStageSchedule`
|
||||
7. Voeg derde tool toe: `getWeather` (externe API, Open-Meteo)
|
||||
8. Schrijf system prompt
|
||||
9. Maak UI-pagina `app/agent/page.tsx`
|
||||
10. Testen in browser met drie voorbeeld-prompts
|
||||
11. Deploy naar Vercel
|
||||
|
||||
Twee referentie-zips staan in `lesbestanden/`:
|
||||
- `polderfest-original.zip` — startpunt
|
||||
- `polderfest-with-agent.zip` — referentie als je vastloopt
|
||||
|
||||
---
|
||||
|
||||
## 8. Veelvoorkomende fouten en debug-tips
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|-----------|
|
||||
| Agent stopt na 1 tool-call | Heb je `stopWhen: stepCountIs(8)` toegevoegd? |
|
||||
| Tool wordt nooit aangeroepen | Description is te vaag — schrijf in heldere taal wat de tool doet |
|
||||
| Tool wordt te vaak aangeroepen | System prompt is te open — wees specifieker |
|
||||
| Loop blijft hangen / oneindig | Check `stopWhen` — zonder maximum kan hij hard doorgaan |
|
||||
| Open-Meteo geeft niets terug | Check ISO datum-format: `YYYY-MM-DD` |
|
||||
| UI toont geen tool-calls | Loop door `m.parts` heen, herken `p.type.startsWith("tool-")` |
|
||||
| Supabase error in agent-route | Check `SUPABASE_SERVICE_ROLE_KEY` in `.env.local` |
|
||||
| Vercel deploy faalt | Env vars in Vercel toegevoegd? Build logs lezen |
|
||||
|
||||
**Debug-tip:** zet `console.log` in elke `execute` om te zien welke tools de agent aanroept en in welke volgorde. Heel leerzaam om dit één keer te doen.
|
||||
|
||||
---
|
||||
|
||||
## Verder lezen
|
||||
|
||||
- Vercel AI SDK docs over tools en agents: <https://ai-sdk.dev/docs/foundations/agents>
|
||||
- Anthropic over "Workflows vs Agents": <https://www.anthropic.com/engineering/building-effective-agents>
|
||||
- Open-Meteo API: <https://open-meteo.com/en/docs>
|
||||
231
Les13-Agents/Les13-Lesstof.pdf
Normal file
231
Les13-Agents/Les13-Lesstof.pdf
Normal file
@@ -0,0 +1,231 @@
|
||||
%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 /F4 6 0 R /F5 11 0 R /F6 13 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 /Symbol /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 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 /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 18 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260609144945+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260609144945+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Lesstof) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Count 8 /Kids [ 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 12 0 R 14 0 R 15 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2195
|
||||
>>
|
||||
stream
|
||||
Gatm<gN)%,&:N_Cm%fg_2%:E)i!%=ta3CIN`,2#JFWRPHcltk6U.BukjMkQa"i6ZR9;WHW)VG*,HT='t=HJ&m^SL^t/c^>>_lNM7#_hdWLgrG!^4:E;j-sISO8=kE(?7:\-s-LK5>]9f#-MTa!=]<RgUF]]JA[5_H,,!j3o;;K'EVl"$j)a`ffjVqil(N"\$MZ/*i3Q(Z!>T_[b'.1ETLM7H*qk[VZ*5DPTY*FX]FRd2K=-]`X<X^q<@_q53/ssVmgrC_V_X1dp!+l8prZ\LUU_EdX3L$T4.X4`\3R-Sl3Vh(kUph._QC).J&s.EZ)kg('<_Lpl#J:)E_38jY:6?J]eW#KCBd#Y$2a'mqPO589#2CeFqCU*nDBFZl=?G2;/'Sbd(N@W3tHOB/-"]ctXdNcr.!NG%PL]*iDJEXIZUXD0BQ.C=q`>MVkghrC>*Vj,@1E2q9+B;hhPU,R1:t_@_dV`D.;NA3TJ&:4:=J[`c;8Bi%I1`nD:FHTPnNDeJE3S)"tZiRjDNf5_2VU^PDOW(R39N$b7JYNGeapLqHCWI`!^F`-iMMtp%o7N)!bU2Bcn6X:g.1Uj7O-M+9i5cTFO%]Tu>J.grDC-@JD-;1I@L^Yp-1_Rc$PgTu,98F2]pEH.o(.KL;b=>J7NVKNX@&ii&g(\`Og'W3`m-8SC$G'nPmaJV52=#,/TU0%sqQj<lOCB[\"`:B;.Z'qdE;k1$l+U4"1.hR%<&`L[I<KM"Q"<ic&N/*PJS=I/B*70a"n=;VbK$@pWgrE*LGP<V(p$aAbeBF#Qqr/p&-]J&gBpfZo/)2Lq04GX9l.dHjTsCQ*ihZg9pheL(*<nG\6(ErO[T=L0jnVug3>"_7gJYR5:MI`^tIgp$,=9F<Dhl^re$S571,/ZVYTs,5<a#]e#'+&LEbh^cF20.'>T4N#jh6dJ,PCkG?*V^Ye["lcnu>L/mLQ@cj<gTnCqRIKWFTbclLIrY^0I-JM_sAkm+V(UM0:6R>N7V(@8'Rf4/Wt\B^!XmVR,Sa((Xo:p\E8*CW3"hn,h.g[_Ulft5Q?p2>r+_;90Aa1O;XWWnN!^7JuHq't+a<-nb@Oe0h67f]tWY5^=C+3n/$mIel3lX$i/l:F#5c^.Hp;H?;%W/fG9s.,*t,3E/Zc(m19g8q.^e5qTmSKprk,YiMnS/'MNRd%&r"%5T.[1UkMP=`3O0]-"]"K3bhJQXW[f;45<X:XV(*sRIi";j$"(ihG6Z:p&Uf"$TJ`rh\_Q`/>Q(PW)o!1Ab&)@WTB/MO4/*CJ%sJZ0G.Ic;J._+C;>#!`hAM9K0);1\ZkhVG?kIY?B(S&tkLT'A'!"N9;s==&$N.9IQ'6WVWIR$_GA2]HHEV9M)1[aGCHnP94D3rfup85M2OI@-=nnjFYZRR06U4rc'X5,qNE@(<,6O5>D+O"HF6!R:$hc^-+T:Mk24FYLJWPXU'eGa(?(T'fa)j1CJ!CD,;U%C=["]2[6UYYFWoFhOn85X.Qt7_P;D6:d)u6HhC3cA9$tV#(,#.h2Z-$9\+a4<K2s%pQ2j8Cr,nbk"9R2U1fX`.El=8DL:U;^T[c6,Ln>VPL1;:&K*r%m[D/6)H7maM,A)4tJsD/+B<n'<trCD0W_&,FnHi37(NdLH=^EBPG*%XXrqJV6sMD@=]Hg7@;%EP_23k8G/\!mAtW:Z5;hR(aefb<Q*%WU>m)_X1&@*gal',QmX@2mR0)P9q7u$di0f\6^B5_1n?9Lm88npUjsC]#Z^QlF0<F$F#J4*8^p].-pHH0Y9I)%iEX7%OK,LP(.(P"XsA=h>\d*/!^.@!6u(Qd=<eA3KOG(1L;V9F.f1lXD3%A(Irh7>L_sQdiCtd%D^ag>ROm"d=BpRS0AgPP\lGrU4I<$5s5bm)\fl7Rh#LTHp-p[kopeb`j.1AdIKA%UFj&5P.kC9*>A9Pc*u?$R[=J7b.mKqN$X[4*Aj"+NhMK2Ym"!i8`=+0O0a%?S1T>i<VYk+T1i]0qWXZ[(i3&7'0&b,/Z-$,EHlq?f*//CBcI%+mYfM4B]&4fiQc:S+M,[<(jD1!`7?N>JQ[#7&C(=;f5:Rk4oBM]EA1E7)3:#8eZ:+%$i:Mdm'Zd$G7&hHB^n)41Wi-gd[YplEq,.\FV_1)LnDjrs4iM\&+hk_C7n2e<YfH/)(KHXj^7$u_X%Fe,,#q-.dI.$PC"D#B`rFM.%8Q~>endstream
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2377
|
||||
>>
|
||||
stream
|
||||
Gb!;egN)%<&q/A5FQnWW.%%LU(VachV.=EjRrr2CZs1M9+YeWm!]()^;NAJ?:CdF<,XkR2Wa$k+#u8h=p%7L29d>&>k3"iS\t%.%T5CkSROlPR/<;-mdSH(55L1n$G62\VQ5bk>a<<'Sq1HiN9[VM%r5a_2$:fX^"s/&`D1uC4Oor6;YQH@$/Uf[caU7rC&:.l@p=KoS"*rMh*=#)]f%B_lIJHpYOO]7?^XWg/2IG)F/*ticc*"3^NU1X=VFaO8hu8'&OZqts<qBOt1\smQb5#mK;o]4.rC=G^CROERQ>^GW8!>%iJDR/p+2S.Ff\Q1$=]Mk91n/k.8ZDnR]a]FG#7o>TK?stG,7gBdgEZ<5@tBKQaMbmI7%f_SYsTc,6qhnFO2tfOhu;taH^^lCVKNO;W7Y2"[B6]NLN`,;C;,^EDXVIl,I=5B)P0t*@O>&Z;,t\2.Zu)@Lg$+L8H*W$plO;#qq8JBle?4i25M&Db*nDb1)Dbfka_9KQC0./4Q5OP.Fn(8;jKJ>mq:nYXDbE:K9i()pC[MV`Ns%;Hq2(\ACiIbV3$l7P!hcZ$7"(fdp2(B<65dAe/H-\Nc"br,h?/<7B)JKjLI7Wr:`AAI&F#ecgOm1;'kW!;([9L,)NtaMA&V31[$ZZ6Ikg6QZ6BIFX]_1]Lue$hCU<g6'4]<;^%GaCs40,e%5e&P,d(!!p^M+ZDt=\S@QVCZY>LN??N_L/*-5(B*lkH6c/C&!*Rku\X$//GD]KmK=#b'0[o^m0tZDl"lh#d/9lX#pm\oNIDKX1k#aHKI%S=C<$eoSke6M?_._DKY#V$me^P&@r3Z$)r3b)BOgn\U/:6IS;*^?B2K[\Mp0pdL9:6*OP_]pflYC`@//Tu\A-*4tO\o^p#Qn$GW\7eOD["!C#VU4><E1A7HU`/ZYH2o.med*61]b:Uj]lEQkIt4"dMY2WN<a5@8'K+U\thE%ZpL4XOGW(ObSH^)J#QV!h<=$EHWOmS,+lr[p&:r.K=.fKF>/,aH6jJEEH<Y\rlSk!g=Z"W`QA1sPL91+PXC0-daO-r;II-uLr_.J&Pce#6k\>!ZDblEWU\ks)igu3jX?8k][U"6kOg7S"VSOZ=8^aR!S0K@`ep.,\[;CKQ&>L')[FXrp@Z:dq`7O-A8%+3:Q'P(p$pNGinutu`k6bj^Y(e5mR5H7nIA)`RE=Ei2Eq_J>plirT[`\6##Z?oG=QJAcp^_Y5&+cl2$<VBrDt.<.GkK&?Yb/o(FUk9C0mZhXj[(DPk(C;-(5gr4[tUT)R*VBELFZZY'<:!+5+daoNYlji#fY=W^TdlV0ZD4<p!ae<\.cg\BL?4P&dErD7Q9\ICm\1&52RHm*Uk]>)P-N1RT7^FrV?])n:[*Wu25USp!H`<qMS^h%NtA?Eg<-<QW!d\NHR$.h0!?!;X$Bl`b69q6:OL-WE-b?'a46P:[q<Tkl7R)*)-Z_Nl+aYgj%c>N)VUqN1d>Z39'0s7mmG=dFd>5X,7J(ZMCEOcfZ2/i%,rnL`#64#lQ5hMpT7;DoC0V\mOk='B3U6LUjoZ!lBEE2-TJ[1N!BrN"(+?l70LgqA[aJNr,jm`_uScuB)o%`VDu%&b>bQi?dCR5Ll=%@6-\FQ/W)I6#UIgh:5,CitaN*HrK@(-G29T#i)2mC*&VeFd+d%BG%7VR=*6<[<>J[7$ipQu`d!Y5Bd4X'.^<Sl&t>O=#%?@3^TQ-E_ts@P,=<bI$L'X8VE%ZC]qpClH&b1<kMAgj&?]V38B(KlMFZ\/;3#Lgi,HR\$$McX6gWj8TQYo7Y`b.CAp'Duq^;ULc!dRLb3?Nq!A_V("C,h-Sq(fC=k;<.E2,'7U:6LiYIL`d,4HLUNF_VXG&$Y*k3K#./a(`nh>q\Y%n0gU$7^4eApWNm30j9?gb#D?RTS;q>,!bq.6N.dR/LNO]j[kgioY+#(18JOT6SH[5nORkK!^^I`5FSN$Z-SRu_^eG%*E>iE>Il]"]'LOGchq'r;;(J;`InWOh%38)O&3?<&IY:_ILZs;l?/d7t/1k>i">d/^%)Z+<_"HhH)KF'3/GDgXJ_nN^o&2p^'E>MfmXd_%Y)*#[<ZWnqh1&dCdj%ddPcsgU7Q\osn`m\u?8VFcFfDY.L.&lE[k6EIYD4Gc/H?gfTY<-,Z#:'hZ!ZXD5S4DGZb4ue-aBj"c$KpW)Y8PWRPF`<L*;$2?1,:L\'q<#T;6;-/R;4\+<\]2P(s,j5BQ*uc@]j.p6(dRNqE<Pj?U&_A`N@[!Gum$3UF?64/[ZERNum@c^3WU3?mN3k!S(bM,^b=!NWnf-k]Cdh=2=a[$MG.qBY;+0S,Wq;B*4/JRhk4@Mt9FWg/25c)i!@d#hX#+.%PS#\5)MP/S!M47s+HS\6[^r~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2227
|
||||
>>
|
||||
stream
|
||||
Gb!#\D/\/e&H;*)EU]=*7Dal1)s(N2g3)5_QYo0s%bNn:Zp`Z+,uKpU]1LEM^8'O9-Z5/X5dA7a9j0YCpQ0UnO;'8DV=jjB"aG1s(G@15#+$j5%WUFtBDCn8nnpd]a*bo2@WT^&(m)Nk5>\F/)7UaX!Z"VjQ;>$ZJAWhp4Q+"oSF9Au$43cR'+2\P/>#!h3-@CPh&1,EO"m0Y+g0u3\&YuQ\:GG-Y=44#Rhm!\-YVgFDq0#e7:"f)b7nQlIXUf#ISaf_L)Zs%pd5iQgh9eDk/5Z7n6GBlU2jC!K$,[SHu(,H(j.s$/pRB#d@XuQC:.f8r$L!7-GUZ>p`17=/9n7MjYKB9CH9I`9e\E-@jgc,RjmW6Z'o5YDU4oWAYr*S:btX1[[%)J?_f6Ge06iB?UJRs>&@hO@V^Tk,E5o-CA\iSD+>eS*F9ES<Gf!<l?I0#aU\>%P07U"dm9d/!L_*U,H57:``ZQDZ>j(:V1_CraCH#WfG27c^TFdMhfc:jZK!3m)1Ip1#lBp.0<g.9jG9^t9R4?2@-WOgRi-Af:+fkWQD4JK0`)<qUdE+X!CX;QAN?rl4A\'pS.ur1U>r9[C"(-AmYt$g8Xumh]FHke,RD):9S`b/GPXF[G>_C"We;mZUPc1.88-I#YE@nQB.tu<RVSE^b,B,X$hgTV1oM+cOck5f<T<LMT:q7gfU9V%n?()A\1-F<*R[u0D8+nrB;F4bB#^ZM%l!)=;_DW6*ltMI&DQ_[_sZao)"/QV2\R15N7VlM<c?n2Y\)WN"=">L,-J2cIf:"^W[uZ0.-S9r]nEdu(0-<?p&W5dCC#kW5]4?o#3PQnS3!6d$]Mr,Zg*J%#@t7OM`B=nb0D-Tnk.pPOJt2Aq-Jdj0$2/QUUT7`?%)qG;^(S!N:?"91pmbB)_K>8P7;,;jO''FaU5q"4q>QOP\`.q%2;,\)L.X.qtF\O9er;Jp*S#&rW(B'j82Opn,&9)K21C@6W]oI4^pF;5O@@b1Z>.uUYI)::/?UmS-5@MOc=?!=Ns2rK"_Ia*?*%Q&ted3f@Id*FdO\L=9gMd2L;Ilpk(M\O5XV*WN2?\D&2NuK`aWS8/C0$r>D\J5QXEC#8C[jn6T1O36`1MQOcegq$=%<"MKVh:JTN/hMB>dS*q=9\`k^$W+a\6K.o<fESU."#'f'Qbs\BB7C[Vtft!V?)\=6X:nLk(eMmrSi0hLT0."*31tFqR(u-!0*1:)?Z&HVi9[?1%>:uV@la3MoTf@fHU6\k9e>)d?)](?hSIoO3(^$Te0Y]8QMap1cJ43oc,G7^U\IN5;c<_d!NA[,(l!jA\FGIsVSXZ50&n,W/]$Di^TDnZ!ah3-`%d0pam,R:0f-^bBD$RESCC[)Oc9a5<X`iTdR;6:B^A\=+#]s>2ffnm']srUE<A'T3Pjki=`V'2ID4C<ClKQ;);HH:mj2B%<AXFQu!pmQAn\'9.:Z#VrQkU(ROP5+\HoJ=qYIcd_ZSCc7E&XA@b<E*H*brtS__7tWIkl*"Pdlrug<Xon?mg;k;mL$IL3M]R/L0s;*#]-gM.NAWV*Q;:=\BC/E(a=\EET%":oj3?Lq>!`mGm7.24C:*rc/_+Vki927>#]&R\IPrFC)D@+*lR?8PN>?$BZ7K<J%0VlGR7]+!&l.Ecdi*0[2?3?96e>ER/MOKSXcQ6i,V8Mc`h,=s`^k6/[+#="8hE`D4(KGMjI45<H(?`#5YP%QAf@c9g%"Pt(c6*NsqlP`fJ1*<bbZ<IG;4imB@XF!J$#Xh9rRh.A./RR"YXU9JU)J5Feq+f^d<Y/tn]s$19qABMnjp/#I#;]j4GS^O5uo?]d;FZm.\l5Bo(eb*#LA(G(k0<DO@ML.">n>)>!X/\k./3cL@%1E%8.l&R[.FLlbIU%W_.a4m/j6mu3q-CpsflWeNF-j,qBRrCN`i1L^4H>^PK5T4I'>#[]4oca:[&Vt4_Hj[qLFb)jHkc@XlMT]VebKbIbjGSto<VR)o1h?)90@$'&>C/&c*;#E-[La@0%/!(9qWNXR@YGpqJ,QaPhF.5!gj>KejgssN&esITCGhL5#0@6_Q3+!joJ!EJ@;L;`WSHD`67Sm#TL<ZU?d'H7b9r*D3+;tG!^$2X,hTj\*nW<i/P:F&C#pYNRYP-i[^6*f=@l>^%:oV;j1cQb)5>5Wc"idggf\^X%3e#MCEaV?Q((YF8(5?C_"DIKSHNW7Tee*a^jONN(;tk4Mp/d_GDb!~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1804
|
||||
>>
|
||||
stream
|
||||
Gb!#\>Ar7S'Roe[3:N,0,Rd7M87gG(XA-/(6.[^VqCiF.,La(68TZ7=>?^[6mnic2DX:ar\&=(S>5Z\=qfieZ'Rs9Y2bF+"SA=B)#)pH1%a6.=9Wm;_1AWsS4Q+*JJ[glS+U8Xkp':0--j)[V6-)CtOpWCe#G,Hob"r/[I6oUg\VBp<&Ha3Y.\m;;1-^l:ep`qgS>0eZpd#I/mVi#ef"I.JNHaSurB`3NK/Gr;W3/W=o7[^='V%+sr-er:RtWJhV(i:h.ru^_9:0`Pi;=3ol$JtgQ-PFuEXS'If"_:8$RCH<LrZ/i\gU0,Y<h$F7@Ygr"ouDI#$Zb22f8p\+O[//B"gkke??nY;"1Q?W[bBYMN-=McXn):JlYr#Oi_3T)=%@$:1VeB37Bq';(A1ZSNV"&V],[!8P0b%2[H00%'W@[HX1JmV%K#YE$*\!84T6_FbRH8AE6<s2tfmM)[(h?<")i=HVZK`mh/$m_"T=WFtFs_(KF,Yi.<gFFWPWT#h<9-,Q&hKi+q&!dG@n\U:i>h??En>qtk('O3qW$WnQ/*ece-`m6OhhL_AAtk+Iac1?/C[4pbI;X0/_HHb\(`)>pbOg"K0'98(K6]aM(.HM,^fZgtDh3c]Ogr:FleWO+3kFtZiM_8^u(P%<j:kA_7RbI<>N`Wu]dH<TtUW2+rQV5&8VlT5QPV+P!C;V=D"7^$q-[*eU'gALPe*u+iYG!5V?8,Ln4Dc9o)?pGKH"p%_IHEK6YjeY:e/?45<29`ERq<Y:E>Eke'#33;uR5-t*,/iuDe5-AUpVu8AED<a%*5Eh(qCW,lC5(GjrsHMd'dU4`eNAj^>kg"/lpIm,<Gh65=dl>]k47NfqruLW0E$tRP\[]Xf.U?RJL8oa7@ZYBDH.k1Ffq2eGeXL6l][auW_3<5Ap4\=>*1m3d<s70G`i".7ItqLKMap41g^m<nY];B5AY6&4QohIU3.9CU3.un#G\u7DldW5`$nG_-84?L:ns&+Ooo'!SspG6.NX(`\8E<61]p&"d`A>.DneP_Q8D</D30Lo]r\c3cZN3Y6.P7$K%*,"2'jaGig!F+Cl;6*P?W-2C-=5*EdV-8PXS@V.`<mX\Y^E`g>Es'G:PcA/.G?73l*C_AX*c$S='PJHi3@^^<HqrMG'>+4@6("Nb5jjAQ87F=X+B^`;t;K$3%?.4\'4Jp76e7l'e:mDbE[9NllLIB40<PC0b^glonKc;'$i%>l$ib.fXkk@)=-D5dSRO9b8e-gW9[6ZG*=0TNe\ld9UkbdB1b:N:s]oZ$\6t,HRg^@aOUW<N2nAkGL1hH-KbucSdSLlLT7#`WcI`^EG>3mQ&%nL9'q;an=_h3o\n3_M<ZMD0.so("sto4jt,*P(Y%K)N/Ti]lkDJFfLP\`8',llh7s8Cu/ml2NDnF1fa@(Kg:ud?;kM*>fWd=WY)ZCM+aBT(..,nHC-_u]+_bXa&)J#!+h\cB6f..l1uIH2a,.i:0u8`_"ldWUuVUd<2de-*:X3;I]riDY#SmliYa/^3Sd#LR3(bOZSA>,H1#4Gs45T/`+W0@O@;.XI;\rA\?;rfK^XECn]`eU4/>Qaed%Q_*gmskmhS=Km0)s9#M&Yngj2kHV;^5Vg`c&9($gmuMH%p*=Z)LL.]14YWnX[L;"Xj]V=/@S06L=ZP/'leQ96eZaOobO5*'XTah>gFP>^9QAT]3PlL(08%2+&bnj6so*)iC)_smZef^?S2<Na`Z^;^b\);Ad7R7cK`>%4!X4EjkOXP'rS3HI9[rZP:orU0e]i4QP!D":I%E#Mkq*f)NH#>6"Ff`~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2259
|
||||
>>
|
||||
stream
|
||||
Gau`TD0+H"&H;*)Z'.s9fOme,CR?kUT[:su#fP(-#h(fWC\_H(:.Q`o$.Jq"Sn-#bS6G.nBUi0cSY"ZlIJ3BFg0i<h?2om?J8]$Rq;)>1J_:ZPQanes#Hqcm^B`!QS\XoK9g`\73P_Agq$0>DOPorYO.=:GcjEkl\DW*"$c=@qLY`C?4+IB@q8L,R(ALoA]"gYeo0`Q=r@.=YLJ5>(oDRnCq`FgEj'K\%STj.,o-#fV@IFu^U/#!+fj<UQ/;)nW5QC]uhP^S#Gbn@H^Zbg[Q%g6u$/j?<p^T6MWZ6p50Y<%l(WJ]FeFc14Jfbtb^;I8+C>",BnO,nX?3fC)MVoPlGBRkL!A.^uKG=4_ZRn90.6R!A'4qoL!QC6_*fW[O%GW(>6JINs"#mU2Mhduar][Yn/*N"Hq`42>*OEkcaLD'cWZC\u*S=n8#B'Ef1TC&?*?hB!9ffL]&;HarecEuUfSTpb_3pO*>$lLB+'*HmN!HCLbp2i/UjM[G$W5/MMhO5^F:o@oaKZbX-q01a40%mP;UThK_`N9&r9*<4\*l=#MZVe<hZLsL!n0dbJk.M3L)3Fa`u]:SDI/8%?o!9:/pW`I*`+q+<aaJ'S=*qce9l1Q3\N2M"^Yug07j%UP-086Z8i]in`C"fr#0n4&ksg8;7Hi6&4G2[#Nk-kQXCD)%h<SilFBmmHQ8^Te5[[NT/-M"O'gF9.7icV;pC/p.@<6Z;@8h^2sNoWFi2R?NIEuSh[>hAd;J->GB',Z.?;)8'J@sK.qm_>K\e+.H6%.AVQ:`K+%PL,:T#b23_jpk$4^(4fa2kqj;_Z'e,E@-L^#>Y^Hsp^e1p8t_!]sm&8/`Bh4rbuh4mXWJECpcdF)t#Ibd&dVKJ7m*59FEa@-U0S9Qs4U_#P6g#h%.()BnMN"1N>F2QoVf2skJ;O.__7@%G@@+IH#H((&rI%W;Uqh5HJUoBmD+u.p6rIO3RJOG'PC6N^5:IQJH;T)gT89n[GXVH+3JNbNNQgm4#$I@0%ZFs"`Sd7q%$F<;]U$9K,L7OL19cYE;KJf*he/Gi%)!;YK0_10DRId_[/&P!nT1dhM]!jBSYne:K)0VOCh@-l\Y+%D:0K396XQQ)in/uTtOtBm"Gi_*-,s-\#$^Khb@3,9K;ums/Q1G+kS/7GmPET4('l'Ch+l."ZUcKa\E[ru7QH=2W9WGn":*UG4-RZEQcqZPN<K?Y12k5$U6nQ5h39r;DI7N@^#d:DfQK3?p#HQ`VGA84FnThN?V[HO_jG*je%RI]q5[dQ??0-IZ9,oUpXtP<?b@L"&a'1)gkTrMW.!eA7AcX$s6]p;-.:P>$>#pgMK$>(?jVl>QB/59CU$3T834Mu=1\*A^U.t^6e:RWu/^t2,nc@]AV*t,*Q?l5Y8bIAIV]?;=4V*%>%:]5[C/h*A2et1,9F@qVM$t/0M@gR:&u[7W3*$bs+RMYYD1'oZ%oOum5?]3_21mF>:TPK>0/->$fd;q4c)nK-T+N'<5J3*Uq;9_oj8-uVaB[F>$^kX3DQEINcY=f7*jd.]mBnmTg1s3[".458ocL4t`uu#7C=IN\qE8l4_W%l(KO2nnJ(W9EG0P1rRj9jD/X3o$Mt_rn91=U??LISbQP$K&Cj<PL$+oD8q2T#@f8B8W9ZXlpfC92(IL-NYc#8GQ7c.(6UDY5_C:`NRLm-*or<^jg;XfN;4)hjM\2#bN]U(A>@@ntcmD*@)Jh^;h<a?FhF`*%?q!H[W,bS1AkN1f9_K3PPWZ#NI**KAKai9q=K6;182kDef]61Ngk/\)aOVdQQMAJB*l%UC'61%h=C4R0G\F:JM798mdE1.Z3_eC4&P0!`qfoGG`"2&ul3!6nQ,/#qNe53,qF&Jh/_9"lOr,+o3+;VN7gbA*tFk8XGMcAneoCNnQ*$8-DklPRR6@U[7+@"(O5B>qoEXiuKiq%,McSqK7eKVm9-0p5sXpSXjjAe[4Ufd,f5;"==Sqb-."c:-EbCuhQ,qX-n,kJ]_-V'd5c*ZOINhG'BWMHNG*k7D$c2NgqR:dIh%\\@4GMc`X0!Uj`m$)=9>(?53,KH^Lg,1MD[js(@Ui%#J?Ttc8QG*7Z]#UK+FOq2:7jk%FA(IE-cfh<95Llp=Y\i!)[>a^QQDoQ508>.?R4"`)el[(8+OV-m5H:PDmUR5gCYJ2t*1,/K'HL(1Kb6TZILu?M2'7a^XGFG:5+'1\@SLn[K!u(tGb$?:QM1eoOa\ii,<3?(c)qs>`9X\2]gb:OdP!<L(GFtuZN~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2529
|
||||
>>
|
||||
stream
|
||||
GauHLCN%rs(B(CCES&hCMV"88pnis%[QtPUfH>T1RfJ.mL*fD'D2P)7.CjHVs5%gP:K_k*oL&Qg`So$hT)56-$bt,;->T=l1OB%(P-er3+R'6"AoHn`D"@U_Y^JTP4VCM'3K[#[EqkF4jeO$F(OfmbP!UZ1@Gm:f8eHeT82e`sJIJJVZoFUI&qOs)Th?SoSfde6k8iO92H[T.oDRnPq,Y4k`J]E,8d@)?57k^BfdbNNF^/?H0CWY`VWlj-hI"WjHE!oTV0,Q5X%S_ZC7<n#5X/N#9=7^/hPfuoc)t`'ap^a->sEuLI]uW?NN0!^0,c$!a31Q?aWD,i"Vs33b7@kjHBnr^j7/=+Kk"*&Qaq^V8PMdJD:gg-[3dj%)iImais*YQ%c<3smJ+T$8',`190Uk_3RGP^M"ps7*T%O#`?fFOiiCTMZ'\F@Mhf>R]:o&-N1:3VkeehaP:*A$%s/DG'nt"96oLPWiS%\]@)+O<F>?%t:#g9;R'#n[_5:eJC%o^8lmc:I27=47Vto'PKLLpGmM_3B!2%!LC<A#(Wh;52O_7bI/!ZrqAr<t0-M@gW-&M4U/!dQ#$\HQi?_#TAK("Usd#Y3(SLOZA%'/%r`Cj4\eN)l:)V:4R%M@/plOBt].T6L&X#f*RR!63=FBbd)N39u?<=Jt4JaR6OWbl(_EIDTSZ`NGQ>*Xf9++CqsV7J4efelh'\n;+RFufkf7^r4`.1eA:`1TuF3Ee.M895YM0L6Vt---I?)&ncIqqU]e!^moODt#kh@Lq0$mU+[`B56>l5Rt:7+;_?*)Y^H0rT#kSZAjLpVU2ip'9YPD=fn:#$<7aVORNHDbYpEJ>d-M&^ES<Ti%7tk=1>1hOSDXMgE&qt-P#`T[pU)h1I;sRB=asL$em/RA+3u9ki0%U2VYX"Ba^<AgMUJf)Rrt0rDs;oR>ohQU;6$(j#d0g9gDXF?0tVW'4du7mN'O:XV??qqJL3c!*ZG"/i.Y,@a@<lCcBp%E_g*_4OC3-#)/q=D0DX0S(XVs[orf4LE8[aTAs+l%7:e\6<Z-2Xr+</Ued(uK^E22U@lG6JgTLS[=$tg(*=N^L=@M#f*]MJ@CIVP%&kqrH"krd<53#Qa-Ku!)WI]-c)+g!IKcO%RsZDK&;Ht#ej.FtGU:F1iLkf\aP*4-Zl-S)PL[U97K2[pS4etF]d:NRac4(QEUpK!FkokrNblj6S>J+@3o4Q\%ceSKgjJtoi<n.7E.#K'-9qj3.Oh\uSa#.(:>Tco1b%H?jua,Ma%\m((ac9&UKt4S-(\#)S/l$O6Z:`M>)lajg",VTQ!25kF=["F'ahp"W?k4k3uVW4HlK7(PGmaZfm,cP5<U?M"fPUegJYRDH9d4Yn:9HjJGMVZgR<brqG\^CTmJee(k)F&3hh?s_=aPLmIb`'*&sYL&8[?NOcbP]?@t/QHDN5I>A*koZ;g$E>;k1'omaM3D(AWc^?GJZIsQdWn!]@*Gpg2%CAMb0e9St2NkVFd-:YkPg4XU%VIp"m_4V=$=Hn+pT:RaMl=Bln<(L!TiKGf]JqgC&N_51n&r&.[;pB?5%7CVU1]$n-T:\4"h`^c@2L2aV^dU"K^`cbfM!p?XZ9o4DLXS!#d*#/`_1^dm;UsO-J:U[&lLLH/^0hVB'!kP!P^ZV[Uq:+fA0@th1:hjAQ\?XHH[E9,"^Mi3E6YQJAg?FnGL,O/CoR?U,")q3c5:Y5a.G6CIC&+!?ldQ^%6sL_^4]"im75#N"_?t:UGrLG+bj6ajiC5s>3l3)FImMCZ;Ui''<oj'7g!1N5e/trdL-3$0#oM)R!oEl>Yk,i&FRHVf^f.=R#>'VA3c,<MAlf&[q".pm@G<'^4NY5\Q+XWXl:<<B@PW5!mBq;@uhkIC_TO6\%94s,V+%>hL.&451h%2iQrb'l`aXoBSZ8$<,:\u/aKRCn[eGUj>TK&M+s$uAY!*AUOjde%bdJVJSa%VmY-"^DRX>p[N:Og\nK/M.#uOMh@,qo96-ZeIUF/B>cU4H>&5;O8'e]lV95+CZYT5jMamO#i<(ui5,cl]!&G%oAM9HF\Sj@cNdNJH1Ru;T[c'%^Rt;pL]gi"Zd&^o70d4rA)+T&QYQ7EO+"3pD*T,9D#6Xu)dI5`;Ro60=Us=;:kXpsmo1Q`u=7e0Bp!#Q$k:LioSLh_Tn`H.lXT?"ocBleKE%kY5:jH:s!JYBD.0UH&al?s/0sTO'eA",DS'bJ^bJ7E(&=Ud)`\.NoKorSToSQ-H"'=pWGtdQ0"ET?<[J/EBDsQrkE%s,oq4g"FmjDX&%bInU_aAI]c:j;51O<Tc)]NWHKA]TM*@X*"6TO0!LeHMn4L.iG2;MZ716XGR`&rLpVsX+tnb1e>?YA/K<?%36M[WN\KD&ObHY$bq0['jShl$Fgl`Xu_%oZN&F[)1\khpj>"ilrdr>QLM97">"9/FgNd8eSS'3)V=1H6"E=F^2DpD=H.TQP9WNe]Qd#O"'n]hSF]-K^krh8ntneo.V;1O`>G4?lElg#N'+ApU(L>G^of.=igq!l$1ZF8~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1914
|
||||
>>
|
||||
stream
|
||||
Gb!#]?$Dbt&:N_CbbNq!<fdk8o%&lhDU(h_l,ISdg#ElX`\n81$?FSgfC&u]N8rhU[Z_Bf_[`=Wh)hCZ*JK,??bH4uRK:&A^rZfk>ej_U>VZV<F80farNM[YKJ-V=Nj>3@!"0e<Jmr2ci-NTT7)2+A5#]/uWAmg+:`sGAmrKQXK^ii9"7n\1<t^n[Y,AplnIG/D+P2deSep0&^R1WKdgMPPQlSo-r[).I"CH[oX?)Nth"=qibb4^VVlsb&G;d9X1sN</^?>Vg;[l);I!B]:n*pUG/B?`b%)0X;[;jPK.Qp334Fc2N3L6Me`jLqeE"]&lpalT?M8?"l:Fr@h4e'!>#)m.0j#.E1U8N:?Bf>Tc`$Z3N>n:=T8`-_ND6>1N<43[S]%$6<0jE@\qheQ@1U:%Y;PF4kR;/me2,T@7N=4ju%lH.VY)oX5+,Rt)Y>jhr]eX3'I#T\(249-sB]clC^I#:'33oD^Pt0u;%en'M#sji[2H,NmBE]qpJ`+BIiR0*o2rQgni2r((V7XIAQu70(aHFQB<6TNqgYqgSCH[3_n>_krf2t;;1!tN=_G(Vs'55&pUNa*!4\*P&k5BR^&LJ[fHM%8#"Y;:YK)+4Bkf5`gD'nX8[QS`Wr@kOe;TttXYk5<I94pl5q>&)0../qpS3&Za_Y(L[>fTrIH)jZ)V:mmp*(-+YgFOkLqho4lf_:aP./=G4DQlnN#Z"L/HiJ@32oiHZW)WTun9!G3_'M7<p]::T;;?h6JaY`f5';k[RSiIT1B3c.gXQac'#S08I2f%\=YDc5%7<;GOtmJ.gh4'65=*f17a,@jr3]qEN-juNWu6IUg?"u2G$LV0h`7hbjo.S]L)2NmPnG`%DQ_:o:&Off`P5O#'-2i(\S?l=@Qo?"$k"9i[MK871=,L@98etn0]*K%_0I&o<2grp4fInTAX+q*lY0Vh/D"h[VC>to8fBn:^J-HMO0UU"Fc:F#356a[U`XCcC$Sq\M*K^cl)-kmWuet>g2-qJM8aZnJ_jrr.j:A-$Op%aTPkEV2IW1eb+Q?A)p?3uYj$b70I?NQ]#G!oZ>/D_C.Ii=ff[etjfkY;Y86l!c?g+diV*W:"OdQZmg@5`/i?i8HTtb1>p)]W"8CXW*9p"[%r=Co=JKWYh2$rp0stRm<qq?\-6l='oFrp`,f4#LV-^:$[/mDKFN7?rCk%2N^P9KBY2cMtR^FTf#D;n+S)jrS=A;3\2^GI$jqEi+(hF<QYEJ8K)@>&79"DQr9X(=sBG(]?St'<4Sc7`>*B&iD7X'Uf3c[AIc$\tG\$B9!ls5?%9Q*8"j)b>]P_B^43a$(]BrC^fUkcG-kD*VM\!NuqNA&[!C+[IVC?D&,AX%!dWXXK&1Lta0cAPl/*^rgS_m1IE7neaO;L)qb$q#(?A['uu;8tSCAJ?9,'6A+h>p241T.-S,;cK"18"i685XW`FUM0EUMdSB\d</`qLkH'E*S4(99+kD<CVAg6549opS6bV9>s34.Igj5187S1I+r&^8Dm!>K$n#I)T2[]1Uj@d[EZ8b='\%.>e8de'e1XOHJk,]2h)=L+:H*Ki<J.W_nI0c2o/sYm93ggU*F+DqE8g6%KfFU@c&K3`&4TAVYQY\_gB]c6V=SjR^cm8KUK+%*a[#5FD-.IaUhUIcPAQ\J;q:dd+KGSU;g!QqE4_`L:O`45C-G(2F)gmYI%oH&rt.,#>M$`3MP8aT(2prk:SBB"mpdbq;:<c501bVTX"":nWH5lcJG(O8<H8%Zf$EZ3&`$[EX/\o*R%jS$$eXJR1?s@4O=^9_Da7K[oLGFg<KZ+'=B*^*.S,L8Q$J(N7/]0FgU=<Gi7pbim0E5Gk"59Im\@O+0u@J=e)r?o*7XglmZgKKe'8[f`e%)8`r>Ed09''D/'bU2rjrp+Y-nQ#J%0"UZi~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1661
|
||||
>>
|
||||
stream
|
||||
Gau`SgN)"=&:N^l])4rob?-]Fn]R43P7=!lm0)EJWi/P3cj1M'7[Y-5iih5\8]V`FLc8=:0(MFoB282CT5N=idG$:Wdi@;b(f"Gk?op'6KC(B(`c#9>mX4CUa*.Tg&'PJ72faF;Y'GRl3ZRu[o-5Wg+g_:d"s/#_c,).A_4c!;?j@edk0H+IYe[`"ciY';I6[Auf"9.M*89+VIG^*/f@Tpe6SDntc&QI<*s*Yp]6:2?FZj?irLa\]*4l/T?4Z#&85N"s6=fR_9:RUk9H5F1o-%As)Db6`Hj8V8(dBquCk6n]81s\RA'h]`PSWitj=3o&*^dF@n=Ci67k:54LOI@@aO>",]NoeZSq0n5J^^.)Aj$=JWg<S@9XYdBi#.QQYFH&6H)f\W*<_EODNf0Zf!k%nDYDchXC+e1Gr7-olAnW3>DR*MEfNcdKM5*&13T9*jaI`sFfU$A*D9\>\8J-156"*=J(<Sk<%iQ;Jm>e^/9-]5/5h-\;fIZ!Ng7nC./'G*IYXokCbj*&RHsZs6hm=*+'=N=cNhd7Jl`_V;["gbE/g6eKoeri,G.h%MNpX@oU4[_.d?3):p'8)o%s!9T-M<hSR_O-C=4=V)^-s)g;^"$#AdLQ'ZPj_T:8i0GT>E.rX1Irbq\;CTNap:K7eV:_Y"C"FTg;Vk[fBN.[%-=86[P'P>#)T&7.qJP9K(D`RCNEO$#(1bV+lJ.$a@6bC3LuU\7umS4f5j;emX'<'3FgCP*sFmnMgn42PJ0n/?4=#o\Im$fksm)\t(18^^F0]&D]f+_$%=]$1.9N(Ij^D@&3ZD<4O6)3GLTeI]b`5q0$'IZ.&3EC&DPVM.bKLK&g_l4>>N>##kh]q%O5@=^mh&9Jo@Nj5H:/7"%!5i#&]M0<T6prfQHX[,G6.8i/?;WJPhMCLJZ?;\C'<1jSgVXkLgD(V#$=`kZ0q1G/q][O)=>WA7*i0a(<RN,g:A%Z()FP30p`T(9d>"QfqDeZr.@2W#-dTbElc6uXY-KKCj2eiUuVBYKB6]D-[7,l6ceAbe)3l\6jW/(mQQS=?j.dtW9b[Z&*TBXmQpTA^[d70g:-0`ID3F#0u,/>9u>TMM3?<tS%3"%_sW8B)I*oqgt<A"T\@/>5$XDGl@'/d!cpS?U(AFcg]QWkd%BN;u0YsL])$(g^nm!V$2]#,,#2+/1KdDr<Z([%R0QdOa/p$h)$bD49MK.h-IT+*,7^1NcUO5&kt)0q3^MGsUIddeF@<#I9Qcu>r7%JZBL5Us*ANNlLTn7%Yp1WVtY+N+(HojM%Zht4VL0KT[`Q[5&e,Hb5\]-2;Uq!_@?p#3&f<r%C'RQiSF[J+plZP\\;:2>pbXKXG3R9E;aIMc:Z6Y"<YVK&6qbpiCcU&O4eios2A=!C!mLh+i'!f,GApX\7]S8L)jgKrr1/acQ[^:SEZaZ!cfXNVc]7Ri<CR4m"BAeXn+>%@qR(G^506OB:ib540D%Hr`rGN/=5oB!3P4(]Rt7*\tkP9u`9j>/l:gV`FT<b(X",8K=.dA>ZPC71Q].,<PP$-rusq"og/LV9-%3s]^g#-TP;T"d`2Ds4:"$H1X<>aZ4,98Ye-d1aCsn5T$J0@.1885Z23b.3^31eQ(MEMm3HTH%FjaU_\Tm12KYFE"9RSmMs$?)^d53r]9IW<-B~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 27
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000144 00000 n
|
||||
0000000251 00000 n
|
||||
0000000363 00000 n
|
||||
0000000440 00000 n
|
||||
0000000645 00000 n
|
||||
0000000750 00000 n
|
||||
0000000955 00000 n
|
||||
0000001160 00000 n
|
||||
0000001365 00000 n
|
||||
0000001571 00000 n
|
||||
0000001687 00000 n
|
||||
0000001893 00000 n
|
||||
0000001977 00000 n
|
||||
0000002183 00000 n
|
||||
0000002389 00000 n
|
||||
0000002459 00000 n
|
||||
0000002744 00000 n
|
||||
0000002850 00000 n
|
||||
0000005137 00000 n
|
||||
0000007606 00000 n
|
||||
0000009925 00000 n
|
||||
0000011821 00000 n
|
||||
0000014172 00000 n
|
||||
0000016793 00000 n
|
||||
0000018799 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<e5505c20b0aa9b5d2861a5b760b291bb><e5505c20b0aa9b5d2861a5b760b291bb>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
/Root 16 0 R
|
||||
/Size 27
|
||||
>>
|
||||
startxref
|
||||
20552
|
||||
%%EOF
|
||||
304
Les13-Agents/Les13-Slide-Overzicht.md
Normal file
304
Les13-Agents/Les13-Slide-Overzicht.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Les 13 — Agents (Klas A)
|
||||
|
||||
> **Deel 3: AI Development** (Les 11–16)
|
||||
> **Lesvorm:** fysiek, demo-driven
|
||||
> **Structuur:** ±1 uur presenteren → pauze → ±1,5 uur zelf bouwen
|
||||
|
||||
---
|
||||
|
||||
## Blok-structuur
|
||||
|
||||
| Blok | Onderwerp | Slides |
|
||||
|------|-----------|--------|
|
||||
| 1 | Welkom + Terugblik tool calling | 1–3 |
|
||||
| 2 | Wat is een agent? | 4–6 |
|
||||
| 3 | AI SDK basics: tool, stopWhen, externe API | 7–9 |
|
||||
| 4 | Drie agent-extra's: prepareStep + hasToolCall/finishPlan + write-tool | 10 |
|
||||
| 5 | Stappenplan Polderfest → Polderfest-met-agent | 11 |
|
||||
| 6 | LIVE DEMO — gedeployde Polderfest-agent | 12 |
|
||||
| – | ☕ Pauze | – |
|
||||
| 7 | Nu jullie — zelf bouwen | 13 |
|
||||
| 8 | Afsluiting | 14 |
|
||||
|
||||
---
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik tool calling
|
||||
|
||||
### Slide 1: Title
|
||||
**Les 13 — Agents**
|
||||
Subtitle: Polderfest krijgt brains — multi-step tools + externe API
|
||||
|
||||
---
|
||||
|
||||
### Slide 2: Wat hebben we vorige les gedaan?
|
||||
|
||||
**Tool calling (Les 12):**
|
||||
- AI kreeg toegang tot tools (functies)
|
||||
- Eén tool per call: bv. `searchBands(genre)`
|
||||
- Goed voor: "ik weet welke functie ik nodig heb, voer 'm uit"
|
||||
- We hebben dit gebouwd in de Polderfest-app — onze chat zocht in Supabase
|
||||
|
||||
**Maar:** wat als de vraag complex is en meerdere stappen nodig heeft?
|
||||
|
||||
Voorbeeld: *"Plan een zaterdagavond met indie + techno, geen overlap, alleen onder een dak als het regent."*
|
||||
|
||||
Eén tool-call is niet genoeg. Je hebt nodig: dag-schedule + bands per genre + weer. En ze moeten worden gecombineerd. **Dat is een agent.**
|
||||
|
||||
---
|
||||
|
||||
### Slide 3: Tool calling vs Agent — een continuüm
|
||||
|
||||
**Belangrijk:** er is geen harde lijn. In AI SDK v6 gebruik je voor beide dezelfde syntax (`streamText`, `tool()`, `stopWhen`). Het verschil zit in **hoe je het ontwerpt**.
|
||||
|
||||
| | Tool calling (Les 12) | Agent (Les 13) |
|
||||
|---|----------------------|----------------|
|
||||
| Vraag-type | "Voer deze functie uit" | "Bereik dit doel — kies zelf hoe" |
|
||||
| Aantal stappen | Meestal 1–3 (ook met `stopWhen`) | Vaak 4–10+, variabel |
|
||||
| Wie kiest de volgorde | Jouw prompt stuurt sterk | AI plant zelf, autonomer |
|
||||
| Combineren van bronnen | Per tool één bron | Meerdere bronnen door elkaar (DB + externe API + …) |
|
||||
| Reflectie / bijsturen | Nee — direct antwoord | AI checkt resultaat, beslist volgende stap |
|
||||
|
||||
**Beide kunnen externe APIs aanroepen** — `execute()` is gewoon JavaScript, een `fetch` is prima.
|
||||
|
||||
**Agent ≠ aparte API**, agent = mindset: "geef AI een doel + tools, laat 'm autonomer beslissen". In code uit zich dat in hogere `stopWhen` waardes (8+ ipv 2-3) en bredere, meer open system prompts.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Wat is een agent?
|
||||
|
||||
### Slide 4: Wanneer voegt een agent écht waarde toe?
|
||||
|
||||
**Tool calling is genoeg als:**
|
||||
- Je weet welke functie nodig is (DB-zoekopdracht, API-call)
|
||||
- Het antwoord ligt in één bron
|
||||
- Het pad is voorspelbaar — je kunt het zelf in 2-3 tool-calls beschrijven
|
||||
|
||||
**Een agent is écht zinvol als:**
|
||||
- De vraag is open ("plan", "vind het beste", "vergelijk en concludeer")
|
||||
- AI moet meerdere bronnen combineren in één antwoord
|
||||
- Het pad is onvoorspelbaar — soms 3 stappen, soms 7
|
||||
- Eerste resultaat triggert vervolgvragen ("die band is uitverkocht — zoek alternatief")
|
||||
|
||||
**Risico's van agents (waarom niet altijd):**
|
||||
- Lopen langer — meer tokens, hogere kosten
|
||||
- Moeilijker te debuggen door non-determinisme
|
||||
- Voor simpele vragen overkill — gewone tool calling is sneller én goedkoper
|
||||
|
||||
**Pragmatisch advies:** start met tool calling. Pas op naar agent-niveau als je merkt dat je system prompt steeds langer wordt om "stappen" voor te schrijven — dan kan AI dat beter zelf bepalen.
|
||||
|
||||
---
|
||||
|
||||
### Slide 5: Hoe ziet de agent-loop eruit?
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────┐
|
||||
│ Gebruiker stelt een vraag │
|
||||
└─────────────┬─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
┌──▶│ LLM │ → kies een tool of geef antwoord
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Voer tool │ → bv. searchBands of getWeather
|
||||
│ │ uit │
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Resultaat │ → terug naar LLM
|
||||
│ └──────┬───────┘
|
||||
└──────────┘
|
||||
|
||||
Stopt wanneer: stopWhen-conditie waar wordt
|
||||
(bv. stepCountIs(8) of geen tool-call meer)
|
||||
```
|
||||
|
||||
Dit gaat door — soms 2 stappen, soms 6 — tot de agent klaar is.
|
||||
|
||||
---
|
||||
|
||||
### Slide 6: Stappen om bij een eindproduct te komen
|
||||
|
||||
Voor de Polderfest-agent:
|
||||
|
||||
1. **Bedenk de tools** — welke acties heeft de agent nodig?
|
||||
- `searchBands(genre, day)` — Supabase
|
||||
- `getStageSchedule(day)` — Supabase
|
||||
- `getWeather(date)` — externe API (Open-Meteo)
|
||||
|
||||
2. **Schrijf de system prompt** — vertel de agent wat hij is, welke tools, welke werkwijze
|
||||
|
||||
3. **Voeg `stopWhen` toe** — veiligheid (`stepCountIs(8)`)
|
||||
|
||||
4. **Bouw een UI** — laat de tool-calls zien, anders is het magic zonder zicht
|
||||
|
||||
5. **Test met echte vragen** — "Plan een avond met...", "Welke band op...?"
|
||||
|
||||
We gaan dit zo live zien.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Nieuwe AI SDK functies voor agents
|
||||
|
||||
### Slide 7: `tool({ description, inputSchema, execute })`
|
||||
|
||||
Bekend uit Les 12, maar essentieel:
|
||||
|
||||
```typescript
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
searchBands: tool({
|
||||
description: "Zoek bands op genre en/of dag.",
|
||||
inputSchema: z.object({
|
||||
genre: z.string().optional(),
|
||||
day: z.enum(["vrijdag", "zaterdag", "zondag"]).optional(),
|
||||
}),
|
||||
execute: async ({ genre, day }) => {
|
||||
// Supabase query
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
- **description** — wat de AI hierover leest om te beslissen
|
||||
- **inputSchema** — Zod-schema, AI vult dit automatisch in
|
||||
- **execute** — wat er gebeurt als de tool wordt aangeroepen
|
||||
|
||||
---
|
||||
|
||||
### Slide 8: `stopWhen` — de agent-knop
|
||||
|
||||
Dit is wat een agent maakt:
|
||||
|
||||
```typescript
|
||||
import { stepCountIs } from "ai";
|
||||
|
||||
streamText({
|
||||
model: openai("gpt-5.2"),
|
||||
messages,
|
||||
tools: { ... },
|
||||
stopWhen: stepCountIs(8), // 👈 dit
|
||||
});
|
||||
```
|
||||
|
||||
Zonder `stopWhen`: AI doet één tool-call en stopt.
|
||||
Met `stopWhen`: AI loopt door tot conditie waar is — een echte agent-loop.
|
||||
|
||||
Andere stop-condities die je kunt combineren:
|
||||
- `hasToolCall("finishPlan")` — stop als specifieke tool is aangeroepen
|
||||
- Eigen functies die op output letten
|
||||
|
||||
---
|
||||
|
||||
### Slide 9: Externe API als tool
|
||||
|
||||
Tools hoeven niet alleen je eigen DB te zijn. Externe APIs werken net zo:
|
||||
|
||||
```typescript
|
||||
getWeather: tool({
|
||||
description: "Weer-forecast voor een festivaldag.",
|
||||
inputSchema: z.object({ date: z.string() }),
|
||||
execute: async ({ date }) => {
|
||||
const res = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?...&start_date=${date}`
|
||||
);
|
||||
const data = await res.json();
|
||||
return {
|
||||
temperatureMaxC: data.daily.temperature_2m_max[0],
|
||||
rainChancePercent: data.daily.precipitation_probability_max[0],
|
||||
};
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
Open-Meteo: gratis, geen API key. Perfect voor demos.
|
||||
|
||||
**Vuistregel:** wat een fetch kan, kan een tool. Stripe-saldo opvragen? Tool. GitHub-issues lezen? Tool. Wikipedia? Tool.
|
||||
|
||||
Externe APIs konden ook met tool calling in Les 12 — dit is niet nieuw. Het verschil bij agents: ze gebruiken vaker **meerdere bronnen door elkaar** in één antwoord (DB + externe API combineren), omdat ze meer stappen mogen zetten.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 4 — Van Polderfest naar Polderfest-met-agent
|
||||
|
||||
### Slide 10: De stappen die we gaan zien
|
||||
|
||||
In de demo straks doen we (en jullie straks ook):
|
||||
|
||||
1. **Polderfest-original** uitpakken (Les 12 versie)
|
||||
2. Nieuwe route `app/api/agent/route.ts` aanmaken
|
||||
3. Eerste tool toevoegen: `searchBands` (Supabase)
|
||||
4. `stopWhen: stepCountIs(8)` toevoegen
|
||||
5. Tweede tool: `getStageSchedule`
|
||||
6. Derde tool: `getWeather` (externe API)
|
||||
7. System prompt schrijven
|
||||
8. UI-pagina `app/agent/page.tsx` met tool-call-weergave
|
||||
9. Testen in browser
|
||||
10. Pushen naar Vercel
|
||||
|
||||
Volledig stap-voor-stap zit in **`Les13-Stap-voor-stap.md`** (in lesbestanden).
|
||||
|
||||
---
|
||||
|
||||
## BLOK 5 — LIVE DEMO
|
||||
|
||||
### Slide 11: LIVE DEMO — Polderfest-agent in actie
|
||||
|
||||
**Wat ik laat zien:**
|
||||
|
||||
1. Open de **gedeployde versie** van `polderfest-with-agent` in de browser
|
||||
2. Plak prompt 1: *"Plan een avond met indie en techno voor zaterdag."*
|
||||
→ Studenten zien meerdere tool-calls (searchBands × 2, getStageSchedule)
|
||||
3. Plak prompt 2: *"Wat speelt er op zondag en hoe is het weer?"*
|
||||
→ Agent gebruikt nu ook `getWeather` (externe API)
|
||||
4. Plak prompt 3: *"Welke band moet ik NIET missen op vrijdag?"*
|
||||
→ Agent doet creatieve combinatie, geeft mening
|
||||
|
||||
**Daarna kijken we in de code:**
|
||||
|
||||
- `app/api/agent/route.ts` — de drie tools + `stopWhen`
|
||||
- `app/agent/page.tsx` — UI met tool-call-weergave
|
||||
- Vergelijking met `app/api/chat/route.ts` (Les 11): "kijk hoeveel meer kan met agents"
|
||||
|
||||
---
|
||||
|
||||
## BLOK 6 — Nu jullie
|
||||
|
||||
### Slide 12: Nu jullie
|
||||
|
||||
**Doel:** bouw zelf een Polderfest-agent vanaf jouw Polderfest-app uit Les 12.
|
||||
|
||||
**Materialen (in `lesbestanden/`):**
|
||||
- `polderfest-original.zip` — startpunt
|
||||
- `polderfest-with-agent.zip` — referentie als je vastloopt
|
||||
- `Les13-Stap-voor-stap.md` — 11 stappen, volg ze één voor één
|
||||
|
||||
**Stappen:**
|
||||
|
||||
1. Unzip de starter OF gebruik je eigen Polderfest-app uit Les 12
|
||||
2. Volg `Les13-Stap-voor-stap.md` — werk in jouw eigen tempo
|
||||
3. Test in de browser met de drie voorbeeld-prompts
|
||||
4. Deploy naar Vercel (env vars niet vergeten!)
|
||||
5. Deel je productie-URL in de chat
|
||||
|
||||
**Vragen?** Type "vraag" in de chat of steek je hand op — ik loop rond.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 7 — Afsluiting
|
||||
|
||||
### Slide 13: Wat we vandaag hebben gedaan
|
||||
|
||||
- Verschil tool calling vs agent — `stopWhen` is de knop
|
||||
- Drie nieuwe AI SDK functies: `tool()`, `stepCountIs()`, multi-step loops
|
||||
- Externe APIs als tool — Open-Meteo, geen key
|
||||
- Polderfest-app uitgebreid: van chat-met-context naar echte agent
|
||||
- Live demo + zelf gebouwd
|
||||
|
||||
**Volgende les (Les 14):** Externe APIs + Cursor + Vercel deploy in diepte.
|
||||
|
||||
Vragen?
|
||||
BIN
Les13-Agents/Les13-Slides.pdf
Normal file
BIN
Les13-Agents/Les13-Slides.pdf
Normal file
Binary file not shown.
BIN
Les13-Agents/Les13-Slides.pptx
Normal file
BIN
Les13-Agents/Les13-Slides.pptx
Normal file
Binary file not shown.
349
Les13-Agents/lesbestanden/Les13-Stap-voor-stap.md
Normal file
349
Les13-Agents/lesbestanden/Les13-Stap-voor-stap.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Les 13 — Stap voor stap: van Polderfest naar Polderfest-met-agent
|
||||
|
||||
Dit document beschrijft welke stappen je doet om van de Polderfest-app
|
||||
(Les 11/12) toe te werken naar een echte agent met vijf tools, een eind-
|
||||
tool, model-switching en een write-tool.
|
||||
|
||||
> **Startpunt:** je eigen Polderfest-app uit Les 12, of `polderfest-original.zip`
|
||||
> uitpakken.
|
||||
> **Eindpunt:** vergelijkbaar met `polderfest-with-agent.zip` — open die als
|
||||
> spiekbriefje.
|
||||
|
||||
## Wat is AL bekend (uit Les 11/12)
|
||||
|
||||
Dit doen we vandaag **niet opnieuw** — het zit al in je Polderfest-app:
|
||||
|
||||
- `tool({ description, inputSchema, execute })`
|
||||
- `stopWhen: stepCountIs(N)` voor multi-step
|
||||
- Supabase-client opzetten
|
||||
- `searchBands` en `getDaySchedule` als read-tools
|
||||
- Bestaande tabellen `bands` en `user_favorites`
|
||||
|
||||
## Wat is NIEUW vandaag
|
||||
|
||||
Zes echt nieuwe features. Allemaal in één nieuwe route + bijbehorende UI.
|
||||
|
||||
| # | Stap | STEP-anchor (Cmd+F in code) |
|
||||
|---|------|------------------------------|
|
||||
| 1 | Project klaarzetten | — |
|
||||
| 2 | Nieuwe route `app/api/agent/route.ts` | — |
|
||||
| 3 | `addToFavorites` write-tool | `STEP 8 — addToFavorites` |
|
||||
| 4 | `getWeather` externe API tool | `STEP 7 — getWeather` |
|
||||
| 5 | `finishPlan` structured-output tool | `STEP 9 — finishPlan` |
|
||||
| 6 | `hasToolCall` extra stop-conditie | `STEP 10 — hasToolCall` |
|
||||
| 7 | `prepareStep` voor model-switching | `STEP 11 — prepareStep` |
|
||||
| 8 | UI-pagina `/agent` met tool-rendering | `STEP 13 — Tool-call rendering` |
|
||||
| 9 | Testen + deployen | — |
|
||||
|
||||
---
|
||||
|
||||
## Stap 1 — Project klaarzetten
|
||||
|
||||
```bash
|
||||
unzip polderfest-original.zip
|
||||
cd polderfest-original
|
||||
cp .env.example .env.local # zelf invullen
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Browser: `http://localhost:3000` — de chat uit Les 11/12 werkt.
|
||||
|
||||
Verifieer in Supabase dat de tabel `user_favorites` bestaat met
|
||||
kolommen `id`, `user_email`, `band_id`, `created_at`. Die hebben we
|
||||
nodig voor de write-tool.
|
||||
|
||||
---
|
||||
|
||||
## Stap 2 — Nieuwe route `app/api/agent/route.ts`
|
||||
|
||||
Kopieer `app/api/chat/route.ts` naar `app/api/agent/route.ts` als
|
||||
startpunt. Verwijder daar voor nu eventuele bands-context — we gaan
|
||||
de agent puur via tools laten werken.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
convertToModelMessages,
|
||||
streamText,
|
||||
stepCountIs,
|
||||
hasToolCall, // ← nieuw vergeleken met Les 12
|
||||
tool,
|
||||
type UIMessage,
|
||||
} from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { z } from "zod";
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY ??
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
);
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
const result = streamText({
|
||||
model: openai("gpt-5.2"),
|
||||
system: "Je bent een Polderfest-agent.",
|
||||
messages: await convertToModelMessages(messages),
|
||||
stopWhen: stepCountIs(8),
|
||||
tools: {
|
||||
// searchBands en getDaySchedule overnemen uit chat/route.ts
|
||||
},
|
||||
});
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
```
|
||||
|
||||
Test eerst dat de route bereikbaar is, bouw daarna stap-voor-stap uit.
|
||||
|
||||
---
|
||||
|
||||
## Stap 3 — `addToFavorites` write-tool (NIEUW)
|
||||
|
||||
Eerste tool die de **wereld verandert** ipv. alleen leest.
|
||||
Anchor: `STEP 8 — addToFavorites`
|
||||
|
||||
```typescript
|
||||
addToFavorites: tool({
|
||||
description:
|
||||
"Voeg een band toe aan de favorieten van de gebruiker. " +
|
||||
"Gebruik dit als de gebruiker zegt: ik vind X mooi, voeg toe, etc.",
|
||||
inputSchema: z.object({
|
||||
bandName: z.string(),
|
||||
}),
|
||||
execute: async ({ bandName }) => {
|
||||
// 1. Zoek band op naam — we hebben band_id nodig (FK)
|
||||
const { data: band, error: findErr } = await supabase
|
||||
.from("bands")
|
||||
.select("id, name")
|
||||
.ilike("name", `%${bandName}%`)
|
||||
.limit(1)
|
||||
.single();
|
||||
if (findErr || !band) {
|
||||
return { error: `Band '${bandName}' niet gevonden` };
|
||||
}
|
||||
|
||||
// 2. Insert in user_favorites
|
||||
const demoUser = "demo@polderwave.app";
|
||||
const { data, error } = await supabase
|
||||
.from("user_favorites")
|
||||
.insert({ user_email: demoUser, band_id: band.id })
|
||||
.select()
|
||||
.single();
|
||||
if (error) return { error: error.message };
|
||||
return { added: true, band: band.name, favoriteId: data.id };
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
Test: zeg tegen de agent *"Voeg Band 5 toe aan mijn favorieten."* en
|
||||
check in de Supabase Table Editor of er een rij in `user_favorites`
|
||||
verschijnt.
|
||||
|
||||
---
|
||||
|
||||
## Stap 4 — `getWeather` externe API (NIEUW)
|
||||
|
||||
Geen account, geen API-key — Open-Meteo is gratis. Toont dat tools
|
||||
ook gewoon `fetch()` kunnen doen naar externe APIs.
|
||||
Anchor: `STEP 7 — getWeather`
|
||||
|
||||
```typescript
|
||||
getWeather: tool({
|
||||
description: "Weer-forecast voor een festivaldag.",
|
||||
inputSchema: z.object({
|
||||
date: z.string().describe("ISO datum, bv. '2027-07-10'"),
|
||||
}),
|
||||
execute: async ({ date }) => {
|
||||
const url = new URL("https://api.open-meteo.com/v1/forecast");
|
||||
url.searchParams.set("latitude", "52.0907");
|
||||
url.searchParams.set("longitude", "5.1214");
|
||||
url.searchParams.set(
|
||||
"daily",
|
||||
"temperature_2m_max,precipitation_probability_max",
|
||||
);
|
||||
url.searchParams.set("start_date", date);
|
||||
url.searchParams.set("end_date", date);
|
||||
url.searchParams.set("timezone", "Europe/Amsterdam");
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return { error: `Weather API faalde: ${res.status}` };
|
||||
const data = await res.json();
|
||||
const d = data?.daily;
|
||||
return {
|
||||
date,
|
||||
temperatureMaxC: d?.temperature_2m_max?.[0],
|
||||
rainChancePercent: d?.precipitation_probability_max?.[0],
|
||||
};
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
Festivaldata: vrijdag `2027-07-09`, zaterdag `2027-07-10`, zondag `2027-07-11`.
|
||||
|
||||
---
|
||||
|
||||
## Stap 5 — `finishPlan` structured-output tool (NIEUW)
|
||||
|
||||
Dwingt een gestructureerd eindplan af via Zod-schema. De UI kan dit
|
||||
later als nette plan-kaart renderen.
|
||||
Anchor: `STEP 9 — finishPlan`
|
||||
|
||||
```typescript
|
||||
finishPlan: tool({
|
||||
description:
|
||||
"Roep dit ALS LAATSTE aan. Geef het eindprogramma in gestructureerde vorm. " +
|
||||
"De agent stopt automatisch na deze aanroep.",
|
||||
inputSchema: z.object({
|
||||
summary: z.string().describe("Korte samenvatting"),
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
time: z.string().describe("HH:MM"),
|
||||
band: z.string(),
|
||||
stage: z.string(),
|
||||
reason: z.string(),
|
||||
}),
|
||||
)
|
||||
.describe("Items in chronologische volgorde"),
|
||||
}),
|
||||
execute: async ({ summary, items }) => {
|
||||
return { summary, items, ok: true };
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 6 — `hasToolCall` extra stop-conditie (NIEUW)
|
||||
|
||||
Combineer `stepCountIs(8)` met `hasToolCall("finishPlan")`:
|
||||
Anchor: `STEP 10 — hasToolCall`
|
||||
|
||||
```typescript
|
||||
import { stepCountIs, hasToolCall } from "ai";
|
||||
|
||||
streamText({
|
||||
// ...
|
||||
stopWhen: [
|
||||
stepCountIs(8), // max 8 stappen (veiligheid)
|
||||
hasToolCall("finishPlan"), // klaar-signaal van de agent
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Effect: zodra de agent `finishPlan` aanroept stopt de loop direct.
|
||||
Gegarandeerde nette afsluiting, ongeacht resterende stappen.
|
||||
|
||||
---
|
||||
|
||||
## Stap 7 — `prepareStep` voor model-switching (NIEUW)
|
||||
|
||||
Per stap een ander model. Stap 0 (planning) krijgt het krachtige model.
|
||||
Stap 1+ (uitvoering van tool-calls) gebruikt een lichter model.
|
||||
Anchor: `STEP 11 — prepareStep`
|
||||
|
||||
```typescript
|
||||
streamText({
|
||||
model: openai("gpt-5.2"), // default
|
||||
|
||||
prepareStep: ({ stepNumber }) => {
|
||||
if (stepNumber === 0) {
|
||||
return { model: openai("gpt-5.2") }; // planning
|
||||
}
|
||||
return { model: openai("gpt-5-mini") }; // uitvoering
|
||||
},
|
||||
|
||||
// ...rest van je streamText config
|
||||
});
|
||||
```
|
||||
|
||||
Resultaat: significant lagere kosten en latency zonder kwaliteitsverlies
|
||||
op het denkwerk.
|
||||
|
||||
---
|
||||
|
||||
## Stap 8 — UI-pagina `/agent` met tool-rendering (NIEUW)
|
||||
|
||||
Maak `app/agent/page.tsx`. Verschil met `/`:
|
||||
Anchor: `STEP 13 — Tool-call rendering`
|
||||
|
||||
```typescript
|
||||
const { messages, sendMessage, status } = useChat({
|
||||
api: "/api/agent", // ← belangrijk: agent-route
|
||||
});
|
||||
```
|
||||
|
||||
Speciaal renderen voor `finishPlan` en `addToFavorites`:
|
||||
|
||||
```tsx
|
||||
{m.parts.map((p, i) => {
|
||||
if (p.type === "text") return <div>{p.text}</div>;
|
||||
|
||||
if (p.type === "tool-finishPlan") {
|
||||
return <PlanCard result={p.result} />; // nette timeline
|
||||
}
|
||||
if (p.type === "tool-addToFavorites") {
|
||||
return <FavoriteBadge name={p.input.bandName} />; // ⭐ badge
|
||||
}
|
||||
if (p.type.startsWith("tool-")) {
|
||||
return <ToolDetails part={p} />; // expandable JSON
|
||||
}
|
||||
})}
|
||||
```
|
||||
|
||||
De gestructureerde output uit `finishPlan` kun je nu prachtig
|
||||
renderen — geen vrije-vorm-tekst meer die je moet parsen.
|
||||
|
||||
---
|
||||
|
||||
## Stap 9 — Testen in de browser
|
||||
|
||||
Open `localhost:3000/agent`. Probeer:
|
||||
|
||||
> "Plan een avond met indie en techno voor zaterdag."
|
||||
|
||||
→ meerdere tool-calls + `finishPlan` aan het eind. Plan-kaart in de UI.
|
||||
|
||||
> "Welke band moet ik NIET missen op vrijdag? Zet hem in mijn favorieten."
|
||||
|
||||
→ Tool `addToFavorites` aangeroepen, badge in UI, rij in `user_favorites`.
|
||||
|
||||
> "Wat speelt er op zondag en hoe is het weer?"
|
||||
|
||||
→ Externe weer-API call. Agent combineert weer + bands.
|
||||
|
||||
---
|
||||
|
||||
## Stap 10 — Deploy
|
||||
|
||||
Push naar GitHub, importeer in Vercel, env vars:
|
||||
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||
- `SUPABASE_SERVICE_ROLE_KEY`
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
---
|
||||
|
||||
## Samenvatting — wat je nu hebt
|
||||
|
||||
Zes nieuwe agent-features bovenop Les 12:
|
||||
|
||||
| Feature | Voorbeeld in code |
|
||||
|---------|------------------|
|
||||
| `addToFavorites` — write naar `user_favorites` | STEP 8 |
|
||||
| `getWeather` — externe Open-Meteo API | STEP 7 |
|
||||
| `finishPlan` — Zod-schema structured output | STEP 9 |
|
||||
| `hasToolCall` extra stop-conditie | STEP 10 |
|
||||
| `prepareStep` model per stap | STEP 11 |
|
||||
| UI met tool-call rendering | STEP 13 |
|
||||
|
||||
Plus de bestaande tools (`searchBands`, `getDaySchedule`) en patterns
|
||||
(`tool()`, `stopWhen`, Supabase) die je al uit Les 11/12 kende.
|
||||
|
||||
## Vastgelopen?
|
||||
|
||||
`polderfest-with-agent.zip` is de complete referentie.
|
||||
Zoek op `// STEP N` om direct bij de juiste plek te komen.
|
||||
Reset `user_favorites` tussen demos met `supabase-reset-user-favorites.sql`.
|
||||
250
Les13-Agents/lesbestanden/Les13-Stap-voor-stap.pdf
Normal file
250
Les13-Agents/lesbestanden/Les13-Stap-voor-stap.pdf
Normal file
@@ -0,0 +1,250 @@
|
||||
%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 /F4 5 0 R /F5 9 0 R /F6 13 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 /Symbol /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 19 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260609144810+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260609144810+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Stap voor stap) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Count 9 /Kids [ 6 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R 14 0 R 15 0 R 16 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2296
|
||||
>>
|
||||
stream
|
||||
GauHLD/\/e&H8h>EPPDdD.?&26qZaRZq[t;7@#iL!u(qT=f-$rZD:*5Z?=fLhiW6X]:63fitkS5j5'79k>%g\if!*J?ifbC!2gVI."f>=W(h(F]S#g,n\lVE0ZW<@/D%6k#/mjt+(S:p"tk`$Qq+tM*TB+<fIeJQlaafMT&WWP1's-\"pNuldrB4I0'0qb)tPX*(rN1e_M.L1k?%Afauk`@auVAcr't<H5qAiOXd2[TarLtE"n\tspiHhW,u^3:ZQtCcA!+l@<kseO#L4s\XaH4-P03,O%-B6gTbci?[;AAqa*(LKh@tDG%nVpMY't(jfD[UcI@qTDmkKp3,/GbpKLLaXE=4^5Dj1]^o+m)Jn1q?t7!RZ&AdW[Y'6<&8XM#A\(sN5\(,7hIC2Q*[LuhGD)Jt^bV,Xd'1D1OqgQ>>ber5uu2R?[?0MF;J_TtigqGO'o4C`!]h@f+bbX12)iks28EA8BhcZ`up9$;4d^,(P3Y0#MNGNMn'c!nqRo"DSIHuDKu<h%bpp:l#WauJ@E]!QV(-B(k6';Wo(od\pMH@]i;Pp$Da<i&=l_3ig'Z'Y\)RV8.MV\/i%k!6$]\Z9CMbl^ZN<7?K-=1^sb7[5!mr&T\S17Q@t!3PY\_HKmd)_s%GHg;(3TUjtIaJ/-!AR$3K/%!5l>_HhA'kg^#$aUH?@[R"8*C2Nue0_DS!:0G-=Dg,&XXjEQBa<UqS6"B@.FB^#qrHYL=u!U(Q&"R%d5k;e?.F@Hfg![O0JDLs-EtI8N8C`t_9Af;+gZqLRR3/Yp>spQepPk9A0hinMP8r&;&%%!IEjssQd=VF:-?XRZj#f+=NjCUP-8:o)DX=8)30AjA,Z.;Fu4I+*,D7+*3k7]<ku-^J^$7QiE%"He84-]!c6h\7"Rf22`h^Og*k_i!U=P3_:u2EE=sf;%Ul05D&1DE"['rk?><q@AINX:6F),P!=sVfA(f+L'S0\L#C9"qn2V2>KL2DnllIOp^>DoST#:Y0a<g%X!tkc"i/0KAM=M>,pVV72f)mD)8B*FMkI<[k%+,7D2SL9>Y`O8Z6?Zh5Tb*^b:5#3u,Z]:@r)"_ZMh;(_-qmWfUP]l='@@LI?#BeSgstE'/C@duedgS!C/T,L8rh^WiN1Ri^B:!a?W!Y^X(#"%0pSU9^Gt(..tR9?e92$MKA.1R)p`%WhXH5DVWXD(.cbc5eVAB\T2O2ghC6-d]I8]edF=&pTiJgD6!/uE2N'YIe.#M\Us$8meJ8`KePd$C+[TWY=\Am2aYbH-9]3(;ZUkq4hCVVCH`%!FruM22lKVB=coX`_D-L!bnj-kd)27\Z&`QdMnST^-gm,g>W14][LuBb;4:UEN-k#-I/iUVVP:16O&-j/sSO?mbZOtjEfWT+CBK/t8hlP&m>JG\BLU22liqEd7_d4#.11tfC;.G[K\fH,UfsT[]\o$J#RSenl'n_HnJ%DmXEMs^$`NSC`#!@"_B.36a2>Ii;RZpjXZF`de<5:W.j2B?,GG/n>NnoKX[;8ASB;^mE#F=ae]@QsrB!3[cCcLe[KC#*5=dTt5&t`R%dUdD3biHW2ErAW89;LEopZsh:PPBF+enaZ-PJ%E#H'+o31E!sE$74.[$0Lqm53uQTIW^4:)s"sn.`/o_h;`_6eGXX,T@cEc/5q&ZkH^.*YGu5uI[*>Fj#p_VmG3itWa>!qG>n?@0RJedODeqW;FWNE:[Lkfn^d_E@=#SFjMhVIadMeA\i\"-7HPs%k"2BE)L9TJW(9qHAJB!<`TYsI/T[?Y3Un1(eMqD8AS7E?Y/$sk1/54cE3g/O:1`_Uo).Nb>p^$t-H=gU$be'b5%=oK#>;Yp?m?rq@[Y?.^_m?(iKpBQBQ:'/E@mJ&MFV$j.%H@Z/A\4egng'u7l7!D%N"jL`_1GheF"=)oeB'Sk6P7GRG28.V]I6;=T>O9H/bK+LHRr,m2jd#pA7g[NNC[ic;k8leZEHPQgKQX<+)quk;@9=)s8G?L,+N<Y"m8WdD>X$hi@lp;f%WR/B0=r'EA2^$s>$Jl`GfKa',f3IADY"XlQXkA?K,3qZn.h%=[2LHo4h[8_ZZ;+:BV1G//o+m&CG7CopJ21d'<*n>RHR7=N%L9$ohd\>)Ko""%#WrcgOr/ntUV496KWUmeGE(.hm-OfPL*R>5LT-JA^KX2M4lh'.'eA;4r%+gY:fg%?X2@:5os)sELj[`Wb2<uqV;"1FImP;!L<fl_sDmWjpH"%@1h5&MURL;t.p[J"mBIBCN7<(1uFj*SN3SFj_",&L8(e=9*FOc4Q90j0`D/$B[LG~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1572
|
||||
>>
|
||||
stream
|
||||
Gatm;gN)%,&:N_ClqC`MUk1iC,XK1&[l/(MF_W3uSoGk[Q=s2B6\85$YO;P*Lt9Q;F=PD0"RR8%^#1I&!l_'Qs2Q(/2fk@7E5?*>^BKr;%eEKcId[,Q#RabUI3>,S?Gq@^Sp<T3E\d+d&k"9k6-t^LO96:O&L%k'cn>g`&5!,8Y8e?L(sl)u+N!dR&KLUn,[_%!c&!MFd@l'5:DeBi$!MLCZ[!]2>.AEG4fZO/FoB[Bq$&1<s8)F$Ia;)qMV!-F_H1$CGcfqnYn2B_#k-8DV0ScWQS?1,q($N=!@[,i6m3'_K3i7p^c'M3W6@>]FX^B`-@lVF^0i.GfEE<B_P\@]0U?AD@s"?u.H>h_c*Og7Jl=!A1AUY%nCfpWC_W4os+&;fPIS&o=DqpnNjT$2j?(0G\BXQ%dfj^n9I,OjhTjX1[&M)4_L*WWS'Knu:1]0>:r;ndm:Q1j"!ec&q,1M5*+2SNd*ZtZ3mUj:(-*3TgR>6U29.%k/(53^:L-TZT=gQ2H+$CgU.3"55tht>K=:(MLo7ogn3cLWN_jQ:MGb1p<**82*PTAVUsj(Fk]iY/WtC@D9#K%n3"@(WEQ!krDIS9/C\IG]ZtOX;9Wh?l@A>Ym9Z+5:fi"*q1g?W-VUs0OH(;mO1a=E"BaF?k7bBV"F&cq>_/!/Q,0g-bfa,"*b:jhO)BJVUs6^ld$Geh=%#2(Q=b.;9+n55E%e3uZ.Df]3%Im@(="$OF*XP`[3^I4=Jmtu]OA6b)(f6$GER)?9Zo<5-dK]5>9HsaVe\LH3Zbr]'f!]QNbPY,S)X^NQb6Om:UZ@sPLC6lH`\ngr;E"q:8c?&jAAlVVV8A2OH3>1BLLHCKIP#\%L4tZbV*:^VTq$kT("(^Q:;7Q6B"1^q6?RHrofVA(#;3[?W+XJ\+OpBXlF8)R@dl82K@0Y2@T5"cO+H`0[f6/;p2=GRm2X5)DflT2eZn[6,[rA^i^/%\H&XT0fgAY&Of,^9DR&T,H't%:RM&jih=_j!pIX:s_SX77;]&=-HQEJV``dloLs!.orKNcq<R$:Ua1Vr,irqQDoG4ia>Gp_r*SE_7bd'6o=Qlj=TGpZ)F*cSdFk=p<X><h+m?uG>^3`^>]'fOeGe_lKk1X*M%rHKE'%Xe>'hmJKq6!WW"k^GS$8tA;[FW=9Ch?VMc7=>%;+b&f%VaAX\YXn&/N)%7L/4g9&YJ%CgSsaMP@n\qO.W39MGaB1:ipUhV+NP/YNeG,A>N[jCljpJ$^Dc%<b&d$[rRC4U/c=MdK2L$b%<ook@qZ`TG/JW,g/Qi?V]KKD.JeNflgL9=>+3n]Qk!'>Ul`'nj_n3>6=7+"`(MJ+QK#E/70a)Ag1_?Fsc"D)0sOW$H$@r'V3io:B\AIs.\DPl(u^_Wf_Uqf36"7=VNYp,gFE%fgVX9n)O=/T+:(UBW<L-e7Utjo\It*p1I^ap,X`l8'Q.)cg7Tu17U[D>ouRZj=A^%r)P4g;.l<tG5<i%6]&_K7Sm%7/e?H"1!+1G+"!DY($_-"VokMB2'*[*e7)T_;Ar,Xjp4r92<#n0SqAL`V7(eNhQ-"I7DS^8ilb\O~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1672
|
||||
>>
|
||||
stream
|
||||
GasapD,]1K&H;*)Z,`chBbX4+A=8o^%qq?mNWM^EHT1+,C(`hf3d._J.)"TSho`,/AT78#(KpY!fba%Y.G4Elj`.^33:L?u7"I,_YCU`2_*<[ZDr"TM8`k$o`)3u'0d*&t5*4T&#ma+ua#j^OG):6"E(,U]ScXXeY"N'A1gIf$-kKJ6Y2V\\'iN-qj;fE^_kf@*;L2.#COXFQo8'L$+jK;-?oWei9!tT--T2^B!.P<-PLGR+Gb<jVI'uSp(bU\>cP?ZeN4H/WJTprca6?3/]AS/>s-6_p"-djr4I<\7T[P)i_-NLAOIN9`lR]]*Oq=,mS3PD_%?P*045gb=3#!B@&Sb6?U6XGRASkp'$dD;2^i9mN(3eTW_X1HAEFg**DQ%]1,W<-fbn8LMR9ApEUHCl>i$D>Yk#(-J;F1KRGYU,qA^HG[<-K![Pdj>&4LR=*]3B&_fc?ItDd'<OI&B\_3O9K%),B<uH$seQ_(3FS-E7Q7%DR2,A!BQ(Ue>NX'TCe@jXSL^q!WK0[j"/3+"/^0!+oqcVmF695GW?@J'`H^@PGL`_;Gf3Y)kM1N^4I6IJp;3PB2]%Ctd%reR?$aMFm8ge0A5hB#-p'q\\^U@3!%:mP>bI#r=V_[Z*3OR,IY4-[$Lq'']'PTFl'`XM=$'QF_TVSR^O?+bf/H#d;XQ5XmnO0M`6/*77;>X>F0UC8:OF.YkN=Q(j=YSY]!3$)=%uH_ao*I*TZ2J,gpL,`JCIq(7P[q>BQW)X`:nGicHiLbk!?66F40:5TN??a%>I0\<)^@e_8<L$+?G%Xl4hHOK[5SbGH1P*e$L]J,3$raU,Z%dnq]J;,A_g!V5boINKMBa=gBZ?1g2,s&R]-f_u>bHm+Zj2p9Ao\s]e@"15O]Jr.TV9dc%9t9T\B-:EGl1k2:jD]KlIX4,jr+#X`]RBZ4-rl-1beH%!:GCTh3B]+DI<;G=M==J52q[Nf%[F].'ed1S$AJl9XYBFW1(;%t+dp1>3gg^[R_[V@e.2bO^>4E_ICn'U&a]4&5K\!'/*_910/qjtXRdJqmVUbHFdqupIk^r\5^4A?"]k(rng.(qi$Xb#$4Err>O,i]?cN>eFbqmkoF\!gl!:pmhU:KN]m;16K:SK30bf<."M9*=Z:.co)Pm93'Cg)RG(/%^AbN=I,^>WpF.HU3;2;*!;NRFj=KIF^%>'aNCW4G2cW?QCd*1f%N^__8fNWI9MfeW+Tp*178eMP309X7g_NeR^P0)NN[)WCQo6o<H1h(%<a`Wuq('e8'cN+K!UPj?T_fW?6o6%,X7@b+*`!:'fK1/n$Z?0Ac;WKJa`j&K&Y%lR7m&dD.FVnls*d^8<@:_.[BJ#>1@\"ffCm#uZ6[r8Dl+,)@(qf\m@_OjhQ)_<#F@=k:R_BcAhfeI(6Q&p91N-@>Hs+(0$r;i5%$0ukru+Ma@MEH9qK`m2)+=:9\4X$Acg_Ja=OYY-9lE*EHWtS!fqS7(8VV)f\Nl>(:1-]&'4-'ufMY=B9&U.Eq$OneV985\WOhNDiI('k8)[aB7>9#O_nA&@9t3MD315^Y4pARsr#)BeNo#hOXj#IdGJakD(`WA;Ef7^qd`0FP/&gf<K6stL4?#D;mK22(p%&B*!7C(l!-W-)Zeuu[#j`r6`ff^TgQpB2VuHH"?\qDJ\$^^[-WpBN(oqP(~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1781
|
||||
>>
|
||||
stream
|
||||
Gat=+CN%rc'`G1AEG)A?gUR_a;%f.f_ceVUJo,@[\EtsQBOmNSPfTRlaj0b(J);RFYag"S[O.tbRQU?6gnu#dr*f$nF9V/M$:h!&(l.>WQllI-^VS%AHg_OfnjimKHqF:(",VI'&'WHP+:(OUo9b*)&Wr1U+W6CtQGE#q'X7q&K#Jsq!Js,]8HD5b+I(h$kC!t`Nhusd_D&]R^>UFk]pOH^4/?$Z+rK`Uc)!I#?P(A3."LOh`\1Z43J1]2;O-L3eiCCGN(U9'\DTK$Ve>M*g8\D]Q$EYaCR@;k#'uq/E"YYbg'9dEOu7'`2Gi*TL\3-"AjlcqL[t`Xf`Hl?AJYQhpJHfijUjsb*26']CTY$Fa[92;%ra[dPp`<)<k)AW4X$df\6J7R&/$I+e6e@*T%*)WI)P(H532[MR"u#&)f-T\E-jVIh$P@nhW,_&Od*P?Bno-$Q;We3W9!`NA(hp[h.?>JH<6[G-Mpa=r9.*Fm"1N9fn;f?CGCZ+j!"1`J<D\MS<hC^;gk*N[E0mu*^bY?E&%?6f^Bi4n-P/-cliD50oSf7Xtj9&%ik3ZnKZ2FK)hZ*=tN%>!bP"Y8*^MFUd.=M3)6KjO1+`(;U*E4]2=<u#ehB7`),PlKKDQIldIQ6.S0,N%hLq)AT\FVkr(mEJ&4hDCkApNglTqe<0`WoAABIfI1X?ah6LM'WQrsC[7ZJ<,LNi5Pu`O[ni.b#i!19@/'Ei]V(>)=RB0LsN%Z4^:uThMOcU$nh=1CL"'tEIWqoqrRgMk:jbG#`,i-kP@.<5I(qN1e:O^gLiogY8)?/(LVY7?&6]iXp\jkI)\5KY9'*I^/3>?e2/^a"NY87]S$jfM(cZXW2\gT*("S8EUh(9aqcPUMZ@=c-U0a@((#pctd`^..<-aTUJ"#g(=f[n[W!*L/P\epJZHoW,+a>%Bh%Jg($b$?uc1?;N0YeqlJPQ!u_,g.oe&]:03^u^2e5IGsOHZ1tN81>--(4F+`>@km5%@q'.OoUF2hN]cQF(M-<LOm??8?hIJ$tft?FmGUETA^`9$]MEC+Ch*ah4mKWW+ofLFFW?Ab@``e^?b%S*>DUT0_ModR&f3ZmY9?u*/edC@s^/=?XWDhbd%#e/hXD]L8(:`SA89).T9dCJ.4]T>I8Nu^a+gDNG*\m$0H!!?R#u6qoCu$[+>%XRG28Q8?uQ_L+Z6+9n\'WQs*3^rT@FCIFn&.JRmKLnR+Og8+ct8_cK&GS:%bO3A<"t=ao/j0PTiG>o*b^R_'^n+gqt+,sRX+6Y?Mf#=ssTY9X_4#Q.-\kD7S,'B=P/],.bNAXP+"?psnE*.6%UfGtqA!t*SXbXU,l2+lO'1P++9B=Mbf^^2u.Mdp8%_e\"a"r^A9C3SQN>DL6g]Lt=E^@)0`T=Z1d_n-lNWL\tRDgoqD``#lHfDfNR]NQdDA"\9>"jZJ^C![h<f2IR-,saJbXKq/d2l2U5F/h)T#)'qoTAJ4J7dT^n6XZA,EWmgMM-pPP7=.UJYFC2=G&:#3E2&%J:_EQD[`[M^O.7]*VqPS83#3rM38G'^i)4Y9^A>NU:NP^/UJpU!?XRr`in*&q7,@/^Q`>A4P3PZ-9:RdfAI8T:?_(VE^Gn<#rH9ilre8#Pq)`I!SGnlg8gS%>kP"3RnD*<"UoE2beHD?C^-aZ:Dq'Q2m`W!f*rh8geW?'#1eEst"K#@4OC/F*Bt4AKDt**FY(#?:"\t(Z?)d&blXj'02Wh@rg+-+FGDq0:kt?X5T,.2Z%J0K]qYBdE'gsq0SZqi&"RdFh"ftu~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1646
|
||||
>>
|
||||
stream
|
||||
Gat%#lYbH8'`P;63&:cg.:SaA.$;[!'j7#6!P10nS/'[=,`GFO8hJ/MBK'YQGMf*1ablQVSS_IA`UfYtKa]AY=nNl7!5FJ#GfkPV(eoPf,%1$I:.F)1O786(&HM>9ocjqlo%aP.R?p2NOtbl-"%=M5;LhT&-l"2h81,^I:`,Q`pD=GZ;Q0d1`I^p?hpD6Gj;_;1&qj]#:VW/j3Un:+DP]]T2a7G_*GhcUKB`h2=Wde`V9j_hma!]a24p9lT',nN/o>%V\V2d$&rt;r[98tf^,3e0N,E(rgbEJn$Gf:ZKmX/P@J(8aH!Wc#mOG;Z4\IO>5muk"1h7%0nle'h&LO4`*T.N!=VRTqRd7uIc:T.HQ1+e*,=js@ji50^g;4M\3l9]Nj+`Xl6"PC3eU(c/=n;`f#bb3$eZ8p"%&&FL"1d+5n8D>=B_Sb#(m.Fu?IKgMmeF^Ah$LC&LITQ@ohL_,D=#N\.[8_4j72*-J>`fX9%5aI>!@j8`#0*%pDNKIC>iDU1\SN0G&E0^YS[Z+rg1YghGCP7Lera>28B=sW(<44ag35U]f2f*9QLUg?.r!u(mDecPVu\1`C/$Cn#um9F!h5;[`)%JjiUoJF-Gl6pe:7J>8,ad+aJ`!%fB]BJmrJ1*.;OZ,b'ejircTLr5O]m]sEW@DT=4iKUXiMB:O!&TQ7j3ZekJM6X['E,)+aVqV&Vi@S@)7b@^09]Q1mRnihs4<NCdq5k*cGkYa7Q4'sml`d*;n?PNrn!L``dQ[cY6rT;Z`bG4#m0,&ptSIJJ,9hs\1*Nn)"p(sb*;[57T#kclrXN63^7MkUm>SfFQnH#30>A0MYe\q(iTbhZ7,:^G1g^f7RG%'ReC^5t79RI('a1UB<9CNE&`^sB[JDN[AMu0Z5k*-KE,LIjF-H4\r+]k$]SmJ8imVn"h8=#+_/si`,7=M"MP!rjKh:jP'9IUT^NGNi]RY8eE"Ha=&p;T^2fIIc$BQ/1[7Lhjee%:]92Yhj4(eW5aeIGiRUul`i<<7-G.PTe9a(]6^1!s]ZW%`eA;mAsk-['K`r@\k3M-$&W'2O,+M8c3%j8$3G6?_s2P#+_o(/(N,#_##qP(18.:`'V*%).6@MB!#sosSDKnbF%r2&95o+$Y3oqpA\m<IVW9Y?a7>-;PKFCOi#'N=$0b[`gO/00A)9*ue-?S@3BmjWIon.TJMpYhT#*^i?@YW>0oO/<$Smb$,27!idJVYTl59X@:[^b2;jIX!="E,i<5!9Cb%?VZU13s7)q#@2CuH?HmU.Rr_NH-F4H`P=bd;N-oG*53G"&?%1thIG?MZpT"f%qW`7mosVo]3:(PU)#(9E8>1nM<mB(CFA:Si6MU7Ubb0I>bR#&srOPO;EFOHUV42<['`GU)dqJWU`p+Y_B3jWpIhOR5#Z70V8](0ma9*qTDMLK>:^#5"iUIAI2]J>N=$3]qjbVkE*Y#..T3'V!bh^*IhS,R.=]3Pk(HT1J0t^W-/p&InHt@;\Wb+Ap/T8s10&M^#](Ou2Cl.sQ(8a,5qYCk#>IKt$nYB"iK,t)XA<S=^#^QjD7)NC-A5H<:*l5/f>b8b:Ho]"eZu@W$]dRlnYrER\Wkeq">m\\A2DP!YpnN>/iGf&PjM!9#7OR@Y)cmg/)/)fonGWRn(O%\~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1782
|
||||
>>
|
||||
stream
|
||||
Gb!#[D/\/e&H;*)EUY?R9c0"e6n-(ZA8U$)CV+\.f+7J'1,:G^d>LNB=kfYMI7HkpA35]aC`"K<Z-De+*o?3M$4ad`of("nI[J":Ese/JF9.U>%b"Ku0cg3V,0X\OHU-B%EC0pUFR9/k*p)>U`"-!A+`IPj":#uE&d2VC%-</8!jmQuUCdYRHq+;Z83f=&nFu!Li\Q=d3UqJ]gm)gI"VtE8r!"1YXP]rbGL5JKX]Cc%LO-giBa0b[5Q,U/08iqtdBY/uG\=C@Q3Lr[#ro\QBBi6g`CfA>R!s_Q,AWsMP]K;\_8Wk2CElo"<tlQFqlS:h-0$8;i:llV77V50q5cm&"X?,8(^=A9W'`oo^!qM^hR6`/Tj%ipE'8kj`.b-QgodIu*]R3)>AD(6'8JZD^CkAV7&)A:!p-E!.TPjS,;,`LUpY6eJpL@RTNsi`h2cYGM$S^i^R\,fe(KUJ:'W,b+"c&5^,o'`2&\&:<BAf7*VInE*5X1B*>12cd>M4"6:R@RN!I:@BcXWNOsfA^.[BfI(ef\;jNm^uhFbCDZ(G&6@#d7CPEe^sr+nWY+`ujkN?H%3k<c:YSesG:FUbP"c_oCD$]m+kn3X)X/<p7$jR\Wh@P+o[6BVH5XO6@TXL1rPXt\gV7UQXc#E2UR3pkULVEi-N2jg\cb(*`d,*O@iJcJKs\b`$O&rd1=pio'5it1X3]MY9]X0oKuQ7A)2n:_Q8+&uhU]h3p-GaRYG_K(V`et9&O4E6P)ZcVQieF(%JZcW!_65p*)bZ'$=NPQXqg!Ke!.X^60!H5(c7V*n"(_H:a<N*#:!0LI.ca1`DTZ8a;"n)c7!C#J0`W6OXU]F/PWP<Z#bElkF?[m\U=F`QYk<$7nA3So%4f!i?iWs05,IYBi*N$pJ#kbl:5[)8B9C!-`To1Ls+(Y7r*>HD/kB\,g=XQ*hH<A)r$*W'+0/%XF\?s@hM`+c(+tq@g[e43I2N8(qhVfl.JlkRN0edt\mLe=k6*jpH;UO3+*+\k*NrTi2;c^q]k$*i7h=iUbLosC(b?3,_;_)ZL3UEb-L>iu\W*V"LqIHNJOhd"Mc,k]Lg3hW14'oFHT_^:W'QEG)]mg+tm=kk69b.ZX,n&Z'3KfS`I^"\KeW\mo-iSmt/>kHsT#rngJ,q?-<\="j.WT@uM8C[R\\*1]IOhnah@J$DBb08?&m>Hk<WFV\k#am%2/aMQjCsb4,WUkkW%?W\=RV]C7]i6tleGa2ZUrLdl*O,,_pPe@e2nN0FuM>"qsnB=Sg1SiB7!6i_Hkj`elS@4pGEKYKR*S6W^JRq)!A8%PpjP9i-B35Q)I@A9eCs?`DWQ+7n]-$'rP!mFLM0YWjL<APAGq/f2d<d5KD-/*diZq/i"9eA3<]gr?hK&4W+KTP1^ObC2e4]2!37c+G:`aC2M50?RhR.^E1gC!Hr8E)"o#(PK?UFK<Z5HQ--?Xgo2Pnih]906SiAY(I(`_a.:'$<(_R:-?1YL$?n6qN`S$PKDP4YL\9mZ6[]jZEEg\CGIWRM!dPKCK4:upH_A'Le+mOXd5;nf6\A6)=qm9ej+Yi^*Zdt6Pc3`u\MD5cje>H#M<15/0.'+/.qrp`)X4f\$7.UH-/[(t*:`O&U4WBV_b)c'C8KHQC3>Lrqk:a9B#S#O-flG,M3`D)6`(T@]mh`<Y#KI^<`fpajUDt,'k/.#P8-P@[iFlCDKYM)2UrQ,<a3ltZ.do-+/8gjl)mjK2HC]6YBM?+ek+O8B+h6a]hWH>AN*=[<G2-d.7S)#p3aC1,8g@_hb,QI~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1917
|
||||
>>
|
||||
stream
|
||||
Gb!#\>B:]A'Ro4HSFI9<fL";]Zm<pjY#BN_a/qr(_F%jq@Dgs5/"ECVrd0_"%bRl3l(eL&m#%R5F8rs3",_"tr1-7V0./H"bCE_L\H1h(#:@4%S&sU7!O4bF4"_(K:trI0ppPH6[MZs@HGoD,T7NZ*#k!3f(_OV$@,X;p)2Co%c'&/1_EH%q"%4ip'AhSpRs:6Q7m"Z^4F_R)AY1[=q8B>86Slmr.ROMC3RqfN*C?1;?'n#6]QjJH*.CHp.\A;V,_Q[h;mW<TWF^'oE=T2&XU:7kA]HM#!g>"oJ<!OJZ&$^H^9.$"b7niX,_OlW'tQ$Z,)@Q7Nh5,_OL6:YWiU8%H=CrH#oF4CRI=RKRdW#c-;"@K/$W>apE:G7S<jh*f"ut_W7ffU2/2OcFXZ^]Lg"CKB\"$SjEoB,<&JGD@@Z::Q#c#4V,1YfZ\#42RrH.K(S^LWD3`NBXP&-4`e8TcfNRZ\]kK-4KB8aS-hAss5'lOF?^*,gU+2*shL+<ebjr!Dn7TT8,&K@&?:GV$9i$OgpZK0AB*leok7mdnU1CFD0jpQj17/Q4(9'K?M01X<4olL[@'4D.3n-;R)(1Zt_Nk:%'SGPYouUpF;OOL7XjSqsfiW5D]i[eV.YH"<(s\^?O"7r(@J!!j&0SJK73+6uB4jJ,<KA1P&_hGI#1Rr!]2Yu?)"%mP#<#@N=VIYhP]7\]Mo,dF5&hPcXQH2#@<n1Q@i&qA&"@\["`^b^ffIKhM9:-]F54'HbG-sCLS\jWE8.\:iJ_!nHY2VE9$nPD8>^D:[k`[k$JN(F=VBTp4Z:mK7NIf9e)#rBZ">2JaUdKGHb**'$E/I_>/H!X]&=_\a240@O!,#LEGuT8A?6r=$\^BaQ!g0%J=fZt`U#\aQ,IaF\^ab!#$M0k^K"DRW#V!(9;SnWimh+qXiQgYpY5*1K\<ePm(QQ:&Zri78DE^aVPU$n9*OfeV5)#R(ZhauE.47Aj8SWU*KB`)LsU=)!-=SXj'\dHL=>,#q3qneVT8Wf3?IWk]H@,.k\Uqnab&J5.9B?9@>88@0U-9TRQ*Wl(")id8to?@,k+ZBLlOc'33fMlZn"@q(GTB52Vo+\R_606AsY6@Qse-K(rBTI*qnLbrGk=*kMYR886He;]&;cZ67mRBi9:$&:h)et@_f7ZBu$uVTrg*(T(S;`YreP33*C4[i65/GNg5d"5`U,:=5KJbU8-=#Cu>`Q\U(ZJr><+%f45eYiFmmOAO.B.[,\Wq,^\;2iRDn=VT?2qJnp'JZH.VNX"b=b::"K5e\=0aD+o@`O$DE\=/hVl;Uu;-6a35B]3TK1E0N"/d@8re1NRW@TBGf;nlFTo)33$jY;c>_N$0;4?0hO+5Ij.7LW=T6-T<$%gF`HdU(hA<B>%44chaO#TCiP.Q-o<,T?TT+$e=g2I,.3o\X^r:,h"3Z6@&^LUF=n5Mld7i9(_jW($+aQ,;h06VBS4Mk/$HV3u#Klc*NPCHhquuq`"Y(Y8Ls5\)i"ecRF(ZDF<6ORJj="2TH!=0@%q6nnJ9%k8iCp/$0QM+AuVZpGMt/&aj(m4*:4GB=GMC&J"8LgY1Uia\+X$.#;H1N6ae[Xu3bUnb`GiC>[csl0V0`pq^ZYeG^#=mKq2t&ai&B+AuU,IgYf\)0ZZ=N(\Y2[_Sg\)+,ZU.kQB-#LbjRQYmBlV4bQ!(ajSL.er]%rP]:U;ie7HYd*GF%>KhQT!:A]_*fqo]N0RXX0_I-"/&UO=+JAHKj^KMIKVi8lf.FX?K4t[S1SlfBVDB_BFAB:#r,DM@eE0f>-3kg7!L'&K;Mn.W$=or)Ii?$'0Ll71T)eq#FRS^FsHL$B'qfUbl*E@WoA:KM"*J,Y-[2)^@dPX#L)jDeWg(:jkm!1TQHPDo3L@nce,+ZQ@*>;G@+t(29'qYZsiG_PXVLs~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1917
|
||||
>>
|
||||
stream
|
||||
Gb"/'D0+Dj&H9tYf]c(BBJ6<M/AL#-:=@RV]3IZ),^5g'0Z$l+M6VK;8Zr$b[uq2f[h.8]9Kt?FCtYk;IC-$i"Tptgs8#ZEP<QYT5_1]ShZ2bk6iRkSrPlS]_JJ@R$]+$cVT443VYYp]gk`ePM=!$YJnm?aFZ=?,'%ce]?oZJPVurH@S(A)V`3BcljZm'cfri"Ch]t*i.%1gm[_;A0Q%#l6cQdV0*NE7kOVq'Db,>JFP7rRPNWoA11saH#47F[:bM8^88HaYiPQ%A.CW7>Rm_F0p0;BeXfdl:D6N=G+:h.X.,G)fD<MmGrUi;qO)<C'XoBM3Q(^k:uHp4;M2mdWS,HbTs$in"j3Kc?2$8ID`)XS@!7q&EFKX7Pk*P6Ep!>,gg#F;=nFbcI[EbL$=*+6:Ng;q-5n0JbG0"BrTO)sh(<?/E2C5S]Eq0916[8@O#^P:=gLC%5!L:HbO`kdB]`%sLlm3Nf@%!["K+f1l?BKbK=[W#&4hU$-j"0<Y\BS`qt5j*Tm>WpWO,R@?I?,Ztn9*MH;7Dle6U)Sq+Ul7Up-`Hc=((eJO-ikD)#a..rA,K%81(2$f]NC#mMXKZm3gXEa.a#3?CJis<VUT%R;b_I`C9D;tbq*(WhQR$G7b&d\X?^a6HkAe-r!!ljYDAZ,@3"DCAa^P,-4'7[">,sR3WV[t\e?8dY(/dSTL1VrhLMtMrIX[f!U2*Ph+8Fi"m8;4q$t\2m644]4@`?uR'j6h(:!cQ^>S=6S>nPE_qQ#GE"uoW/7a,>cHYOgH!f\$F]aPEkU.IZhM'1_6$k/h7&D_'6D"BjWJhj>NW=L#DV+^/Eg[ATG4Bpr6En+0$fNI9#bli3Pqp5#Q7:U=_\=cD.^`%b`'LPih?.B9[g/S^Ep@=&9W(8)hYn$9:+Wf%XlF:(<+74o4I.]_5+.GJ(uLSSA%Gb-4l6^pV>>FLEnaLWD^SrdK&G$>i/5'gBeF;`oL=h*7d)uJR\_r5mq!C`?_=!b_,h=1.-TJi5(OQ/(OQXL:UAF\K[Vd"$KWO<LR0dfATWS:\;,EnjhL7kc^_O'Fe4Or>c[R,0'!BVXrrBYc;#Z[8h<S?>*JWh\+hHEZhE;T@<Z[2g3GXD%D'S-a1_1cE,iHf@$sWYbGqD?+1LP1m/1\pBD\;k9Ol*6RC8?&OD,)DHL6X6cF`f>ir$0sc`CcdlFW!9n$^Q&4%%53>:nNrM)k6bbLu;_B"I,Vi.k7!Qt"JT71mQ"17p^;H+B1Oc`i@eMZ%hF[-<H+#N`?H15@iRZNge'<IiY['ZDW@Vk`<Z``V"knXd$6,W+e4:\0IDhUJ]pDtM#]WE5]P%HlJZLo7h4'@sAY=j5OAfsAGGZ,?;+j]d_u_j>5JEktfn"L_VJC8&%d>"g"YHmiL6rNq,qRs:HM$gLCO5=`5A#ugrsPFt*X]c;<Y#\T`@\%6"NTWIS)ZFOQHqhU+EcX+BB>rhR[Q#)[4-=^1`U#cq=`EK:CFu\Zp3H+kM>t4s`"87SQQt_]./s2NZ'[a5L/L>j;Jl_;La!2][ZC.nj&3r!$dTNhFDT,J2olc2"*)/7%\a!2OiJ(&ER42iAg@B7\)&D2F\5uA2,\lZYKhGBBV3t"G-@:G?Z.fXs"K<qJV"R?Yk*K!0/h(@V#9A4HZ8c/Ukf87u0GEjd;F:[HCU>QArIP!X%74?eR@&Gjj)S(j'tTcoi8S7!=R`Q90@T%:;o8le]B]N@3MDJTG1Hh%=UThhL/m@5a#hja2k&jn=C`])c+*'lcWtK92:m2s>`-atS>`"SH6[^Bd<O[9GV?f)&=pWgP3_blY>j8>@f=+hZV?b6i>Ti3$W:*>R`LIuTVHd_R_;iR7O"0I<#p[$OV8>N_P4+doF3.74,&24Zl@N5G1?60h6Bes^D4SGf'#bh(LqlV)o#ZW%3#6KNDN[&~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
|
||||
>>
|
||||
stream
|
||||
GarW5;/_pX'SYH?($@<P'F,ReS&\6=[7XBLY&"WtUkpE765!aB4.3H_5Jp$W2T#`%_WO#DhTM&$`Q.GQ"$VrOgi.4`Z3HXhUATu913lW^:dW>F/2M!@#d>Kl*k3QM,?IPRU>R`)Ak#Ea_jN65JVZ@h\V;?Y<s],\.b."?)4i@R0(^#[6:>V2Ei"Z&qC/$SiY>-]l!hkd42Ttd4r*`Wbp5r-Y&fgJAp7Urhr9i:a=l/rmdMef>-7sV$DI"h(*Dd3455sGLN8%<C+qW"8h!l9V1&l%$rr@`[17XFNNE#WC%i@"1X8@(j9_X#PYMV&9V0BdJGB#c._]Mfo5P+q)[W)GKekH#f3m1ah25H350SMc@6Le4g'@>Lm75oNR%n>Lemh-Ng90=1T\.V[>eJf(_[fCEfH-7=A2ILAn5s#Sj6ct\407/g\:CK#1N5:.ed"7d3f>%`7$k%CB[?154G=1@_3=ar=%DT15lSVF*5&1tKQl"BqiBSUqWb'N1j9%N52bQV39HiWDisD:/05ELIf]28`/+~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 29
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000143 00000 n
|
||||
0000000250 00000 n
|
||||
0000000362 00000 n
|
||||
0000000439 00000 n
|
||||
0000000544 00000 n
|
||||
0000000749 00000 n
|
||||
0000000954 00000 n
|
||||
0000001159 00000 n
|
||||
0000001274 00000 n
|
||||
0000001480 00000 n
|
||||
0000001686 00000 n
|
||||
0000001892 00000 n
|
||||
0000001976 00000 n
|
||||
0000002182 00000 n
|
||||
0000002388 00000 n
|
||||
0000002594 00000 n
|
||||
0000002664 00000 n
|
||||
0000002956 00000 n
|
||||
0000003070 00000 n
|
||||
0000005458 00000 n
|
||||
0000007122 00000 n
|
||||
0000008886 00000 n
|
||||
0000010759 00000 n
|
||||
0000012497 00000 n
|
||||
0000014371 00000 n
|
||||
0000016380 00000 n
|
||||
0000018389 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<3079c5437762c8adeb291f8ff4fab280><3079c5437762c8adeb291f8ff4fab280>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 18 0 R
|
||||
/Root 17 0 R
|
||||
/Size 29
|
||||
>>
|
||||
startxref
|
||||
19015
|
||||
%%EOF
|
||||
BIN
Les13-Agents/lesbestanden/polderfest-original.zip
Normal file
BIN
Les13-Agents/lesbestanden/polderfest-original.zip
Normal file
Binary file not shown.
BIN
Les13-Agents/lesbestanden/polderfest-with-agent.zip
Normal file
BIN
Les13-Agents/lesbestanden/polderfest-with-agent.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user