# Les 12 — Tool Calling ## Lesstof **Vak:** AI-Assisted Development **Opleiding:** NOVI Hogeschool Utrecht **Onderwerp:** Tool Calling — AI besluit zelf welke functie aan te roepen **Demo:** Polderfest 2027 (vervolg op Les 11) --- ## Inhoudsopgave 1. [Het schaalprobleem dat we oplossen](#1-het-schaalprobleem) 2. [Wat is Tool Calling?](#2-wat-is-tool-calling) 3. [Anatomie van een tool](#3-anatomie-van-een-tool) 4. [Multi-step met stopWhen](#4-multi-step-met-stopwhen) 5. [Refactor: chat-route met tools](#5-refactor-chat-route) 6. [Tool-invocations in de UI](#6-tool-invocations-in-de-ui) 7. [Edge cases & error handling](#7-edge-cases--error-handling) 8. [Tool Calling vs context-all — vergelijking](#8-tool-calling-vs-context-all) 9. [Best practices](#9-best-practices) 10. [Wat komt hierna? Agents teaser](#10-wat-komt-hierna) 11. [Bronnen](#11-bronnen) --- ## 1. Het schaalprobleem In Les 11 stuurden we **alle 500 bands** mee als tekst-context bij elke chat-request: ```typescript const { data: bands } = await supabase.from("bands").select("*"); const context = bands.map((b) => `- ${b.name} (${b.genre}, ...)`).join("\n"); const system = `Hier zijn alle bands:\n${context}\nBeantwoord vragen...`; ``` **Werkt voor 500 records. Werkt niet voor 50.000.** | Records | Tokens per call | Cost (gpt-4o-mini) | Probleem | |---------|----------------|---------------------|----------| | 500 | ~30.000 | $0.005 | OK | | 5.000 | ~300.000 | $0.05 | Te traag, context-window grenst aan limit | | 50.000 | ~3 miljoen | n.v.t. | **Past niet in context** | Daarnaast: - Context is een **snapshot** — als data verandert tijdens chat, weet AI 't niet - **Geen write-acties** mogelijk — alleen lezen - AI moet zelf "denken" om de juiste records te vinden in een lap tekst Tool Calling lost dit op. --- ## 2. Wat is Tool Calling? Het idee: in plaats van **alle data** mee te sturen, geef je AI **tools** (functies). AI ziet je vraag, kiest welke tool relevant is, roept 'm aan met de juiste parameters, en gebruikt het resultaat om te antwoorden. ### Flow ``` User: "Welke bands spelen vrijdag op de Main Stage?" ↓ AI: kiest tool → searchBands({ day: "Vrijdag", stage: "Main Stage" }) ↓ Supabase: 12 bands terug ↓ AI: formuleert antwoord op basis van resultaat ↓ User: "Op vrijdag op de Main Stage spelen: ..." ``` ### Wat win je - **Schaalbaar** — werkt voor 100 of 10 miljoen records - **Real-time** — tool draait elke keer opnieuw, geen snapshot - **Type-safe** — parameters via Zod schema, gevalideerd - **Multi-step** — meerdere tools combineren voor complexe vragen - **Write-acties** — AI kan ook iets in je database zetten (favorieten, votes, notes) --- ## 3. Anatomie van een tool Drie verplichte delen. ```typescript import { tool } from "ai"; import { z } from "zod"; const searchBands = tool({ description: "Zoek bands in de Polderfest line-up. Filter op dag, stage, genre, of tier.", inputSchema: z.object({ day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(), stage: z.string().optional(), genre: z.string().optional(), tier: z.enum(["headliner", "mid", "opener"]).optional(), }), execute: async ({ day, stage, genre, tier }) => { let q = supabase.from("bands").select("*"); if (day) q = q.eq("day", day); if (stage) q = q.eq("stage", stage); if (genre) q = q.eq("genre", genre); if (tier) q = q.eq("tier", tier); const { data, error } = await q.limit(20); if (error) return { error: error.message }; return data; }, }); ``` ### `description` Wat doet deze tool en wanneer gebruik je 'm? **AI kiest tools op basis van descriptions.** - ❌ Vaag: `"Doe iets met bands"` - ✅ Goed: `"Zoek bands op dag, stage, genre of tier. Gebruik dit voor filtervragen."` Schrijf alsof je 't aan een collega uitlegt. Twee zinnen vaak genoeg. ### `inputSchema` Zod schema. Type-safe. AI weet welke parameters mogen. (Heette `parameters` in AI SDK v3 — sinds v5/v6 `inputSchema`.) - `z.string()` — vrije tekst - `z.enum([...])` — vaste keuze (AI mag alleen een van deze) - `z.number().min(1).max(100)` — getal met bounds - `z.boolean()` - `.optional()` — parameter mag weg - `.describe("...")` — extra context voor AI ### `execute` Async functie. Returnt wat AI moet zien. **Belangrijk:** errors als data terug returnen, niet als exception throwen: ```typescript // ❌ Niet doen — AI ziet de error niet execute: async (...) => { const data = await supabase.from(...).select(); if (data.error) throw new Error(data.error.message); } // ✅ Wel doen — AI kan dit zelf afhandelen execute: async (...) => { const { data, error } = await supabase.from(...).select(); if (error) return { error: error.message }; return data; } ``` --- ## 4. Multi-step met stopWhen Met `stopWhen: stepCountIs(5)` geef je AI toestemming om tot 5 keer een tool aan te roepen voordat hij definitief antwoordt. (In AI SDK v3 heette dit `maxSteps: 5` — sinds v5/v6 gebruik je `stopWhen` met een conditie zoals `stepCountIs(N)`.) ```typescript const result = streamText({ model: openai("gpt-4o-mini"), tools: { searchBands, getStats, getBandByName }, stopWhen: stepCountIs(5), messages, }); ``` ### Voorbeeld User: *"Vergelijk de top headliner met de drukst geplande opener."* ``` Stap 1: AI roept searchBands({ tier: "headliner" }) aan → 50 bands terug, sorteer op popularity, top 1 gevonden Stap 2: AI roept searchBands({ tier: "opener" }) aan → 100 bands terug, top 1 gevonden Stap 3: AI vergelijkt + antwoordt ``` Drie stappen, één request. AI plant zelf de volgorde. ### Wanneer welk step-limit? | Use case | `stepCountIs(N)` | |----------|------------------| | Simpele query ("welke bands op vrijdag?") | 1-2 | | Vergelijking ("X vs Y") | 3-5 | | Onderzoek ("vat alle X samen") | 5-10 | | Agentic ("plan mijn weekend") | 15-30+ (volgende les) | **Default in AI SDK:** 1 stap. Dus expliciet `stopWhen` zetten als je multi-step wilt. --- ## 5. Refactor: chat-route met tools De volledige `app/api/chat/route.ts` na refactor: ```typescript import { streamText, tool } 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.NEXT_PUBLIC_SUPABASE_ANON_KEY!, ); const searchBands = tool({ description: "Zoek bands op dag, stage, genre, of tier.", inputSchema: z.object({ day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(), stage: z.string().optional(), genre: z.string().optional(), tier: z.enum(["headliner", "mid", "opener"]).optional(), }), execute: async ({ day, stage, genre, tier }) => { let q = supabase.from("bands").select("*"); if (day) q = q.eq("day", day); if (stage) q = q.eq("stage", stage); if (genre) q = q.eq("genre", genre); if (tier) q = q.eq("tier", tier); const { data, error } = await q.limit(20); if (error) return { error: error.message }; return { count: data.length, bands: data }; }, }); const getStats = tool({ description: "Geef verdeling van bands per groep (genre, day, stage, tier).", inputSchema: z.object({ groupBy: z.enum(["genre", "day", "stage", "tier"]), }), execute: async ({ groupBy }) => { const { data, error } = await supabase.from("bands").select(groupBy); if (error) return { error: error.message }; const counts: Record = {}; for (const row of data) { const key = row[groupBy as keyof typeof row] as string; counts[key] = (counts[key] ?? 0) + 1; } return { total: data.length, counts }; }, }); const getBandByName = tool({ description: "Haal alle details op van één band bij naam (inclusief bio).", inputSchema: z.object({ name: z.string() }), execute: async ({ name }) => { const { data, error } = await supabase .from("bands").select("*").ilike("name", name).single(); if (error) return { error: `Band '${name}' niet gevonden.` }; return data; }, }); export async function POST(req: Request) { const { messages } = await req.json(); const system = `Je bent een festival-assistent voor Polderfest 2027. Gebruik de beschikbare tools om vragen te beantwoorden. Verzin nooit data. Antwoord beknopt en in het Nederlands.`; const result = streamText({ model: openai("gpt-4o-mini"), system, messages, tools: { searchBands, getStats, getBandByName }, stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse(); } ``` **Wat is veranderd t.o.v. Les 11:** - Geen `select("*")` op start meer - Geen grote context-string in system prompt - Tools-object toegevoegd - `stopWhen: stepCountIs(5)` voor multi-step - System prompt veel korter — alleen rol + regels --- ## 6. Tool-invocations in de UI `useChat` returnt `messages` met **parts** — een array van delen per bericht. Tekst-parts én tool-invocation-parts. ```tsx "use client"; import { useChat } from "@ai-sdk/react"; import { useState } from "react"; export default function ChatPage() { const { messages, sendMessage, status } = useChat(); const [input, setInput] = useState(""); return (
{messages.map((m) => (
{m.role}: {m.parts?.map((part, i) => { if (part.type === "text") { return
{part.text}
; } // In AI SDK v6 zijn tool-parts genaamd `tool-` if (part.type?.startsWith("tool-")) { const toolName = part.type.replace("tool-", ""); return (
🔧 {toolName}({JSON.stringify(part.input)}) {part.state === "output-available" && (
Toon resultaat
{JSON.stringify(part.output, null, 2)}
)}
); } return null; })}
))}
{ e.preventDefault(); if (!input.trim()) return; sendMessage({ text: input }); setInput(""); }} > setInput(e.target.value)} />
); } ``` ### Waarom tool-invocations tonen? - **Debug** — zien wat AI aanroept met welke args - **Vertrouwen** — gebruiker ziet "ja, hij heeft echt iets opgezocht" - **Demo** — voor presentaties, hackathons, onboarding In productie kun je dit optioneel verstoppen, voor jullie eindopdracht: **wel tonen**. ### Part states (v6) | state | Wat | |-------|-----| | `"input-streaming"` | Tool args worden nog gestreamed | | `"input-available"` | Args compleet, tool draait | | `"output-available"` | Tool is gerund, resultaat beschikbaar | | `"output-error"` | Tool gaf een error | --- ## 7. Edge cases & error handling ### Ongeldige input (enum-restrictie) User: *"Welke bands spelen op Donderdag?"* AI ziet dat `day` enum is — `["Vrijdag", "Zaterdag", "Zondag"]`. Donderdag past niet. AI weigert tool aan te roepen en legt uit. **Les:** gebruik enums voor restricted values. AI respecteert ze. ### Lege resultaten User: *"Death metal bands?"* Tool returnt `{ count: 0, bands: [] }`. AI legt uit: "Geen death metal op Polderfest 2027." **Les:** lege array is OK, AI handelt 't af. ### Database errors ```typescript execute: async (...) => { const { data, error } = await supabase.from(...).select(); if (error) return { error: error.message }; return data; } ``` AI ziet `{ error: "..." }`, communiceert dit netjes. **Niet** een exception throwen — dan crasht de hele chat. ### Write-tools en user-intent Write-tools (insert/update/delete) zijn gevaarlijker — ze veranderen echt iets. Bescherm via: 1. **Descriptions**: "Alleen gebruiken als gebruiker expliciet vraagt om X toe te voegen" 2. **Confirmation UI**: laat user nog een keer bevestigen voor de write definitief gaat 3. **Permissions**: write-tool checked of user ingelogd is via auth.uid() Voor demo open laten kan. Voor productie: streng. --- ## 8. Tool Calling vs context-all | Aspect | Les 11 (context-all) | Les 12 (tool calling) | |--------|---------------------|----------------------| | Tokens per call | ~30.000 (500 bands) | ~2.000 (tools + 1 result) | | Schaal | Tot ~1000 records | Tot duizenden makkelijk | | Live data | Snapshot bij chat-start | Actueel per call | | Write operaties | Niet mogelijk | Wel (addFavorite etc.) | | Multi-step | Beperkt — alleen reasoning | Native (`stopWhen` + `stepCountIs`) | | Cost | Hoger | Lager (kleinere context) | | Complexiteit | Lager | Iets hoger (tools definiëren) | **Wanneer toch context-all?** - Hele kleine dataset (<100 records) - Snel prototype - Geen schaal nodig **Voor productie: bijna altijd Tool Calling.** --- ## 9. Best practices ### Descriptions schrijven ```typescript // ❌ description: "zoek dingen" // ❌ Te vaag description: "zoek bands" // ✅ description: "Zoek bands in de Polderfest line-up. Filter op dag, stage, genre, " + "of tier. Gebruik dit voor filtervragen zoals 'welke X op vrijdag?'" ``` ### Parameter design - Gebruik `enum` voor categorische waarden (niet string) - `.optional()` voor filter-parameters - `.describe()` op elke parameter — extra context - Houd parameter-sets klein (3-5 max) ### Returns - Returnt **JSON-serializable** waarden (geen Date-objecten direct, gebruik ISO strings) - Errors als `{ error: "..." }` terug — niet throwen - Beperk grootte van responses (limit 20 records, niet 500) ### System prompts ```typescript const system = `Je bent een festival-assistent voor Polderfest 2027. Gebruik de beschikbare tools om vragen te beantwoorden. Tips: - Voor "welke bands op X?" → searchBands - Voor "hoeveel" → getStats - Voor specifieke band → getBandByName Verzin nooit data. Als tools een error returnen, leg dat uit aan de gebruiker.`; ``` Schrijf het system prompt als handleiding voor AI — wanneer welke tool. ### Tools registeren Houd tools in een aparte module als ze veel worden: ```typescript // lib/tools.ts export const searchBands = tool({ ... }); export const getStats = tool({ ... }); export const getBandByName = tool({ ... }); // app/api/chat/route.ts import * as tools from "@/lib/tools"; streamText({ ..., tools, stopWhen: stepCountIs(5) }); ``` --- ## 10. Wat komt hierna? ### Volgende les: Agents (Les 13) Tool Calling met `stopWhen: stepCountIs(5)` is een eerste stap richting agents. Volgende les: **AI Agents** met `stepCountIs(20)` en hoger, of zelfs custom stop-condities. ```typescript const result = streamText({ model: openai("gpt-4o-mini"), tools: { ... }, stopWhen: stepCountIs(30), experimental_telemetry: { isEnabled: true }, messages, }); ``` **Voorbeeld autonome workflow:** *"Plan mijn volledige Polderfest weekend op basis van mijn smaak."* Agent doet: 1. Vraag user smaakprofiel (Hip-Hop + Indie) 2. searchBands per dag + genre 3. Filtert op overlap-vermijding 4. addFavorite per geselecteerde band 5. listFavorites om finaal schema te tonen 6. Wijst conflicten aan in tijdsloten 7. Optimaliseert en herhaalt 30+ tool-calls in één user-request. ### Daarna in deze leerlijn - **Les 14:** RAG + embeddings — semantic search op grote tekst-corpora - **Les 15-16:** Testing + Deployment + Performance - **Les 17-18:** Eindopdracht-werkdagen + Pitch --- ## 11. Bronnen ### Vercel AI SDK - Tools-documentatie: https://ai-sdk.dev/docs/foundations/tools - Agents + stopWhen: https://ai-sdk.dev/docs/foundations/agents - streamText reference: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text - useChat reference: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat ### Underlying providers - OpenAI Function Calling: https://platform.openai.com/docs/guides/function-calling - Anthropic Tool Use: https://docs.anthropic.com/en/docs/build-with-claude/tool-use ### Zod - Docs: https://zod.dev - Schema reference: https://zod.dev/?id=primitives ### Supabase JS - Query builder: https://supabase.com/docs/reference/javascript/select - Filters: https://supabase.com/docs/reference/javascript/using-filters