323 lines
8.4 KiB
Markdown
323 lines
8.4 KiB
Markdown
# Les 14 — Huiswerk
|
|
## Agent uitbreiden + prepareStep + custom stop + AGENT.md
|
|
|
|
**Vak:** AI-Assisted Development
|
|
**Opleiding:** NOVI Hogeschool Utrecht
|
|
**Deadline:** Voor Les 15 — RAG + embeddings
|
|
**Inleveren:** GitHub repo + `AGENT.md` in root
|
|
|
|
---
|
|
|
|
## Doel
|
|
|
|
Bouwt voort op de **lesopdracht** (research-agent met 3 tools + `stepCountIs(10)`). In dit huiswerk maak je de agent slimmer: extra tools, dynamic step-control, een eigen stop-conditie, en documenteer je het geheel.
|
|
|
|
> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt nodig.
|
|
|
|
---
|
|
|
|
## Onderdeel A — Extra tool (verplicht)
|
|
|
|
Voeg een 4e tool toe: **`listReports`** om opgeslagen rapporten op te halen.
|
|
|
|
```typescript
|
|
export const listReports = tool({
|
|
description:
|
|
"Toon eerder opgeslagen onderzoeksrapporten. " +
|
|
"Gebruik dit als de gebruiker vraagt naar bestaand onderzoek " +
|
|
"voordat je nieuw onderzoek doet.",
|
|
inputSchema: z.object({
|
|
limit: z.number().int().min(1).max(20).default(5),
|
|
}),
|
|
execute: async ({ limit }) => {
|
|
const { data, error } = await supabase
|
|
.from("research_reports")
|
|
.select("id, query, created_at")
|
|
.order("created_at", { ascending: false })
|
|
.limit(limit);
|
|
if (error) return { error: error.message };
|
|
return data;
|
|
},
|
|
});
|
|
```
|
|
|
|
Voeg toe aan `agent.tools` en test:
|
|
|
|
> "Heb ik eerder iets over AI in de bouw onderzocht? Zo nee, doe het nu."
|
|
|
|
Verwacht: agent roept eerst `listReports`, dan eventueel `webSearch` → `readPage` → `saveReport`.
|
|
|
|
### Eisen
|
|
|
|
- [ ] `listReports` werkt
|
|
- [ ] System prompt aangepast — agent weet wanneer te checken
|
|
- [ ] Minstens 1 test-query gedraaid die `listReports` triggert
|
|
|
|
---
|
|
|
|
## Onderdeel B — prepareStep toevoegen (verplicht)
|
|
|
|
Voeg een `prepareStep` callback toe aan je `ToolLoopAgent`. Kies **minimaal één** use case:
|
|
|
|
### Optie B1 — Dynamic model
|
|
|
|
Begin met `gpt-4o-mini`, switch naar betere model bij complexe stappen.
|
|
|
|
```typescript
|
|
import { openai } from "@ai-sdk/openai";
|
|
|
|
new ToolLoopAgent({
|
|
model: openai("gpt-4o-mini"),
|
|
tools: { webSearch, readPage, saveReport, listReports },
|
|
prepareStep: async ({ stepNumber, messages }) => {
|
|
if (stepNumber > 4 || messages.length > 12) {
|
|
return { model: openai("gpt-4o") };
|
|
}
|
|
return {};
|
|
},
|
|
});
|
|
```
|
|
|
|
### Optie B2 — Fase-gebaseerde tools
|
|
|
|
```typescript
|
|
prepareStep: async ({ stepNumber }) => {
|
|
if (stepNumber <= 2) {
|
|
return { activeTools: ["listReports", "webSearch"] };
|
|
}
|
|
if (stepNumber <= 5) {
|
|
return { activeTools: ["readPage"] };
|
|
}
|
|
return { activeTools: ["saveReport"], toolChoice: "required" };
|
|
}
|
|
```
|
|
|
|
### Optie B3 — Context-trimming
|
|
|
|
```typescript
|
|
prepareStep: async ({ messages }) => {
|
|
if (messages.length > 15) {
|
|
return {
|
|
messages: [messages[0], ...messages.slice(-10)],
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
```
|
|
|
|
### Eisen
|
|
|
|
- [ ] Minimaal 1 `prepareStep` use-case geïmplementeerd
|
|
- [ ] Effect zichtbaar in console-logs (welk model / welke tools per stap)
|
|
- [ ] In `AGENT.md` uitleggen welke optie gekozen + waarom
|
|
|
|
---
|
|
|
|
## Onderdeel C — Custom stop-conditie (verplicht)
|
|
|
|
Schrijf een eigen `StopCondition`. Twee voorbeelden — kies één of bedenk eigen:
|
|
|
|
### Optie C1 — Token-budget
|
|
|
|
```typescript
|
|
import type { StopCondition } from "ai";
|
|
|
|
const tokenBudget: StopCondition<typeof tools> = ({ steps }) => {
|
|
const total = steps.reduce(
|
|
(sum, s) => sum + (s.usage?.totalTokens ?? 0),
|
|
0
|
|
);
|
|
return total > 30_000;
|
|
};
|
|
```
|
|
|
|
### Optie C2 — Genoeg sources verzameld
|
|
|
|
```typescript
|
|
const enoughSources: StopCondition<typeof tools> = ({ steps }) => {
|
|
const readPages = steps.flatMap((s) =>
|
|
s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
|
|
);
|
|
return readPages.length >= 5; // 5 pagina's gelezen = genoeg
|
|
};
|
|
```
|
|
|
|
Combineren met andere condities:
|
|
|
|
```typescript
|
|
stopWhen: [
|
|
stepCountIs(30),
|
|
tokenBudget,
|
|
hasToolCall("saveReport"),
|
|
]
|
|
```
|
|
|
|
### Eisen
|
|
|
|
- [ ] Custom `StopCondition` werkt
|
|
- [ ] Gecombineerd met `stepCountIs(N)` voor veiligheid
|
|
- [ ] In `AGENT.md` uitleggen wat de conditie doet
|
|
|
|
---
|
|
|
|
## Onderdeel D — `AGENT.md` documentatie (verplicht)
|
|
|
|
Schrijf in repo-root.
|
|
|
|
### Sectie 1 — Tools
|
|
|
|
| Tool | Wat doet 't | Read / Write |
|
|
|------|-------------|--------------|
|
|
| webSearch | Tavily query → 5 resultaten | Read |
|
|
| readPage | Tavily extract → fulltext | Read |
|
|
| saveReport | Insert in research_reports | Write |
|
|
| listReports | Bestaande rapporten ophalen | Read |
|
|
| (eigen tool?) | ... | ... |
|
|
|
|
### Sectie 2 — Stop-strategie
|
|
|
|
- `stepCountIs(N)` — safety cap, N = ...
|
|
- Custom: ... (uitleg)
|
|
- Combinatie waarom: ...
|
|
|
|
### Sectie 3 — prepareStep keuze
|
|
|
|
- Welke optie gekozen (B1/B2/B3)
|
|
- Waarom
|
|
- Wat zie je in de console-logs verschillen tussen "met" en "zonder" prepareStep
|
|
|
|
### Sectie 4 — Voorbeeld-run
|
|
|
|
Pak een echte query, draai 'm, en log alle steps. Plak in `AGENT.md`:
|
|
|
|
```
|
|
Query: "Wat zijn de belangrijkste AI-trends in 2026?"
|
|
|
|
Step 0: webSearch({ query: "AI trends 2026" })
|
|
→ 5 resultaten
|
|
Step 1: readPage({ url: "..." })
|
|
→ 4500 char tekst
|
|
Step 2: readPage({ url: "..." })
|
|
Step 3: webSearch({ query: "AI regulation EU 2026" })
|
|
Step 4: readPage({ url: "..." })
|
|
Step 5: saveReport({ ... })
|
|
→ reportId: 42
|
|
|
|
Finale rapport: (200-300 woorden)
|
|
```
|
|
|
|
### Sectie 5 — Eén observatie
|
|
|
|
Iets wat opviel:
|
|
- Onverwacht goed gedrag?
|
|
- Loop die te lang duurde?
|
|
- Tool die niet werd gekozen?
|
|
- Effect van `prepareStep` zichtbaar?
|
|
|
|
### Vorm
|
|
|
|
- Max 700 woorden totaal
|
|
- Concrete logs, geen abstracte tekst
|
|
- Mag wat informeel
|
|
|
|
---
|
|
|
|
## Bonus (optioneel)
|
|
|
|
### Bonus 1 — UI met live steps
|
|
|
|
`app/page.tsx` — een form + lijst die elke step toont terwijl agent draait.
|
|
|
|
Gebruik `agent.stream()` ipv `agent.generate()`:
|
|
|
|
```typescript
|
|
const result = researchAgent.stream({ prompt: query });
|
|
return result.toUIMessageStreamResponse();
|
|
```
|
|
|
|
Client: tonen welke tool aangeroepen wordt + resultaat collapsed.
|
|
|
|
### Bonus 2 — Sub-agent voor samenvatten
|
|
|
|
Maak een 2e `ToolLoopAgent` (kleiner model) die per URL samenvat. Hoofdagent gebruikt deze als tool:
|
|
|
|
```typescript
|
|
const summarizer = new ToolLoopAgent({
|
|
model: openai("gpt-4o-mini"),
|
|
system: "Vat één pagina samen in 5 bullets.",
|
|
tools: { readPage },
|
|
stopWhen: stepCountIs(3),
|
|
});
|
|
|
|
const summarizePage = tool({
|
|
description: "Vat een URL samen",
|
|
inputSchema: z.object({ url: z.string().url() }),
|
|
execute: async ({ url }) => {
|
|
const r = await summarizer.generate({ prompt: `Samenvat: ${url}` });
|
|
return { summary: r.text };
|
|
},
|
|
});
|
|
```
|
|
|
|
### Bonus 3 — Done-tool pattern
|
|
|
|
Vervang `hasToolCall("saveReport")` door een expliciete `done`-tool zonder execute, met `toolChoice: "required"`. Documenteer in `AGENT.md` wat het verschil was.
|
|
|
|
---
|
|
|
|
## Inleveren
|
|
|
|
1. **GitHub repo URL** in Brightspace
|
|
2. **`AGENT.md`** in repo-root
|
|
3. **Updated `lib/tools.ts`** met `listReports` (+ optionele extra tool)
|
|
4. **Updated `lib/agent.ts`** met `prepareStep` + custom stop
|
|
5. **Schema-update** in `schema.sql` (mocht je nieuwe tabel hebben)
|
|
|
|
---
|
|
|
|
## Beoordeling
|
|
|
|
| Criterium | Punten |
|
|
|-----------|--------|
|
|
| A — listReports werkt + agent gebruikt 'm correct | 2 |
|
|
| B — prepareStep geïmplementeerd + zichtbaar effect | 2 |
|
|
| C — Custom StopCondition werkt | 2 |
|
|
| D — AGENT.md compleet met 5 secties | 3 |
|
|
| Agent draait end-to-end (geen broken runs) | 1 |
|
|
| **Totaal** | **10** |
|
|
|
|
Voldoende = 6+. Bonus telt mee bij twijfelgevallen.
|
|
|
|
---
|
|
|
|
## Tijd-indicatie
|
|
|
|
| Onderdeel | Tijd |
|
|
|-----------|------|
|
|
| A — listReports + test | 20 min |
|
|
| B — prepareStep | 30 min |
|
|
| C — Custom StopCondition + tests | 30 min |
|
|
| D — AGENT.md schrijven met logs | 30 min |
|
|
| **Totaal** | **~2 uur** |
|
|
|
|
---
|
|
|
|
## Veelvoorkomende valkuilen
|
|
|
|
- **`prepareStep` returnt niks** → je moet `{}` returnen voor "geen wijziging", niet `undefined`
|
|
- **Custom stop returnt nooit true** → log `steps.length` en je conditie-waarde per stap
|
|
- **Agent stopt te vroeg** → check of je `stopWhen` array niet te restrictief is
|
|
- **Sub-agent recursie** → een sub-agent gebruikt zelf weer een sub-agent? Stack-overflow risico — limit op depth
|
|
- **Token-cost out of control** → log per stap usage en zet kostencap
|
|
|
|
---
|
|
|
|
## Tips
|
|
|
|
- **Log per stap** — `console.log({ step, toolCalls, usage })` is de beste debug-tool
|
|
- **Test isolated** — test je `StopCondition` met fake steps eerst
|
|
- **Houd `AGENT.md` bij tijdens werken** — niet aan eind
|
|
- **Eindrapport kwaliteit** — als agent rapport een 4 is van 10, ligt het meestal aan system prompt — niet aan tools
|
|
|
|
Volgende les: RAG + embeddings. Combo met agents: een RAG-tool in een agent. Tot dan!
|