add les 12
This commit is contained in:
246
Les12-Tool-Calling/tools-demo.ts
Normal file
246
Les12-Tool-Calling/tools-demo.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user