# 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