# 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<string, number> = {};
    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 (
    <main>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong>
          {m.parts?.map((part, i) => {
            if (part.type === "text") {
              return <div key={i}>{part.text}</div>;
            }
            // In AI SDK v6 zijn tool-parts genaamd `tool-<toolName>`
            if (part.type?.startsWith("tool-")) {
              const toolName = part.type.replace("tool-", "");
              return (
                <div key={i} className="bg-yellow-50 p-2 rounded">
                  🔧 {toolName}({JSON.stringify(part.input)})
                  {part.state === "output-available" && (
                    <details>
                      <summary>Toon resultaat</summary>
                      <pre>{JSON.stringify(part.output, null, 2)}</pre>
                    </details>
                  )}
                </div>
              );
            }
            return null;
          })}
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim()) return;
          sendMessage({ text: input });
          setInput("");
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
      </form>
    </main>
  );
}
```

### 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
