# Les 14 — Huiswerk ## Eigen PDF + UI + RAG-tool in agent + analyse **Vak:** AI-Assisted Development **Opleiding:** NOVI Hogeschool Utrecht **Deadline:** Voor Les 15 — Cursor + Vercel deploy **Inleveren:** GitHub repo + `RAG.md` in root --- ## Doel Bouwt voort op de **lesopdracht** (werkende PDF Q&A app). In dit huiswerk: - Index een echte PDF van minimaal 20 pagina's - Maak een nette chat-UI - Bouw een RAG-tool in een ToolLoopAgent - Documenteer 5 vragen + 1 fail-case in `RAG.md` --- ## Onderdeel A — Echte PDF + UI (verplicht) ### Stap 1 — Kies een PDF Kies een PDF van minimaal 20 pagina's, iets dat jou interesseert: - Studieboek-hoofdstuk - Open-source whitepaper (LLaMA, RAG-papers) - Bedrijfsjaarverslag - Wikipedia-artikel als PDF geprint - Game manual - Wat dan ook — moet text-based zijn (geen scan) ### Stap 2 — Upload-UI `app/page.tsx` — file upload + lijst van geïndexeerde docs: ```tsx "use client"; import { useState } from "react"; export default function Page() { const [file, setFile] = useState(null); const [indexing, setIndexing] = useState(false); const [result, setResult] = useState(""); async function handleUpload() { if (!file) return; setIndexing(true); const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/index", { method: "POST", body: fd }); const data = await res.json(); setResult(`${data.chunks} chunks geïndexeerd`); setIndexing(false); } return (

PDF Q&A

