Files
novi-lessons/Les14-Agents/Les14-Lesopdracht.md
2026-06-07 13:35:02 +02:00

6.7 KiB

Les 14 — 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

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:

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:

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:

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:

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:

pnpm dev

In een tweede terminal:

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 Les14-Huiswerk.md.