final lessons
This commit is contained in:
282
Les15-Agents/Les15-Lesopdracht.md
Normal file
282
Les15-Agents/Les15-Lesopdracht.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user