setFile(e.target.files?.[0] ?? null)} /> {result &&

{result}

}
); } ``` ### Stap 3 — Chat-interface Gebruik `useChat` van AI SDK (zelfde als Les 12): ```tsx // In een aparte component "use client"; import { useChat } from "@ai-sdk/react"; export function ChatBox() { const { messages, sendMessage, status } = useChat({ api: "/api/chat", // nieuwe route — zie hieronder }); const [input, setInput] = useState(""); // ... form + render messages ... } ``` Pas `app/api/ask/route.ts` aan naar `app/api/chat/route.ts` die `streamText` returnt voor mooie streaming. ### Eisen - [ ] Upload werkt — file input + index endpoint call - [ ] Geïndexeerde docs zichtbaar (count of lijst) - [ ] Chat-interface met streaming antwoorden - [ ] Antwoorden zijn duidelijk gebaseerd op PDF-inhoud --- ## Onderdeel B — RAG-tool in een agent (verplicht) Combineer Les 13 (Agents) + Les 14 (RAG): ```typescript // lib/agent.ts import { ToolLoopAgent, tool, stepCountIs } from "ai"; import { openai } from "@ai-sdk/openai"; import { embedOne } from "./embeddings"; import { supabase } from "./supabase"; import { z } from "zod"; const ragSearch = tool({ description: "Zoek in geüploade documenten op basis van semantic similarity. " + "Gebruik voor inhoudelijke vragen over de documenten.", inputSchema: z.object({ query: z.string().describe("Wat je wilt vinden"), }), execute: async ({ query }) => { const embedding = await embedOne(query); const { data } = await supabase.rpc("match_chunks", { query_embedding: embedding, match_count: 5, }); return data; }, }); export const docAgent = new ToolLoopAgent({ model: openai("gpt-4o-mini"), system: `Je beantwoordt vragen over geüploade documenten. Werkwijze: 1. Zoek met ragSearch voor relevante info 2. Lees de top chunks 3. Eventueel: tweede ragSearch met andere query voor meer context 4. Antwoord met bronvermeldingen (source filenames). Verzin niets — alleen wat in de chunks staat.`, tools: { ragSearch }, stopWhen: stepCountIs(10), }); ``` Test in chat-route: ```typescript // app/api/chat/route.ts import { docAgent } from "@/lib/agent"; import { convertToModelMessages } from "ai"; export async function POST(req: Request) { const { messages } = await req.json(); const result = docAgent.stream({ messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` ### Eisen - [ ] `ragSearch` tool gedefinieerd + werkend - [ ] ToolLoopAgent gebruikt deze tool - [ ] In UI: tool-invocations zichtbaar (zoals Les 12) - [ ] Voor complexe vragen: agent doet 2-3 searches --- ## Onderdeel C — `RAG.md` documentatie (verplicht) Schrijf in repo-root. ### Sectie 1 — Document info - Bestandsnaam + onderwerp - Aantal pagina's - Aantal chunks na indexing - Chunk-size + overlap ### Sectie 2 — 5 succesvolle vragen Voor elk: ```markdown **Vraag 1:** "Wat zijn de drie hoofdkenmerken van X?" **Antwoord (samenvatting):** ... **Top chunks (top 3 met similarity score):** - Source page 12 (sim: 0.87) — "X heeft drie kenmerken..." - Source page 14 (sim: 0.81) — "Het derde kenmerk..." - Source page 11 (sim: 0.74) — "Overzicht van X..." ``` ### Sectie 3 — 1 fail-case Een vraag waar RAG het **niet goed deed**: ```markdown **Vraag:** "Wat staat er over Y?" **Antwoord (incorrect):** "Y wordt niet besproken in het document." **Wat ging mis:** Y wordt wel besproken op pagina 8, maar onder een ander woord (Z). Embedding van "Y" was te ver van embedding van paragraaf over "Z" voor cosine similarity threshold. **Wat ik probeerde:** ... **Wat zou helpen:** Hybrid search (keyword + semantic), of synoniem-expansion in de query. ``` ### Sectie 4 — Chunking experiment Probeer 1 andere chunking-strategie naast je default: - Andere chunk-size (200 of 1000) - Andere overlap (0 of 100) - Of: split per pagina i.p.v. fixed chars Documenteer: ```markdown **Default:** chunk-size 500, overlap 50 → 30 chunks, avg quality 8/10 **Experiment:** chunk-size 200, overlap 30 → 75 chunks, avg quality 6/10 **Conclusie:** ... ``` ### Sectie 5 — Eén observatie Iets wat opviel: - Welke vragen werken het best? - Hoe lang duurt index van een PDF van X pagina's? - Effect van top-k (3 vs 5 vs 10)? - Verschil tussen simple RAG en RAG-in-agent? ### Vorm - Max 800 woorden totaal - Concrete cijfers + voorbeelden - Mag wat informeel --- ## Bonus (optioneel) ### Bonus 1 — Streaming antwoorden + sources Gebruik `streamText` met source-citations in UI. Toon naam + page-number naast elke antwoord-claim. ### Bonus 2 — Hybrid search Voeg keyword filter toe naast semantic. Postgres full-text search of simpel `ilike`: ```sql -- in match_chunks where to_tsvector(content) @@ plainto_tsquery($keyword) or embedding <=> $query_embedding < 0.5 ``` ### Bonus 3 — Per-document filter UI dropdown: selecteer welk doc te bevragen. Backend: filter op `source` in match function. ### Bonus 4 — Re-ranking Top-20 ophalen via vector search, dan re-rank met Cohere of LLM-as-judge naar top-5. ### Bonus 5 — Multi-document Index 3-5 verschillende PDFs. Vraag over meerdere docs tegelijk. Documenteer hoe agent omgaat met conflicting sources. --- ## Inleveren 1. **GitHub repo URL** in Brightspace 2. **`RAG.md`** in repo-root (5 secties) 3. **Updated `lib/agent.ts`** met RAG-tool 4. **Updated `app/api/chat/route.ts`** met agent integration 5. **Working app** lokaal demonstreerbaar --- ## Beoordeling | Criterium | Punten | |-----------|--------| | A — UI + indexing + chat werkend | 3 | | B — RAG-tool in ToolLoopAgent werkend | 2 | | C — RAG.md compleet met 5 secties | 3 | | Fail-case analyse is concreet en doordacht | 1 | | Chunking-experiment uitgevoerd + reflectie | 1 | | **Totaal** | **10** | Voldoende = 6+. Bonus telt mee bij twijfelgevallen. --- ## Tijd-indicatie | Onderdeel | Tijd | |-----------|------| | A — UI + chat + streaming | 45 min | | B — RAG-tool in agent | 25 min | | C — RAG.md schrijven (incl. testen) | 50 min | | **Totaal** | **~2 uur** | --- ## Veelvoorkomende valkuilen | Probleem | Oplossing | |----------|-----------| | Scan-PDF — geen text | unpdf werkt niet op image-PDFs. Gebruik OCR (tesseract) of kies andere PDF | | Vector dimension mismatch | Index én query moeten zelfde model gebruiken | | Top-1 is altijd irrelevant | Chunks te klein, of vraag te abstract — experimenteer | | Agent doet 1 search en stopt | System prompt expliciet: "doe meerdere searches indien nodig" | | Cost-pieken bij grote PDF | Indexing per chunk-batch van 100 ipv all-at-once | | RAG hallucineert | Prompt strikter: "Als info niet in context staat, zeg dat" | --- ## Tips - **Test met simpele vragen eerst** — zorg dat basis werkt voor complexer wordt - **Log similarity scores** — onder 0.4 is meestal niet relevant - **Source-citations zijn goud** — gebruiker kan zelf verifiëren - **Chunk-experiment is verplicht** — het verschil is groter dan je denkt - **Eén goede fail-case is meer waard dan 10 succesvolle** Volgende les: Cursor + Vercel deploy. Voice transcriptie met Whisper, foto-analyse met GPT-4o vision, image generation. We gaan letterlijk de zintuigen toevoegen aan onze apps. Tot dan!