/** * Polderfest Tool-Calling demo — reference snippets (AI SDK v6) * -------------------------------------------------- * Dit zijn de tools die we live opbouwen tijdens Les 12. * Matcht met de actuele stack van polderfest-demo: * - ai@^6 * - @ai-sdk/openai@^3 * - @ai-sdk/react@^3 * * Gebruik dit als naslag — niet 1-op-1 kopiëren tijdens demo. * Plek in project: app/api/chat/route.ts */ import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage, } from "ai"; import { openai } from "@ai-sdk/openai"; import { createClient } from "@supabase/supabase-js"; import { z } from "zod"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; const supabase = createClient(supabaseUrl, supabaseKey); // ──────────────────────────────────────────────────────────── // Tool 1: searchBands — filter op velden // ──────────────────────────────────────────────────────────── const searchBands = tool({ description: "Zoek bands in de Polderfest line-up. Filter op dag, stage, genre of tier. " + "Gebruik dit als de gebruiker iets zoekt op één of meerdere criteria.", inputSchema: z.object({ day: z .enum(["Vrijdag", "Zaterdag", "Zondag"]) .optional() .describe("Festival-dag"), stage: z .string() .optional() .describe("Bv. Main Stage, Tent Stage, Beach Stage, Acoustic Bar, Late Night Tent"), genre: z .string() .optional() .describe("Bv. Indie Rock, Electronic, Hip-Hop, Jazz Fusion"), tier: z.enum(["headliner", "mid", "opener"]).optional(), minPopularity: z .number() .min(1) .max(100) .optional() .describe("Minimale populariteit (1-100)"), }), execute: async ({ day, stage, genre, tier, minPopularity }) => { let q = supabase .from("bands") .select( "id, name, genre, sub_genre, stage, day, start_time, " + "origin_city, tier, popularity, bio", ); 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); if (minPopularity) q = q.gte("popularity", minPopularity); const { data, error } = await q.limit(20); if (error) return { error: error.message }; return { count: data.length, bands: data }; }, }); // ──────────────────────────────────────────────────────────── // Tool 2: getBandByName — exacte lookup // ──────────────────────────────────────────────────────────── const getBandByName = tool({ description: "Haal alle details op van één specifieke band, inclusief members en bio. " + "Gebruik dit als de gebruiker naar een specifieke band vraagt.", inputSchema: z.object({ name: z.string().describe("Exacte band-naam"), }), 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; }, }); // ──────────────────────────────────────────────────────────── // Tool 3: getStats — aggregate over alle bands // ──────────────────────────────────────────────────────────── const getStats = tool({ description: "Geef statistieken over de festival-line-up — totaal aantal bands, " + "verdeling per genre, per dag, of per stage. Geen filters — overzicht.", inputSchema: z.object({ groupBy: z .enum(["genre", "day", "stage", "tier"]) .describe("Hoe te groeperen"), }), 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 }; }, }); // ──────────────────────────────────────────────────────────── // Tool 4: getScheduleByDay — slot-overzicht // ──────────────────────────────────────────────────────────── const getScheduleByDay = tool({ description: "Geef het volledige tijdschema voor één festival-dag. " + "Bands gesorteerd op tijdslot. Handig voor 'wat speelt er om 22:00?'", inputSchema: z.object({ day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]), stage: z .string() .optional() .describe("Optioneel filteren op specifieke stage"), }), execute: async ({ day, stage }) => { let q = supabase .from("bands") .select("name, stage, start_time, duration_min, tier") .eq("day", day); if (stage) q = q.eq("stage", stage); const { data, error } = await q.order("start_time", { ascending: true }); if (error) return { error: error.message }; return data; }, }); // ──────────────────────────────────────────────────────────── // Tool 5 (write): addFavorite — user_favorites tabel // ──────────────────────────────────────────────────────────── const addFavorite = tool({ description: "Voeg een band toe aan de favorieten van de gebruiker. " + "Gebruik alleen als de gebruiker expliciet vraagt 'voeg X toe aan mijn favorieten'.", inputSchema: z.object({ userEmail: z.string().email().describe("Email van de gebruiker"), bandName: z.string().describe("Exacte band-naam"), }), execute: async ({ userEmail, bandName }) => { const { data: band } = await supabase .from("bands") .select("id") .ilike("name", bandName) .single(); if (!band) return { error: `Band '${bandName}' niet gevonden.` }; const { error } = await supabase .from("user_favorites") .insert({ user_email: userEmail, band_id: band.id }); if (error) return { error: error.message }; return { success: true, bandName, bandId: band.id }; }, }); // ──────────────────────────────────────────────────────────── // Tool 6 (read): listFavorites // ──────────────────────────────────────────────────────────── const listFavorites = tool({ description: "Geef de favoriete bands van de gebruiker.", inputSchema: z.object({ userEmail: z.string().email(), }), execute: async ({ userEmail }) => { const { data, error } = await supabase .from("user_favorites") .select("bands(name, genre, day, start_time, stage)") .eq("user_email", userEmail); if (error) return { error: error.message }; return data?.map((r) => r.bands) ?? []; }, }); // ──────────────────────────────────────────────────────────── // De chat-route met alle tools // ──────────────────────────────────────────────────────────── export async function POST(req: Request) { try { const { messages }: { messages: UIMessage[] } = await req.json(); const system = `Je bent een festival-assistent voor Polderfest 2027. Je hebt toegang tot tools om de bands-database te bevragen. Gebruik altijd tools voor concrete vragen — verzin nooit data. Antwoord beknopt en in het Nederlands. Tips: - Voor "welke bands op X?" → searchBands - Voor specifieke band → getBandByName - Voor "hoeveel" / "verdeling" → getStats - Voor tijdschema → getScheduleByDay - Voor favorieten → addFavorite / listFavorites Als een tool een error returnt, leg dat netjes uit aan de gebruiker.`; const result = streamText({ model: openai("gpt-4o-mini"), system, messages: await convertToModelMessages(messages), tools: { searchBands, getBandByName, getStats, getScheduleByDay, addFavorite, listFavorites, }, stopWhen: stepCountIs(5), }); return result.toUIMessageStreamResponse({ onError(error: unknown) { if (error == null) return "unknown error"; if (typeof error === "string") return error; if (error instanceof Error) return error.message; return JSON.stringify(error); }, }); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; return Response.json( { error: "Chat route failed", details: message }, { status: 500 }, ); } }