# Les 15 — Lesopdracht ## Setup research-agent + eerste run **Vak:** AI-Assisted Development **Opleiding:** NOVI Hogeschool Utrecht **Duur:** 30 min in-class **Status:** Tijdens de les afronden — daarna huiswerk uitbreiden --- ## Doel Aan het einde van deze opdracht heb je een **werkende research-agent** in een eigen Next.js repo, draait er één test-query, en kun je in de console alle stappen zien die de agent zette. --- ## Vereisten - Node 20+, pnpm, git - Supabase account (mag dezelfde zijn als Les 11/12) - OpenAI API key (zit waarschijnlijk al in je `.env`) - **Tavily API key** — gratis tier op https://tavily.com/ (1000 calls/maand) - VS Code / Cursor --- ## Stap 1 — Nieuwe Next.js app ```bash pnpm create next-app@latest research-agent \ --typescript --tailwind --app --no-src-dir --import-alias "@/*" cd research-agent pnpm add ai @ai-sdk/openai zod @supabase/supabase-js pnpm add -D @types/node ``` Open in editor. --- ## Stap 2 — Environment variabelen `.env.local` (in `.gitignore`!): ``` OPENAI_API_KEY=sk-... TAVILY_API_KEY=tvly-... NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... ``` > **Let op:** `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*` — moet server-side blijven. --- ## Stap 3 — Supabase tabel In Supabase SQL editor: ```sql create table research_reports ( id bigserial primary key, query text not null, summary text not null, sources jsonb default '[]'::jsonb, created_at timestamp default now() ); alter table research_reports enable row level security; create policy "demo open" on research_reports for all to anon using (true) with check (true); ``` --- ## Stap 4 — Tools schrijven `lib/tools.ts`: ```typescript import { tool } from "ai"; import { z } from "zod"; import { createClient } from "@supabase/supabase-js"; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); export const webSearch = tool({ description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.", inputSchema: z.object({ query: z.string().describe("Zoekterm"), }), execute: async ({ query }) => { const res = await fetch("https://api.tavily.com/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: process.env.TAVILY_API_KEY, query, max_results: 5, }), }); const data = await res.json(); return data.results?.map((r: any) => ({ url: r.url, title: r.title, snippet: r.content, })) ?? []; }, }); export const readPage = tool({ description: "Haal de volledige tekst van een URL op.", inputSchema: z.object({ url: z.string().url(), }), execute: async ({ url }) => { const res = await fetch("https://api.tavily.com/extract", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: process.env.TAVILY_API_KEY, urls: [url], }), }); const data = await res.json(); const content = data.results?.[0]?.raw_content ?? ""; return { url, content: content.slice(0, 5000) }; }, }); export const saveReport = tool({ description: "Sla een onderzoeksrapport op in de database. " + "Alleen aanroepen als het rapport compleet is.", inputSchema: z.object({ query: z.string(), summary: z.string().min(100), sources: z.array(z.object({ url: z.string().url(), title: z.string(), })), }), execute: async ({ query, summary, sources }) => { const { data, error } = await supabase .from("research_reports") .insert({ query, summary, sources }) .select() .single(); if (error) return { error: error.message }; return { reportId: data.id, success: true }; }, }); ``` --- ## Stap 5 — Agent definiëren `lib/agent.ts`: ```typescript import { ToolLoopAgent, stepCountIs } from "ai"; import { openai } from "@ai-sdk/openai"; import { webSearch, readPage, saveReport } from "./tools"; export const researchAgent = new ToolLoopAgent({ model: openai("gpt-4o-mini"), system: `Je bent een research-assistent. Werk als volgt: 1. Begin met een kort plan (3-5 bullets). 2. Gebruik webSearch voor oriëntatie. 3. Gebruik readPage voor de 2-3 meest relevante URLs. 4. Schrijf een samenvatting van min 200 woorden. 5. Sla op met saveReport. Inclusief alle gebruikte sources. Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`, tools: { webSearch, readPage, saveReport }, stopWhen: stepCountIs(10), }); ``` --- ## Stap 6 — API route `app/api/research/route.ts`: ```typescript import { researchAgent } from "@/lib/agent"; export async function POST(req: Request) { const { query } = await req.json(); const result = await researchAgent.generate({ prompt: query }); return Response.json({ text: result.text, steps: result.steps.map((s, i) => ({ step: i, toolCalls: s.toolCalls?.map((tc) => ({ tool: tc.toolName, input: tc.input, })), hasText: !!s.text, })), }); } ``` --- ## Stap 7 — Test in terminal In één terminal: ```bash pnpm dev ``` In een tweede terminal: ```bash curl -X POST http://localhost:3000/api/research \ -H "Content-Type: application/json" \ -d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \ | jq ``` Verwacht: JSON met `text` (eindrapport) en `steps` (welke tools werden aangeroepen in welke volgorde). --- ## Eisen - [ ] Repo werkt — `pnpm dev` zonder errors - [ ] `.env.local` met alle 4 keys + in `.gitignore` - [ ] Supabase tabel aangemaakt - [ ] Alle 3 tools werken - [ ] Agent draait 1 succesvolle query - [ ] Je kunt in steps zien welke tools werden aangeroepen - [ ] Eindrapport staat in Supabase (`research_reports` tabel) --- ## Tijdsindeling (30 min) | Stap | Tijd | |------|------| | 1 — Next.js setup | 5 min | | 2-3 — Env + Supabase | 5 min | | 4 — Tools | 10 min | | 5-6 — Agent + route | 5 min | | 7 — Test + debug | 5 min | --- ## Veelvoorkomende problemen | Symptoom | Oplossing | |----------|-----------| | `TAVILY_API_KEY undefined` | Restart `pnpm dev` na env-edit | | Tavily 401 | Key fout — kopieer opnieuw | | Supabase insert faalt | RLS policy check (`alter table ... enable rls; create policy "demo open" ...`) | | Agent stopt te vroeg | `stepCountIs(10)` → `stepCountIs(20)` | | Steps array leeg | Check je `result.steps`-mapping — niet `result.text.steps` | | `gpt-4o-mini` does not exist | Check OpenAI key heeft GPT-4 access — anders `gpt-4o` | --- ## Klaar? Verder met huiswerk Het huiswerk bouwt voort op deze setup: - Extra tools (listReports, eigen tool) - `prepareStep` toevoegen - Custom stop-conditie - `AGENT.md` documentatie Zie `Les15-Huiswerk.md`.