247 lines
9.7 KiB
TypeScript
247 lines
9.7 KiB
TypeScript
/**
|
|
* 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<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 };
|
|
},
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────
|
|
// 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 },
|
|
);
|
|
}
|
|
}
|