fix: update lessons
This commit is contained in:
1
Les14-RAG-Embeddings/.~lock.Les14-Slides.pdf#
Normal file
1
Les14-RAG-Embeddings/.~lock.Les14-Slides.pdf#
Normal file
@@ -0,0 +1 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 10:59,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
1
Les14-RAG-Embeddings/.~lock.Les15-Slides.pdf#
Normal file
1
Les14-RAG-Embeddings/.~lock.Les15-Slides.pdf#
Normal file
@@ -0,0 +1 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 10:32,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
575
Les14-RAG-Embeddings/Les14-Docenttekst.md
Normal file
575
Les14-RAG-Embeddings/Les14-Docenttekst.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# Les 14 — RAG + Embeddings
|
||||
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
|
||||
|
||||
**Les:** 15 van 18
|
||||
**Onderwerp:** RAG (Retrieval-Augmented Generation) — AI laten antwoorden op eigen documenten
|
||||
**Duur:** 180 minuten
|
||||
**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk.
|
||||
**Demo-app:** PDF Q&A from scratch — nieuwe kleine app
|
||||
|
||||
---
|
||||
|
||||
## Hoe deze tekst werkt
|
||||
|
||||
- `[SLIDE X]` — klik naar slide X
|
||||
- `[SCHERM: slides | terminal | editor | browser | supabase]`
|
||||
- **Vertel:** "..." — wat je zegt
|
||||
- `*[stage direction]*` — instructie voor jezelf
|
||||
- 💬 = verwachte studentenvraag
|
||||
|
||||
---
|
||||
|
||||
## VÓÓR DE LES — Setup (45 min)
|
||||
|
||||
### 1. Demo-folder + Supabase
|
||||
|
||||
- Maak vooraf werkende `pdf-qa` lokaal (backup)
|
||||
- Supabase project: pgvector extension AL geactiveerd
|
||||
- Schema (chunks tabel + match_chunks function) al gedraaid
|
||||
- 1 PDF al klaar om te indexeren: `polderfest-lineup-2027.pdf` (genereer met AI of pak echte)
|
||||
|
||||
### 2. Voorbeeld-PDF klaar
|
||||
|
||||
- Print of genereer een PDF over Polderfest 2027 — ~10-20 pagina's
|
||||
- Liefst: line-up, dagschema's, locatie-info
|
||||
- Studenten kunnen deze ook downloaden
|
||||
- Backup-PDF: Wikipedia-artikel over willekeurig onderwerp als geprinte PDF
|
||||
|
||||
### 3. Browser-tabs
|
||||
|
||||
- `localhost:3000` (dev server)
|
||||
- Supabase dashboard (Table Editor + SQL Editor)
|
||||
- https://ai-sdk.dev/docs/ai-sdk-core/embeddings (referentie)
|
||||
- https://platform.openai.com/usage (cost-monitoring)
|
||||
|
||||
### 4. Backup als demo crasht
|
||||
|
||||
- Werkende eindstaat (alle code) op USB
|
||||
- Console-log met "voorbeeld-embedding" als slide-content
|
||||
- Backup-vraag-en-antwoord screenshots
|
||||
|
||||
### 5. Terminals
|
||||
|
||||
- 3 tabs: dev, watch (`watch curl ...`), supabase SQL via CLI als nodig
|
||||
|
||||
---
|
||||
|
||||
# HET SCRIPT — Lees mee tijdens de les
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik (10 min)
|
||||
|
||||
`[SLIDE 1 — Title]` `[SCHERM: slides]`
|
||||
|
||||
**Vertel:** "Welkom bij les 15. Vandaag: RAG. Retrieval-Augmented Generation. AI laten antwoorden op basis van jouw eigen documenten."
|
||||
|
||||
`[SLIDE 2 — Terugblik]`
|
||||
|
||||
**Vertel:** "Lessen 11-14: AI SDK basics, tool calling, agents, externe APIs en deploy. Nu hebben we een complete stack.
|
||||
|
||||
Wat we nog niet hadden: AI laten werken met **jouw documenten**. Stel je hebt 200 pagina's productdocumentatie of een dik handboek of een lange whitepaper. Hoe stel je daar vragen aan?
|
||||
|
||||
Twee naïeve oplossingen. Eén: alles meesturen in context. Werkt voor 5 pagina's, niet voor 500. Te duur. Twee: AI zelf laten zoeken. Werkt voor publieke info, niet voor jouw private data.
|
||||
|
||||
RAG is de derde oplossing. In één zin: haal de relevante stukjes uit je documenten op, geef die aan AI, AI antwoordt."
|
||||
|
||||
`[SLIDE 3 — Planning]`
|
||||
|
||||
**Vertel:** "Drie uur. Eerst vijftig minuten theorie — embeddings, similarity, pipeline, pgvector, chunking. Veel om te begrijpen, maar het loont. Daarna vier demos. Pauze rond minuut 100."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Theorie embeddings (20 min)
|
||||
|
||||
`[SLIDE 4 — Wat is een embedding]`
|
||||
|
||||
**Vertel:** "Een embedding is een vector. Letterlijk een array van getallen. Voor OpenAI's small model: 1536 getallen tussen min-één en plus-één.
|
||||
|
||||
Je stopt tekst erin, krijgt vector terug. Maar het magische zit hier — vergelijkbare betekenissen produceren vergelijkbare vectors.
|
||||
|
||||
`*[Wijs naar slide voorbeeld]*`
|
||||
|
||||
'De kat zit op de mat' en 'Een poes ligt op het tapijt' hebben bijna geen woorden gemeen. Maar betekenis is hetzelfde. Hun embeddings staan dichtbij elkaar in de 1536-dimensionale ruimte. 'Voetbal in Nederland' — totaal andere kant op.
|
||||
|
||||
Dit is fundamenteel anders dan keyword search. Keyword zou de eerste twee zinnen totaal niet matchen — geen woord overlap. Semantic search wel.
|
||||
|
||||
Welke modellen. OpenAI 3-small en 3-large — de standaarden. Open-source alternatieven zoals nomic-embed-text als je lokaal wilt embedden. Voor vandaag: 3-small. Goedkoop, snel, prima kwaliteit voor de meeste apps."
|
||||
|
||||
💬 *Vraag: 'Wat doet zo'n embedding-model eigenlijk?'*
|
||||
|
||||
**Antwoord:** "Het is een neural network, getraind om de betekenis van tekst te 'condenseren' naar een dichte vector. Vergelijkbaar met de eerste lagen van een GPT-model, maar zonder de generative head. Heel veel context distilleren tot 1536 nummers. Wiskundig: een functie die tekst projecteert in een 'semantic space'."
|
||||
|
||||
`[SLIDE 5 — Vector similarity]`
|
||||
|
||||
**Vertel:** "Hoe meet je 'dichtbij'? Drie metrics, één winnaar voor text.
|
||||
|
||||
Cosine similarity — hoek tussen twee vectors. Van min-één tot plus-één. Plus-één betekent identiek georiënteerd, nul ongerelateerd, min-één tegenovergesteld. Voor genormaliseerde vectors (OpenAI's zijn dat) is cosine effectief dot product.
|
||||
|
||||
Voor RAG: cosine. Standaard, robuust, simpel.
|
||||
|
||||
In pgvector schrijf je `<=>` voor cosine distance. Een kleinere afstand = meer similar. Order by `<=>` ascending = top resultaten eerst. Onthoud die operator."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Theorie RAG pipeline + pgvector (30 min)
|
||||
|
||||
`[SLIDE 6 — RAG pipeline]`
|
||||
|
||||
**Vertel:** "Twee pipelines. Index time en query time.
|
||||
|
||||
Index time gebeurt eenmalig — wanneer je een document uploadt. PDF binnen, parsen, opknippen in chunks van ongeveer vijfhonderd tokens, elke chunk embedden, alles in Postgres.
|
||||
|
||||
Query time gebeurt elke vraag. Vraag binnen, embedden — zelfde model als index, dat is belangrijk — similarity search in Postgres, top vijf chunks terug. Die geef je als context aan LLM, LLM antwoordt.
|
||||
|
||||
Belangrijkste inzicht: LLM ziet nooit de hele DB. Alleen de vijf meest relevante chunks. Schaalbaar tot miljoenen documenten."
|
||||
|
||||
💬 *Vraag: 'Waarom moet het embedding-model hetzelfde zijn?'*
|
||||
|
||||
**Antwoord:** "Embeddings zijn alleen vergelijkbaar binnen hetzelfde model. Model A's vector voor 'kat' staat op een totaal andere plek dan Model B's vector voor 'kat'. Hun coördinatensystemen zijn niet uitwisselbaar. Dus: zelfde model voor index én query — always."
|
||||
|
||||
`[SLIDE 7 — pgvector]`
|
||||
|
||||
**Vertel:** "Hoe sla je vectors op? Postgres heeft een extensie: pgvector. Voegt `vector` datatype toe, plus operators voor cosine, euclidean, dot product. In Supabase: één klik in dashboard om te activeren.
|
||||
|
||||
`*[Toon SQL op slide]*`
|
||||
|
||||
Schema: chunks tabel met content kolom en embedding kolom. `vector(1536)` — dimensie matcht model. Voor performance: een HNSW index. Hierarchical Navigable Small Worlds. Wiskundige magie waardoor je in miljoenen vectors in milliseconden zoekt. Bij minder dan tienduizend rows kun je het skippen — exacte search is dan al snel."
|
||||
|
||||
`[SLIDE 8 — Chunking]`
|
||||
|
||||
**Vertel:** "Hoe knip je een document op. Klinkt simpel — is het niet.
|
||||
|
||||
Drie strategieën. Fixed size — vijfhonderd tokens per chunk, vijftig tokens overlap. Simpel, default. Recursive — knip eerst op paragrafen, dan zinnen, dan woorden. Behoudt structuur beter. Semantic — embed zinnen, group similar. Beste kwaliteit, duurder.
|
||||
|
||||
Sweet spot voor chunks: tweehonderd tot vijfhonderd tokens. Te klein, je verliest context. Te groot, irrelevant info verdunt het signaal. Overlap van tien tot vijftien procent zodat info aan chunk-grenzen niet verdwijnt.
|
||||
|
||||
Vandaag: fixed size, vijfhonderd char chunks, vijftig overlap. Werkt prima voor de meeste docs."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 4 — Demo 1: pgvector setup (25 min)
|
||||
|
||||
`[SLIDE 9 — Wat we bouwen]`
|
||||
|
||||
**Vertel:** "Vandaag bouwen we een PDF Q&A app from scratch. Upload PDF, hij wordt geïndexeerd, je stelt vragen. Klassieke RAG-use-case."
|
||||
|
||||
`[SLIDE 10 — DEMO 1]` `[SCHERM: terminal + editor]`
|
||||
|
||||
**Vertel:** "Klas, kijk mee."
|
||||
|
||||
```bash
|
||||
cd ~/novi/novi-lessons/Les14-RAG-Embeddings
|
||||
pnpm create next-app@latest pdf-qa \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd pdf-qa
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js unpdf
|
||||
cursor .
|
||||
```
|
||||
|
||||
`*[Supabase dashboard]*` `[SCHERM: supabase]`
|
||||
|
||||
**Vertel:** "Eerst pgvector activeren. Database → Extensions → zoek vector → enable. Eén click.
|
||||
|
||||
Of via SQL:"
|
||||
|
||||
```sql
|
||||
create extension if not exists vector;
|
||||
```
|
||||
|
||||
`*[SQL Editor — voer uit:]*`
|
||||
|
||||
```sql
|
||||
create table chunks (
|
||||
id bigserial primary key,
|
||||
source text not null,
|
||||
page int,
|
||||
content text not null,
|
||||
embedding vector(1536),
|
||||
created_at timestamp default now()
|
||||
);
|
||||
|
||||
create index on chunks
|
||||
using hnsw (embedding vector_cosine_ops);
|
||||
|
||||
alter table chunks enable row level security;
|
||||
create policy "demo open" on chunks
|
||||
for all to anon using (true) with check (true);
|
||||
```
|
||||
|
||||
**Vertel:** "Tabel, HNSW index, RLS, open policy. Dezelfde regels als alle eerdere demos."
|
||||
|
||||
`*[Editor → lib/embeddings.ts]*`
|
||||
|
||||
```typescript
|
||||
import { embed, embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export const embedModel = openai.textEmbeddingModel("text-embedding-3-small");
|
||||
|
||||
export async function embedOne(text: string) {
|
||||
const { embedding } = await embed({ model: embedModel, value: text });
|
||||
return embedding;
|
||||
}
|
||||
|
||||
export async function embedBatch(texts: string[]) {
|
||||
const { embeddings } = await embedMany({ model: embedModel, values: texts });
|
||||
return embeddings;
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Twee functies. embedOne voor één string, embedBatch voor veel tegelijk. Batch is veel sneller — één API-call in plaats van een loop."
|
||||
|
||||
`*[Voeg chunkText functie toe]*`
|
||||
|
||||
```typescript
|
||||
export function chunkText(text: string, size = 500, overlap = 50): string[] {
|
||||
const chunks: string[] = [];
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
chunks.push(text.slice(i, i + size).trim());
|
||||
i += size - overlap;
|
||||
}
|
||||
return chunks.filter((c) => c.length > 0);
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Simpelste chunker mogelijk. Slice op chars. Voor productie: gebruik LangChain's RecursiveCharacterTextSplitter — handelt edge cases beter. Voor demo: dit volstaat."
|
||||
|
||||
`*[Test embed]*`
|
||||
|
||||
```typescript
|
||||
// Eenmalig in een test-script of API route
|
||||
const e = await embedOne("De kat zit op de mat");
|
||||
console.log(e.length, e.slice(0, 5));
|
||||
// 1536 [0.012, -0.0345, 0.0123, ...]
|
||||
```
|
||||
|
||||
**Vertel:** "Vector van 1536 nummers. Eerste vijf: kleine getallen tussen min-één en plus-één. Dat is alles."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 5 — Demo 2: Index pipeline (20 min)
|
||||
|
||||
`[SLIDE 11 — DEMO 2]` `[SCHERM: editor + terminal]`
|
||||
|
||||
**Vertel:** "Index endpoint. POST een PDF erin, krijg chunk-count terug."
|
||||
|
||||
`*[app/api/index/route.ts]*`
|
||||
|
||||
```typescript
|
||||
import { extractText } from "unpdf";
|
||||
import { embedBatch, chunkText } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
if (!file) return Response.json({ error: "No file" }, { status: 400 });
|
||||
|
||||
const buffer = new Uint8Array(await file.arrayBuffer());
|
||||
const { text } = await extractText(buffer, { mergePages: true });
|
||||
|
||||
const chunks = chunkText(text, 500, 50);
|
||||
const embeddings = await embedBatch(chunks);
|
||||
|
||||
const { error } = await supabase.from("chunks").insert(
|
||||
chunks.map((content, i) => ({
|
||||
source: file.name,
|
||||
content,
|
||||
embedding: embeddings[i],
|
||||
}))
|
||||
);
|
||||
|
||||
if (error) return Response.json({ error: error.message }, { status: 500 });
|
||||
return Response.json({ chunks: chunks.length, source: file.name });
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Vier stappen. Eén: ontvang file via formData. Twee: parse met unpdf. Drie: chunk + embed. Vier: insert alles in Supabase."
|
||||
|
||||
`*[Terminal:]*`
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
`*[Tweede terminal:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/index \
|
||||
-F "file=@/path/to/polderfest-lineup.pdf"
|
||||
```
|
||||
|
||||
`*[Wacht 5-10s]*`
|
||||
|
||||
**Vertel:** "Response: chunks: 30. Dertig chunks van onze PDF in de DB."
|
||||
|
||||
`*[Supabase Table Editor]*` `[SCHERM: supabase]`
|
||||
|
||||
**Vertel:** "Daar staan ze. Content kolom met tekst. Embedding kolom — clickbaar voor de vector. Source filename. Klaar voor querying."
|
||||
|
||||
💬 *Vraag: 'Hoe lang duurt embedding van 1000 chunks?'*
|
||||
|
||||
**Antwoord:** "Met embedMany batch: enkele seconden voor duizend chunks. OpenAI's API is snel — duizenden tokens per seconde. Voor één PDF van 200 pagina's: misschien 5-10 seconden totaal. Indexing is goedkoop én snel."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 6 — Pauze (15 min)
|
||||
|
||||
`[SLIDE 12 — Pauze]`
|
||||
|
||||
---
|
||||
|
||||
## BLOK 7 — Demo 3: Query pipeline (20 min)
|
||||
|
||||
`[SLIDE 13 — DEMO 3]` `[SCHERM: editor + terminal + supabase]`
|
||||
|
||||
**Vertel:** "Nu querying. We schrijven een SQL function voor de similarity search."
|
||||
|
||||
`*[Supabase SQL Editor:]*`
|
||||
|
||||
```sql
|
||||
create or replace function match_chunks(
|
||||
query_embedding vector(1536),
|
||||
match_count int default 5
|
||||
)
|
||||
returns table (id bigint, content text, source text, similarity float)
|
||||
language sql stable as $$
|
||||
select chunks.id, chunks.content, chunks.source,
|
||||
1 - (chunks.embedding <=> query_embedding) as similarity
|
||||
from chunks
|
||||
order by chunks.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
**Vertel:** "Postgres function neemt vector + count, returnt top N met similarity score. `1 - (a <=> b)` converteert distance terug naar similarity score."
|
||||
|
||||
`*[app/api/ask/route.ts]*`
|
||||
|
||||
```typescript
|
||||
import { embedOne } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { question } = await req.json();
|
||||
const embedding = await embedOne(question);
|
||||
|
||||
const { data: chunks } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
|
||||
if (!chunks?.length) {
|
||||
return Response.json({ answer: "Geen relevante info gevonden." });
|
||||
}
|
||||
|
||||
const context = chunks
|
||||
.map((c: any, i: number) => `[Source ${i + 1}: ${c.source}]\n${c.content}`)
|
||||
.join("\n\n---\n\n");
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
prompt: `Beantwoord de vraag op basis van deze context. Als geen antwoord in de context staat, zeg dat eerlijk.
|
||||
|
||||
CONTEXT:
|
||||
${context}
|
||||
|
||||
VRAAG: ${question}
|
||||
|
||||
ANTWOORD:`,
|
||||
});
|
||||
|
||||
return Response.json({ answer: text, sources: chunks });
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Vier stappen. Eén: embed de vraag. Twee: RPC call naar match_chunks — top 5 terug. Drie: bouw context-string met source-labels. Vier: generateText met prompt-template."
|
||||
|
||||
`*[Terminal:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question": "Wie zijn de headliners op Polderfest 2027?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
`*[Wacht 2-3s]*`
|
||||
|
||||
**Vertel:** "Antwoord: 'De headliners op Polderfest 2027 zijn... blah blah, gebaseerd op de line-up uit het document.'
|
||||
|
||||
Plus: sources. Welke chunks waren de top vijf. Similarity scores. Volledig traceerbaar."
|
||||
|
||||
`*[Tweede vraag voor demo:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question": "Op welk podium spelen de jazz acts?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
**Vertel:** "Andere vraag, andere chunks opgehaald, ander antwoord. Werkt."
|
||||
|
||||
💬 *Vraag: 'Wat als vraag niet in document staat?'*
|
||||
|
||||
**Antwoord:** "Twee dingen. Eén: similarity scores zijn laag — top vijf is alsnog niet relevant. Twee: LLM merkt dat context het niet bevat, en zegt 'staat niet in document'. Dat tweede dwingen we af met de prompt: 'Als geen antwoord in context staat, zeg dat eerlijk.' Anders zou-ie kunnen hallucineren."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 8 — Demo 4: RAG-tool in agent (15 min)
|
||||
|
||||
`[SLIDE 14 — DEMO 4]` `[SCHERM: editor]`
|
||||
|
||||
**Vertel:** "Laatste demo. Combineer Les 13 met Les 14. Een RAG-tool in een ToolLoopAgent."
|
||||
|
||||
`*[lib/agent.ts]*`
|
||||
|
||||
```typescript
|
||||
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 chunks
|
||||
3. Eventueel: tweede ragSearch met andere query voor meer context
|
||||
4. Antwoord met bronvermeldingen.
|
||||
|
||||
Verzin niets - alleen wat in de chunks staat.`,
|
||||
tools: { ragSearch },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "Agent met één tool: ragSearch. System prompt vertelt hem multi-step te werken — zoek, lees, evalueer of nog een query nodig is.
|
||||
|
||||
Het voordeel boven simple RAG: voor complexe vragen kan agent meerdere searches doen. 'Vergelijk de jazz- en rock-headliners' — agent doet twee searches, één voor jazz, één voor rock, dan vergelijkt."
|
||||
|
||||
`*[Quick test in script of route:]*`
|
||||
|
||||
```typescript
|
||||
const result = await docAgent.generate({
|
||||
prompt: "Vergelijk de jazz- en rock-headliners op Polderfest 2027.",
|
||||
});
|
||||
console.log(result.text);
|
||||
console.log("Steps:", result.steps.length);
|
||||
```
|
||||
|
||||
`*[Run]*`
|
||||
|
||||
**Vertel:** "Antwoord: vergelijking van beide. Steps: drie of vier — twee searches, dan reasoning, dan finale text. Agent doet zijn werk."
|
||||
|
||||
💬 *Vraag: 'Wanneer agent vs simple RAG?'*
|
||||
|
||||
**Antwoord:** "Simple RAG voor single-shot vragen. 'Wie is X?' → één search → antwoord. Agent voor vragen die multiple lookups vereisen. Vergelijkingen, multi-hop, of als gebruiker breed vraagt en je wilt dat AI verfijnt. Trade-off: agent kost 3-5x meer en duurt langer. Begin met simple, upgrade als nodig."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 9 — Wanneer RAG (10 min)
|
||||
|
||||
`[SLIDE 15 — Wanneer wel/niet]`
|
||||
|
||||
**Vertel:** "Niet alles is RAG. Wanneer wel: veel documenten, ze veranderen, semantic search nodig, privacy belangrijk, bronvermelding gewenst.
|
||||
|
||||
Wanneer niet: klein document — stop in prompt. Exacte data — gebruik tool-calls of SQL. Structured data — SQL is beter dan vector search. Code-base — gebruik grep en tree-sitter.
|
||||
|
||||
In productie zie je vaak hybrid. Semantic search voor concept-vragen, keyword search voor exacte termen, metadata filter voor structured criteria. Drie technieken in één query.
|
||||
|
||||
En: re-ranking. Top-twintig ophalen via vector, dan re-rank met Cohere of LLM-as-judge naar top-vijf. Twintig procent betere kwaliteit. Voor productie: doen. Voor demo: skip."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 10 — Lesopdracht + Huiswerk (10 min)
|
||||
|
||||
`[SLIDE 16 — Lesopdracht + Huiswerk]`
|
||||
|
||||
**Vertel:** "Lesopdracht: zelfde app als ik liet zien. Half uur. PDF Q&A, pgvector, index, ask. Voorbeeld-PDF deel ik in de groep.
|
||||
|
||||
Huiswerk: drie dingen.
|
||||
|
||||
Eén: echte PDF van minimaal twintig pagina's, eigen interesse. UI met file upload + chat-interface.
|
||||
|
||||
Twee: RAG-tool in een ToolLoopAgent. UI laat tool-invocations zien zoals Les 12.
|
||||
|
||||
Drie: RAG.md schrijven. Document-info, vijf werkende vragen met sources, één fail-case waar RAG het verkeerd had, één chunking-experiment, één observatie.
|
||||
|
||||
Bonus: hybrid search, streaming met citations, re-ranking, multi-document.
|
||||
|
||||
Tien punten, voldoende zes."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 11 — Afsluiting (5 min)
|
||||
|
||||
`[SLIDE 17 — Afsluiting]`
|
||||
|
||||
**Vertel:** "Wat hebben we vandaag gedaan. Embeddings als vectors. Cosine similarity. RAG pipeline — index + query. pgvector. Chunking. RAG-tool in een agent. Wanneer wel/niet RAG.
|
||||
|
||||
Volgende les: Multimodal. Voice, vision, image generation. Whisper voor transcriptie. GPT-4o voor foto-analyse. Flux of DALL-E voor afbeeldingen genereren in je app. Heel visueel.
|
||||
|
||||
Daarna: les 17 over MCP — Model Context Protocol — eigen MCP server bouwen. Les 18: browser automation en Computer Use. Spannende lessen om mee af te sluiten.
|
||||
|
||||
Vragen?"
|
||||
|
||||
`*[Vragenronde — minstens 5 min over laten]*`
|
||||
|
||||
---
|
||||
|
||||
## NA DE LES — Wrap-up
|
||||
|
||||
- Push working `pdf-qa` repo naar GitHub als referentie
|
||||
- Deel voorbeeld-PDF voor lesopdracht in Brightspace
|
||||
- Brightspace: huiswerk-instructies + repo URL
|
||||
- Voor Klas B: check of zelfde stack werkt (Supabase free tier limiet pgvector?)
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende fouten tijdens live coding
|
||||
|
||||
| Fout | Oplossing |
|
||||
|------|-----------|
|
||||
| `extension "vector" does not exist` | Supabase: enable in dashboard |
|
||||
| Embedding dimension mismatch | Check `vector(1536)` matcht `text-embedding-3-small` |
|
||||
| `match_chunks function not found` | SQL function niet aangemaakt — kopieer uit slide |
|
||||
| RLS error op insert | Open policy nog niet uitgevoerd |
|
||||
| `unpdf` geen text uit PDF | Image-based PDF (scan) — gebruik OCR of andere PDF |
|
||||
| HNSW index trager dan exact | Voor <10k rows: skip index of `set hnsw.ef_search = 100` |
|
||||
| Antwoord is generic, mist details | Top-k te laag of chunks te klein — experimenteer |
|
||||
| Hallucination | Strengere prompt: "Als info ontbreekt, zeg dat" |
|
||||
|
||||
---
|
||||
|
||||
## Mentale model voor de klas
|
||||
|
||||
Als studenten verward zijn over wat embeddings doen:
|
||||
|
||||
> Een embedding is een **adres** in de betekenis-ruimte. Twee adressen dichtbij elkaar = twee zinnen met vergelijkbare betekenis. RAG werkt door: vertaal vraag naar adres, vind alle adressen in de buurt, lees wat daar woont, geef dat aan AI.
|
||||
|
||||
En RAG vs Agent:
|
||||
|
||||
> RAG = bibliotheek met index. Eén vraag, één bezoek, top-vijf boeken, antwoord.
|
||||
> RAG-in-Agent = bibliothecaris die voor jou zoekt. Stelt eerst sub-vragen, doet meerdere zoekrondes, vergelijkt, antwoordt.
|
||||
330
Les14-RAG-Embeddings/Les14-Huiswerk.md
Normal file
330
Les14-RAG-Embeddings/Les14-Huiswerk.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 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<File | null>(null);
|
||||
const [indexing, setIndexing] = useState(false);
|
||||
const [result, setResult] = useState<string>("");
|
||||
|
||||
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 (
|
||||
<main className="max-w-2xl mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">PDF Q&A</h1>
|
||||
<input type="file" accept=".pdf"
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
|
||||
<button onClick={handleUpload} disabled={indexing}
|
||||
className="ml-3 bg-blue-600 text-white px-4 py-2 rounded">
|
||||
{indexing ? "..." : "Index"}
|
||||
</button>
|
||||
{result && <p className="mt-4 text-green-600">{result}</p>}
|
||||
|
||||
<ChatBox />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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!
|
||||
175
Les14-RAG-Embeddings/Les14-Huiswerk.pdf
Normal file
175
Les14-RAG-Embeddings/Les14-Huiswerk.pdf
Normal file
@@ -0,0 +1,175 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 6 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105950+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105950+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Huiswerk) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Count 6 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2045
|
||||
>>
|
||||
stream
|
||||
Gb!;dbBDVu&Dcq.As6.NXUmVi!5!:ja76.X]]<V`%7NSrD2M:`b"f;,[s%gM+=16tX<qXuq,$&^Qj5<dkCO*jI[I[cF+EEuGkR:$j"Qo%F,::"1H6F9T,kds*k37&+fP`-OAP.*.&TfMSko9GLC>F;L^[O-,pQ<B&ie/E!Z"&#@.MfU5Q[P^dKLh_k=bj+HpN/*dBlP/h[_[/\,Jtd?:b"F4*7h)7qFZ3[6[rt_BkFlJHqr1%/tYGnJ#:E;cUfCU<)9*/lb$7iI^Y$N_Y@"<L$Um9&PE7<`u\pX1\EL7[GfTWbqG$]>>eqTY\5=1KC.Gn7n@l2laN*oY-<^Bc[<@.=-c-]$f[2el,p0&qljlc.KSdNfWeoNZ35f1/l9`-7#,XqFPW$I49DbmmHHo)o`#ST#^KY:?E3%YRmlo`kl>P2.e/>dsITFXYD?"SMo1>M]I[mA9_t3[pN<r8a+A<gh4L"j8riSQn-$aNPsBBG-Q+>.RbKWZ1EKB8NkbGcGOgi2.e-qG'd_/[h@D/UReZ>*P5A7&W\Zg]"hRf4VhM3XD1FYRSnD(&oIET=tm4+>;!T'`Y)p8huWt%OjYB=;V5b?GEp57-"&ui)3>CkonFaQ9RD;)O41)NiU)Pin-bbYioU-dafs>RR-"t=ZPmCP*bBh$lJ=in$\:bc/<p,6>P"H0m=<Lm231gSK)SX-41F^fRSQdJ1Bk[2rmsF>'2/Y+(Ud=Q0QQY;])$9gl+U4Bk*',*I<Dl=h=m\m\iC*a*YcO#'d["OX8=,E)7ZfU;:WY4Erf@A-qZ2XjYuGsHC<=J]L\b*Z$/]'S/rBkBqF:0Pq'O5U4"V0m*/+*^KQZ&oG0lZd:Aj4HnL=df*:bt_AT2:aW?Qa'Zf/OQHSibnol$BC^#.;osdD[g8Mf)!$]s._!Sl9rWIp6#14^?_=b\MbRdJ,_qcX+HD+@6fm,>RfMeK(`gO^*O)'QXFJ6g0D@_QU^24Abd!\^2p%H1Thqb$X?E7I[P.f?6m<2++0j*i<43"i+Q*hN)'5FFAXXVhoJraV-TXdt\B+-TkoFu?/M9;#dZ&k'%P2_oRL3e[E,]&i5gBmeLqa8#1i.#[D_gQ1qN[i&7djnqbir!sXK!8+NbTr4WkAD'K:8cC"_=M,];o:Ho5n1d7&d=]'MFCE4aMJ3&j&MKoE?4-p`uW1:>T!>0;h]pjW%Y`RIt`+_eO+-tA(7FF=3Y9@_#p2g_Y^[6=<*akP300@QHed)bs51F99#cO9(l&0f:sTVU:)rT>q3*0L[naq-SQX<E.->.:4j;1r3S4i(<@`!.sj$(gJPQ(qReKO<b#UZb4#Ya"[gY<#`DEMpJs5Y"#IqXYS+n<i9GOK0RV$SLX[fJer$=a?KXK=S89S-0RAkjb\*-M0U#/N+m!Qq&JF/mS81#TKTO9]1rVH>D'<?412hX9oG_CqFS-+JKCki\<GVHClnfF=@Xu^7OejJ>q"[+r,!p$Cik^WGNte)G[]2$P<#KkERT-Gi*A-(+.ItZR6[RjJK[=n0Xbj_]1V7$\Ygl<=Z%h@pLNTp6oeK+h\566o4QT;#Wa9"K2'ubN'7WD<SZN,coOaff@9"l%=M`65XjnV!W"N%uln!I2eDQr2<?%pI*be,1^ON$adbX(9.FZI3FW!UoelS,*diAI3<;67pZnJQ<#j>35PqH%!7`21+djp\LPaQ:C&QVCl^OK@@;Vu/j?lnFQ.,,KX%ufNkBY>6V'd@9*8,3_jUkR9CTk^OVQ[1WO.Qhp'9eZ*"$>V.Zi*IMmGP@iDU2O5-BmbR)dll'J"oL&2IftRH^;u0[),?'0%WgEl8W#VjbnnRj8$#2]%)6QfCKgEW6<Mj5/M!ZoM!JCUN=tBXb]cDM,CkX^Ff?R\/Q3qOoaPsA7-4G!b]mMXqI"jnG?r-aSJHL)n:2KtN"'Q4j'^HP5C3ZZkdTI,$9O9sC=h4$m`g)gntGNq*^uJSn,[hSFofU4;+D/`<@?%LdD:P+)C#?*+BDT?qC&tILN0T;;f=CA/A8,C%k*L?i!^rII&p@>ZDF),q$15hmns~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 437
|
||||
>>
|
||||
stream
|
||||
Gas2E?VeNm'ZJu(.F+]O=F0joNu0.((klMsj9r7SVQf#;p$5g?5`fYte!Kcuqn"uo*fAZmYm.?EoLB#s87"Q=.3uC48YYs8#pZP"@+'$4$a<`N(TK#9TFE]^L^B)Gk,a/T`12`+7*o(0(6up50-W)c\9//&OZ(LTkp8,PK[u#pA)cDB*M;*G`Q[]4!bSoo=a36ETan5'0i$G;,\pZ?h*4Xg/i-ifQ=[VD6W<g"ZGD_9(5'`ZTW[mtnL94le$$p=g)&![P#ipBD6G16,rJVagJe,V?O(t;5P"Zo=)XYuFIqU5*9,%Q(-_DMe4RQIjsl#rY'GBM%FrJ8gK;B;PeB9YF/Sd\V'Bph[70JD`87fEq;R744Wuq"Pf,D@URtTNp)i.5l9-PaYHk6-?Qf<g;npI^hb\2E*KH6WkBZHCQ(Bk*[>!f`+]t3o~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1620
|
||||
>>
|
||||
stream
|
||||
GauHJgMYe)&:N/3%/dJ-D477KOkLu_S2M/c5uT_3'IX_fA1b-g=?#PT`sM\4*E#s#[Y=>sX2R[migcFqS/De=MANV6;$uf/"\13_0Fk-Z^]Y7RH4A2%c@nWqc_^,]\;Iiu#84Q/"6J+U_"3C)n:6(aOshbE-q9F.'QDQcP"@&ZI!Xd++O[/c#p>+N^@K%fGDXX+RMNj5rSXi-kFXNt"iTlB6T<aQK&I'f"Z;saJl(T$%ot,1rd=>:"CC1o>t)@f:Z,Mal=a8:RWAeU-\o`B'j%g"^`WDN-[Cj>XPS`_B37;O[O1ddon[Rd]T)qp=6Mi\C?"MNQffsg?f<og6?";;Vmr]hP0Z3c,uql@OAYl<N]Sl]N,jV*Hq5:GOnNdV,n?8p;P>EmY.@Vb0^82.`9Z#Cc61"0VAM*cK.+NXJHl:cbM"h+"2,gD#Q*]&@JH.J0BiI)#:kp(7kYMQ1H=&:%q/-UO.;bN8S,%_pI?kFGq=;jU,m`sEY]5+!mVN7`-%?CCsJVHnj6Hjc$/UiI,EogKR]%93:U+$`4^m)p[QGEg5mo3+P\07@(j)"AWapP]_27D.th<=WXL<n7<gAYiSKa8Oojs0nCE!P#V"L@*+XmN'K(cCBW+k%:dO2BUA?a01KLi;;C\Jrl=E.*JpBhl"\7:TMZs[r#ssW$$nnDSPLjmZUSBD.%5l-j7pe@V0q.Y&?>?t]fZ_b?R1XDXO_Cmnbh_@IkQm@Hm)Z&_eCFM7?r5otS^Y"T7!Vh`:km`63\7$H49^rKp#=^7#33X5/HiinHW1k!Fe!*f)*NlS7K_$e!polTlZ5G&X_s"A%lYCbH.83nVDH:$3"*"9_.B!"V:.-!is;YENK^T*lYEeMn@1WY'_.90S\7!5Regm.6_l#U-2H5,*RtuX>Vs%)2PR3(j(u>6XS:[c]g%9)IGtF1O%+WH;,cd,K[k2\_16<_i9>/6Z_u"`[Z6*UBlg&u1VMJ7;ja'D.Z?I2RnU"%(4NsslO:#<Oap?b!WE:mYR]W(^oa%L`9l-O/>DfH0+I5\pEA6Jfm0uL0^HD;[OO1]%rLeX#61$"-jS+Mk=nu1_401lm(S&C`rL#>@qOBj,otejFkr:M%=(^6HA)*#Ds_/V\\Q0fmR],$(q([@'A04&q8992J-_h_\"NRjOdC3W26;gJgR4[#VQXB]ebV>Q$S.\u>>GlARrtF$q*l`)X>6?#0kPf;e;#549]I\\RjGi=M!msd5ulMrMkj#@B^7\/PFcdpM:U2Xi0]\XNReDTNQ'\e_MZ(Mo5\>@Wk9c*?'11(VHoNZf,TgQD:>bi-1Z=t@rk6%GUSr1XnTQTMgon2>N1i!IQ+>jWR_GHB6U&ahin!8O%AF,8*%Y5&`2j5N+(Wl'8&F_(=2#5hs.\K[nMf\-'!J-8S[/V#7V_8e0=HlIdY`$!d%a44Pr<[AW(88o#7%hq4Nd.BmC-MWei<@^CPk$gYcSYmF8Gl)uU)bX#1OT(GK@$c8H2:=5^*N(ZK,oLI/XRi#(EUfu3(+#4tu6K75(,:4HSlUTGVqmNdF/-VGE"@@UWc;.t]?$GMs[o.1<na27_1dJ^9,hIC'5Y+6=7:'PZ?Rdk2#r6XiBT<>o7Htk<RrW@dBI=q~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1352
|
||||
>>
|
||||
stream
|
||||
Gb!SkhfIL2&:X@\Z#<.dTN\X;bC-pp9mZ%tVq:XPgcb/$Qj$amK3AK-rq3b+_)S,bg<4$0(DuM[ddFT:^>5&7\FebQDk$j@(rEU\*4UOX"LqtTL2Z!jk^7E<#Xg(h@%MDP8-"Q1qZl64ah\Ki&;c3qE^Q]j#uYc?<2;e+\hN1gPR*I#%L3aRDQ"g7`Vi%*Hg<g1#XPU>o7J0s^OCf2=a1Md2WtEYo-Im'K5Z?VXa6/@g^efMB$bo%qt4a7.]-n==])!aB2rD'X'd2@#_Ub-JME"\E"!Pm_R*EUQrdoSr!:1felTo+oR%uK2nWr+%2sg/=3JO`Z(*arhs"j<CQKQ(mtoPrV*JjYL9>!W2&U$s)0\2P7X6bN!G%5[=``>XV2bhLlUo\F'5B<r1*-Xo45t2A`(iB`2dC??9sm>#c&6s_D0rp`dfS/3_ut%+T/rL6o%c[B2<;2P@`R;u8El`^jCWBN7ORcZR5q[Rm&/P6B<SC]C@qd)D=l6/Z\WTB$))m5>u19rhQrQ>I:9d^q>Rd35H*s>9^LN#K)%MQiSLj>@IP5Z`\?\[Y[!t1a_c.n/L^hFD5Ha$l[lXO7X6$10&FLUU%\f3<J%Td(?7gq0<M5taX>%J(gS8$p&qP^!-.>F>;D"Z[jFJLKjqZ#ObIDh&L$Uu<<q%+XG]km`5D'e8<T4`8O+;'d!Q?@kBDp#5Z?opCiZX6GR?l'`fdWNER:B\:H`#`4($k[eNi;_:17!9SR9U'@"eE%_<cRJ820RMREpqKnq`G+:`qE."FZJ]6)uiB"KC80g&$+[gN)5-ntF6XBZbl:Af8X&<^iH$X@bXpr#c5+4FS*mG>`Ed'I@-JIr=*,krI*3YA%h.2ju5F8lo#Udr8;<W89I;%iJDUOt91IR8ffp`'oN;SUaZB&'^Wr0ZRUrPI)EIK."HRK7?1SV$Y^VQU&4eAjce/BT?2T@A_'R#.[Jn)m#qO[Zmn:"^d-59hcFkT0SSKeO.e2)k(O#ds_AT0!J%;*N0IaOXKCOX0;):JJ5^jM'P-?Ogq?i,FGL\'i#(eW4*eAA3Ac3,2Gr.:j]AA"LCnS'1Z]:m?Y\5/\so9rO>s-#CX/mUc2Xr>)BW=l526=Kl[1Nb4HXq01VV[F!to7QBY+c@=@PRae&@d[e[\:endtufMae9C_#8tqu??Pme'Wb6E2KF&sDmhc*EV[iU=5N*t.Nbkl-p4#mn5(M4-`&?Di+@c"b!gD0L)F1qs"dOl.-NH4*GFgXd4jHu3_u\EM@:o"J,i=6+YrLu=,.]J7L9SCAZB?u8OhZfea"*QfF6qNg![?)X5Kk3!9lH!Fg4&18PIKUt6LMOrX#_&MjP)0#QUZBW*7~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2116
|
||||
>>
|
||||
stream
|
||||
Gb"/'>>s99'Roe[3%s*]V(n8eg[a^Tc9PS@4*6h;d(\un*0IbPQE_H6p=Jbqlo6?Q#"s5F'&qQ+^2q#`F-?lSp`fDfaT)g*Y]iWg:`*L>@4m2k$%*RN2i^Nu)R09IT6l#\_2Wm7#NaU[+UHmXVD/,>74sW<0MYlu@h'4971kBLP]6<8+lGh`(NBX3-UGhk#P3@&U_H6R2[I<GEIOr12sSA"fP61V2K&`9&Yp`'U&*"+8.YqYb[BV;^V:i-OWM[qR:r_\]ImbNWSrHW#kj;aqc/n^[2d#0Gq5.417?at@JCjtDiCb=54\lCq-J.bs$Tnd4rd;GlA$po)r+@H:3-G%&*PefFbUUY<n#.MY&>GHCotYs+"bBY(:"K,LB_Yf[<nbj)S)H[GjcO3m3q+[.&?eh_/)g)FO0FaiIi&HmSSr'iW)#UT/:%="_J8X<,hP:3<XgXo,kXUaQoWIN]5QViD$4\41V-6A"GXna#]K22/BsNBUm+hDs6cC5C.)\]7e+eEZg2(o75RQHf&$DQS`;\]_:h^M]]S'+[2Z=5[nijHh[h,T2op9UPh1`\WlC=oa4LYHo<6k:UkC,qt!JbV!D.uBG[8Hq&D*bm$'Bqe'f*BD)AW,)GD+@3u90"m]"i?&f3;G\87_;LO]iX*#<DA]ZW%rI)1mX=dkC[(Qu-@`[I+;2WqNt"AZ(A%[EWOM&bSCA:Pb%%>b^3<io>_UE*^e$8YhG[)15HAXSROP+kVsbLorP'7$,qBW"d7e`.9`bLKRk5qRT;CY8O^jk.mi4d,cC/-dDj9MKheX&RWT>],2c@i1KB)!N.<gNF_'U@j@#2PApMfjH/mOZ#1_K2Ud^(^R14^DJ*ci"((1-aJt`-RYP1STu9XU],khT1&V+<7P]lr6*4ZK)e[(+'*==[M30Z<?^o?1+CBYRc8m1T,0?N8h/D'f2e:=/*\dfW.Du1<7DGn\=t&-$JVmbk*R%tUt4`DC)0<TW[aK"_kW)=pR`\+Lq>F8R+Jd.F%KZM/;5GFTg,FIdI\'`j?gc[fa4H7*0f]4;Ie@\9p=PB0V_i+$_PH*D#^9"N=6stDGY)-^%tKS[5"XZ=Y4_npW9[Q+m3c0N]iBWGqfqDYG'Cf4Yar]kgh*$eUX_+f__Ui=oMRFYq=&Es$f`t`L\L+]jr>P]i%XA3FcbPj3lM!>hUfL>u1&3CkF=l5)nnf?(+)taF_jl67<\m%/X5pF_,hI7(I`f@P!s\W?nC+GGWjPjjj/AG96A2Lf&dC([Xul<i_@Kq+*i^]-`9DbXj%/Y1)+B!1Xhqq$FieZ>T9k<Y[]NZ"7G#fUN9kLP>&1(:HT"OQ5g:^_,CYbEe=FJ4l67Hr+2==.\93A$lfFXa'<W'O0I:R]SQ.n?@^Y-18dF:,/jH!OZ%ti\`K*CbbYB[qm=iamZ)7ho.i$ae.t/%:^:"q$+FW-A>VWJ,)'gju"M6#G&5)p+oj$Lt<<BMg_'F[M7C=dT%KrF1!pn9/KWqd93.gAgW_4\f.n@=pqd%?P(oJ%n3K&M5V\U>c9`s-9=pV401JLRc1aIaXVZH4A09A[<ma]r)o\ffT"E63o$"+/2dcJ5un>,eo7R'b\S5kR\@[c%5S=P:]16Qbol3!a4*C"c?ss[a>'*ufgj'li('+QZ?b5m>6SA7k=a#g/rQYkN*)O]8AMe;2pSHs3C_&h`MJ`2T>V]sUkLg``*"KI$6-Ms8nCldL4R[e]6C.:pMrq-8P`F62m_YU!>8pa(uk(r!-=@^Hui:>XSM:,o/mDJi&/:^?9>%[oW5b59]4YEaIQBL((WBuIH-+f/-a^pI@a!CIPs!+9kN+j-RL1EQW0p)?eG=>9dT)*Pj0$be@!S<4h;BnHCCilG8V^ZiWdhU<H#cg[]8gC&buDcN(nSek/HQ,Ar@Su'SBO)OCm-`Qm#0h)j"[s=1";H@:WiE_Cbts[)te-9H#IOp+V_KVR!jeJ#'YB/MiAdFTH-2qf2`>pU8ta@4b-/HPDUO"ZJ0p=[=Xu>"Ng#IFW4OnVOco2F/V]!a_)]YY<Y^g?+n2E5to9rkAW=m]'H&:m@?VC;@VdBp-V-*)(6(&P>c]1rj4(S_Hn+am&KboOY)sKJ;;b?eg;R3<'&-Y`D3~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 775
|
||||
>>
|
||||
stream
|
||||
GarnT_/A!]%)(gSGT"jCLaq)`,TjY#710gaML!sQ(KY2h[>pbK::fiY8(hU>VH@Y:U!<?,6p:q8FG]O%QN/*W34fkI18l6Z81(%AJW"::CW8,pBitGZ%F<3+=IH'/KQcLO:e[-W.DToIN0YTG@)eE81;[`W1^&Jee!U35("YnV1SR]8]trl*AJpc!XAGoZ`c(O<,d1`nX,^^c2j$slfPYHERS1XhTaRfbE:WhJO))8j>E!aQpDnW'!ntTnG?Lg%r-Ef5UPf44n-M&+orBD8iB:/$:n=Ie#Q->1R*<*ic$tA.jMTO%`FfTT'CXPaFhS'&5]fXnIA9c`N1#8j)?tV=GE2lZTt]uW)INfSSQhq>&S_VMX/X2\3XVf-7@o+Kk3$J$(1?lfi-@ZV_h'3[pG@JkI=W)7_Pafrd\d%lLZ9VQV6us)^<N(;I+2q?!C'Arceb@6hX$)a]JR!6<bCENP1I`eUHSCJk!eSu7?\hgfYD4fUN-*Cgh^WXQO_J4kj@$QK*cSFGoE+gI=??2`Y43es(7%Mb,sY_``,`/g"Vu[Vr@&tSD4FQTu&e)&dO51\O,$D-IaVn5R_Y<<^KNk2gq1D>ZV.hr./PY5^jV7eGBFPE3f9PM6ZKa4!G-P\>fE]dC4QF/C(@:lG<-W_I*'$U$^IV;<#X8g-<jPpBlOG?3_9TK43<@h5R'/%ZBYG%K/O1RbGY(6A,:%\Eq1??AMPc)]DQ?]"+pf-;G0oi46!X1ZGB"EG[;I:#C*gl<L-KEB@J\qZdT!-V4~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000536 00000 n
|
||||
0000000741 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001737 00000 n
|
||||
0000002030 00000 n
|
||||
0000002121 00000 n
|
||||
0000004258 00000 n
|
||||
0000004786 00000 n
|
||||
0000006498 00000 n
|
||||
0000007942 00000 n
|
||||
0000010150 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<fdfd9a649f26f3dd357aeedfd55ab30b><fdfd9a649f26f3dd357aeedfd55ab30b>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 12 0 R
|
||||
/Root 11 0 R
|
||||
/Size 20
|
||||
>>
|
||||
startxref
|
||||
11016
|
||||
%%EOF
|
||||
277
Les14-RAG-Embeddings/Les14-Lesopdracht.md
Normal file
277
Les14-RAG-Embeddings/Les14-Lesopdracht.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Les 14 — Lesopdracht
|
||||
## PDF Q&A app opzetten + eerste embeddings
|
||||
|
||||
**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 **PDF Q&A app**: je uploadt een PDF, hij wordt gechunked + embedded + opgeslagen in Supabase pgvector, en je kunt er vragen aan stellen die accuraat beantwoord worden op basis van de PDF.
|
||||
|
||||
---
|
||||
|
||||
## Vereisten
|
||||
|
||||
- Node 20+, pnpm, git
|
||||
- Supabase account
|
||||
- OpenAI API key
|
||||
|
||||
---
|
||||
|
||||
## Stap 1 — Nieuwe Next.js app
|
||||
|
||||
```bash
|
||||
pnpm create next-app@latest pdf-qa \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd pdf-qa
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js unpdf
|
||||
```
|
||||
|
||||
`.env.local`:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-...
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 2 — Supabase: pgvector + schema
|
||||
|
||||
In Supabase SQL Editor:
|
||||
|
||||
```sql
|
||||
-- 1. Extension activeren
|
||||
create extension if not exists vector;
|
||||
|
||||
-- 2. Chunks tabel
|
||||
create table chunks (
|
||||
id bigserial primary key,
|
||||
source text not null,
|
||||
page int,
|
||||
content text not null,
|
||||
embedding vector(1536),
|
||||
created_at timestamp default now()
|
||||
);
|
||||
|
||||
-- 3. HNSW index voor snelle search
|
||||
create index on chunks
|
||||
using hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- 4. RLS open voor demo
|
||||
alter table chunks enable row level security;
|
||||
create policy "demo open" on chunks
|
||||
for all to anon using (true) with check (true);
|
||||
|
||||
-- 5. Match function
|
||||
create or replace function match_chunks(
|
||||
query_embedding vector(1536),
|
||||
match_count int default 5
|
||||
)
|
||||
returns table (id bigint, content text, source text, similarity float)
|
||||
language sql stable as $$
|
||||
select chunks.id, chunks.content, chunks.source,
|
||||
1 - (chunks.embedding <=> query_embedding) as similarity
|
||||
from chunks
|
||||
order by chunks.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 3 — lib/embeddings.ts
|
||||
|
||||
```typescript
|
||||
import { embed, embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export const embedModel = openai.textEmbeddingModel("text-embedding-3-small");
|
||||
|
||||
export async function embedOne(text: string) {
|
||||
const { embedding } = await embed({ model: embedModel, value: text });
|
||||
return embedding;
|
||||
}
|
||||
|
||||
export async function embedBatch(texts: string[]) {
|
||||
const { embeddings } = await embedMany({ model: embedModel, values: texts });
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
export function chunkText(text: string, size = 500, overlap = 50): string[] {
|
||||
const chunks: string[] = [];
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
chunks.push(text.slice(i, i + size).trim());
|
||||
i += size - overlap;
|
||||
}
|
||||
return chunks.filter((c) => c.length > 0);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 4 — lib/supabase.ts
|
||||
|
||||
```typescript
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 5 — Index endpoint
|
||||
|
||||
`app/api/index/route.ts`:
|
||||
|
||||
```typescript
|
||||
import { extractText } from "unpdf";
|
||||
import { embedBatch, chunkText } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
if (!file) return Response.json({ error: "No file" }, { status: 400 });
|
||||
|
||||
const buffer = new Uint8Array(await file.arrayBuffer());
|
||||
const { text } = await extractText(buffer, { mergePages: true });
|
||||
|
||||
const chunks = chunkText(text, 500, 50);
|
||||
const embeddings = await embedBatch(chunks);
|
||||
|
||||
const { error } = await supabase.from("chunks").insert(
|
||||
chunks.map((content, i) => ({
|
||||
source: file.name,
|
||||
content,
|
||||
embedding: embeddings[i],
|
||||
}))
|
||||
);
|
||||
|
||||
if (error) return Response.json({ error: error.message }, { status: 500 });
|
||||
return Response.json({ chunks: chunks.length, source: file.name });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 6 — Ask endpoint
|
||||
|
||||
`app/api/ask/route.ts`:
|
||||
|
||||
```typescript
|
||||
import { embedOne } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { question } = await req.json();
|
||||
const embedding = await embedOne(question);
|
||||
|
||||
const { data: chunks } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
|
||||
if (!chunks?.length) {
|
||||
return Response.json({ answer: "Geen relevante info gevonden." });
|
||||
}
|
||||
|
||||
const context = chunks
|
||||
.map((c: any, i: number) => `[Source ${i + 1}: ${c.source}]\n${c.content}`)
|
||||
.join("\n\n---\n\n");
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
prompt: `Beantwoord op basis van deze context. Als geen antwoord staat, zeg dat eerlijk.
|
||||
|
||||
CONTEXT:
|
||||
${context}
|
||||
|
||||
VRAAG: ${question}
|
||||
|
||||
ANTWOORD:`,
|
||||
});
|
||||
|
||||
return Response.json({ answer: text, sources: chunks });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stap 7 — Test in terminal
|
||||
|
||||
`pnpm dev` in één terminal. In een ander:
|
||||
|
||||
```bash
|
||||
# Index een PDF (Tim deelt voorbeeld-PDF in de les)
|
||||
curl -X POST http://localhost:3000/api/index \
|
||||
-F "file=@./polderfest-lineup.pdf"
|
||||
# {"chunks": 30, "source": "polderfest-lineup.pdf"}
|
||||
|
||||
# Stel een vraag
|
||||
curl -X POST http://localhost:3000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question": "Wie zijn de headliners?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
Verwacht: JSON met `answer` (accuraat antwoord) en `sources` (welke chunks gebruikt).
|
||||
|
||||
---
|
||||
|
||||
## Eisen
|
||||
|
||||
- [ ] pgvector extension actief in Supabase
|
||||
- [ ] `chunks` tabel + HNSW index + match_chunks function
|
||||
- [ ] 1 PDF succesvol geïndexeerd
|
||||
- [ ] 3 vragen werken met correcte antwoorden
|
||||
- [ ] In Supabase Table Editor: chunks zichtbaar met embedding kolom
|
||||
|
||||
---
|
||||
|
||||
## Tijdsindeling (30 min)
|
||||
|
||||
| Stap | Tijd |
|
||||
|------|------|
|
||||
| 1 — Setup | 3 min |
|
||||
| 2 — Supabase schema | 5 min |
|
||||
| 3-4 — lib files | 5 min |
|
||||
| 5 — Index endpoint | 7 min |
|
||||
| 6 — Ask endpoint | 5 min |
|
||||
| 7 — Test | 5 min |
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende problemen
|
||||
|
||||
| Symptoom | Oplossing |
|
||||
|----------|-----------|
|
||||
| `extension "vector" does not exist` | Supabase: Database → Extensions → enable vector |
|
||||
| `dimension mismatch` | Embed model en `vector(N)` moeten matchen — gebruik altijd `text-embedding-3-small` (1536) |
|
||||
| `match_chunks does not exist` | SQL function niet aangemaakt — stap 2 stuk 5 |
|
||||
| `embedMany` slow | Normaal voor grote PDFs — geduld of split batches |
|
||||
| RLS error op insert | `create policy` niet uitgevoerd — stap 2 stuk 4 |
|
||||
| PDF parsing leeg | Scan-PDF (image-based)? unpdf werkt op text-PDFs |
|
||||
|
||||
---
|
||||
|
||||
## Klaar? Verder met huiswerk
|
||||
|
||||
Het huiswerk bouwt voort:
|
||||
|
||||
- Eigen PDF + UI + RAG-tool in een agent
|
||||
- `RAG.md` met analyse + fail case
|
||||
- Bonus: hybrid search of streaming antwoord
|
||||
|
||||
Zie `Les14-Huiswerk.md` of `Les14-Huiswerk.pdf`.
|
||||
175
Les14-RAG-Embeddings/Les14-Lesopdracht.pdf
Normal file
175
Les14-RAG-Embeddings/Les14-Lesopdracht.pdf
Normal file
@@ -0,0 +1,175 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105950+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105950+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesopdracht) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Count 6 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2150
|
||||
>>
|
||||
stream
|
||||
Gb"/'D,8qH&H88.0fl9K7p0=`06SgF@XFqqgnBFfV7i(S:e4h^aX/1Q_R7/X`_"b,_X^UmMXEi7[##h#nP&l)1ZN%3n\a)LGR!Y`$CbrO0FlQ-^c^VmF;MB`c@nW1^kLqRE@#`p%O?/h+&o.5%XpZpOG=5i'grB>4NWDDT`gDIKO=j2"eX?d#"/Sn8.UMeINX7^o3DuJN_=`;%ta<Ir8'W:bmR+i>DHg6rs4?(=RVJ?FZi'Z079/4?u<4D@0_63:/r=,'h&=^9r6jU#*(YZ0]!\,lT2-eQ,">]WNr7gWP&$EVOq;.Y1FTMHe]i.oY`NaY,]ls^e7NO<FhcsUXM&\J.8a>iHnj\"hZcPE#B7]n)AfOq9kf6kKOuBpt_F:OC9@ZS-Ji3?j?btEn_'HG"dNCQVcRPHEQWCLauR5#*#ml)eBd@4g?efS2pOWVMA>3C);dKMIpDoh2h"X\efYUS2n+0KLs9koUR44gm0o4ho:0/4*M:b'lrM"(PGEG9C"YMqb^r[e"_=M%HG(bTb`XA`W!:6)U'QmA!1!8OGH(.0jYW(VE\?CCqdkG!Z`sc/D`n@JAiAe9Xrj]JL48E6H`iQA+"libCS#4"`'sO]b<FP+_TL77,EFb(>_1r6WB@JP[@G>W#T,o;ADYAW7d#"Ou,D*mu7uK(KtT.:5/jEeGB8(""kSI5$kl7[#MM;F!GX^q%R)e9gP,Ws(,>!IVD=EHgO+&Q!F1tS\JDbH>$<7FOG*cU"F0!qgMgS?4Jt^+6CG1q7O;rMhcmko>8J>:%PpPn2)6+=]F!sH133e%[s>af+2ho6beK1iE4h>lbhF5rLBh!L."=^&JBMlMB"^h3(]jC6A7XnVtH=Tc>T*>m[>SZb.`pN:1CkCUTtou\RrY%'`.f1m(@(u6,F&C+RDJs70sH)a+bsp!PEBCSLD]VcpT7L63W2bLpOJ<H!/<A%M"HA[_`mVY$@8qmKU+VVut.QHBMuV)>#Hc$EQP6q\\MU-0l.I1%2#:N>1Ela2bP/d!aBoc5=jbj#Z8H1R0=HHm'XB-,k(_6KFn:P-E-.]qP]5o1.HsAG!^,JXg/L#_g\i?ki(Ko(K/FnQ2Zj-&u<X]?WW8-YpfW7q<JuX.\"mG1t[AmaCCd9:2&K&LI;L#/k0Gr*1bSQ<:fN9n@3A^)tSBkTm/CPc'lC_MW(1T;`5OX6:8.SYU!djg?Lsk>`3RE2')>\LNIdJLgZ8@7:J.P1FRijj;[K*ZbAh+Gmn>kLL46G^qe,PkUJ40![gT?Jkuh+^h26DG;58C!=`LPCl%Oqb]C]"ant_0?1qjl,+H6^2:Dj\hN2M[,,2#Y&22r!u;n3)S!<'*/77WI7OeY265qm_7.uoCLfZ_\2!KcY9U4%!N]iH<b-5>ZDOQ+%$NP9>ZYCbU9jNs>U?Iuc.B-$r3jndibq7?i,R21-qj=W<2F\?bATuH[Y>URm/K"TF#h=O\!#kSC#t-WEGJ=cmHa&B7qVKeJapVAM[_*+Ejc"Hl0XmE%[-;TGNb0GhC&`r75;QP:6@HX5CkVOnn#"(q03.aq`Ac"^-#ZJ>^[1Q./tGm:F]KE3suhTQR+h3$bf34k#Vm:lL0ITY'%atn'Jq"XCh+N)4D7u,!;04k%[O%a0S8sIEiSZNd:KB9(bU,ASWh`4Xu1LE\V0Fq@*Q>/F^@3kr6uP1su;Mdt%nX%l+of*e^g.bmP]Vi%_WIe?5XI+fu:)g=jl^hUpSJeA*kO]5r'/1D\#*cR8+J>OS%^<er7@i?:"t%3+8DD.VA")^pnGUPfD<N`Q2*Z[d+:mp+."4FkhXq,44RY0sW'N@<&S;Cjt$)B-B^@T6G!-qPq*UpIA*E7$4rCA;LYF8`#KfAa:dqNnC/XZ5Q>]R.rnPhini,mcsgCdmTe:<kd+<&/9=/J?^9H\L<]gl.?1o,O+-QbonD./_>!8lU72Z6G=7gJ;Kp-)PrTjo6H*I+0sl:s0*[i_)Turg@0a_g[]P_"?hu0b"dIVMA>/C=ddk?s-t5hZ!4Z]Q9$'n+_t2^L#IR,=9I10Qsc#kOa$!nFc4p0?4<Qqr?;gKC.^:pUdWSR\m/nDJgZRJpAUo*mX):^uNPnUpX-Lq0grAe\&a=a(4ri0gBA]G+Ts5o.DcT377$ODLA<nd1SCRq$6]SFqX~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1226
|
||||
>>
|
||||
stream
|
||||
GasaogQ(#H&:N/3%"`os"rg%EL_lj=OJ31&+D;_0l'$[P<haGR-.+5?g%b"tZu5TG[:1'j8WdZ2ba]Q-6B#8tqTgXAT@`T+!sQhT]`fmAJFt;bFWEV6KLr%/!efm,)[=7Mn:>0feW<<G%5S]?XGBmYKVh.QUOhAaV5T!c_Y/+?JLDBd/V%N'81jjaSeK3_RMI02d_%\,nC)r[+`^tG2#AAHi=7n.25,%V@[a4ueeP98s5U;Dn=/1c2VU;p_AD+^D!tfMR"G&Shn3m^U@gF"?gLiu\jL%EIgbSW@Jr7>3l;b-&8$U,`;a@PU^q]iLV-2+kWdO%;5_4TYWcQ%7ki$hZ2mru8MM;!TeSDaS?IA16dR$911:Kl)lOsS<$1UDXc.pn_ngV4b).ILLt2<Y*qs3m[)T'hMDK^5Raqm%JgW7(F%6%$'8(!&U#noaiBZ_8g[P[fcaL,-T#m3VRCEaiB5cp@(@@l\]#(om_:oJ?bXoc.d=Y3>EefAeAJ1A%LeN&c\u`09[pZsr>IT<eU"D^!4>*9N>52hgn!\J#/VXR.F/"<tGZQdP%&[JL<djg@>0-DCrReu![9t;s&Qbn%!(@A.`NHUCOd9s"MCNJ'n7[l,$Qqd"R+#S2BjK>$a916_H!PW*GmB+HB;):&&f-fZ>^NEneJLXQ!c%f[<"k0[2R[q9H!]"4+mb[59t9!6/tl*2As`A@k4^<&.4X*KZoUHi[qSnJ?I,+kk&pbYNerRO3!!o7(QqS%-4*`:p$>U'8Rf%dc[JaSp)B[c"p&g_E.)*&c_uPaZ+/+"hR<1Hcn87[im_(*`mW;ik#,p%50Y#?$tX*sd+YR:$@c<t9c2HV\o?Zeas&paX5$_cgqhRFArd_S"BMM!B#r`X23=+MGR%rB&:HAn/)PGlEsp<Jh]o5I*iHCl[S.@0Uf^.r2PF6lk1h=pdb$pT@V'Af3qK$s<2n4;pUsp,PtL(i:mm-UZ_u),a8>lOpB^.IX<+IS8Z^$V.u%Oa*$p:NC96lm\fF\^=nNdZPjq1ZhOPHG:5Z&"du\t0Q7hi'`ISO'Sd9]M#Zf-UhK>MpZqi*EhJh2cc-:A>Bo!=(Z`shi&gK/FL(sI>3#lU-o:G_D2b4Vhr9++jGj>f.@t9rQnAC;O^!$lh&_YJB\Sasr;hQYab-uB@GKHHJnBb\*3S3=oSe"?d*eI%5OQc2r1a&.KLHcU,6G8?0E;J_Zh8MG`7/m7[-'r*~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1376
|
||||
>>
|
||||
stream
|
||||
Gb"/&9lo&I&A@[:m._j(&rc8i\'?gu-<&6d9f#drM/luPfO@41MF]g(4oXnUO;()CNgWg@Pr&]U`4k&UqZ+tO."5=r2d6AHT>ScD#)pIt!gk5Gk-*^^4Sb?UY.9Z%Lp0i<%`F4nJaI#HK$m%KmMK&@Lc#g@A].bbJYShlbeVfI`KPrU0q'"2JQSeVeR/(ZU,g9@OsmHgIK)Jq)q&dqpXruDh%.>@#4%2)W'V->PYj,A_rURDJ"OW^eYTIU<E0e'=0qSQWdt?:#+hC3T%.si2gu(JphO59S$ao=A!m8uo-Y^4!+0b<\<H%ds-9cBIDDQ1/@T>HrZf,#b7qkplOEEr"4OnYekf"mTEb;`.-ZRREib@+C02igOmY!YMM)p8WO)aH\,@jLR*g:p;7ehk&oDm0/7S]1RN0Uo$6lodh40kJcB)Id]B^YLplt.fdKi6NPUrO*/.0%H8bUH7N'ZQ.Z:\;+Kl>TBPF"R"Xai:6]gN'AA!CrU\,_8n_CF<K9b."d=@_u]EIb5sHq3suplZ<0*,>3Js3E%J@,e:&MZ!F0?bc?YAR8]oUoDBk+WMt+nQ$9]+ula#3-N_`-%QF7kI&.XM);)6K@*^FdaI'`2oUKN$'j!K/Ts'#9;5-Wlk%dWKo.c]W5()M[\0dm',-0;7,$N$;&F'RO(lP,qaa!gG$b:+BA0udCY<&Y-jp'Ym2CCD'ok9!GgO)G7Wpr<f:-O0^b)q<RWcSeC&&Y7i*oomi@)s3IXHB]Lj0,<P$B"/<Z>DCH%buRc"H_?_b"J^KiE:C9L*u9Opk$i0!;s'j(;:o2glFkdMHlR-UJ`$e%F<^Z@00K*!P@Rl?"@bof=[^UW)b*IR95P5+Z-64?5RG0<II*)-gF3=d)ouQg!b`0.%j+;lH0;k-6n#gQD8iP?m:s(Z"g>'S<EY62E%THR]ep>?HZD:C#BCiArQnr$d<GE&V)ZV*n3/qk.0N>]%Z4bRnc*/Q'$:>OT1d\OI7oXF-OEVIdA`N7%MbD6d/3E4ocVmCR@uRGtaQN;OU9omN.DZ?_Dn\tck9#-OB4'l9<8$J4s<4!$B,B_bA51Hah/FX(GW?H':76a@r]g$5U80ZVTOANYZ-s48JcMUEqQf7$r^!>04ia`dg\\R#?"r&2inLPYWsh$=b*KaT3sAF"aQha!3NFMpm8,,M][GPebi$P/.e%R7?a,_[m)S(2a(b)d1*of1=M5W2\dY=VV.H%m8'5/kHhBIR.*'<H/dQn47fPp-sj_Us[*(hXlXZQ8-P#Qdd(_>+RM6nY!`1Wu.QAbK,0j@/+<R.hjnC<iD;ee83_JW?&%)t/^YiV*Mp6JCH!GKB7L%]R1_4/D6"+j\?.^B%[o2cY=r&'C@EP5bNmb!8r~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1166
|
||||
>>
|
||||
stream
|
||||
Gasao;3RJ-&:WeDm+ddXj/s[O,iMKZ]pnYkRE7:"iq0)>J]"cfC;:&u\]i=spF]jh^3320#JjU@mC0-7-$*I9nb/Pb!4OI\gi;6R<ZBs0-*5t>0.ZM-6_T1!!MF@u*t#g1Da%!8]c7*-.MO$2__`8`5oPd'KE_STC!AN"(,i?Y%]C,k7KOJL.&6e'n"UMI6;%`O$=EWOq3ctDM!E'ABQd_aUD`use:?gWA3A3>X>61ApAU,^n8#;:<O+-44a_FL9m.!j+Z.uerSaWRd`U6#]Wki2^,TX]GLs(q!ioBsX:ot1,`5F`F8U6j,5OPlEEJRt!<"(tVKiU@"RC93CCd;\OfVLj"%*PijrnA"<)g55m#'[7c(cb`#fn&Rk"2\eJe,<'-Oji28(4[l$CPH$e7/87M)VT.&T(i+e6jLXd<]uo_PhA#^gOAXeG-?D3,2quIV'fA)e108)p.I2A`.n,+0uh,'6!'Al>!Is"H,>`_H+Vabu#i9CDUG.\54:LHLc3jEf1+>LVk<"Ul.':gW)`ceBXehrd4GG#\fCr]Nb=k3u"ND-a8Yl(>l6-KP/r%70Zh9ITUNkI[kr['fP)4>'=i9\$8SLF%CIsrq@;uc\m3t^M)f3dsP@oL1l5jfF,]!I>I@Pb.`Dnc'+f*i3<V*-8C7<KsH\S+5=kVofZ)Y;s0!1M]=pns0RYqV0uI_8,H-;b!jqOF?)6<"p=.L()A#Ao#eeG.'kk;Mr?P3hr</5(@m!h0q(P_'+&Z\0F3Y##>OrY!.,R<=r5Gd)XA-cN%;E5a!kCBgKm)H^99I7>7u$q^VT(iGNPbRj6tBsj5*2>\?Xht'-$r")$GP;7\=>_"Kp3K?^;A%7^J,pS7Z?@gfiF=CpaCIeg-1<G`mBtnF3^f\1eb7Fj@!%F[_f)F[rcmk8XQCRbMq'Y*!FA?Ic;.`cQu:MIp/DBkS8)=+S#S=6S+V2!UoqeoqW_/O`:r,*Gt\bq!!rQBstf^?Z#ecIi[ANeP>$5*,nZPbk/HBauSkn+BHfbpGJ?Kd`cP@_`)EMRlLJZBJB3B6FbJlt*7`@mc6J?k)d8Ss96#oWJQH)5-Q.)1\:tEF3nZIFPtB^!GaX$9Gtk^CnT_V0-pKdgf:s8157g<(0R$4-k*DQ[*N@XkWdbl1\\?rd8n-KD,mU86)5~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2012
|
||||
>>
|
||||
stream
|
||||
Gb"/'>Ar7S'RnZ;3,f8TBk*U-/u1V:gNj00T]ftWg&^fl1,9LnOX3GZdnAj'^8&<1UfKNahFh?'P)dWPgck9KRK:A</,jlgJB(S*+#%(BVT6J'@_q[_4I[8P#2p%.Q4:cse9!\^f?F7;3AEk)*TFTaAK[rp;HR<u7\k,PE1A9Y'9Hle^2G\J_P';"5U<ZVl!0]r&r1O-D%YNBgoL#sqdaT99:X-c1pd*((li!h;[oa@&^"ou_fQ4&=0LMpJEP=::;'(1"n6l)BQ'$LN,o$7^)6g;VLFuu3BYh.3f)dl%^S<cT#1,KY,d1nEJnef&tAYLFc,;`?D;\9W!<*X3q5Q@FpI=b`jH"lG\*tLP*dR2R#e@82bXX?1bHag?tjSLPdcW'0Xr6i>-]C5Z5R9d-61m-f_$*-BOlA*,\&i6,B@doCI<_kZ>rWK5V;KP_"p5K(qsSK9A0Bn7Z1p+eqsGL)C)<)N>ina4_%n7]ZNHMqSrh$IgQ6=6rk#dUlA6g-0$=+`&4ACMmPL8]F\+uq<GA?mhGOLiRdTO:2K&:G;o"%1DPOJYq/$?*R<]dhk8P[FKfSF#_@QLl+e\Kiu2[8,qTHbi1_I/<4k%=+j0K0a9'8B1nWEXRRQKQC/NBDaEieXZqK(mORB&"kfP/39Ci<oJodEIK,P+9#su$13?s3AS_(4PUTL\h'XIC_[^K6.>Ar6M)bFIi>NCb"`!Md?S8k?,^8*b@=7USE.ZVP!p;\XG)^j#_dj.S)h@(;*`ehf0!<"p<Opk=d-1p,?'mc8iL%+6tiH-tOo_AW=e)r6PB!XY1lp7J4=Oa_c=9'u]dgRk&R%7`ii?>-qb0P%!4=@%@'[DH>]q;`c15YNj!c<JQjcQ.\rU?UK/0qDd4WDu;qN6J!Cf_D7DAhP<"j*r-2-637J@AX7B5,DkF;:i$Q"lK#hJ>/RWm"[p-q=a?qr5Y4NcYkUr#h&*o&W7!m%j<$+"HJf'kS,`pNquR,a;iI'C5YMi-shrV_9q:2cr@)@tXe;rAhcWP^\gmMD<FE%ghmt;3+mq'B[qa"?ug1pX1:#Zq<HorbFD"o>a0bR&rSj#BNUEC/o]pCpli/EA,52+:N]hUJZ:mk`#da,c5G;hRYQ_N_:or8q)IC75.o"SENcs.aq<1c\=TAYQ`/AZ6gX9BWcso7'R5eO05"kfuT%?90`'rN?/j;2K;D6)mD;YQ8,].j(#4.lr+TEF$p5nYFi[c$L:Ydb.*cKlS</B&uC\OG;&:D[f4pSh8Bq7E\;:T6&s]DhH*\j&tQ.f&9jm+`lRF[s0r1HVOqrc.H7uN,urB:C%eR+YITsADkY'hR?Qo+hm:eG8*lu=EQZ".ML5Um6p;p,XAmP:$''?_[06O0HQRu#\INbn]asP.Qma;NhM4*d3N03UOlX2Oo?d+nNJ+`J[+2SEhIiUHh8*We8HO)4H8^`"GR8G>d5.Utghp9"3qi(3W(l+u$"3Bqd<!,JbMpWg*(6A*(/^Mp9M0$HhYH,8ItlYCp9b6Oc-ZP#Nr;4-r.U*i.%'>])#6<%r(sS+'$lUDgYk&Jg)V"nK$-I1cV2-MleC$pgXW-)`WBB#hFLm#32>XjGZ&\pUZn+I:\)4[n3*%<YAmlC#/-+"9%%A_pFZ\n"iPp"hYS5*qPPLb$Plpjo/J,^c\(o(?L2PM!r*^0chhl,k$iSGE./LrSJEIG*uC\bI]C;(jM(GtLs\-Tk6<MEf1PYDKi6e*0NQMdJB*RWaqhj9/B#ot2AM7:R)P,'_\j+s_o/^?Qo^^FU,olYr-ea<D1]eU(Y[',"L+ZM@@A8Y'qS%jEF+qAWtX&C[7^qVmTaRNI@#jlGFGKU*P_c%rp&u9Pl(=[kT1Y3"8N^V9t<?0CL,Nm9:4;X:-`4.i6B^Q4</_#RU8G`*eF`!_jP5*8r'tG=srnnIWE"]5?hFNPu\PN&qq\q^_=g-?C!W:%jq\3=[RVWdbBr,GNe)%Eq)`CbHJXhaGuKW\n_qUl6t$KD!p?K]k5(7iWI1bAb'j?~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1371
|
||||
>>
|
||||
stream
|
||||
Gat=*D/\/e&H8h>EF6:()9e6X.?PIO,"%MO?-kF5RfPm,6'3!gNSZ/,C%Le;Uh!VOS/@`&,a,`cmZ)\i:^eb$RCgI!^`P@WGK,/QR,%iX7)8-j:1rF-O77[T63?ZloecuKIJ\SAmM-iH&nnEc-lN^5$4r(,V6g;aIUU;aW"%N(Gmm4\cqk#kMTJ*echr2(E/pUqO[5YLrVQMW9i$tGcOSFjIL(ZDX=n<Ro`_$_L(\)Y*.5K53JW[WgbePq/2R(Teg=_1E6EM4a6/ZHVFd9.,]I369ib<*.G@.VI=p48NA\Qh>S+&,\;>]D)GD7B<`Vd4%FAIVH!3TIN3KI(?<:M8mun3[,/-i?>`Vr6h=GW,PV3(M5jiV`]&k]4Gs!@:$8ia^Wj`UoK`N9VaH]Kd%:e$uMD5Jr$u*)t&dE0EFZ)SgX+;*$>dZAKiD[168-^7u7#=(<c6C0OOC[fBPVK1#p(j7[Q2PmLs6e,%`t:9RWi*pOfbc#?1%#-2>[L(aQ;*FkKs5H`\*kR>^u9(GTAp;S,h'ij%K%/TZH'8P+%deX@\BaE]?F[["[3@UQ%9eIY\+nQ]5Q8IDo0JtmJ?'e2Os3@+kl?S/9^@9Hg+okE6/%aY#'Ibq<Ko?Q@ugqfs)Yj+Pn]eoI6utZHRO]B4MrID$gO>b:5#>%>su&SiYn3=9eVeW3fJ+o<U;CPElBbI5GqJeo)]\;'h+*"nNDHAm[S^c?*2XG>Vl.,\js2jEERhJN(#MM?m@m)_V31*);[ZI6Mr1jA?jLp*A#^S`Y,EMg:_X$j/dsbO*./gHur;Qj,OS%8(QWQ_D<:<r^*rAb".F\!cF\VH4RQ^F\Bqeki0>Kd\G5-BFmV(4OAhW*0$&9cI*j^Q](C)2iTb0a1M-AIW2SC!i;o?OFL,AVK5HAX:>3R/O\@@[XZGo2qGBGq#h<c<j>lTSdL-/BEd<mg7Z-:#i1>=O5`S1lpd2@Mt9_*kmD@;'ku+_phdOQI-o6XC3o:[CYo8s5-Z-Pi=k6#fflrGNZ4t8V-1<*g,r%:l'M4kA3)0.E^,imNY@s::00-9%7_fUNA3bm_-.eGSEJ[WJ[0*DB[<Rb.\(1Mi2_i\2L>mnX,ZW!kRnsiK)JZ6G2aEBq10/)Tre0&BWPffT+>qDcr-W9tGA=(+I7m`5f5DkcWFs)8cOT3eG4qK6$sm(8@d/']2KdBSf4[d@FP:MC-A9M4IDh[\[P.dFRoHV]1Z-\K(,Cs)*0;pX81LO]d4$Nsn#C1Q/"(4kq/k9as+'Gd0p]9-m(LRt^&[qB%b$9!-Q(3)/SG'eD(hKs0Za\N^!HC$hB)XtfLSBY-!^-P."ip='o\J#C<#+Yc"QfZ,5X@Bskg=seBV"7KTj3._b~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001737 00000 n
|
||||
0000002033 00000 n
|
||||
0000002124 00000 n
|
||||
0000004366 00000 n
|
||||
0000005684 00000 n
|
||||
0000007152 00000 n
|
||||
0000008410 00000 n
|
||||
0000010514 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<9f48df53726ecc7f762a28209cc7a8fa><9f48df53726ecc7f762a28209cc7a8fa>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 12 0 R
|
||||
/Root 11 0 R
|
||||
/Size 20
|
||||
>>
|
||||
startxref
|
||||
11977
|
||||
%%EOF
|
||||
569
Les14-RAG-Embeddings/Les14-Lesstof.md
Normal file
569
Les14-RAG-Embeddings/Les14-Lesstof.md
Normal file
@@ -0,0 +1,569 @@
|
||||
# Les 14 — Lesstof
|
||||
## RAG + Embeddings — AI laten antwoorden op basis van eigen documenten
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Vorige les:** Les 13 — Agents + Cursor + Vercel
|
||||
**Volgende les:** Les 15 — Cursor + Vercel deploy
|
||||
|
||||
---
|
||||
|
||||
## Inhoud
|
||||
|
||||
1. [Het probleem dat RAG oplost](#1-het-probleem-dat-rag-oplost)
|
||||
2. [Wat is een embedding?](#2-wat-is-een-embedding)
|
||||
3. [Vector similarity](#3-vector-similarity)
|
||||
4. [RAG pipeline](#4-rag-pipeline)
|
||||
5. [pgvector in Supabase](#5-pgvector-in-supabase)
|
||||
6. [Chunking strategieën](#6-chunking-strategieën)
|
||||
7. [Index pipeline in code](#7-index-pipeline-in-code)
|
||||
8. [Query pipeline in code](#8-query-pipeline-in-code)
|
||||
9. [RAG-tool in een agent](#9-rag-tool-in-een-agent)
|
||||
10. [Wanneer wel/niet RAG](#10-wanneer-welniet-rag)
|
||||
11. [Productie-overwegingen](#11-productie-overwegingen)
|
||||
|
||||
---
|
||||
|
||||
## 1. Het probleem dat RAG oplost
|
||||
|
||||
AI-modellen weten heel veel — maar niet wat in **jouw** documenten staat. Je interne kennisbank, klantcontracten, productdocumentatie, dat handboek dat vandaag binnenkwam.
|
||||
|
||||
Twee naïeve aanpakken die niet werken:
|
||||
|
||||
- **Hele document meesturen in context** — werkt voor 5 pagina's, niet voor 500. Te duur, te traag, past niet in context-window.
|
||||
- **AI zelf laten zoeken op het web** — werkt voor publieke info, niet voor jouw private data.
|
||||
|
||||
**RAG (Retrieval-Augmented Generation)** is de oplossing. Drie woorden, simpele kern:
|
||||
|
||||
> Haal de **relevante stukjes** uit je documenten op, geef die aan AI als context, AI antwoordt.
|
||||
|
||||
Het magische zit in 'relevant'. Niet keyword matching — semantic search. AI vindt stukjes die *over hetzelfde gaan*, ook al gebruik je andere woorden.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wat is een embedding?
|
||||
|
||||
Een embedding is een **vector** — een array van ~1536 nummers tussen -1 en 1. Je stopt tekst erin, krijgt vector terug.
|
||||
|
||||
```
|
||||
"De kat zit op de mat" → [0.12, -0.45, 0.88, ...]
|
||||
"Een poes ligt op het tapijt" → [0.14, -0.41, 0.85, ...]
|
||||
"Voetbal in Nederland" → [-0.73, 0.21, -0.32, ...]
|
||||
```
|
||||
|
||||
Magisch: de eerste twee vectors zijn **dichtbij elkaar in de ruimte**. De derde is ver weg. Niet omdat woorden overlappen — omdat **betekenis** vergelijkbaar is.
|
||||
|
||||
### Embedding-modellen
|
||||
|
||||
| Model | Dimensies | Snelheid | Cost | Wanneer |
|
||||
|-------|-----------|----------|------|---------|
|
||||
| `text-embedding-3-small` | 1536 | Snel | $0.02/1M tokens | Default |
|
||||
| `text-embedding-3-large` | 3072 | Trager | $0.13/1M tokens | Betere accuracy |
|
||||
| `nomic-embed-text` | 768 | Snel | Gratis (local) | Privacy, on-device |
|
||||
| `mxbai-embed-large` | 1024 | Middel | Gratis (local) | Open-source alt |
|
||||
|
||||
Voor dit vak: `text-embedding-3-small`. Goed genoeg voor de meeste apps.
|
||||
|
||||
### In AI SDK
|
||||
|
||||
```typescript
|
||||
import { embed, embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
const model = openai.textEmbeddingModel("text-embedding-3-small");
|
||||
|
||||
// Eén embedding
|
||||
const { embedding } = await embed({
|
||||
model,
|
||||
value: "De kat zit op de mat",
|
||||
});
|
||||
|
||||
// Veel tegelijk (sneller)
|
||||
const { embeddings } = await embedMany({
|
||||
model,
|
||||
values: ["chunk 1", "chunk 2", "chunk 3"],
|
||||
});
|
||||
```
|
||||
|
||||
`embedMany` is veel sneller dan een loop met `embed` — minder roundtrips.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vector similarity
|
||||
|
||||
'Dichtbij elkaar' kun je op drie manieren meten:
|
||||
|
||||
| Metric | Wat | Wanneer |
|
||||
|--------|-----|---------|
|
||||
| **Cosine similarity** | Hoek tussen vectors (-1 tot 1) | Default — werkt op text |
|
||||
| **Dot product** | Sum van producten (na normalize) | Sneller — als al genormaliseerd |
|
||||
| **Euclidean** | Afstand in ruimte | Zelden voor text |
|
||||
|
||||
Cosine is de standaard voor text. OpenAI-embeddings zijn al genormaliseerd, dus cosine = dot product (mathematisch equivalent).
|
||||
|
||||
### Cosine similarity waarden
|
||||
|
||||
- `1.0` — exact gelijke betekenis
|
||||
- `0.8-0.95` — sterk gerelateerd
|
||||
- `0.5-0.8` — zwak gerelateerd
|
||||
- `< 0.5` — meestal niet relevant
|
||||
- `0.0` — totaal ongerelateerd
|
||||
- `-1.0` — tegenovergesteld (zelden in praktijk)
|
||||
|
||||
### In pgvector
|
||||
|
||||
```sql
|
||||
-- < => > = cosine distance (1 - cosine similarity)
|
||||
SELECT content, 1 - (embedding <=> '[0.12, ...]'::vector) as similarity
|
||||
FROM chunks
|
||||
ORDER BY embedding <=> '[0.12, ...]'::vector
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
`<=>` is cosine distance — kleinste eerst betekent meest similar eerst.
|
||||
|
||||
Andere operators in pgvector:
|
||||
- `<->` — Euclidean
|
||||
- `<#>` — negative dot product (sneller als al genormaliseerd)
|
||||
|
||||
---
|
||||
|
||||
## 4. RAG pipeline
|
||||
|
||||
RAG splits in twee pipelines: **index time** (eenmalig per document) en **query time** (per vraag).
|
||||
|
||||
### Index time
|
||||
|
||||
```
|
||||
PDF / docs
|
||||
↓
|
||||
Parse + chunk (~500 tokens per chunk)
|
||||
↓
|
||||
Embed elke chunk
|
||||
↓
|
||||
Store: chunk_text + embedding in pgvector
|
||||
```
|
||||
|
||||
Doe je één keer per document, of bij elke document-update.
|
||||
|
||||
### Query time
|
||||
|
||||
```
|
||||
User vraag
|
||||
↓
|
||||
Embed vraag (zelfde model als index!)
|
||||
↓
|
||||
Cosine similarity search → top-k chunks (k=3-10)
|
||||
↓
|
||||
Geef chunks als context aan LLM
|
||||
↓
|
||||
LLM antwoordt op basis van context
|
||||
```
|
||||
|
||||
**Belangrijkste inzicht:** LLM ziet nooit de hele DB. Alleen ~5 relevante chunks per vraag.
|
||||
|
||||
### Context prompt template
|
||||
|
||||
```typescript
|
||||
const prompt = `Beantwoord de vraag op basis van deze context.
|
||||
Als de context geen antwoord bevat, zeg dat eerlijk.
|
||||
|
||||
CONTEXT:
|
||||
${chunks.map((c) => c.content).join("\n\n---\n\n")}
|
||||
|
||||
VRAAG: ${question}
|
||||
|
||||
ANTWOORD:`;
|
||||
```
|
||||
|
||||
Drie ingrediënten: instructie, context, vraag. Variëren werkt, maar deze structuur is robuust.
|
||||
|
||||
---
|
||||
|
||||
## 5. pgvector in Supabase
|
||||
|
||||
Postgres extension die `vector` datatype toevoegt. In Supabase: één click activeren, schaalbaar tot miljoenen rows.
|
||||
|
||||
### Activeren
|
||||
|
||||
```sql
|
||||
create extension if not exists vector;
|
||||
```
|
||||
|
||||
Of via Supabase Dashboard → Database → Extensions → enable `vector`.
|
||||
|
||||
### Schema
|
||||
|
||||
```sql
|
||||
create table chunks (
|
||||
id bigserial primary key,
|
||||
source text not null, -- filename
|
||||
page int, -- voor PDF page reference
|
||||
content text not null, -- de chunk-tekst
|
||||
embedding vector(1536), -- de vector
|
||||
created_at timestamp default now()
|
||||
);
|
||||
```
|
||||
|
||||
`vector(1536)` — dimensie moet matchen met embedding model. `text-embedding-3-small` = 1536.
|
||||
|
||||
### Index voor snelle search
|
||||
|
||||
```sql
|
||||
create index on chunks
|
||||
using hnsw (embedding vector_cosine_ops);
|
||||
```
|
||||
|
||||
**HNSW** = Hierarchical Navigable Small Worlds. Approximate nearest neighbor. Tot 100x sneller dan exact search bij grote tabellen.
|
||||
|
||||
Trade-off: HNSW is *approximate* — niet 100% accurate maar 99%+. Voor RAG: prima.
|
||||
|
||||
Voor kleinere tabellen (<10k rows) kun je het index weglaten — exact search is dan al snel.
|
||||
|
||||
### Match function (Postgres RPC)
|
||||
|
||||
Best practice: definieer een SQL function die je vanuit JS aanroept:
|
||||
|
||||
```sql
|
||||
create or replace function match_chunks(
|
||||
query_embedding vector(1536),
|
||||
match_count int default 5,
|
||||
filter_source text default null
|
||||
)
|
||||
returns table (
|
||||
id bigint,
|
||||
content text,
|
||||
source text,
|
||||
page int,
|
||||
similarity float
|
||||
)
|
||||
language sql stable as $$
|
||||
select
|
||||
chunks.id,
|
||||
chunks.content,
|
||||
chunks.source,
|
||||
chunks.page,
|
||||
1 - (chunks.embedding <=> query_embedding) as similarity
|
||||
from chunks
|
||||
where filter_source is null or chunks.source = filter_source
|
||||
order by chunks.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
Voordeel: één call vanuit JS, optionele filters (bijv. per document).
|
||||
|
||||
---
|
||||
|
||||
## 6. Chunking strategieën
|
||||
|
||||
Hoe je documenten opknipt heeft enorme impact op RAG-kwaliteit.
|
||||
|
||||
### Drie aanpakken
|
||||
|
||||
| Strategie | Hoe | Wanneer |
|
||||
|-----------|-----|---------|
|
||||
| **Fixed size** | 500 tokens per chunk, 50 overlap | Default, simpelste |
|
||||
| **Recursive** | Splits op `\n\n` → `\n` → `.` → ` ` | Behoudt structuur |
|
||||
| **Semantic** | Embed zinnen, group similar | Beste kwaliteit |
|
||||
|
||||
### Wat is een goede chunk
|
||||
|
||||
- **~200-500 tokens** (~1000-2500 chars)
|
||||
- **Overlap 10-15%** — info aan grenzen niet verliezen
|
||||
- **Houdt semantisch geheel** — niet midden in zin knippen
|
||||
|
||||
Te kleine chunks → fragmentatie, AI mist context
|
||||
Te grote chunks → ruis verdunt relevant info
|
||||
|
||||
### Fixed size in JS
|
||||
|
||||
```typescript
|
||||
function chunkText(text: string, size = 500, overlap = 50): string[] {
|
||||
const chunks: string[] = [];
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
chunks.push(text.slice(i, i + size));
|
||||
i += size - overlap;
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
```
|
||||
|
||||
Voor productie: gebruik `langchain` of `llamaindex` recursive splitter — handelt edge cases beter.
|
||||
|
||||
### Contextual retrieval (Anthropic, 2024)
|
||||
|
||||
Nieuwe techniek: voor elke chunk genereert een LLM een kort 'context-stukje' dat de chunk in context van het document plaatst. Embed dat samen met de chunk. Resulteert in 30-50% betere retrieval. Voor productie de moeite, voor demo overkill.
|
||||
|
||||
---
|
||||
|
||||
## 7. Index pipeline in code
|
||||
|
||||
Volledig pattern voor PDF-upload → chunks → embeddings → DB:
|
||||
|
||||
```typescript
|
||||
// app/api/index/route.ts
|
||||
import { extractText } from "unpdf";
|
||||
import { embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
const buffer = new Uint8Array(await file.arrayBuffer());
|
||||
|
||||
// 1. Parse PDF
|
||||
const { text } = await extractText(buffer, { mergePages: true });
|
||||
|
||||
// 2. Chunk
|
||||
const chunks = chunkText(text, 500, 50);
|
||||
|
||||
// 3. Embed all chunks
|
||||
const { embeddings } = await embedMany({
|
||||
model: openai.textEmbeddingModel("text-embedding-3-small"),
|
||||
values: chunks,
|
||||
});
|
||||
|
||||
// 4. Insert in Supabase
|
||||
const { error } = await supabase.from("chunks").insert(
|
||||
chunks.map((content, i) => ({
|
||||
source: file.name,
|
||||
content,
|
||||
embedding: embeddings[i],
|
||||
}))
|
||||
);
|
||||
|
||||
if (error) return Response.json({ error: error.message }, { status: 500 });
|
||||
return Response.json({ chunks: chunks.length });
|
||||
}
|
||||
```
|
||||
|
||||
Belangrijk: `embedMany` doet alle chunks in één API-call. Veel sneller dan een loop.
|
||||
|
||||
### Kosten-indicatie
|
||||
|
||||
`text-embedding-3-small` = $0.02 per 1M tokens.
|
||||
|
||||
- 200-pagina PDF ≈ 80k tokens ≈ 160 chunks van 500 tokens
|
||||
- Embeddings: ~$0.0016 (minder dan een cent)
|
||||
|
||||
Indexing is goedkoop. Doe je één keer per document.
|
||||
|
||||
---
|
||||
|
||||
## 8. Query pipeline in code
|
||||
|
||||
```typescript
|
||||
// app/api/ask/route.ts
|
||||
import { embed, generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { question } = await req.json();
|
||||
|
||||
// 1. Embed de vraag
|
||||
const { embedding } = await embed({
|
||||
model: openai.textEmbeddingModel("text-embedding-3-small"),
|
||||
value: question,
|
||||
});
|
||||
|
||||
// 2. Similarity search
|
||||
const { data: chunks } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
|
||||
if (!chunks?.length) {
|
||||
return Response.json({ answer: "Geen relevante info gevonden." });
|
||||
}
|
||||
|
||||
// 3. Build context
|
||||
const context = chunks
|
||||
.map((c, i) => `[Source ${i + 1}: ${c.source}, page ${c.page}]\n${c.content}`)
|
||||
.join("\n\n---\n\n");
|
||||
|
||||
// 4. Generate answer
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
prompt: `Beantwoord de vraag op basis van deze context. Als geen antwoord in context staat, zeg dat.
|
||||
|
||||
CONTEXT:
|
||||
${context}
|
||||
|
||||
VRAAG: ${question}
|
||||
|
||||
ANTWOORD:`,
|
||||
});
|
||||
|
||||
return Response.json({ answer: text, sources: chunks });
|
||||
}
|
||||
```
|
||||
|
||||
Tips:
|
||||
- Geef bronnen mee in output — gebruiker kan verifiëren
|
||||
- `top-k = 5` is goede default, experimenteer per case
|
||||
- Stream de output (`streamText`) voor betere UX
|
||||
|
||||
---
|
||||
|
||||
## 9. RAG-tool in een agent
|
||||
|
||||
Combo van Les 13 (Agents) + Les 14 (RAG):
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, tool, stepCountIs } from "ai";
|
||||
import { embed } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
const ragSearch = tool({
|
||||
description: "Zoek in geüploade documenten op basis van semantic similarity.",
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe("Wat je wilt vinden"),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const { embedding } = await embed({
|
||||
model: openai.textEmbeddingModel("text-embedding-3-small"),
|
||||
value: query,
|
||||
});
|
||||
const { data } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const docAgent = new ToolLoopAgent({
|
||||
model: openai("gpt-4o"),
|
||||
system: `Je beantwoordt vragen over documenten. Werkwijze:
|
||||
1. Zoek met ragSearch voor relevante info
|
||||
2. Lees de resultaten
|
||||
3. Eventueel: tweede ragSearch met andere query voor meer context
|
||||
4. Antwoord met bronvermeldingen.`,
|
||||
tools: { ragSearch },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
|
||||
const result = await docAgent.generate({
|
||||
prompt: "Vergelijk de jazz- en rock-headliners op Polderfest 2027.",
|
||||
});
|
||||
```
|
||||
|
||||
Wat krijg je extra t.o.v. simple RAG:
|
||||
|
||||
- **Multi-hop reasoning** — agent kan 2-3 searches doen voor complexe vragen
|
||||
- **Query rewriting** — agent kan zijn eigen zoek-query verbeteren
|
||||
- **Iterative refinement** — eerste resultaat niet goed? Probeer andere query
|
||||
|
||||
Trade-off: duurder + langzamer dan single-call RAG. Voor open-ended vragen wel waard.
|
||||
|
||||
---
|
||||
|
||||
## 10. Wanneer wel/niet RAG
|
||||
|
||||
### Wanneer WEL
|
||||
|
||||
- Veel documenten (>50 pagina's totaal)
|
||||
- Documenten veranderen vaak
|
||||
- Semantic search nodig (niet alleen exact match)
|
||||
- Privacy: data moet in jouw DB blijven
|
||||
- Bronvermelding is belangrijk
|
||||
|
||||
### Wanneer NIET
|
||||
|
||||
- Klein document (<10 pagina's) — gewoon in system prompt
|
||||
- Exacte data (prijzen, IDs) — gebruik tool-calls / SQL
|
||||
- Structured data (tabellen) — SQL is beter
|
||||
- 1 keer per dag bevraagd — overhead niet de moeite
|
||||
- Code-base — gebruik grep / tree-sitter, geen embeddings
|
||||
|
||||
### Hybrid is vaak best
|
||||
|
||||
In productie combineren teams:
|
||||
- **Semantic search** (RAG) voor concept-vragen
|
||||
- **Keyword/full-text** voor exacte termen
|
||||
- **SQL filter** voor metadata (datum, categorie)
|
||||
|
||||
```sql
|
||||
-- Voorbeeld hybrid in Supabase
|
||||
select content
|
||||
from chunks
|
||||
where source = 'product-handleiding.pdf' -- metadata filter
|
||||
and (
|
||||
content ilike '%firmware%' -- keyword
|
||||
or embedding <=> $1 < 0.5 -- semantic
|
||||
)
|
||||
order by embedding <=> $1
|
||||
limit 5;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Productie-overwegingen
|
||||
|
||||
### Re-ranking
|
||||
|
||||
Top-5 van vector search is niet altijd de beste top-5 voor de LLM. **Re-ranking** = stap erna:
|
||||
|
||||
1. Vector search → 20 candidates
|
||||
2. Re-rank model (Cohere, BGE) → top 5 echt
|
||||
3. Aan LLM geven
|
||||
|
||||
Cohere `rerank-3` is de populairste, kost ~$1/1k searches.
|
||||
|
||||
### Evaluation
|
||||
|
||||
Hoe weet je dat je RAG goed werkt? Eval-suite:
|
||||
|
||||
- 20 vragen + verwachte antwoorden
|
||||
- Run RAG, vergelijk output
|
||||
- LLM-as-judge: tweede model beoordeelt kwaliteit
|
||||
- Track over tijd: retrieval-accuracy, generation-quality
|
||||
|
||||
Tools: Ragas, LangSmith, Vercel's eval helpers.
|
||||
|
||||
### Updates
|
||||
|
||||
Wat als een document verandert?
|
||||
- **Delete + re-index** — simpel maar duur als alles change
|
||||
- **Incremental** — alleen veranderde chunks her-embedden
|
||||
- **Versioning** — oude versies bewaren met `version` column
|
||||
|
||||
### Cost
|
||||
|
||||
Embeddings zijn goedkoop. Vergelijk:
|
||||
|
||||
- 1000 documenten van 100 pagina's:
|
||||
- Indexing: ~$1 (eenmalig)
|
||||
- Storage in Supabase: ~$5/maand
|
||||
- Query: $0.0001 per vraag (embed) + LLM cost
|
||||
|
||||
LLM-cost domineert. Optimaliseer daar.
|
||||
|
||||
### Privacy
|
||||
|
||||
- Embeddings zijn NIET reversible — je kunt geen tekst terughalen uit een vector
|
||||
- Maar: vergelijkbare zinnen produceren vergelijkbare vectors — niet 100% anoniem
|
||||
- Voor gevoelige data: lokaal embedding model (Ollama, nomic-embed-text)
|
||||
|
||||
---
|
||||
|
||||
## Bronnen
|
||||
|
||||
- **AI SDK Embeddings:** https://ai-sdk.dev/docs/ai-sdk-core/embeddings
|
||||
- **OpenAI Embeddings guide:** https://platform.openai.com/docs/guides/embeddings
|
||||
- **pgvector GitHub:** https://github.com/pgvector/pgvector
|
||||
- **Supabase pgvector docs:** https://supabase.com/docs/guides/database/extensions/pgvector
|
||||
- **Supabase vector search guide:** https://supabase.com/docs/guides/ai/vector-columns
|
||||
- **Anthropic Contextual Retrieval:** https://www.anthropic.com/news/contextual-retrieval
|
||||
- **unpdf (PDF parsing):** https://github.com/unjs/unpdf
|
||||
- **Cohere rerank:** https://docs.cohere.com/docs/reranking
|
||||
- **LlamaIndex RAG concepts:** https://docs.llamaindex.ai/en/stable/getting_started/concepts/
|
||||
- **Ragas (RAG evaluation):** https://docs.ragas.io/
|
||||
309
Les14-RAG-Embeddings/Les14-Lesstof.pdf
Normal file
309
Les14-RAG-Embeddings/Les14-Lesstof.pdf
Normal file
@@ -0,0 +1,309 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 7 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 20 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105950+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105950+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesstof) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Count 13 /Kids [ 4 0 R 5 0 R 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
|
||||
15 0 R 16 0 R 17 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1865
|
||||
>>
|
||||
stream
|
||||
Gatm<a`?,q&A@ZcqN?`".3Vg'J@S?6R4PMNZ>/BW0/NjUZmh1#@`S<@Bn37[Lo*&8?'50gh>MllpR5\ZnX#ZqE!>`C%^4_F?l\<q^hdJtr"S#4X]p^&Q[?.C6qAeT%`B1[#P+^VJE%9ok<8u>4X*$.CrqKt^''Jn4HFcL/%Bt@0d*&\4_Y"\b]0Jk[9jK3F*)74a*%@*rCqZ;LZ]>[M%7G)s3MUc?k''[Wiue@Np"3*"\53"r&fOkku&8T<LUqsDjdZqWe>H=4ZGWn*>_.DP\L7!W&k`lfh*RP$^b;=r2'*EY]&9d3#LM`rDQ^(pjd9OR9nHV^BVk0$%NKOI)@aW%Cs+Xe.mpe^*E,"7M"9pESnpE4f:JT$8E$<_PsTN)4\Dk"n0JaKN:$>9G$JK9^hB3>Tu;m6YDB5^L#GNlXiJ!n9&P)cFOnI>GJm<d%V@HN3CM&k"X02%U)pL?8H"fEe2k'VM['!,OmT#LRef0.6KW/c94K?F_$Q(qou.d=Z<+\GCpBP2a_q-[!25YZ"mhZE>Xn@#A+Y;0HY),/M[3.=2M$4kekriJr.I9C)LP9<]K[R(Z8_XP^<<_K2(KW5Q1Y4_7S6O=tHCY=fb\Vi<j2,6B'XmM9hTM"O/$\5TH"<JKQE1dkkFia#&F,!:(-_Jcr7*l5ip3`;)h?<k892nn@9\02GLWQHjG87fj1h(Yi3DI,1A,7S4*shXOm`T0g-n!S2bd/;#CL@HjP)N#9u*qAJ)$?42&rQ&cf[cXWtl[Vkt^RTao*nJLs$nKj)nUbD^e-rW9F5hSHD,>a'K$d,A,-ltl5eMu;A%``O;,bV/X\9o\KF`[Ok"9k_#AX1F,dWgTPo/N&8/iKr$ne[9,@M+pF(l\Di+9[r\'Z=b'SP8\G-Q#Sfj)(%tBoYT9`tt`m(+P6kLFpR(2?N*``Bb[-&kRA$>l"WVpTt=:H^sOHE:Vl4[tGLKY$WtD.L5nH1Kt0`2gB+$]27-F5T*hF$>D[T+cN0i2bmNa;4cQ<$]TJCNf`R1aG&@Zd:tmRfOMk;TnhU]]c#tZhLQ`$\6m"kNEQ4JG5J3m(i%40'sq+B^JY_`#Zl!=&eaLJVSEroi_s2^4?F5c#=XXIFo0Y/T8BG)Ff%[:l:pD^bRW2Le63J>B%j^TGtc'n?/+1hkmUMd,#rV-:>2gUn*(H"FFNoQ*1\n3K/[P58(SX$gO2nM_KXPq/"d#'o?RfP6A:@b.'aspcf%3UlDA-<D7?tI2D(Xk5nU5n367lc!H:o1N@&UCR87CoZCiV73;]g]gp8D-Yr7\U^K$+Wq\gZ[b[419.tn^Z>H.P:$oU"Vb<*?nXK!BoUH=a9+&('(<>M6j.p/b`5(<KN<a4=;q5R&)&FQVJF)2#G;/Q6p>WAJWPRLAn=/^4['<0kn#YqbsR9jF`S7+luBnV]S-ms8\GBHlWeZY-@(YeFlV:HXAJ61/cckeIXl-hK6%0(0\4&_ud+q.9/jp^]Yr$mO70J]('<akXNDhYY\SR[=)M20@U:6#pFG7-m4a4Bh8m0M?,2"@"S:Qhcc>3oeVrcu^G0#\Kn-=e)GSegL]Kn>GX1"s![kSgsWP!FE5$d8)Tksqj`JJG)d1.>V"G'>%LJFXhup]Ot,=&/m@L0-9n%#O*70hC^u+GhP/h#/AoXJr'E\M6);6/^Boi7RqC)%uBT&q1B;c4hOO59ud,%(Lcqd:$_)SV!e_[;E)^q%h.MY`Dt)K&,-;!q7o1i(_"r-TN$_lr>.2/PHsj=*>F$O:r2maUR-$L[kR9LZ&Jf!i#YXTl!I3M[.G1LO9!WXFk1<SKbN>8lFIsFe'.3*!(-u4B%](rSW(Jdm\t-YF;<?q;1FmZ_cBQKt>k<lTaT=;%3~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 401
|
||||
>>
|
||||
stream
|
||||
Gas2Eb>,r/&4Q?hMRtF)R+`4gZ>9[ZPq&G)@)\uMmK9j',SAppo4XCe6#8OhB?m$hPn7BGp<8Q4/B)-P+W9MK;NX;W`-iPP7Dsq_d\,6L3+5Vr2\4&JPX?BF#q$?\=E7G.3D%LB%NOc8Pnp4:AfOC(28VBXE`*ihY3KLPbb;EgD+sN4^@u].?:(Nu$_#l]YjdA5LH8@D77(aiOWH6'I!T)VU"K2kgD.[.06QNc?&^dTMs95E>#kI"p@#N_hu6H"*dqAP+T4PdDL)ce8hoS'GKF4g.d/+iWk-i)f]VpNT<H'5c78b@.o@5nEa#E'7CX-#FZ36gMJ,rIF!df2LUaD[-_u=&J8.b*F$:c%Qq*>(6?&m%6pK\A'>",I>$dq9AWc.>^V[2.L$/c)WfMf~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1247
|
||||
>>
|
||||
stream
|
||||
GauHJ9lo&I&A@[:lm4Hf7mR-:pgA%.1oW?r8Xg3R?@7;P8>8P($40k'r;&%^&(-].A9t1#Mc^Pfn?@/L!!A*#(4Pj<^i(+'H,kGTaO\->P!J^^hIb0%LiCI9,(RV#`[M.U`pFktS&g/B7&E(XTW',=-'2e0Zq`JtoM^VrW%G)h+pLG,WcS;11Ej0=n;iFe*2cPfO>;7Pht,;K]p-U0*Ra;g#_jR+^Sh`%:'bDt$-;foqs`e3"l*)sgZ7+h.cHSpD8,[^caeY^`Tu9U.2m,+n@O+UATcX0kC8'L!OA2<XpKQ?E$CD#;uHBu62H+m``MI9p+Na$S:Pp:o+>In!(l#\<E,/AV@*(W])dt%%#g2[R7Pt#>`c*Kp6?4kK[N7aqRGr`-%m_hV`eK+P=K+)2OJ*XKc7R$*1%!LZ")@E)$p]l3>*=cA]4'i427R9&05E*,C3s!U+qhtS]>;8<h;h9-toXZFBee-?)sM=?:f5lO3*\B9"8Iq#)9Xm(-67LS*UMk?'q6K-hoITVhAF:[u+QBlLkP`Sb6s#X(?#4*`g:uhm\)#FXX<1s%!`"a\`XWVDI1\!,!HD6djDK(UE>?o`f$;$TM:Zr6a5=%XV^NcMk%mS&#Df'Fk1sYccnKH`2t&^2LfW_5bY=D'E'eNAnij=J/MQ$TmZZd4C:3c!aZlkOZjlg>k584tLT+MdIKGO!r78"CQ$#C0B4q0HSUqAcp5n3U:i9ZR8PcnoA,k^)!*sLN1Ei$K+UFATf.'e[7*i_&.AM4'WN%$62uPZqe&!qq%m=`]>?tPS4M0#MfeN(`uE9i/^m$FtV[oq9'MEpT.Tbm%,YKPpBt6@$k`i>h;?_?OGJW/aXqgfN[,*@r:%U;F8'4,DQ)!<Ab%AR*E8OACj-3<K\2Us"P)b.1V_Che]iXd^7-c%5n%s5r]jNARTf30HBB;&L#j20GdtY?;h]4M%bTV>$=$i^`0@+GX1C4o!16^k:(`E+i2kE%Uf\j)L,SG\2C.=F"GY7JGa4/Wc]5^'[Q1-$mPHa*SWUNihPiPYt50:g*5YR&GV^f"1hhXGil5aIr<%jYH1r6XG*H,eJ&PgPiI08`FgQSE,D8!a2%4&)Vm"(V_\j-R_9sK]Pfp_!UFiH9p@>I.lqPOee0lAQ+hr%.F&PtSh$hspka1^N%&1H0?S<pq1jY:'n_@PHa(bKk`1@P#<ONfeR8q,CY,8E@:@(>%@FGP[RAlRINE=g:T2=!^T=u:$6f6^3:^NM~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1795
|
||||
>>
|
||||
stream
|
||||
Gau`SgN)%,&:N_CltH6gA1Rud5nH_fQ_\e&[n5:1Wq-ebJYlhC2D5D/4&%Nq[s&.?+Jm/F44@p*$`2tElriiE@`<lW%l=6L*8np?k"cjK%Z>C9Jd>I@LSKRthBQ:r:]s%27K&#EM>[&Ne]KBbO@2Q#@\@C*<%(![7[/cEE*O36/;-X5q*U2D#pg%G!2<R<Q2G+):F,*?>*)"lBR>0J:UTg=,!0s=e,CO.HE]"NL[#76=V(eAaC4r9Q]kqSZ!?NS\L>W0IW$^,>VpEBGmKIlB92@>XW+s,6[b`9Eh2NjGlWaq=?LD0NUVY[,X?[Fps4)BoDpUf;k@4G?fP-i`#qN/YAd9a+NZ^>+]ZJTBo(*pc40Mqg_f,\.4!sq%u>Z"Cmi=26E*d+)crSs+=rb)C^GE[`,dha_\?T4PVYbl(Bra$I:dmIJ_lJ2_>\lF0tb\aS(%I9(FAV8^`pZ,Nb#WOV(p+TnLcr!WV4@rqYAT@4%?lGW(nD/8&ct9MK'^Tiqb-$k)o2]F!8bFoCd<[0+E@upm:cX,$9N0h8"LR2iuU`FTFNfe&u<+XM3Ge"W:18L%jUE\2c,j1UUqsQ*u@-Nr?(\0Kg>0l=6<>-3LS^HD?\$X?(,-1UW+"X"+)S2fSkJZ+u]&>j&Z#C1sd9)PLaOn38/%crY>l\k7k_Y^"OYl?YlWdbC<8\&E4&c7!QLFpE6!U,QQ%G!`^pf%^pIBKM-YAY)`*>r\3VqiQO=)NHDLpIRJ[$W)/.;/W%QRj+*W$[V:blC8grB0iKT6`]rXWR5QDJC,N@.sX#55R4IJqB;?em7te#&/dpb?&F\6`#6V,nO_qcD?W$\e8U,9r%i2_>f0H,*]Fbr?F+S//`t7f?q,tY%KM(1!>%TSm;@T*%$#[ISu@LnC2dHf2pB^8$$2dZB]=/\M4qKt_T#t17W=t:rRHQ\.<T\IB93nu;D30X`PljB%0R4?(7[/oald$LfW@jtS2UCenX\lYV6fgbl7BqRlV'868f(T^hdD.sn(;q0n1qtBq,!C-E`V&(8ZZb<DV(pVg+M'*'O)-k;*oIc\ZK+,4k8b3HIOm>=^E_'QKdD[q!JD'iHR`Y*%!(%Ei@S-(N+E[7,PS/I$Y4tg6i$6ft)?bK\D@-`LSX[&Rb='m5P(kN\Eql0A<P]/S-7HM.dImY(p`*gQA6<"V5kHpd83A15bQT+Lo[Fn+e:_$E^@k1GtbP$ES"\4#7.Y!B9k9B''ScR6RbD389ct\$eFUp>j_KIRUM!cIn+=H$.u!qE2L;DV9]OamE]3`E7>2R!3?9`8u?5GFlmGq>6S(hG:A#;+ttm>ZYq9n[8iMj'7goRb/$\D)-I:gI7f8H\bn@B3!`/4lge;f(F<6(!#!T?)9KLSD+DN$3)W_PpjV"/7ji?]l3%[KuFM94nD2E+8THkLU+cm)8%ke@V;_SinnNO`s%LqnG;Y;r>h%k3*8eC4oY1qK_Z4ON0,*@,>@#\.B.@J#SXppEX\J7*Cnli2i]j**PgkMlTZF\(rJ!=+gj'uo6"1nnB$]l#PR^4/j@-b33UIE-?2*BqX1nEd=<ngLeY[*Xc#/]0]0q8XBo5XXa)"!jC=dD16Atli=kbad8bHi(,uen@hFcK)ht^7m*8:G/t>Za]=[CioYDSH1*T%mak]Z@#YE3r),g&d*,V4n3u3X&83aK1iMY()LX2>Wr#s'?dF_4aVTB=GrA_F?NrTj0DZCIG]U/G=!kmb*M9JaPDIoU1fEA,$\nEIOlL_pka2Ch=ol`[(b-mM6+2Q]'4?7r6g#Ut4@]qo`rW`-Jl](~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1514
|
||||
>>
|
||||
stream
|
||||
GauHK>>s9G'RnZ;3"LFV1A,6tg0IF:cB*@M*HFP>\BOV]7h-';_/qs/TN(Udk@:'YS[Bmn:9k<"$.AjGn*WrG?nn9tCVNYsM\M]6&e86DD!dDG_E&?`O//eDHin2LtTP^edsS-3(cPfGokL2(2)"DJ]-`\YD-:MZgNZZ,Ni@6h2og.3?@\]Uj&I6eS1\@!Tb;Vn:"A"P3po8%._6Sn$eJtW<_89Ljt'$=JH_BO`f0B\s&i+'@(H#T7M&c>[?2Rk?sRWAoC-hDsQ&+dXjiQsd@./ae5Uj*'$4'9-JpDVs_abbr<4iKXn3LUJ'ac#egLAfY0Ko$?.!9XE3g&`_[(ojPY6;WYSZ.%Zl5fX]bPuJ;Vga2W1[h^u/'2#)=XDu][8]%jo_\KYG$(PlBDjXt7K^+jcPk//DYQ_(=3Wn)i!"Ec5,DY6eXT+H,5d]8lE+j/YKFaW9NH.O!^cZRRSLUMZ=+0^ZNm$(SJl`Q5KZE#l(a[rk/q>+J0WqC89FT4#YhI?kf)`Ucn3+Xtn]Q_r*ldR\r(f4!dL>\#%O41GMn^UuTF4l@GS^U3;NHF-qK/3Jfk"@IPu:$A0bNE1)2JtcY)ST$HZ?UO.FL89<Xp'gQbnEZ4EP1n/.BPIN1m:&=#RA!q\/eHNCU"akQI*fg2%m*/'6?t#2Z^qY-'HBUj8?[nY7&0Yu1Ccg3Y7!H#=:?W(oR4le.doOVYa8?gT#`TH)Z)IiT9nOCIhrp=f^g9F=Y?,P[TV?*8edaj/+"iG'CNK:fMC2VDSAX-VBdZ5GE$fd+gO572IRoQ#hU.NOLp8*_9)Qe%e]VR*Fk5.J]B5?U=?5dh0__X9.]f)!ibbL5W"/h7V&BDgOnOrM:BAe`9d,c[_P0ZW,B-\o5$f]Gl_7lNV#E.utk0j_Cr,DR/KZ\Z!2Qsuo6K:5e?XR2C-8)MXY:JU(^]PpDO#%h\+qaMe'qT)J#XYgcXf9k*]0Y:q#:E#)_hdopW]=kWRCR?.Y95ORP<\S$:<nKR!=l!BN2a8]uAY\b_hSNo0fR'*@q4o1$V<[0p[X10$/L@:2SfpO&hbbrOiuZ!]-+cYXA2a":1)"kN(Zshh[sL9-]=Z>DN)TcRdJp]"3INpMm9\+LrYA]*>=;t:YoZArKXSZc)d+`I`@eSP8G59>njG[>afb6*/AY63s/cEr?r(%XNI-h;\$8be86=>?4/4e8@7TRb[G.a]*N(4#lhN5XUt0[(l..%K)HnWa93qQpKM&8"/#JR<SA%XAjf^&s<`#n<?A7;i[,=D16Ce;9$:=Crjl!;$)15/*0\YtqcL.Rm5^Q]SrLQ`54$(!3Ag!jYUUs#Yc?M>g&Q]<Ug1@accf+9'+!JiOW12p)/;JtqibWWW=^8e*oNg3DVrj0Y>.R*.?-hk=8#Jj!('pDf=B'[/;KKd`nRoI=L<6ILp,ef*^sDaMLFp4O6jq<+%?<*fcQK`'-hmFS/WN"JIfrhVkEgroQgjtbfX,0J7XM7dTUq[E/AV5C8RR2B&,)BT_Z~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1839
|
||||
>>
|
||||
stream
|
||||
Gb!#[D/\/e&H8h>E@Y5.l"66W.#H*n!H-A0"gY!YDS$D`<^gV:-$jPV2/Q9n^O8UEU?";=1DK%b)jYa.4ZnnRF$)1MpgrSPc2\n[iPAFsN5P8jQllI-L[\j>S3)E$4:iPF=&M!<5XJP%TNjH:GWtXb*\rg6+>OKN,U63A&if^k!=PQ"E,siE5Q[P^;?2j'o;04\mh276**Np"pVX'2D1#6:(D=KB%`<+3#_kKE4"6*Q$::-sGVIBMo]#)jF83FNW%)*h71*u!;T6ftAt0)[Gs7g!<Dq@^%L;8cV8Xo_H+aI3;jcFn@h%p]i5EP&f's8NL#E,?Wk(0(?scB],`f__i4Qoam=9T@"r^<0>aGZ)(L%^2EH,E_.t$;Ck&?A<\(6j+4<Tjq3O!MVp(E#^4s*t@Y#"Cd)r<9jR"hoD&Gq/&=DX<]I;AnS+iXMoqp]'LPk>Qd"ep1)fV*^-.5l"(Xg=S<@-4`#F:>)>\(UT_:Gm>)H$pl@H476@;8OX>/hk4dV'jNRM8?n;eh$FClN#g$p`\UcpmMdG7,G**^RdQ?<O,M:P,hk&n3^q>E'Arp2:5gU8CE<Yl8UJTk#X0C2#X57*N@`t1RO$PoGo7R>^_>fa]//56Q*^VUf<pSBsO,nJ]]oVcTeH#b>#,i<Ph0B7HdCq-@#J'nZJMDp'&2dqn`9#h&SXcT@)504c)h4Z\qj#UUX*eNpO/NR8Fd_0>a3,bT%2t9e*KXe.ILiNkNQs4.f>8K:t_WI:B,C'p*tAShO!3/L2JM9^Z`4"4hY$&QfE-oaNLW5g$oQg4Re%/?LY[c[kep[f65AqV,dCk!EaiJ6jTX4Yl&bAj#(b5pLoH#>a:Zb_s!B5*L.RY"Jh_o>V)Er#97nZ;U0J`645;))CTK7k)s,RE@&,X>BS'hKE"]9<=2:*qi0KN#>ruB<"8Mmb_<PqjY/FeUHQT=04[<(noB>o-@[.P/D$STjmAq9C8=k/LA/5$Er"q\lB*SP=.t;5EJ2Cl3!Z)6Rh6"lZ1<D6+Un\)hGD1.G0B?K!irHN:pc[?n$=$nV$3`6_LngQRoV0DQ'0.B8n8GiDQL"Es5]$="-OO"X^_YZqCaq$@?#WGZ[4rA4[])8c8fB'p\;e5HB^mpoL1b%cqS#m$Ua!XfI:3/om_O.*CN<n;i'u3:q_LY<Naj3No,Y_/Y->)I+Ce$[dTVqB@WSQLa4a]jlaB):[&RWj&pjCP^GMV+X\Xb)g7b__VWK2NTkhWuJsff(Jne;JJIEVO_N2=54TTM!t*%GTW\6?IBT$De\B$2,]AL,dsk@9S_c&RQ+eZ_Ya4l482]<hj=cXBdbF]`OM-Rns>$CXu5nqr%U*u!h5Wile($anVl5s!*`#$D;?a@)O>SI$Do6&<%0a@ZhasB9X"N+AVIWtf'$B[e/Oh`Q\h_$8sHt:P'Wla6'3]50$M-V!g>I[.L3\5387CUH"tl,e<VckhSdSc8roaG6ikef\i;9@Y4@T],<X%2lIQ*&DcSs'DG%IB0*_@O^/Y-Be^CHKZ;h@Ab#W4Uh]hN1c:Nh>q![f<_m]n("<6KOL+`bJo3@dZ)87no:,W/hd<XV-Cu2W:g4Re%X<3u,)>/e7]lY111f`3LHO(]7W@jfWMDf0s/[*FLV<X3_fo[mm$oX?p5SHi"F6a,?c$2>"0s\Om"YS=,k"uR[Q"b8b[K@TFa41E^L;188essSg>R0fokf*29T@<c+^hX28h62eP8J\F?UY.'8(QjKf5toKrnf$C;:5^BiNUG#&3W@%YEH?26[eKNucdH#=NUL1L-I;DEmT4f)WTp;#GDn4<s)*%^9jUNN+!&$Yf`1I,RVot;#CkV5"9~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1884
|
||||
>>
|
||||
stream
|
||||
Gb!#[gMYb*&:N/3bX2^s7E^i.78gbR"hL%p"kmk+osQLX!a2C;26u:6Eglq)%Om6tF&Vq@=N@Ye&U?>U*mU0-T_QR_Dn6&GD%84*1LM3bCLD`"R(n8<kJ9SP\E$7M5XJ2_Qii14qgiJq'G*h[@idGQ<"Z58#gJ1VYm-6[P[XtL"7)ic"/7&@=ocr$P$6_2*tl[U/hKU>AUJQJkF]3N`C+n+TptEc_/KPC2W[e'5q%Xj"S3YX55b*8/\EMkVG=U3RDV6`A^YDt$dKL"HMdV??"\:i<q(mT1jQXhRA)]qq)=[*[;1ckL#=4CpQ"e$R2XldI+1m$DIGaVS#f.GNGq'i9R@o(4%C%9,RgZhZZn1^6&:*>To_UX0&R=US)0G9>S0S^*ne'k7L?qrjC:j4O%BFl_ZnSK=X\3GEEjK7`su66`]MNd;KT-]k5iK0Cj8N7&G<*"!?JhoZ/_DA-MqY=G+Jom^i@Nr[7C8V[H3DnY9^]Sa6:S]-!W5I!^ka>8(ntd1ccGig+_E3Nprm_NsWR\D]-B#jR!QYIWK5OVQ:mji:CBL/<ed];*i4O;rM2u"NJH513YV3AYms"oVVRf)jl%)lXj'=&^=<cME36q!cNk%ItC*+1$?AhBGHWk=aCQLg16PI1'K]/nP1_^]W[iFkm5H\DB3Z*J%!:<KZ00@GT3)W+I$WIK=Brnd*tf19Kn/$63UA!MseW]J[[?[LmF2a-uVJM.MY8\cBN2OGn7OkTn\ThXO;FeRLW_I=Nf,n%sst%YMK+b2ad13g4g/#<meN])jAnXR7/*(Pro"K>+)0@_[/]=L8&M2JEa+hQ>o.HS[gkU_"BOB/`6`+n<V'd*\H+lbM5@o4V[':/7oR=0H3(<QC.ba/+B0B[])ud,rt=mCmS,QE,MhN9u,/40H#]WXtC5bC0J+O_r7=d*:eIY-:[8iZ0l&oai<sZAOSC0)(?=j!qS`?3@tV$L?=8>O>.>6CYlmph9rB*=g:8[p&Gmb(56dK3e7qn@_H3`47822B*in6A33^Ka_hV>ZVD"@p$MWc[Q&[P;+*g,Wm+n/dg7(L+]0EGqI(Fb]TuUCa#qM%Q+6s,DbHp`'C27W-4dKne@)<j,6<S@[;e9$971U%b'e^)rA]f4!^VL^Vm20<ajQheG8/*WTWs>nDp&ib[DUcCgY>(<`t"CY;.r&ORO==e2hH.9ba=s^HPe]S<uscP!?KoDcG`<1+_$Z/;lE,WnIa_qg'q_FWUhp"/UVsR7I\50X'9qell7MsM&>P4c,6K\j/DRBp[1%bg9QXJJG$IQQQ4"nFm/Ku=R0hGC73aj(2>7=RWYgb_X,O&cX5M5g%E9XKsQOp3&5iER4>2aD3eWTq2ein)n4_N]-P_]CK]c"R_Z!&^+-<?aWTsXPFt%t<A<*<].>H6F1e`9dOV7(PNSfp<Dm3W75=/N1TQL@"&mc0fMlFO"RT;*f.SGMqo`#1)*NbTr*I;:Tn%%c[\eWP4PT>n;UeM:.BB-3Llr9M,r\GK;%_m-AG/3$d-pY$5\8!:[[@$lnb8gUIub,1g@?fo+,IOp`;LK+(3bb<Uj5(Y4'#$mclhbi`JJs!)8s4tciK(8C]2lE/qj]13'/=`.^RG7PW+)<4+!?-D7Z5OhJp@3E,dS^p"@Shq+->[f*[>a\PXDEoh[9S\3puYj'#naK9#5Oau"mRW4Q"e\b"i"!D<p(1p!d*WO=S\X=kX+::A@:Uro-A=Qa79cbFmPedk\`3N<ndUt?Z'\PZ_p:q%B9C,e6LTjj]R\]a/r#p`_<l^Q]?ID=UuO&ZaAYF]6g4,AGD07Wfj-BAq?f)$V8O"W$aelB8-PkL/2%%LC"/'QDaCNtej%jN0],lfsnUD_O-U@oH@,pt(\V$7)&!hsNPDZ~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1594
|
||||
>>
|
||||
stream
|
||||
GauHKD0+/q&H8h>Z!Z4)W,pabmS0O""pue59I[ns5@?Y\j^O.hP*+gYT``'CH`,I@Fu8T.L_EEBm6:F6hX96T!Q>T@hoQ)\nH/-N0OPk9kJ.34Y]pIM(W".BE5f5H5XIiUL]`K#pB0M,a?_;M+s&Hm<"l#1#fDHQOTCJg=u-VaL%BWl"/6O\7K>/:.\gnGLI?-BV?N)S75smDI`BcnLhbTdc/ohY:kH)Mor+Q,PY!H8Z%:SHYPrnZc\e!iP''W4n4-d<A6m*9Et+2%+$Zd*[&WL24L(52/%-Cdf13(?!/l3Led,Ug:*e9"NrRO.80@>!e\ue9qg5tJo,,[Wk;C8d"&ccXWi7.M82]hSSH06f%#bLH&dgMtQ__1Zf<TpB6E)Z62/ZSH6't]#]HTG*,j<kLL)IuN<50-@>U6#`%Tt%*'Bg2E,hUpc!Q2B_GY*6)g07,M3j?]pTdSMCpMPpP:P*`IF38p"XhTAFElTQ2,,S(qd?ZLZ\7%K&L:Eq>`c%7`m0d>sQu?;$pr_VeK\sdn)8RN@gM?T!XEXJ/R]iBabIG!]e:)=[S*WFi+<UHu<K-\J0N!E9+uPV@0c,8W[F&t<RdUn5OfLllou=C>&nVcCM`XA=^el2`4^h8kpc_TR?%DWM*2A[9`d@5g`;r9-0\J.d3l/Sj4L_a=#u`M1O^eqCjsILV3(Z>C=/aQn;rh,-8L`0.L!?YY8.*>4/[K[`_7.sC@aF(?<mT1KAns3h8,Mt`*^[("`[8='8h[*P[Gn+=/?LEN[]kb7(lj*#cY;f"=%,uX9lOo'*nthB40)Xbmui"+.h>L9)O#=XkB34e&8[#lDVem)O]-/h3NUA4[rnB8ZhT'9Sm9cc_iV^R<K(hLFPOW:Q+auJ%p8)9n.<d"j*;@8PK@3`_4+d6,2oOPR*C`HplrTDQKmf7m9:J`0%W0H8Z.O?jVX$sQZ$)qYr/<l=I[4.Y\+^af0lc!N4gf1nhGoh=am+K#CQ]JRHu[6BK)<-<1#8*ed"e+#p>]GC2Mc2Cg[HLCpM<(\UrL9:?h:F<o2-[eI7bMYDf1]G;@9$0!"*]5r(Ehcu8)eBBroNr2OHLq!+6gmqt5RL5RlQ0-H90:J.++`dE(#$e+6^\c*%$s%eZah#_4G=P:*ifm_iMeCLiqcd5mTK5Y38H+Mi"EW0@rMFd\a<BH]_Nm<R3KAl'sLJmnm`fGM+ip_JhW4t7Fi^Z0p@L6t-MbP>$DH]rQQ3G-%V`67#I7k/%W#i.0:*p;D226/m(P6q2IOua1aq-UQLAU7iH3X<T64FHClI3b3?IJq6%^t0NB*KIA^pV]7]GYV]%Xqi5K[Y?j?UQ)f]!u7).Hrd0G.u.`3\Bp"[4^gDH9&%rqaYk+iOCda2Y&#_hK7VFhu7V@"Fol)LH7e"Lh%aZ(]m6c]W)o4<<rQt*Fc^1pY]p3g^-Z:p;dEY0],gYUD%AL6S7eTCXoT=BkFV7VXP6Y(J/61<P>!t6YbV^ZPEdL+38V,Xda!d.SCG5cuUK+K+HZ+BR)GN:Iq3HD0f;`IFs5M\;Tt^65U=UEo'*0\QbM46G*7C>NsSe]<^8fC;NekFdc[?'f6'n!4!,T>l~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
|
||||
>>
|
||||
stream
|
||||
GasapD/\/e&H8h>E<oU\m8A\7.#Q0o!A3#u`O`J5h!N"l%uO%BC@i95mL+pE\*lj[Pao1*j1qHca60$=+-L7@'L0fjB-k?W-(J6p?lYIO@"JYd^4^.Bi$KROnlS:F6:JeF"%biN#OOFJ+Hf,>4COmr9N3Q.-q9Fn'Q>$.a"2sG_ra8#JAdVj8cV^>P"L0\4sZ;4$nY!qATE"m^eYho$'><OV!7jj$pE7)PXc/dTe)'G=I-2dj43$?i-E#d?$96""ha5,SR(omi]Mt90-3=F]"IA%)P66g\u;)r=8dPL^4YML'5N=U"=Ff0gW+fC4/<D>FFCiBYclMm?DWb-E8KNCN^/a<6t<5f(t'8Qio#Ip+\mt'KbP0BJ4IP#]r_Qb9ILWjB&dXq&qn5ecRmQ.P6Kft*MsZ0:u7G0?P7q1%[lqY"@bb5"NPs'0RV'@Qdc+7+!h<Y`1k$P:N@<=e!O^<"]p9*Pkhhc5J&W'Z/V$:%J#l<)5Dj8`^=-i)lcH^-&ZF%6C=8)]Bp#6>5X'CYORSr=G"-?_gbOcog`]d7Mg?H+g3#hfD:jO"AVl><Fj(a.)P!3nZ<IY'a&>4_HkeL&97&[T!oGc:t,oD9R_=\63WA3(,br!_6f&/*7$%B:u92`aI1`H%NNIQS3-ml6)a%akTNuAp6n`aFGaWTTr&gn`,un]AM\JD.>\TK]41D:MSM77N2+1A+uEhJ&"fCYg,*GVPFprKmEXF3f5&fYh`_<.7OX+-%7FM\FkafHcl`-Cc2&`pE!r/D0&^>B2=&N/01XAKVT!7k#Hk_hleg1VP6XbPUIYfkf;3Z+M,GO2,&B7;d=t<+:K7Ug?'Y).?,>i.Ik=`[<o9FbTDE/t]7+lE4s,g0PTg]Se\V@p^Zp-te8>"<m8J64l%_+anT;MhUN*\Dh*7&6Sdcr;Ph65c$]TZ]%,'op[5]nl';V%>E.QhT^f&h@[g.;kgMCtid$=S1DtBu1O4uSkZs#HW-3?bujb\+:r/YJ%EE$qjT>>1(SP$n9R;$<AE&:>;X46N;j.2pVF6MALL>1bY"KGh6jKJh*T#XDRpK;'/]BZsp::B2m(nRW:;/3h*#A89k`&,#gXHi?;YI<7Lj\ap9"X],IA8qtFdpcs1*LQai[PT1%7cJBq8VkF,U`pVigGS5,g1b"S([B&[6j8^cl?%rp*tF:n@i,Nb0H;JLUe-t4.#\>lhIAik$9;kldBA('*laBM&OIr7nGp.Hf4Om7$*QroI0q4c@B:k(L)QOiq)(.(/]YX%In,Sg6t@,b%s2S4Is=L$OuVNM*-6323F4P>p)Ep6h='&)c^h4)q.T?%X",D,2nbtE8>3)$J+WmV)-_`Bqjjd4.Ni<dYMu&0hPJ54%f5f1%id,AEJkNVA'.mn<T<s)h>Kj;^]uDhUX2-ICZ8/cO,,j/OI7!IT5`(p2gQ#a-Zn]f3#7um5f>8qLJWVf@2%U83]OWfC3aE1"og]8._)CWoiJ\3Kos;9&dd.RMMKSUk>TQ3AG6WW>ZjhhKT5`..C8ApbPeUL?[iAl[Bhhs'+.Kubl6D%2&7tJhlk",nQUuAH=[o<7'nfn<b1V?*R&P!<#0_j4`&Ou]RG0X8'Mi=YG&/'F_/k+020ek5O`LE"`TG5i6+n1Md7n0<f>Cj!\LKP/DkEMiJ@QCYVX#(Y7)3$>-G-q\q-KIX)pZ$Zt7b[`dT'**t.0g^]~>endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1584
|
||||
>>
|
||||
stream
|
||||
GauHKD/\/e&H8h>E<u9Sl!g*b,3>L#>&<R5'\fq)`IN5I'U`sj=>,ftUd^CF^6>KS2bmWE6S63M:?6KB*M4Qj"r&`Ar'h".b6?*r0jkt:(Vh;YY]pPj0"bt\E1^pFJ-\M2^juW_r]ma^;G]RI"!0-M<"l#Q8Ag6<JH:ekZt:3`$Q%,O#"8>f)[XG:jW"69GX6`&dBt7BpOfZi\,8oo0-G_e%V`L-Jmf2@#1]IU8LRM@_'4HPmsf_0"2AD_>=Or^[Xn0Q@AD!QQCM`8nRhWl/E=%A/,7*?fM.YZ2Q`!JnHfuWC0r35.1F$poIg,I>9\L2>?PMR%ee4f*N(&73W_@]Uio9mDjb?TL_*PGA3?9":LIdnOM:?1n[KdGg"SD+f7[%97Q,/<?BR#0o<C`,I3Pl(@=oH;C8fu'@)"rmO"HabdhCpR&JFHa)irr^2S>hK&FI/!*Z\@!n=_5_%dq4O736`@?6cKHEcXHl<K!bPY>7imO3sIP9"8;7!(1""P\K:(N95gJQfbL?,CI8B%g[Z@00)l\5iM.hT5JN9"nFX^RlIeeV\!DiHUnQC$)0;SW>5T2#`Ifnhjl/60jq[,mbB.\eCp0oDqfNbd!FVXlKcnH<)PlfR''6G%94s-_?4nh);>RS9']Z)i*9.2WFsR^&8HHgoq79^]qtlK*IQD#0LH7ji7a25U)BDH,p=CEArQ.V6pLh!3!tTh#cFh]B/9hbHktl7/1du:7CK<LWQP2*5``]?&^18D=^"jF#M30a<cOGB4ZnMh2N0.kaf`a`1/!stjmc+O&1Jog#-ru2]GEAk4B04YA"0"CeLF>G,L;?d3OsbFe[IPiRrG^Ao+B#qKWle[$HsY[^2,(-6YI38i>eT8D0f3B30i5\4:b(XMEG';NfpW(/jHpR7ji8"J<&V;"l7On5TNDcj^TD.Hq;*\[(t?QXlc9O%cBsDMIA,fDo`9g@Y+4`AVK*KWTcAmpflFt'dgge9Th)"=\PP'LmQ0Or9"02W?TPf?2/(4]Z[`PADr<Upd]Kla*U4s`cV7LEgbL>NQ8E;5C4;/&6Mcr4JPTlV^t<N*[(f<VphV\*$*63/f$cF>Wn_'N#rT[NSusFBbE5cA8P7LKU#i9)(#3f>GXpYQ*lH_j?A>M[qQ[h)!ei'b&!NbBoF2Td"568(YZ5mH''C5.d65q%FaT#bDI%o!\-/^Z(BB]9'7hamc!$!lU)8=lZuUCK"F6/i*5+D+?2=;W'cS<F+%;>3nrK(`[kr6/Q2AeQ8thu[d2?^KP>^IMg7)b-f?g\%b827AH1d<<53_/Vak^`h%L]Eo/2Nia@Y3MZX0:PM`<BQ4!)C*L3"AQ1kQ=m]^FAK*g-1efP\>&@6.SQ*jPFDjLZt09^>2?iaG>34PKGA*WeBiJa+XT0R7O&"e6B3RbAGggDh,N(Ws5S8TemrMf!iQ$b/ORh:NpcL`8+5V,5%:$Ca6%&%>pBrK>P'H9;L]?P9@iT3gIhTGe>.VC$iD[a+6^_TH2<gC9%iIa0YSokE5k;QpTBDC\N%ruK#W?WHS*s-\O35O4N=H#=D5A%9@O#L70tnW2)p^E693D(b>W"6$+I2?~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1631
|
||||
>>
|
||||
stream
|
||||
GauHKD/\/e&H8h>EQBN*Mcl4;7*7kUADOg#kgjnL2IJ48-o,;5PseWHPJ/1>I4+;;NDj9_d)_%('D_..]B>22",[n9s-"G\LG_2C`di&2?NCg._"p(h[.`@%KSNfi!/RhW+)rEchu#dJo*Pai&k4umTVs,.$4r(,V'6'_k=Nc@<#Vn\GmoK;WDiqT@R6s0Hh2XuEfNd#&qj]cq>?8FC?S)3m+<K+p4Ha4JosftW4jC="YtUN%ZB4Nr,i%TR%:CD]eLE&Be,b@/SmD5E.5&SYNrR(H4)t&CRpkKf7cAAoc%!tC=\j3$XXM<7Gc"5Y%sgDGcRsWC12UQGA.Wk@hYN%(t9TUj;h/j(%H]%0c=>W\E@@4PV5?86LOAo[#]hko=-'1$8o).eVt2pacK:M5sH^d"rf`[`E34"Qm*Ld"Ya?]X;s).kR@h2m%6(piEJ4SU]V/K<_5D7ZH2OY5dHkA\2!M9nb/Of<HGRYJ,HA[O&Ksf<2YdKc57j41%&O(0[M:G/n`QW08-`Arqnsr%ip*epjIP+8\cMamN)3ic@90S7Vlc>=r.b=mY^;.(;bk;>%m13g<X&aA"]8*>g_!.BH'j-V^B/8i3_^cAoZZY=DpsnS(8@J%7tD3=*IadjWOcXU^EV;)+E_mLkg=-9d`N6HGWnLF&,?[2LrW0Gs]njib07A6qF8LEb\JMR)Bqj$.bLdq9hP<2CrkuR[G`ea.d[."pT\`TuJaF.]$%,f5q.jj'%Ere`[)U\T[R7DoThm_Xo!l$)5I^)3K_2N\Y7;poNj.M(*0I<P3[Tj!ra"'5050=aW9eNWob]M4g![`<'G*NNPlHS!mdi-iY%^[Sc!Y+3MgG>7Ff#*1@qHMj(J#"#HcEJGJV`e"StW=34e3nHqGmF889Q(>6O9K<@BpVV5'bZ.gg+GIO'j3HQ.%+At@>rE^iVX&fka+,U1*g]e(VHj;Gn@%hQ1mEbje@-[YfDmr7EK,NRbBkM]gkfpu\Dbe'<!=h*O?&HsY5J]7d1#*8Koq@<tj'%YH9K%fm4LWB2Fg=4GDsNUn=nVncU=dO.>3B6'r)]XDB@)4!6Xa%-bf(H(DC!-IJ_PIB))ck67llpDLT5;GcV:5:@:@NdFur&Do!*jo&^1p+0&Qs9RkkP(GZ5Ll+dCuMU-d8>QAFcToF7n,Y+?S23nnJ%"d1^#ZHfB'Cj!/f;sGQ%1"Ena/qmHjBp#"2Wa5fRoVLiGl48.1aLa2jK1tV-%.G*3fRq`mE"03j"_SA_p;WJ7$b8a/R7!8'R`lMNmh=r,hQPLPPBqr2*F$`mIQ4dlk!7Mh[ANkKYHb>Tn+(0;^l*NF@!'ugrdH-=a#>.WoUF4>R4\9lei90*>i/ibo\NV'=CT?/JND'!9EX^:&+>233AQt`T^H[SgkY9`%k$Lg\cQ73Hh`4@\d*IU\+C,nnj/:\[L*kCUkZ;F"1Bm.m,UP8MdI!_9p^3*@Vs/n.Bp"c[78IXm8;FT?/TR)!+5Sn*:ZG%27e$o*,`(SlU@IRlT[ksV;#9*I8ctc`qsOW?#$Djgc\Ol^dkKWV/+Fb$Gff&_#FbW"aUm;oNG`9Y0oWI5CDlf60emD*J`1DRQ/:kPVk\!^5'!LQhV3*doKQH*mp>B!q0Kb(1Kc~>endstream
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1621
|
||||
>>
|
||||
stream
|
||||
Gb!;d=``=U&:WfGfS=(?A0Mij5ltkdc6Tf=9Kd)fX3$9,+;0iNo2&!#Y5@Osi9`m(eB)r?*@:pkhsRtc=R;IW`AajtL+0q()'$A!^c9OR^]Y6'4+ZF[o@5#>k>HOi>XdJ!!sr60Jn!tTi;(;%GecRA8X'o3'I-3R$952PP"@&1/=nj1M"IZG!f1<WYP^KJ?/fpcU':(75IeD@ePB?@!og]CTpj42_/KTo8bK$b+I%0"K38PF%tAtU&+B(c.Do/W-h(2;QuH'A)<VW=]q<"gbIFWI"@qV&,l'JY=nNJ@k:h6pAW?25WsEa.2t9[;M<Y1LaspaTYTNNnc8:u4"S<"[g*-:)ePoF8>aJ')`_MFR/m:`T_A>D3%eiM8gQ*WjQ\8817Uo]2ZNO^bkMm/XRhIj](u?scR"ag\!h:'Ocss\hRF<\5%>:ZfGFpi'T<dE!m=Aq(-$8@D7=maM-/6C%N=kd/=ZLV$fluKR_`'JulKIntC?&gb1EW_:E3=^7Y,aZbqi&pb@gHl:I/>r;i7A#3_X@gCS9QU#aLrbTMp=(f$Fhk5@2>=>rd=D4>I!*:g;h(^DeR,dI4VJ4OVMUM9YML=g^uO^g2@^%M31V<5OO?&Q"c6"qlJ/H=+HdSd;sZaDUGf,X>A,^N(LQ[8W4aP@C1u(61`b2c>J5`2C<u'%l\Z)RgdRh^NpUFH3mSXBq:bWh'uU%KcZ93+DWWZ7g$.l;s5bfKtZ%5@L"Pf4WLXkem?!a9rbNL%\Y_j#Zm,L;7o<ceJh(-M*[HiD]Ml?l7-'+F<d[o7sC'&(Yt-@I&Y$LT-;XJL,;D`E<bV%jpI,T_`We%028m5iWCt6:BY#@H%l?gRO!cs-,_+R9qnt*&YIs?e5*PapZ^)CSFrZ*j4iVS&)W]qKhq_5q=-(F"#Hl:nf1>P:CIG2QQU:#eWM74RL?/pjk8+[U03qc4O7hOQIf4jV6DTCFBq&8"M1K3NboC`c+Z9/X>(dr1"gVmIoLTu/$'Sc+m,:-%cR'/aWjH[Hh'>%MG72i$7p@)cBZDOR$au8M\L/P<i1+P.<SF\$ncAh[Dm-<a8Odu(0WAdL1?7SBOu8gom%/fRr[Q34K8h%-f<hPa@td.2!nm,bSoLV)]^^disOW<*&k)b2:K*mC_AA85OSrGs0005mWr3XOZ`_]Q%FH6[qCe/!LIgN"g'@D(r]r']?:-,\>&DRYRMnk.E5:V8D/*\rE/%bXKZ^'0\!S65,!SNnVHJmKMFbOQtL2^^P;$o,k6PQdt0CB'C&0nfo%6nZu,aB92V?7YUAD'^L%34T)CV,%'1YgjA.Cg#>^7b3X8c]5Xj$!Su@K`_)hded%cZrj@Z&juReN,@9e'2(NBY2#RmZ>d^XsYn]/5g0pK*>JHgN']R"eHbHXu=s%\JrHtL_*2K)WYXe'eYaHV?dXDKEF>h,"u\!("U*DQJ'&#lY(8<J;U8%dWLj[`6$8WG)mKdg>-mS/Hu4F]&Rbq'&aG[Q)\*j4gd8+0+8g_=i<aTV$lnU!6-\Am)!"7j[HNqhK=ZsD<'@/kJaZRiPiuIi?ng_J.`B1j-OW]]NYBV!4MoCBa*:jU@4"up1e68nsESCf67s%i\[gRqrPi##3T>K\gH)~>endstream
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1654
|
||||
>>
|
||||
stream
|
||||
Gb"/'D/\/e&H8h>E<A*Al"XCthP^JrOL/^;/8`hbl_f,=,fWk9MloH.gOB)rYEn5&M2]YqJ_>c0PLSDOp[[f%$7<K$q&RZZN^kdN7tEE4h#b!@%ARu()&Zci_Hc6V%M4PfQHt*V<q1c`Ca@rK`:]NoJX[_>SddTUKQEaf_tuQWHB:b/I:5qpSniZ-#/[A"Ed-o"Mn"_>C7@2clT`%s[Z;6eq8BD;6SkbR$H!Uc#1Z>RCgp;_F'phWK+Acjces")Q<>n=.Y^01.YES4CW8Ir3ZB&M=d3V<_HXR4Z's!LU(:[S#GW)hY)[naiWQN2X7FSJL?8U;A6Rei@P>\[8d%tk,Np@5[?9OUg/?8X`3N&IK<`teL4u5XQL3BYbREWqlEVFg%H\8O[!?;CnTpHpW9:JSLsrF3N-5U_.&XE`eIMDbc?GqHD:*)mCHsZeTRk@1Cf$Xub=T#TA@>YC_d$@Wj.FoDqW8P_#C0]NN&%AiG8:#6]-;OClu`W<o:lsT(VT&h`dsjhM`\S1)5Wn>&6eo*iGV5;>:T+K>?>2gi_U^QG=B>ZWkpmFOW:<R9^a:)_cS7HAV81N2B0@m@M`l3fe,+)r(q^ui(=JWO\AX@MUnusF]\3ElBRtNMU;0272MepOdZe#=GR(q1=.M)9StZo^e_/m7UfeWXqiFU3^8*i?tgKh>0L>j['-TEa*0W=RH8N(S^n_1;HIp(a,4^VD(MduY3[@.A>>!i=Ymll;_R<^??+ZV2dj_M+pk#@BUN^:M`gkfaOh/fZ/(Ml;21JGAIR/qDq%1Png,9JSm3?Q822^3fn/]r!*W.u\/W.,9R'6D(uqc]X=<tTWRMoKmp9ao%rlA94?Ke*2>g.'FBYT'bVk)Zl?Ah\@SR[_c@q;.3rZN"[QV''#s7QrjM<_H]T#D?535VrPrcL&K`u78OUC/np_=.E(YE%b&;e+kc3qCM*M"J=qa7jBeZY)+"Kmb[?P_#.161e"TQ33AoeptKpBPK5Wu!=gh1<WOGBl06e`J^%(NH6^Zk_47EUC1RV]rnV@B;^aGN%#-ai4hg\VT3"N><`5qf,Z@\@ih3I#GR=(KcMsRbET5ZK(:@L*:_#)14D*)'Nd*:pT:3QIQj]/<MWnS7W\SdsO7i0b39,Y5>.>]=cu.OhP!s`f7c^l^oRs&0gQ@X5arO%%DnqOrEGL^E@kQr%krKr/lp-F$V>/+=P0c/;$*VGQk@@$5%o,d\H2k0&g!W`)dLgXB@Y6On(4!s5@siLI_%[+!-JA<><T1dpbtAeFT0npYnq_n$/^(Q1\\6UGM];p(-AKBM"X:)J%fkKQ8dU<KIZjs+<S5EnFFB[W<*nHe^VbANdk:b[lA9GR/a>$[o)(),W@H+qghWjb7[qKee"L"6^dbO&fCZ#L(6]Z\3(kN:\C1.H*/umDTj:rZ:fh7TWBUTCTP&&Xe;P^f`h5(?&^-.40f?0.i@r:L;^Q4Q4ujo0f?h=&5n]6n31@iY2t6X.,1Tid@&.bPlF"RdA4FM4UMX*X2%&,oQlVA%-D'FW*h6Dq?huX=9^&aSTcj[#50'ZhQUDGkQ5IT$5+,2$V-c]51SEI:j"0:NS,5E[m;B;Bs1jI$s'GJo-Qg^I9C&'C:u$Mk[u$3][Gt4uKo8m,1Ws,pZA-\mI/j"ej-QA,~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 34
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000536 00000 n
|
||||
0000000741 00000 n
|
||||
0000000946 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001873 00000 n
|
||||
0000002079 00000 n
|
||||
0000002285 00000 n
|
||||
0000002491 00000 n
|
||||
0000002697 00000 n
|
||||
0000002903 00000 n
|
||||
0000003109 00000 n
|
||||
0000003179 00000 n
|
||||
0000003471 00000 n
|
||||
0000003615 00000 n
|
||||
0000005572 00000 n
|
||||
0000006064 00000 n
|
||||
0000007403 00000 n
|
||||
0000009290 00000 n
|
||||
0000010896 00000 n
|
||||
0000012827 00000 n
|
||||
0000014803 00000 n
|
||||
0000016489 00000 n
|
||||
0000018280 00000 n
|
||||
0000019956 00000 n
|
||||
0000021679 00000 n
|
||||
0000023392 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<ac774e881c346d045e81ff782e65a794><ac774e881c346d045e81ff782e65a794>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 19 0 R
|
||||
/Root 18 0 R
|
||||
/Size 34
|
||||
>>
|
||||
startxref
|
||||
25138
|
||||
%%EOF
|
||||
478
Les14-RAG-Embeddings/Les14-Slide-Overzicht.md
Normal file
478
Les14-RAG-Embeddings/Les14-Slide-Overzicht.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# Les 14 — RAG + Embeddings
|
||||
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
|
||||
|
||||
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
|
||||
**Demo-app:** Nieuwe PDF Q&A app from scratch
|
||||
**Vervolg op:** Les 13 — Agents
|
||||
**Aansluit op:** Les 15 — Cursor + Vercel deploy
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: Title
|
||||
### Les 14 — RAG + Embeddings
|
||||
|
||||
**Visual:**
|
||||
- Background: CREAM
|
||||
- "Les 14" in BLUE
|
||||
- "RAG + Embeddings" in BLACK
|
||||
- Subtitle: "AI laten antwoorden op basis van jouw eigen documenten"
|
||||
|
||||
---
|
||||
|
||||
## Slide 2: Terugblik
|
||||
### Waar staan we?
|
||||
|
||||
**Vorige lessen:**
|
||||
- Les 12: Tool Calling — AI kiest welke functie
|
||||
- Les 13: Agents — autonoom 20-50 stappen
|
||||
- Les 14: Externe APIs + Vercel deploy
|
||||
|
||||
**Het probleem dat we nu oplossen:**
|
||||
|
||||
AI weet veel, maar weet NIET wat in jouw documenten staat:
|
||||
- Interne kennisbank
|
||||
- Klantcontracten
|
||||
- Productdocumentatie
|
||||
- 200-pagina PDF die vandaag binnenkwam
|
||||
|
||||
**Twee opties:**
|
||||
- ❌ Alle 200 pagina's elke vraag meesturen — niet schaalbaar
|
||||
- ✅ **RAG** — alleen de relevante stukjes ophalen per vraag
|
||||
|
||||
**Visual:** Pijl van "alle docs context" naar "RAG → top 5 chunks".
|
||||
|
||||
---
|
||||
|
||||
## Slide 3: Planning
|
||||
### Vandaag — 180 minuten
|
||||
|
||||
| Onderwerp | Duur |
|
||||
|-----------|------|
|
||||
| Welkom + Terugblik | 10 min |
|
||||
| Theorie: Wat is een embedding? | 20 min |
|
||||
| Theorie: RAG pipeline (index + query) | 15 min |
|
||||
| Theorie: pgvector + chunking | 15 min |
|
||||
| **Live Demo 1** — pgvector setup + embed pipeline | 25 min |
|
||||
| **Live Demo 2** — PDF upload + index | 20 min |
|
||||
| **Pauze** | 15 min |
|
||||
| **Live Demo 3** — Query pipeline + AI antwoord | 20 min |
|
||||
| **Live Demo 4** — RAG-tool in een agent | 15 min |
|
||||
| Wanneer RAG wel/niet | 10 min |
|
||||
| Lesopdracht + Huiswerk | 10 min |
|
||||
| Vragen + Afsluiting | 5 min |
|
||||
|
||||
---
|
||||
|
||||
## Slide 4: Wat is een embedding?
|
||||
### Tekst als een punt in de ruimte
|
||||
|
||||
**Het idee:**
|
||||
- Stop een stuk tekst in een embedding-model
|
||||
- Krijg een **vector** terug — array van ~1536 nummers
|
||||
- Vergelijkbare betekenissen = vergelijkbare vectors
|
||||
|
||||
```
|
||||
"De kat zit op de mat" → [0.12, -0.45, 0.88, ...]
|
||||
"Een poes ligt op het tapijt" → [0.14, -0.41, 0.85, ...]
|
||||
"Voetbal in Nederland" → [-0.73, 0.21, -0.32, ...]
|
||||
```
|
||||
|
||||
Eerste twee zijn **dichtbij in vector-space**. Derde is ver weg. Niet omdat woorden overlappen — omdat **betekenis** vergelijkbaar is.
|
||||
|
||||
**Welk model:**
|
||||
- OpenAI `text-embedding-3-small` — 1536 dim, snel + goedkoop
|
||||
- OpenAI `text-embedding-3-large` — 3072 dim, beter maar duurder
|
||||
- Open-source: `nomic-embed-text`, `mxbai-embed-large`
|
||||
|
||||
**Visual:** 2D plot met 3 punten — twee dichtbij, één ver weg.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5: Vector similarity
|
||||
### Hoe meet je 'dichtbij'?
|
||||
|
||||
**Drie metrics:**
|
||||
|
||||
| Metric | Wat | Wanneer |
|
||||
|--------|-----|---------|
|
||||
| **Cosine similarity** | Hoek tussen vectors (–1 tot 1) | Default — werkt op semantic search |
|
||||
| **Dot product** | Sum van producten | Sneller — als vectors genormaliseerd zijn |
|
||||
| **Euclidean distance** | Afstand in ruimte | Zelden voor text — meer voor images |
|
||||
|
||||
**Cosine similarity = 1** → exact gelijke betekenis
|
||||
**Cosine similarity = 0** → ongerelateerd
|
||||
**Cosine similarity = -1** → tegengesteld
|
||||
|
||||
```sql
|
||||
-- In pgvector
|
||||
SELECT * FROM chunks
|
||||
ORDER BY embedding <=> '[0.12, -0.45, ...]'::vector
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
`<=>` is cosine distance. Sorteren op `<=>` = dichtstbijzijnde eerst.
|
||||
|
||||
**Visual:** Drie vectors met hoeken — kleine hoek = similar.
|
||||
|
||||
---
|
||||
|
||||
## Slide 6: RAG pipeline
|
||||
### Index time vs query time
|
||||
|
||||
**Index time (eenmalig of bij upload):**
|
||||
|
||||
```
|
||||
PDF / docs
|
||||
↓
|
||||
Parse + chunk (500 tokens per chunk)
|
||||
↓
|
||||
Embed elke chunk
|
||||
↓
|
||||
Store: chunk_text + embedding in pgvector
|
||||
```
|
||||
|
||||
**Query time (elke vraag):**
|
||||
|
||||
```
|
||||
User vraag
|
||||
↓
|
||||
Embed vraag
|
||||
↓
|
||||
Similarity search → top 5 chunks
|
||||
↓
|
||||
Geef chunks als context aan LLM
|
||||
↓
|
||||
LLM antwoordt op basis van context
|
||||
```
|
||||
|
||||
**Belangrijkste inzicht:** AI ziet nooit de hele DB. Alleen 5 relevante chunks per vraag.
|
||||
|
||||
**Visual:** Twee parallelle flow-diagrammen — index vs query.
|
||||
|
||||
---
|
||||
|
||||
## Slide 7: pgvector
|
||||
### Postgres met vector-support
|
||||
|
||||
**Wat is het:**
|
||||
- Postgres extension — voegt `vector` datatype toe
|
||||
- Cosine, euclidean, dot product operators
|
||||
- Schaalbaar tot ~1M+ vectors per tabel
|
||||
- Ingebouwd in Supabase — één click activeren
|
||||
|
||||
**Schema voorbeeld:**
|
||||
|
||||
```sql
|
||||
create extension if not exists vector;
|
||||
|
||||
create table chunks (
|
||||
id bigserial primary key,
|
||||
source text not null,
|
||||
page int,
|
||||
content text not null,
|
||||
embedding vector(1536),
|
||||
created_at timestamp default now()
|
||||
);
|
||||
|
||||
create index on chunks
|
||||
using hnsw (embedding vector_cosine_ops);
|
||||
```
|
||||
|
||||
`hnsw` index = snelle approximate nearest neighbor search. Voor production met >10k rows.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8: Chunking strategieën
|
||||
### Hoe knip je een document op?
|
||||
|
||||
**Drie aanpakken:**
|
||||
|
||||
| Strategie | Hoe | Wanneer |
|
||||
|-----------|-----|---------|
|
||||
| **Fixed size** | 500 tokens per chunk, 50 overlap | Default, simpelste |
|
||||
| **Recursive** | Splits op paragraaf → zin → woord | Behoudt structuur |
|
||||
| **Semantic** | Embed zinnen, group similar | Beste kwaliteit, duurder |
|
||||
|
||||
**Wat is een goede chunk:**
|
||||
- ~200-500 tokens (~1000-2500 chars)
|
||||
- Genoeg context maar niet te groot
|
||||
- **Overlap** (10-15%) zodat info aan grenzen niet verdwijnt
|
||||
|
||||
**Te kleine chunks** → fragmentatie, AI mist context
|
||||
**Te grote chunks** → ruis, irrelevant info verdunt het antwoord
|
||||
|
||||
**Visual:** Document opgeknipt in chunks met overlap.
|
||||
|
||||
---
|
||||
|
||||
## Slide 9: Wat we vandaag bouwen
|
||||
### PDF Q&A from scratch
|
||||
|
||||
**Doel:** App waar je een PDF uploadt en daarna vragen kunt stellen over de inhoud.
|
||||
|
||||
**Stack:**
|
||||
- Next.js 16 + TypeScript + Tailwind
|
||||
- Supabase + pgvector
|
||||
- AI SDK `embedMany` + `generateText`
|
||||
- OpenAI `text-embedding-3-small`
|
||||
- `unpdf` voor PDF parsing
|
||||
|
||||
**Twee endpoints:**
|
||||
|
||||
| Route | Wat | Wanneer |
|
||||
|-------|-----|---------|
|
||||
| `POST /api/index` | PDF upload → chunks → embeddings | Eén keer per document |
|
||||
| `POST /api/ask` | Vraag → embed → search → answer | Per vraag |
|
||||
|
||||
**Bonus:** RAG-tool in een ToolLoopAgent — combineer met Les 13.
|
||||
|
||||
**Visual:** Index + query pipelines uitgelegd in 1 diagram.
|
||||
|
||||
---
|
||||
|
||||
## Slide 10: LIVE DEMO 1 — pgvector + embed pipeline
|
||||
### ~25 min
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. `pnpm create next-app pdf-qa` + `pnpm add ai @ai-sdk/openai @supabase/supabase-js unpdf`
|
||||
2. **Supabase:** activeer `pgvector` extension (één SQL command)
|
||||
3. Schema: `chunks` tabel met `vector(1536)` kolom + HNSW index
|
||||
4. `lib/embeddings.ts`:
|
||||
```typescript
|
||||
import { embed, embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
const model = openai.textEmbeddingModel("text-embedding-3-small");
|
||||
|
||||
export async function embedText(text: string) {
|
||||
const { embedding } = await embed({ model, value: text });
|
||||
return embedding;
|
||||
}
|
||||
```
|
||||
5. Test: `console.log` een embedding — array van 1536 nummers tussen -1 en 1
|
||||
|
||||
**Visual:** Console-log met vector + Supabase tabel.
|
||||
|
||||
---
|
||||
|
||||
## Slide 11: LIVE DEMO 2 — PDF upload + index
|
||||
### ~20 min
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. `app/api/index/route.ts` — POST endpoint accepteert file upload
|
||||
2. `unpdf`: extract text per page
|
||||
3. **Chunking**: split text in ~500 char chunks met 50 char overlap
|
||||
4. `embedMany`: batch embed alle chunks (sneller dan loop)
|
||||
5. Insert in Supabase met `embedding` als vector
|
||||
|
||||
```typescript
|
||||
const chunks = chunkText(fullText, 500, 50);
|
||||
const { embeddings } = await embedMany({ model, values: chunks });
|
||||
await supabase.from("chunks").insert(
|
||||
chunks.map((c, i) => ({
|
||||
source: filename,
|
||||
content: c,
|
||||
embedding: embeddings[i],
|
||||
}))
|
||||
);
|
||||
```
|
||||
|
||||
6. Demo: upload Polderfest 2027 line-up PDF — 30 chunks in DB
|
||||
7. Check Supabase tabel — chunks zichtbaar, embedding kolom gevuld
|
||||
|
||||
---
|
||||
|
||||
## Slide 12: Pauze
|
||||
### 15 minuten
|
||||
|
||||
---
|
||||
|
||||
## Slide 13: LIVE DEMO 3 — Query pipeline
|
||||
### ~20 min
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. `app/api/ask/route.ts` — POST endpoint accepteert vraag
|
||||
2. Embed vraag (zelfde model als index!)
|
||||
3. Supabase RPC of raw SQL: cosine similarity search
|
||||
|
||||
```typescript
|
||||
const { embedding } = await embed({ model, value: question });
|
||||
const { data: chunks } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
|
||||
const context = chunks.map((c) => c.content).join("\n\n");
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
prompt: `Beantwoord op basis van deze context:\n\n${context}\n\nVraag: ${question}`,
|
||||
});
|
||||
```
|
||||
|
||||
4. Supabase function `match_chunks` definiëren (SQL):
|
||||
|
||||
```sql
|
||||
create function match_chunks(query_embedding vector(1536), match_count int)
|
||||
returns table (content text, similarity float)
|
||||
language sql as $$
|
||||
select content, 1 - (embedding <=> query_embedding) as similarity
|
||||
from chunks
|
||||
order by embedding <=> query_embedding
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
5. Test: vraag "Wie zijn de headliners?" → AI geeft accuraat antwoord op basis van PDF
|
||||
|
||||
---
|
||||
|
||||
## Slide 14: LIVE DEMO 4 — RAG-tool in een agent
|
||||
### ~15 min
|
||||
|
||||
**Wat ik laat zien:**
|
||||
|
||||
Combineer Les 13 (Agents) + Les 14 (RAG):
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, tool, stepCountIs } from "ai";
|
||||
|
||||
const ragSearch = tool({
|
||||
description: "Zoek in geüploade documenten",
|
||||
inputSchema: z.object({ query: z.string() }),
|
||||
execute: async ({ query }) => {
|
||||
const { embedding } = await embed({ model, value: query });
|
||||
const { data } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const docAgent = new ToolLoopAgent({
|
||||
model: openai("gpt-4o-mini"),
|
||||
system: "Je beantwoordt vragen door eerst in documenten te zoeken.",
|
||||
tools: { ragSearch },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
```
|
||||
|
||||
**Demo vraag:** "Vergelijk de jazz en rock-headliners op Polderfest" — agent doet 2-3 RAG searches, vergelijkt, antwoordt.
|
||||
|
||||
**Krachtig:** RAG + Agent = AI met geheugen + redenering.
|
||||
|
||||
---
|
||||
|
||||
## Slide 15: Wanneer RAG wel/niet?
|
||||
### Niet alles is RAG
|
||||
|
||||
**Wanneer WEL RAG:**
|
||||
- Veel documenten (>50 pagina's totaal)
|
||||
- Documenten veranderen (klantcontracten, kennisbank)
|
||||
- Semantic search nodig (niet alleen keyword)
|
||||
- Privacy: data blijft in jouw DB
|
||||
|
||||
**Wanneer NIET RAG:**
|
||||
- Klein document (<10 pagina's) — gewoon meesturen
|
||||
- Exacte data (prijzen, IDs) — gebruik tool-calls / DB query
|
||||
- Structured data (tabellen, records) — SQL is beter
|
||||
- 1 keer per dag — overhead niet de moeite
|
||||
|
||||
**Alternatieven:**
|
||||
|
||||
| Probleem | Beter dan RAG |
|
||||
|----------|---------------|
|
||||
| 5-pagina manual | Volledige tekst in system prompt |
|
||||
| 10k product records | SQL tool-calls (Les 12) |
|
||||
| Live data | API tool-call |
|
||||
| Code-base navigatie | Tree-sitter / grep, geen embeddings |
|
||||
|
||||
**Hybrid is vaak best:** semantic search + keyword filter.
|
||||
|
||||
---
|
||||
|
||||
## Slide 16: Lesopdracht + Huiswerk
|
||||
### PDF Q&A app deployen
|
||||
|
||||
**Lesopdracht (in-class, 30 min):**
|
||||
- PDF Q&A app opzetten (volg demo)
|
||||
- pgvector in Supabase actief
|
||||
- 1 PDF indexed
|
||||
- 3 vragen werkend
|
||||
|
||||
**Huiswerk (voor Les 16):**
|
||||
- **A:** Eigen PDF (interesse, studie, hobby) — minimaal 20 pagina's
|
||||
- **B:** UI met chat-interface (`useChat` van Les 12)
|
||||
- **C:** RAG-tool in een ToolLoopAgent
|
||||
- **D:** `RAG.md` met:
|
||||
- Beschrijving van je PDF + chunks count
|
||||
- 5 vragen + antwoorden + welke chunks werden opgehaald
|
||||
- 1 vraag waar RAG het **fout** had — analyse waarom
|
||||
- Eén chunking-strategie geprobeerd + resultaat
|
||||
|
||||
**Bonus:**
|
||||
- Hybrid search (semantic + keyword via Postgres full-text)
|
||||
- Re-ranking met `cohere-rerank` of LLM
|
||||
- Streaming antwoord met source-citations
|
||||
|
||||
---
|
||||
|
||||
## Slide 17: Volgende les + Afsluiting
|
||||
### Vragen?
|
||||
|
||||
**Vandaag gezien:**
|
||||
- Embeddings — tekst als vectors in semantic space
|
||||
- Cosine similarity voor 'dichtbij'
|
||||
- RAG pipeline — index time vs query time
|
||||
- pgvector in Supabase met HNSW index
|
||||
- Chunking strategieën
|
||||
- RAG-tool in een agent
|
||||
|
||||
**Volgende les (Les 15): Cursor + Vercel deploy**
|
||||
- Externe APIs in Next.js Server Components
|
||||
- Cursor Composer + Background Agents
|
||||
- Deploy naar Vercel productie + preview per branch
|
||||
- GitHub Actions CI (lint + build)
|
||||
|
||||
**Daarna in deze leerlijn:**
|
||||
- Les 16: MCP — eigen Model Context Protocol server bouwen
|
||||
- Les 17: Externe APIs in diepte (OAuth, webhooks, paid APIs)
|
||||
- Les 18: Supabase Auth + RLS — multi-user apps
|
||||
|
||||
**Vragen? Feedback?**
|
||||
|
||||
---
|
||||
|
||||
## Slide Summary
|
||||
|
||||
| # | Title | Type |
|
||||
|---|-------|------|
|
||||
| 1 | Title | Opening |
|
||||
| 2 | Terugblik | Recap |
|
||||
| 3 | Planning | 180-min |
|
||||
| 4 | Wat is een embedding | Theorie |
|
||||
| 5 | Vector similarity | Theorie |
|
||||
| 6 | RAG pipeline | Theorie |
|
||||
| 7 | pgvector | Theorie |
|
||||
| 8 | Chunking strategieën | Theorie |
|
||||
| 9 | Wat we bouwen | Intro demo |
|
||||
| 10 | **LIVE DEMO 1** — pgvector + embed | Demo |
|
||||
| 11 | **LIVE DEMO 2** — PDF upload + index | Demo |
|
||||
| 12 | Pauze | Break |
|
||||
| 13 | **LIVE DEMO 3** — Query pipeline | Demo |
|
||||
| 14 | **LIVE DEMO 4** — RAG-tool in agent | Demo |
|
||||
| 15 | Wanneer RAG wel/niet | Reflectie |
|
||||
| 16 | Lesopdracht + Huiswerk | Praktijk |
|
||||
| 17 | Afsluiting + Les 16 preview | Closing |
|
||||
|
||||
---
|
||||
|
||||
## Bronnen
|
||||
|
||||
- **AI SDK Embeddings:** https://ai-sdk.dev/docs/ai-sdk-core/embeddings
|
||||
- **OpenAI Embeddings:** https://platform.openai.com/docs/guides/embeddings
|
||||
- **pgvector:** https://github.com/pgvector/pgvector
|
||||
- **Supabase pgvector:** https://supabase.com/docs/guides/database/extensions/pgvector
|
||||
- **unpdf:** https://github.com/unjs/unpdf
|
||||
- **Anthropic — contextual retrieval:** https://www.anthropic.com/news/contextual-retrieval
|
||||
- **LlamaIndex RAG concepts:** https://docs.llamaindex.ai/en/stable/getting_started/concepts/
|
||||
BIN
Les14-RAG-Embeddings/Les14-Slides.pdf
Normal file
BIN
Les14-RAG-Embeddings/Les14-Slides.pdf
Normal file
Binary file not shown.
BIN
Les14-RAG-Embeddings/Les14-Slides.pptx
Normal file
BIN
Les14-RAG-Embeddings/Les14-Slides.pptx
Normal file
Binary file not shown.
175
Les14-RAG-Embeddings/Les15-Huiswerk.pdf
Normal file
175
Les14-RAG-Embeddings/Les15-Huiswerk.pdf
Normal file
@@ -0,0 +1,175 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 6 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105902+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105902+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Huiswerk) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Count 6 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2045
|
||||
>>
|
||||
stream
|
||||
Gb!;dbBDVu&Dcq.As6.NXUmVi!5!:ja76.X]]<V`%7NSrD2M:`b"f;,[s%gM+=16tX<qXuq,$&^Qj5<dkCO*jI[I[cF+EEuGkR:$j"Qo%F,::"1H6F9T,kds*k37&+fP`-OAP.*.&TfMSko9GLC>F;L^[O-,pQ<B&ie/E!Z"&#@.MfU5Q[P^dKLh_k=bj+HpN/*dBlP/h[_[/\,Jtd?:b"F4*7h)7qFZ3[6[rt_BkFlJHqr1%/tYGnJ#:E;cUfCU<)9*/lb$7iI^Y$N_Y@"<L$Um9&PE7<`u\pX1\EL7[GfTWbqG$]>>eqTY\5=1KC.Gn7n@l2laN*oY-<^Bc[<@.=-c-]$f[2el,p0&qljlc.KSdNfWeoNZ35f1/l9`-7#,XqFPW$I49DbmmHHo)o`#ST#^KY:?E3%YRmlo`kl>P2.e/>dsITFXYD?"SMo1>M]I[mA9_t3[pN<r8a+A<gh4L"j8riSQn-$aNPsBBG-Q+>.RbKWZ1EKB8NkbGcGOgi2.e-qG'd_/[h@D/UReZ>*P5A7&W\Zg]"hRf4VhM3XD1FYRSnD(&oIET=tm4+>;!T'`Y)p8huWt%OjYB=;V5b?GEp57-"&ui)3>CkonFaQ9RD;)O41)NiU)Pin-bbYioU-dafs>RR-"t=ZPmCP*bBh$lJ=in$\:bc/<p,6>P"H0m=<Lm231gSK)SX-41F^fRSQdJ1Bk[2rmsF>'2/Y+(Ud=Q0QQY;])$9gl+U4Bk*',*I<Dl=h=m\m\iC*a*YcO#'d["OX8=,E)7ZfU;:WY4Erf@A-qZ2XjYuGsHC<=J]L\b*Z$/]'S/rBkBqF:0Pq'O5U4"V0m*/+*^KQZ&oG0lZd:Aj4HnL=df*:bt_AT2:aW?Qa'Zf/OQHSibnol$BC^#.;osdD[g8Mf)!$]s._!Sl9rWIp6#14^?_=b\MbRdJ,_qcX+HD+@6fm,>RfMeK(`gO^*O)'QXFJ6g0D@_QU^24Abd!\^2p%H1Thqb$X?E7I[P.f?6m<2++0j*i<43"i+Q*hN)'5FFAXXVhoJraV-TXdt\B+-TkoFu?/M9;#dZ&k'%P2_oRL3e[E,]&i5gBmeLqa8#1i.#[D_gQ1qN[i&7djnqbir!sXK!8+NbTr4WkAD'K:8cC"_=M,];o:Ho5n1d7&d=]'MFCE4aMJ3&j&MKoE?4-p`uW1:>T!>0;h]pjW%Y`RIt`+_eO+-tA(7FF=3Y9@_#p2g_Y^[6=<*akP300@QHed)bs51F99#cO9(l&0f:sTVU:)rT>q3*0L[naq-SQX<E.->.:4j;1r3S4i(<@`!.sj$(gJPQ(qReKO<b#UZb4#Ya"[gY<#`DEMpJs5Y"#IqXYS+n<i9GOK0RV$SLX[fJer$=a?KXK=S89S-0RAkjb\*-M0U#/N+m!Qq&JF/mS81#TKTO9]1rVH>D'<?412hX9oG_CqFS-+JKCki\<GVHClnfF=@Xu^7OejJ>q"[+r,!p$Cik^WGNte)G[]2$P<#KkERT-Gi*A-(+.ItZR6[RjJK[=n0Xbj_]1V7$\Ygl<=Z%h@pLNTp6oeK+h\566o4QT;#Wa9"K2'ubN'7WD<SZN,coOaff@9"l%=M`65XjnV!W"N%uln!I2eDQr2<?%pI*be,1^ON$adbX(9.FZI3FW!UoelS,*diAI3<;67pZnJQ<#j>35PqH%!7`21+djp\LPaQ:C&QVCl^OK@@;Vu/j?lnFQ.,,KX%ufNkBY>6V'd@9*8,3_jUkR9CTk^OVQ[1WO.Qhp'9eZ*"$>V.Zi*IMmGP@iDU2O5-BmbR)dll'J"oL&2IftRH^;u0[),?'0%WgEl8W#VjbnnRj8$#2]%)6QfCKgEW6<Mj5/M!ZoM!JCUN=tBXb]cDM,CkX^Ff?R\/Q3qOoaPsA7-4G!b]mMXqI"jnG?r-aSJHL)n:2KtN"'Q4j'^HP5C3ZZkdTI,$9O9sC=h4$m`g)gntGNq*^uJSn,[hSFofU4;+D/`<@?%LdD:P+)C#?*+BDT?qC&tILN0T;;f=CA/A8,C%k*L?i!^rII&p@>ZDF),q$15hmns~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 437
|
||||
>>
|
||||
stream
|
||||
Gas2E?VeNm'ZJu(.F+]O=F0joNu0.((klMsj9r7SVQf#;p$5g?5`fYte!Kcuqn"uo*fAZmYm.?EoLB#s87"Q=.3uC48YYs8#pZP"@+'$4$a<`N(TK#9TFE]^L^B)Gk,a/T`12`+7*o(0(6up50-W)c\9//&OZ(LTkp8,PK[u#pA)cDB*M;*G`Q[]4!bSoo=a36ETan5'0i$G;,\pZ?h*4Xg/i-ifQ=[VD6W<g"ZGD_9(5'`ZTW[mtnL94le$$p=g)&![P#ipBD6G16,rJVagJe,V?O(t;5P"Zo=)XYuFIqU5*9,%Q(-_DMe4RQIjsl#rY'GBM%FrJ8gK;B;PeB9YF/Sd\V'Bph[70JD`87fEq;R744Wuq"Pf,D@URtTNp)i.5l9-PaYHk6-?Qf<g;npI^hb\2E*KH6WkBZHCQ(Bk*[>!f`+]t3o~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1620
|
||||
>>
|
||||
stream
|
||||
GauHJgMYe)&:N/3%/dJ-D477KOkLu_S2M/c5uT_3'IX_fA1b-g=?#PT`sM\4*E#s#[Y=>sX2R[migcFqS/De=MANV6;$uf/"\13_0Fk-Z^]Y7RH4A2%c@nWqc_^,]\;Iiu#84Q/"6J+U_"3C)n:6(aOshbE-q9F.'QDQcP"@&ZI!Xd++O[/c#p>+N^@K%fGDXX+RMNj5rSXi-kFXNt"iTlB6T<aQK&I'f"Z;saJl(T$%ot,1rd=>:"CC1o>t)@f:Z,Mal=a8:RWAeU-\o`B'j%g"^`WDN-[Cj>XPS`_B37;O[O1ddon[Rd]T)qp=6Mi\C?"MNQffsg?f<og6?";;Vmr]hP0Z3c,uql@OAYl<N]Sl]N,jV*Hq5:GOnNdV,n?8p;P>EmY.@Vb0^82.`9Z#Cc61"0VAM*cK.+NXJHl:cbM"h+"2,gD#Q*]&@JH.J0BiI)#:kp(7kYMQ1H=&:%q/-UO.;bN8S,%_pI?kFGq=;jU,m`sEY]5+!mVN7`-%?CCsJVHnj6Hjc$/UiI,EogKR]%93:U+$`4^m)p[QGEg5mo3+P\07@(j)"AWapP]_27D.th<=WXL<n7<gAYiSKa8Oojs0nCE!P#V"L@*+XmN'K(cCBW+k%:dO2BUA?a01KLi;;C\Jrl=E.*JpBhl"\7:TMZs[r#ssW$$nnDSPLjmZUSBD.%5l-j7pe@V0q.Y&?>?t]fZ_b?R1XDXO_Cmnbh_@IkQm@Hm)Z&_eCFM7?r5otS^Y"T7!Vh`:km`63\7$H49^rKp#=^7#33X5/HiinHW1k!Fe!*f)*NlS7K_$e!polTlZ5G&X_s"A%lYCbH.83nVDH:$3"*"9_.B!"V:.-!is;YENK^T*lYEeMn@1WY'_.90S\7!5Regm.6_l#U-2H5,*RtuX>Vs%)2PR3(j(u>6XS:[c]g%9)IGtF1O%+WH;,cd,K[k2\_16<_i9>/6Z_u"`[Z6*UBlg&u1VMJ7;ja'D.Z?I2RnU"%(4NsslO:#<Oap?b!WE:mYR]W(^oa%L`9l-O/>DfH0+I5\pEA6Jfm0uL0^HD;[OO1]%rLeX#61$"-jS+Mk=nu1_401lm(S&C`rL#>@qOBj,otejFkr:M%=(^6HA)*#Ds_/V\\Q0fmR],$(q([@'A04&q8992J-_h_\"NRjOdC3W26;gJgR4[#VQXB]ebV>Q$S.\u>>GlARrtF$q*l`)X>6?#0kPf;e;#549]I\\RjGi=M!msd5ulMrMkj#@B^7\/PFcdpM:U2Xi0]\XNReDTNQ'\e_MZ(Mo5\>@Wk9c*?'11(VHoNZf,TgQD:>bi-1Z=t@rk6%GUSr1XnTQTMgon2>N1i!IQ+>jWR_GHB6U&ahin!8O%AF,8*%Y5&`2j5N+(Wl'8&F_(=2#5hs.\K[nMf\-'!J-8S[/V#7V_8e0=HlIdY`$!d%a44Pr<[AW(88o#7%hq4Nd.BmC-MWei<@^CPk$gYcSYmF8Gl)uU)bX#1OT(GK@$c8H2:=5^*N(ZK,oLI/XRi#(EUfu3(+#4tu6K75(,:4HSlUTGVqmNdF/-VGE"@@UWc;.t]?$GMs[o.1<na27_1dJ^9,hIC'5Y+6=7:'PZ?Rdk2#r6XiBT<>o7Htk<RrW@dBI=q~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1352
|
||||
>>
|
||||
stream
|
||||
Gb!SkhfIL2&:X@\Z#<.dTN\X;bC-pp9mZ%tVq:XPgcb/$Qj$amK3AK-rq3b+_)S,bg<4$0(DuM[ddFT:^>5&7\FebQDk$j@(rEU\*4UOX"LqtTL2Z!jk^7E<#Xg(h@%MDP8-"Q1qZl64ah\Ki&;c3qE^Q]j#uYc?<2;e+\hN1gPR*I#%L3aRDQ"g7`Vi%*Hg<g1#XPU>o7J0s^OCf2=a1Md2WtEYo-Im'K5Z?VXa6/@g^efMB$bo%qt4a7.]-n==])!aB2rD'X'd2@#_Ub-JME"\E"!Pm_R*EUQrdoSr!:1felTo+oR%uK2nWr+%2sg/=3JO`Z(*arhs"j<CQKQ(mtoPrV*JjYL9>!W2&U$s)0\2P7X6bN!G%5[=``>XV2bhLlUo\F'5B<r1*-Xo45t2A`(iB`2dC??9sm>#c&6s_D0rp`dfS/3_ut%+T/rL6o%c[B2<;2P@`R;u8El`^jCWBN7ORcZR5q[Rm&/P6B<SC]C@qd)D=l6/Z\WTB$))m5>u19rhQrQ>I:9d^q>Rd35H*s>9^LN#K)%MQiSLj>@IP5Z`\?\[Y[!t1a_c.n/L^hFD5Ha$l[lXO7X6$10&FLUU%\f3<J%Td(?7gq0<M5taX>%J(gS8$p&qP^!-.>F>;D"Z[jFJLKjqZ#ObIDh&L$Uu<<q%+XG]km`5D'e8<T4`8O+;'d!Q?@kBDp#5Z?opCiZX6GR?l'`fdWNER:B\:H`#`4($k[eNi;_:17!9SR9U'@"eE%_<cRJ820RMREpqKnq`G+:`qE."FZJ]6)uiB"KC80g&$+[gN)5-ntF6XBZbl:Af8X&<^iH$X@bXpr#c5+4FS*mG>`Ed'I@-JIr=*,krI*3YA%h.2ju5F8lo#Udr8;<W89I;%iJDUOt91IR8ffp`'oN;SUaZB&'^Wr0ZRUrPI)EIK."HRK7?1SV$Y^VQU&4eAjce/BT?2T@A_'R#.[Jn)m#qO[Zmn:"^d-59hcFkT0SSKeO.e2)k(O#ds_AT0!J%;*N0IaOXKCOX0;):JJ5^jM'P-?Ogq?i,FGL\'i#(eW4*eAA3Ac3,2Gr.:j]AA"LCnS'1Z]:m?Y\5/\so9rO>s-#CX/mUc2Xr>)BW=l526=Kl[1Nb4HXq01VV[F!to7QBY+c@=@PRae&@d[e[\:endtufMae9C_#8tqu??Pme'Wb6E2KF&sDmhc*EV[iU=5N*t.Nbkl-p4#mn5(M4-`&?Di+@c"b!gD0L)F1qs"dOl.-NH4*GFgXd4jHu3_u\EM@:o"J,i=6+YrLu=,.]J7L9SCAZB?u8OhZfea"*QfF6qNg![?)X5Kk3!9lH!Fg4&18PIKUt6LMOrX#_&MjP)0#QUZBW*7~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2116
|
||||
>>
|
||||
stream
|
||||
Gb"/'>>s99'Roe[3%s*]V(n8eg[a^Tc9PS@4*6h;d(\un*0IbPQE_H6p=Jbqlo6?Q#"s5F'&qQ+^2q#`F-?lSp`fDfaT)g*Y]iWg:`*L>@4m2k$%*RN2i^Nu)R09IT6l#\_2Wm7#NaU[+UHmXVD/,>74sW<0MYlu@h'4971kBLP]6<8+lGh`(NBX3-UGhk#P3@&U_H6R2[I<GEIOr12sSA"fP61V2K&`9&Yp`'U&*"+8.YqYb[BV;^V:i-OWM[qR:r_\]ImbNWSrHW#kj;aqc/n^[2d#0Gq5.417?at@JCjtDiCb=54\lCq-J.bs$Tnd4rd;GlA$po)r+@H:3-G%&*PefFbUUY<n#.MY&>GHCotYs+"bBY(:"K,LB_Yf[<nbj)S)H[GjcO3m3q+[.&?eh_/)g)FO0FaiIi&HmSSr'iW)#UT/:%="_J8X<,hP:3<XgXo,kXUaQoWIN]5QViD$4\41V-6A"GXna#]K22/BsNBUm+hDs6cC5C.)\]7e+eEZg2(o75RQHf&$DQS`;\]_:h^M]]S'+[2Z=5[nijHh[h,T2op9UPh1`\WlC=oa4LYHo<6k:UkC,qt!JbV!D.uBG[8Hq&D*bm$'Bqe'f*BD)AW,)GD+@3u90"m]"i?&f3;G\87_;LO]iX*#<DA]ZW%rI)1mX=dkC[(Qu-@`[I+;2WqNt"AZ(A%[EWOM&bSCA:Pb%%>b^3<io>_UE*^e$8YhG[)15HAXSROP+kVsbLorP'7$,qBW"d7e`.9`bLKRk5qRT;CY8O^jk.mi4d,cC/-dDj9MKheX&RWT>],2c@i1KB)!N.<gNF_'U@j@#2PApMfjH/mOZ#1_K2Ud^(^R14^DJ*ci"((1-aJt`-RYP1STu9XU],khT1&V+<7P]lr6*4ZK)e[(+'*==[M30Z<?^o?1+CBYRc8m1T,0?N8h/D'f2e:=/*\dfW.Du1<7DGn\=t&-$JVmbk*R%tUt4`DC)0<TW[aK"_kW)=pR`\+Lq>F8R+Jd.F%KZM/;5GFTg,FIdI\'`j?gc[fa4H7*0f]4;Ie@\9p=PB0V_i+$_PH*D#^9"N=6stDGY)-^%tKS[5"XZ=Y4_npW9[Q+m3c0N]iBWGqfqDYG'Cf4Yar]kgh*$eUX_+f__Ui=oMRFYq=&Es$f`t`L\L+]jr>P]i%XA3FcbPj3lM!>hUfL>u1&3CkF=l5)nnf?(+)taF_jl67<\m%/X5pF_,hI7(I`f@P!s\W?nC+GGWjPjjj/AG96A2Lf&dC([Xul<i_@Kq+*i^]-`9DbXj%/Y1)+B!1Xhqq$FieZ>T9k<Y[]NZ"7G#fUN9kLP>&1(:HT"OQ5g:^_,CYbEe=FJ4l67Hr+2==.\93A$lfFXa'<W'O0I:R]SQ.n?@^Y-18dF:,/jH!OZ%ti\`K*CbbYB[qm=iamZ)7ho.i$ae.t/%:^:"q$+FW-A>VWJ,)'gju"M6#G&5)p+oj$Lt<<BMg_'F[M7C=dT%KrF1!pn9/KWqd93.gAgW_4\f.n@=pqd%?P(oJ%n3K&M5V\U>c9`s-9=pV401JLRc1aIaXVZH4A09A[<ma]r)o\ffT"E63o$"+/2dcJ5un>,eo7R'b\S5kR\@[c%5S=P:]16Qbol3!a4*C"c?ss[a>'*ufgj'li('+QZ?b5m>6SA7k=a#g/rQYkN*)O]8AMe;2pSHs3C_&h`MJ`2T>V]sUkLg``*"KI$6-Ms8nCldL4R[e]6C.:pMrq-8P`F62m_YU!>8pa(uk(r!-=@^Hui:>XSM:,o/mDJi&/:^?9>%[oW5b59]4YEaIQBL((WBuIH-+f/-a^pI@a!CIPs!+9kN+j-RL1EQW0p)?eG=>9dT)*Pj0$be@!S<4h;BnHCCilG8V^ZiWdhU<H#cg[]8gC&buDcN(nSek/HQ,Ar@Su'SBO)OCm-`Qm#0h)j"[s=1";H@:WiE_Cbts[)te-9H#IOp+V_KVR!jeJ#'YB/MiAdFTH-2qf2`>pU8ta@4b-/HPDUO"ZJ0p=[=Xu>"Ng#IFW4OnVOco2F/V]!a_)]YY<Y^g?+n2E5to9rkAW=m]'H&:m@?VC;@VdBp-V-*)(6(&P>c]1rj4(S_Hn+am&KboOY)sKJ;;b?eg;R3<'&-Y`D3~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 775
|
||||
>>
|
||||
stream
|
||||
GarnT_/A!]%)(gSGT"jCLaq)`,TjY#710gaML!sQ(KY2h[>pbK::fiY8(hU>VH@Y:U!<?,6p:q8FG]O%QN/*W34fkI18l6Z81(%AJW"::CW8,pBitGZ%F<3+=IH'/KQcLO:e[-W.DToIN0YTG@)eE81;[`W1^&Jee!U35("YnV1SR]8]trl*AJpc!XAGoZ`c(O<,d1`nX,^^c2j$slfPYHERS1XhTaRfbE:WhJO))8j>E!aQpDnW'!ntTnG?Lg%r-Ef5UPf44n-M&+orBD8iB:/$:n=Ie#Q->1R*<*ic$tA.jMTO%`FfTT'CXPaFhS'&5]fXnIA9c`N1#8j)?tV=GE2lZTt]uW)INfSSQhq>&S_VMX/X2\3XVf-7@o+Kk3$J$(1?lfi-@ZV_h'3[pG@JkI=W)7_Pafrd\d%lLZ9VQV6us)^<N(;I+2q?!C'Arceb@6hX$)a]JR!6<bCENP1I`eUHSCJk!eSu7?\hgfYD4fUN-*Cgh^WXQO_J4kj@$QK*cSFGoE+gI=??2`Y43es(7%Mb,sY_``,`/g"Vu[Vr@&tSD4FQTu&e)&dO51\O,$D-IaVn5R_Y<<^KNk2gq1D>ZV.hr./PY5^jV7eGBFPE3f9PM6ZKa4!G-P\>fE]dC4QF/C(@:lG<-W_I*'$U$^IV;<#X8g-<jPpBlOG?3_9TK43<@h5R'/%ZBYG%K/O1RbGY(6A,:%\Eq1??AMPc)]DQ?]"+pf-;G0oi46!X1ZGB"EG[;I:#C*gl<L-KEB@J\qZdT!-V4~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000536 00000 n
|
||||
0000000741 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001737 00000 n
|
||||
0000002030 00000 n
|
||||
0000002121 00000 n
|
||||
0000004258 00000 n
|
||||
0000004786 00000 n
|
||||
0000006498 00000 n
|
||||
0000007942 00000 n
|
||||
0000010150 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<9861c8c80f13d2ef89e49cc9fc09ac33><9861c8c80f13d2ef89e49cc9fc09ac33>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 12 0 R
|
||||
/Root 11 0 R
|
||||
/Size 20
|
||||
>>
|
||||
startxref
|
||||
11016
|
||||
%%EOF
|
||||
175
Les14-RAG-Embeddings/Les15-Lesopdracht.pdf
Normal file
175
Les14-RAG-Embeddings/Les15-Lesopdracht.pdf
Normal file
@@ -0,0 +1,175 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105902+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105902+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesopdracht) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Count 6 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2150
|
||||
>>
|
||||
stream
|
||||
Gb"/'D,8qH&H88.0fl9K7p0=`06SgF@XFqqgnBFfV7i(S:e4h^aX/1Q_R7/X`_"b,_X^UmMXEi7[##h#nP&l)1ZN%3n\a)LGR!Y`$CbrO0FlQ-^c^VmF;MB`c@nW1^kLqRE@#`p%O?/h+&o.5%XpZpOG=5i'grB>4NWDDT`gDIKO=j2"eX?d#"/Sn8.UMeINX7^o3DuJN_=`;%ta<Ir8'W:bmR+i>DHg6rs4?(=RVJ?FZi'Z079/4?u<4D@0_63:/r=,'h&=^9r6jU#*(YZ0]!\,lT2-eQ,">]WNr7gWP&$EVOq;.Y1FTMHe]i.oY`NaY,]ls^e7NO<FhcsUXM&\J.8a>iHnj\"hZcPE#B7]n)AfOq9kf6kKOuBpt_F:OC9@ZS-Ji3?j?btEn_'HG"dNCQVcRPHEQWCLauR5#*#ml)eBd@4g?efS2pOWVMA>3C);dKMIpDoh2h"X\efYUS2n+0KLs9koUR44gm0o4ho:0/4*M:b'lrM"(PGEG9C"YMqb^r[e"_=M%HG(bTb`XA`W!:6)U'QmA!1!8OGH(.0jYW(VE\?CCqdkG!Z`sc/D`n@JAiAe9Xrj]JL48E6H`iQA+"libCS#4"`'sO]b<FP+_TL77,EFb(>_1r6WB@JP[@G>W#T,o;ADYAW7d#"Ou,D*mu7uK(KtT.:5/jEeGB8(""kSI5$kl7[#MM;F!GX^q%R)e9gP,Ws(,>!IVD=EHgO+&Q!F1tS\JDbH>$<7FOG*cU"F0!qgMgS?4Jt^+6CG1q7O;rMhcmko>8J>:%PpPn2)6+=]F!sH133e%[s>af+2ho6beK1iE4h>lbhF5rLBh!L."=^&JBMlMB"^h3(]jC6A7XnVtH=Tc>T*>m[>SZb.`pN:1CkCUTtou\RrY%'`.f1m(@(u6,F&C+RDJs70sH)a+bsp!PEBCSLD]VcpT7L63W2bLpOJ<H!/<A%M"HA[_`mVY$@8qmKU+VVut.QHBMuV)>#Hc$EQP6q\\MU-0l.I1%2#:N>1Ela2bP/d!aBoc5=jbj#Z8H1R0=HHm'XB-,k(_6KFn:P-E-.]qP]5o1.HsAG!^,JXg/L#_g\i?ki(Ko(K/FnQ2Zj-&u<X]?WW8-YpfW7q<JuX.\"mG1t[AmaCCd9:2&K&LI;L#/k0Gr*1bSQ<:fN9n@3A^)tSBkTm/CPc'lC_MW(1T;`5OX6:8.SYU!djg?Lsk>`3RE2')>\LNIdJLgZ8@7:J.P1FRijj;[K*ZbAh+Gmn>kLL46G^qe,PkUJ40![gT?Jkuh+^h26DG;58C!=`LPCl%Oqb]C]"ant_0?1qjl,+H6^2:Dj\hN2M[,,2#Y&22r!u;n3)S!<'*/77WI7OeY265qm_7.uoCLfZ_\2!KcY9U4%!N]iH<b-5>ZDOQ+%$NP9>ZYCbU9jNs>U?Iuc.B-$r3jndibq7?i,R21-qj=W<2F\?bATuH[Y>URm/K"TF#h=O\!#kSC#t-WEGJ=cmHa&B7qVKeJapVAM[_*+Ejc"Hl0XmE%[-;TGNb0GhC&`r75;QP:6@HX5CkVOnn#"(q03.aq`Ac"^-#ZJ>^[1Q./tGm:F]KE3suhTQR+h3$bf34k#Vm:lL0ITY'%atn'Jq"XCh+N)4D7u,!;04k%[O%a0S8sIEiSZNd:KB9(bU,ASWh`4Xu1LE\V0Fq@*Q>/F^@3kr6uP1su;Mdt%nX%l+of*e^g.bmP]Vi%_WIe?5XI+fu:)g=jl^hUpSJeA*kO]5r'/1D\#*cR8+J>OS%^<er7@i?:"t%3+8DD.VA")^pnGUPfD<N`Q2*Z[d+:mp+."4FkhXq,44RY0sW'N@<&S;Cjt$)B-B^@T6G!-qPq*UpIA*E7$4rCA;LYF8`#KfAa:dqNnC/XZ5Q>]R.rnPhini,mcsgCdmTe:<kd+<&/9=/J?^9H\L<]gl.?1o,O+-QbonD./_>!8lU72Z6G=7gJ;Kp-)PrTjo6H*I+0sl:s0*[i_)Turg@0a_g[]P_"?hu0b"dIVMA>/C=ddk?s-t5hZ!4Z]Q9$'n+_t2^L#IR,=9I10Qsc#kOa$!nFc4p0?4<Qqr?;gKC.^:pUdWSR\m/nDJgZRJpAUo*mX):^uNPnUpX-Lq0grAe\&a=a(4ri0gBA]G+Ts5o.DcT377$ODLA<nd1SCRq$6]SFqX~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1226
|
||||
>>
|
||||
stream
|
||||
GasaogQ(#H&:N/3%"`os"rg%EL_lj=OJ31&+D;_0l'$[P<haGR-.+5?g%b"tZu5TG[:1'j8WdZ2ba]Q-6B#8tqTgXAT@`T+!sQhT]`fmAJFt;bFWEV6KLr%/!efm,)[=7Mn:>0feW<<G%5S]?XGBmYKVh.QUOhAaV5T!c_Y/+?JLDBd/V%N'81jjaSeK3_RMI02d_%\,nC)r[+`^tG2#AAHi=7n.25,%V@[a4ueeP98s5U;Dn=/1c2VU;p_AD+^D!tfMR"G&Shn3m^U@gF"?gLiu\jL%EIgbSW@Jr7>3l;b-&8$U,`;a@PU^q]iLV-2+kWdO%;5_4TYWcQ%7ki$hZ2mru8MM;!TeSDaS?IA16dR$911:Kl)lOsS<$1UDXc.pn_ngV4b).ILLt2<Y*qs3m[)T'hMDK^5Raqm%JgW7(F%6%$'8(!&U#noaiBZ_8g[P[fcaL,-T#m3VRCEaiB5cp@(@@l\]#(om_:oJ?bXoc.d=Y3>EefAeAJ1A%LeN&c\u`09[pZsr>IT<eU"D^!4>*9N>52hgn!\J#/VXR.F/"<tGZQdP%&[JL<djg@>0-DCrReu![9t;s&Qbn%!(@A.`NHUCOd9s"MCNJ'n7[l,$Qqd"R+#S2BjK>$a916_H!PW*GmB+HB;):&&f-fZ>^NEneJLXQ!c%f[<"k0[2R[q9H!]"4+mb[59t9!6/tl*2As`A@k4^<&.4X*KZoUHi[qSnJ?I,+kk&pbYNerRO3!!o7(QqS%-4*`:p$>U'8Rf%dc[JaSp)B[c"p&g_E.)*&c_uPaZ+/+"hR<1Hcn87[im_(*`mW;ik#,p%50Y#?$tX*sd+YR:$@c<t9c2HV\o?Zeas&paX5$_cgqhRFArd_S"BMM!B#r`X23=+MGR%rB&:HAn/)PGlEsp<Jh]o5I*iHCl[S.@0Uf^.r2PF6lk1h=pdb$pT@V'Af3qK$s<2n4;pUsp,PtL(i:mm-UZ_u),a8>lOpB^.IX<+IS8Z^$V.u%Oa*$p:NC96lm\fF\^=nNdZPjq1ZhOPHG:5Z&"du\t0Q7hi'`ISO'Sd9]M#Zf-UhK>MpZqi*EhJh2cc-:A>Bo!=(Z`shi&gK/FL(sI>3#lU-o:G_D2b4Vhr9++jGj>f.@t9rQnAC;O^!$lh&_YJB\Sasr;hQYab-uB@GKHHJnBb\*3S3=oSe"?d*eI%5OQc2r1a&.KLHcU,6G8?0E;J_Zh8MG`7/m7[-'r*~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1376
|
||||
>>
|
||||
stream
|
||||
Gb"/&9lo&I&A@[:m._j(&rc8i\'?gu-<&6d9f#drM/luPfO@41MF]g(4oXnUO;()CNgWg@Pr&]U`4k&UqZ+tO."5=r2d6AHT>ScD#)pIt!gk5Gk-*^^4Sb?UY.9Z%Lp0i<%`F4nJaI#HK$m%KmMK&@Lc#g@A].bbJYShlbeVfI`KPrU0q'"2JQSeVeR/(ZU,g9@OsmHgIK)Jq)q&dqpXruDh%.>@#4%2)W'V->PYj,A_rURDJ"OW^eYTIU<E0e'=0qSQWdt?:#+hC3T%.si2gu(JphO59S$ao=A!m8uo-Y^4!+0b<\<H%ds-9cBIDDQ1/@T>HrZf,#b7qkplOEEr"4OnYekf"mTEb;`.-ZRREib@+C02igOmY!YMM)p8WO)aH\,@jLR*g:p;7ehk&oDm0/7S]1RN0Uo$6lodh40kJcB)Id]B^YLplt.fdKi6NPUrO*/.0%H8bUH7N'ZQ.Z:\;+Kl>TBPF"R"Xai:6]gN'AA!CrU\,_8n_CF<K9b."d=@_u]EIb5sHq3suplZ<0*,>3Js3E%J@,e:&MZ!F0?bc?YAR8]oUoDBk+WMt+nQ$9]+ula#3-N_`-%QF7kI&.XM);)6K@*^FdaI'`2oUKN$'j!K/Ts'#9;5-Wlk%dWKo.c]W5()M[\0dm',-0;7,$N$;&F'RO(lP,qaa!gG$b:+BA0udCY<&Y-jp'Ym2CCD'ok9!GgO)G7Wpr<f:-O0^b)q<RWcSeC&&Y7i*oomi@)s3IXHB]Lj0,<P$B"/<Z>DCH%buRc"H_?_b"J^KiE:C9L*u9Opk$i0!;s'j(;:o2glFkdMHlR-UJ`$e%F<^Z@00K*!P@Rl?"@bof=[^UW)b*IR95P5+Z-64?5RG0<II*)-gF3=d)ouQg!b`0.%j+;lH0;k-6n#gQD8iP?m:s(Z"g>'S<EY62E%THR]ep>?HZD:C#BCiArQnr$d<GE&V)ZV*n3/qk.0N>]%Z4bRnc*/Q'$:>OT1d\OI7oXF-OEVIdA`N7%MbD6d/3E4ocVmCR@uRGtaQN;OU9omN.DZ?_Dn\tck9#-OB4'l9<8$J4s<4!$B,B_bA51Hah/FX(GW?H':76a@r]g$5U80ZVTOANYZ-s48JcMUEqQf7$r^!>04ia`dg\\R#?"r&2inLPYWsh$=b*KaT3sAF"aQha!3NFMpm8,,M][GPebi$P/.e%R7?a,_[m)S(2a(b)d1*of1=M5W2\dY=VV.H%m8'5/kHhBIR.*'<H/dQn47fPp-sj_Us[*(hXlXZQ8-P#Qdd(_>+RM6nY!`1Wu.QAbK,0j@/+<R.hjnC<iD;ee83_JW?&%)t/^YiV*Mp6JCH!GKB7L%]R1_4/D6"+j\?.^B%[o2cY=r&'C@EP5bNmb!8r~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1166
|
||||
>>
|
||||
stream
|
||||
Gasao;3RJ-&:WeDm+ddXj/s[O,iMKZ]pnYkRE7:"iq0)>J]"cfC;:&u\]i=spF]jh^3320#JjU@mC0-7-$*I9nb/Pb!4OI\gi;6R<ZBs0-*5t>0.ZM-6_T1!!MF@u*t#g1Da%!8]c7*-.MO$2__`8`5oPd'KE_STC!AN"(,i?Y%]C,k7KOJL.&6e'n"UMI6;%`O$=EWOq3ctDM!E'ABQd_aUD`use:?gWA3A3>X>61ApAU,^n8#;:<O+-44a_FL9m.!j+Z.uerSaWRd`U6#]Wki2^,TX]GLs(q!ioBsX:ot1,`5F`F8U6j,5OPlEEJRt!<"(tVKiU@"RC93CCd;\OfVLj"%*PijrnA"<)g55m#'[7c(cb`#fn&Rk"2\eJe,<'-Oji28(4[l$CPH$e7/87M)VT.&T(i+e6jLXd<]uo_PhA#^gOAXeG-?D3,2quIV'fA)e108)p.I2A`.n,+0uh,'6!'Al>!Is"H,>`_H+Vabu#i9CDUG.\54:LHLc3jEf1+>LVk<"Ul.':gW)`ceBXehrd4GG#\fCr]Nb=k3u"ND-a8Yl(>l6-KP/r%70Zh9ITUNkI[kr['fP)4>'=i9\$8SLF%CIsrq@;uc\m3t^M)f3dsP@oL1l5jfF,]!I>I@Pb.`Dnc'+f*i3<V*-8C7<KsH\S+5=kVofZ)Y;s0!1M]=pns0RYqV0uI_8,H-;b!jqOF?)6<"p=.L()A#Ao#eeG.'kk;Mr?P3hr</5(@m!h0q(P_'+&Z\0F3Y##>OrY!.,R<=r5Gd)XA-cN%;E5a!kCBgKm)H^99I7>7u$q^VT(iGNPbRj6tBsj5*2>\?Xht'-$r")$GP;7\=>_"Kp3K?^;A%7^J,pS7Z?@gfiF=CpaCIeg-1<G`mBtnF3^f\1eb7Fj@!%F[_f)F[rcmk8XQCRbMq'Y*!FA?Ic;.`cQu:MIp/DBkS8)=+S#S=6S+V2!UoqeoqW_/O`:r,*Gt\bq!!rQBstf^?Z#ecIi[ANeP>$5*,nZPbk/HBauSkn+BHfbpGJ?Kd`cP@_`)EMRlLJZBJB3B6FbJlt*7`@mc6J?k)d8Ss96#oWJQH)5-Q.)1\:tEF3nZIFPtB^!GaX$9Gtk^CnT_V0-pKdgf:s8157g<(0R$4-k*DQ[*N@XkWdbl1\\?rd8n-KD,mU86)5~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2012
|
||||
>>
|
||||
stream
|
||||
Gb"/'>Ar7S'RnZ;3,f8TBk*U-/u1V:gNj00T]ftWg&^fl1,9LnOX3GZdnAj'^8&<1UfKNahFh?'P)dWPgck9KRK:A</,jlgJB(S*+#%(BVT6J'@_q[_4I[8P#2p%.Q4:cse9!\^f?F7;3AEk)*TFTaAK[rp;HR<u7\k,PE1A9Y'9Hle^2G\J_P';"5U<ZVl!0]r&r1O-D%YNBgoL#sqdaT99:X-c1pd*((li!h;[oa@&^"ou_fQ4&=0LMpJEP=::;'(1"n6l)BQ'$LN,o$7^)6g;VLFuu3BYh.3f)dl%^S<cT#1,KY,d1nEJnef&tAYLFc,;`?D;\9W!<*X3q5Q@FpI=b`jH"lG\*tLP*dR2R#e@82bXX?1bHag?tjSLPdcW'0Xr6i>-]C5Z5R9d-61m-f_$*-BOlA*,\&i6,B@doCI<_kZ>rWK5V;KP_"p5K(qsSK9A0Bn7Z1p+eqsGL)C)<)N>ina4_%n7]ZNHMqSrh$IgQ6=6rk#dUlA6g-0$=+`&4ACMmPL8]F\+uq<GA?mhGOLiRdTO:2K&:G;o"%1DPOJYq/$?*R<]dhk8P[FKfSF#_@QLl+e\Kiu2[8,qTHbi1_I/<4k%=+j0K0a9'8B1nWEXRRQKQC/NBDaEieXZqK(mORB&"kfP/39Ci<oJodEIK,P+9#su$13?s3AS_(4PUTL\h'XIC_[^K6.>Ar6M)bFIi>NCb"`!Md?S8k?,^8*b@=7USE.ZVP!p;\XG)^j#_dj.S)h@(;*`ehf0!<"p<Opk=d-1p,?'mc8iL%+6tiH-tOo_AW=e)r6PB!XY1lp7J4=Oa_c=9'u]dgRk&R%7`ii?>-qb0P%!4=@%@'[DH>]q;`c15YNj!c<JQjcQ.\rU?UK/0qDd4WDu;qN6J!Cf_D7DAhP<"j*r-2-637J@AX7B5,DkF;:i$Q"lK#hJ>/RWm"[p-q=a?qr5Y4NcYkUr#h&*o&W7!m%j<$+"HJf'kS,`pNquR,a;iI'C5YMi-shrV_9q:2cr@)@tXe;rAhcWP^\gmMD<FE%ghmt;3+mq'B[qa"?ug1pX1:#Zq<HorbFD"o>a0bR&rSj#BNUEC/o]pCpli/EA,52+:N]hUJZ:mk`#da,c5G;hRYQ_N_:or8q)IC75.o"SENcs.aq<1c\=TAYQ`/AZ6gX9BWcso7'R5eO05"kfuT%?90`'rN?/j;2K;D6)mD;YQ8,].j(#4.lr+TEF$p5nYFi[c$L:Ydb.*cKlS</B&uC\OG;&:D[f4pSh8Bq7E\;:T6&s]DhH*\j&tQ.f&9jm+`lRF[s0r1HVOqrc.H7uN,urB:C%eR+YITsADkY'hR?Qo+hm:eG8*lu=EQZ".ML5Um6p;p,XAmP:$''?_[06O0HQRu#\INbn]asP.Qma;NhM4*d3N03UOlX2Oo?d+nNJ+`J[+2SEhIiUHh8*We8HO)4H8^`"GR8G>d5.Utghp9"3qi(3W(l+u$"3Bqd<!,JbMpWg*(6A*(/^Mp9M0$HhYH,8ItlYCp9b6Oc-ZP#Nr;4-r.U*i.%'>])#6<%r(sS+'$lUDgYk&Jg)V"nK$-I1cV2-MleC$pgXW-)`WBB#hFLm#32>XjGZ&\pUZn+I:\)4[n3*%<YAmlC#/-+"9%%A_pFZ\n"iPp"hYS5*qPPLb$Plpjo/J,^c\(o(?L2PM!r*^0chhl,k$iSGE./LrSJEIG*uC\bI]C;(jM(GtLs\-Tk6<MEf1PYDKi6e*0NQMdJB*RWaqhj9/B#ot2AM7:R)P,'_\j+s_o/^?Qo^^FU,olYr-ea<D1]eU(Y[',"L+ZM@@A8Y'qS%jEF+qAWtX&C[7^qVmTaRNI@#jlGFGKU*P_c%rp&u9Pl(=[kT1Y3"8N^V9t<?0CL,Nm9:4;X:-`4.i6B^Q4</_#RU8G`*eF`!_jP5*8r'tG=srnnIWE"]5?hFNPu\PN&qq\q^_=g-?C!W:%jq\3=[RVWdbBr,GNe)%Eq)`CbHJXhaGuKW\n_qUl6t$KD!p?K]k5(7iWI1bAb'j?~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1371
|
||||
>>
|
||||
stream
|
||||
Gat=*D/\/e&H8h>EF6:()9e6X.?PIO,"%MO?-kF5RfPm,6'3!gNSZ/,C%Le;Uh!VOS/@`&,a,`cmZ)\i:^eb$RCgI!^`P@WGK,/QR,%iX7)8-j:1rF-O77[T63?ZloecuKIJ\SAmM-iH&nnEc-lN^5$4r(,V6g;aIUU;aW"%N(Gmm4\cqk#kMTJ*echr2(E/pUqO[5YLrVQMW9i$tGcOSFjIL(ZDX=n<Ro`_$_L(\)Y*.5K53JW[WgbePq/2R(Teg=_1E6EM4a6/ZHVFd9.,]I369ib<*.G@.VI=p48NA\Qh>S+&,\;>]D)GD7B<`Vd4%FAIVH!3TIN3KI(?<:M8mun3[,/-i?>`Vr6h=GW,PV3(M5jiV`]&k]4Gs!@:$8ia^Wj`UoK`N9VaH]Kd%:e$uMD5Jr$u*)t&dE0EFZ)SgX+;*$>dZAKiD[168-^7u7#=(<c6C0OOC[fBPVK1#p(j7[Q2PmLs6e,%`t:9RWi*pOfbc#?1%#-2>[L(aQ;*FkKs5H`\*kR>^u9(GTAp;S,h'ij%K%/TZH'8P+%deX@\BaE]?F[["[3@UQ%9eIY\+nQ]5Q8IDo0JtmJ?'e2Os3@+kl?S/9^@9Hg+okE6/%aY#'Ibq<Ko?Q@ugqfs)Yj+Pn]eoI6utZHRO]B4MrID$gO>b:5#>%>su&SiYn3=9eVeW3fJ+o<U;CPElBbI5GqJeo)]\;'h+*"nNDHAm[S^c?*2XG>Vl.,\js2jEERhJN(#MM?m@m)_V31*);[ZI6Mr1jA?jLp*A#^S`Y,EMg:_X$j/dsbO*./gHur;Qj,OS%8(QWQ_D<:<r^*rAb".F\!cF\VH4RQ^F\Bqeki0>Kd\G5-BFmV(4OAhW*0$&9cI*j^Q](C)2iTb0a1M-AIW2SC!i;o?OFL,AVK5HAX:>3R/O\@@[XZGo2qGBGq#h<c<j>lTSdL-/BEd<mg7Z-:#i1>=O5`S1lpd2@Mt9_*kmD@;'ku+_phdOQI-o6XC3o:[CYo8s5-Z-Pi=k6#fflrGNZ4t8V-1<*g,r%:l'M4kA3)0.E^,imNY@s::00-9%7_fUNA3bm_-.eGSEJ[WJ[0*DB[<Rb.\(1Mi2_i\2L>mnX,ZW!kRnsiK)JZ6G2aEBq10/)Tre0&BWPffT+>qDcr-W9tGA=(+I7m`5f5DkcWFs)8cOT3eG4qK6$sm(8@d/']2KdBSf4[d@FP:MC-A9M4IDh[\[P.dFRoHV]1Z-\K(,Cs)*0;pX81LO]d4$Nsn#C1Q/"(4kq/k9as+'Gd0p]9-m(LRt^&[qB%b$9!-Q(3)/SG'eD(hKs0Za\N^!HC$hB)XtfLSBY-!^-P."ip='o\J#C<#+Yc"QfZ,5X@Bskg=seBV"7KTj3._b~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001737 00000 n
|
||||
0000002033 00000 n
|
||||
0000002124 00000 n
|
||||
0000004366 00000 n
|
||||
0000005684 00000 n
|
||||
0000007152 00000 n
|
||||
0000008410 00000 n
|
||||
0000010514 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<f6f6cd7ee638d86a396799cfe445d582><f6f6cd7ee638d86a396799cfe445d582>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 12 0 R
|
||||
/Root 11 0 R
|
||||
/Size 20
|
||||
>>
|
||||
startxref
|
||||
11977
|
||||
%%EOF
|
||||
309
Les14-RAG-Embeddings/Les15-Lesstof.pdf
Normal file
309
Les14-RAG-Embeddings/Les15-Lesstof.pdf
Normal file
@@ -0,0 +1,309 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 7 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 20 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521105902+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521105902+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesstof) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Count 13 /Kids [ 4 0 R 5 0 R 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
|
||||
15 0 R 16 0 R 17 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1865
|
||||
>>
|
||||
stream
|
||||
Gatm<a`?,q&A@ZcqN?`".3Vg'J@S?6R4PMNZ>/BW0/NjUZmh1#@`S<@Bn37[Lo*&8?'50gh>MllpR5\ZnX#ZqE!>`C%^4_F?l\<q^hdJtr"S#4X]p^&Q[?.C6qAeT%`B1[#P+^VJE%9ok<8u>4X*$.CrqKt^''Jn4HFcL/%Bt@0d*&\4_Y"\b]0Jk[9jK3F*)74a*%@*rCqZ;LZ]>[M%7G)s3MUc?k''[Wiue@Np"3*"\53"r&fOkku&8T<LUqsDjdZqWe>H=4ZGWn*>_.DP\L7!W&k`lfh*RP$^b;=r2'*EY]&9d3#LM`rDQ^(pjd9OR9nHV^BVk0$%NKOI)@aW%Cs+Xe.mpe^*E,"7M"9pESnpE4f:JT$8E$<_PsTN)4\Dk"n0JaKN:$>9G$JK9^hB3>Tu;m6YDB5^L#GNlXiJ!n9&P)cFOnI>GJm<d%V@HN3CM&k"X02%U)pL?8H"fEe2k'VM['!,OmT#LRef0.6KW/c94K?F_$Q(qou.d=Z<+\GCpBP2a_q-[!25YZ"mhZE>Xn@#A+Y;0HY),/M[3.=2M$4kekriJr.I9C)LP9<]K[R(Z8_XP^<<_K2(KW5Q1Y4_7S6O=tHCY=fb\Vi<j2,6B'XmM9hTM"O/$\5TH"<JKQE1dkkFia#&F,!:(-_Jcr7*l5ip3`;)h?<k892nn@9\02GLWQHjG87fj1h(Yi3DI,1A,7S4*shXOm`T0g-n!S2bd/;#CL@HjP)N#9u*qAJ)$?42&rQ&cf[cXWtl[Vkt^RTao*nJLs$nKj)nUbD^e-rW9F5hSHD,>a'K$d,A,-ltl5eMu;A%``O;,bV/X\9o\KF`[Ok"9k_#AX1F,dWgTPo/N&8/iKr$ne[9,@M+pF(l\Di+9[r\'Z=b'SP8\G-Q#Sfj)(%tBoYT9`tt`m(+P6kLFpR(2?N*``Bb[-&kRA$>l"WVpTt=:H^sOHE:Vl4[tGLKY$WtD.L5nH1Kt0`2gB+$]27-F5T*hF$>D[T+cN0i2bmNa;4cQ<$]TJCNf`R1aG&@Zd:tmRfOMk;TnhU]]c#tZhLQ`$\6m"kNEQ4JG5J3m(i%40'sq+B^JY_`#Zl!=&eaLJVSEroi_s2^4?F5c#=XXIFo0Y/T8BG)Ff%[:l:pD^bRW2Le63J>B%j^TGtc'n?/+1hkmUMd,#rV-:>2gUn*(H"FFNoQ*1\n3K/[P58(SX$gO2nM_KXPq/"d#'o?RfP6A:@b.'aspcf%3UlDA-<D7?tI2D(Xk5nU5n367lc!H:o1N@&UCR87CoZCiV73;]g]gp8D-Yr7\U^K$+Wq\gZ[b[419.tn^Z>H.P:$oU"Vb<*?nXK!BoUH=a9+&('(<>M6j.p/b`5(<KN<a4=;q5R&)&FQVJF)2#G;/Q6p>WAJWPRLAn=/^4['<0kn#YqbsR9jF`S7+luBnV]S-ms8\GBHlWeZY-@(YeFlV:HXAJ61/cckeIXl-hK6%0(0\4&_ud+q.9/jp^]Yr$mO70J]('<akXNDhYY\SR[=)M20@U:6#pFG7-m4a4Bh8m0M?,2"@"S:Qhcc>3oeVrcu^G0#\Kn-=e)GSegL]Kn>GX1"s![kSgsWP!FE5$d8)Tksqj`JJG)d1.>V"G'>%LJFXhup]Ot,=&/m@L0-9n%#O*70hC^u+GhP/h#/AoXJr'E\M6);6/^Boi7RqC)%uBT&q1B;c4hOO59ud,%(Lcqd:$_)SV!e_[;E)^q%h.MY`Dt)K&,-;!q7o1i(_"r-TN$_lr>.2/PHsj=*>F$O:r2maUR-$L[kR9LZ&Jf!i#YXTl!I3M[.G1LO9!WXFk1<SKbN>8lFIsFe'.3*!(-u4B%](rSW(Jdm\t-YF;<?q;1FmZ_cBQKt>k<lTaT=;%3~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 401
|
||||
>>
|
||||
stream
|
||||
Gas2Eb>,r/&4Q?hMRtF)R+`4gZ>9[ZPq&G)@)\uMmK9j',SAppo4XCe6#8OhB?m$hPn7BGp<8Q4/B)-P+W9MK;NX;W`-iPP7Dsq_d\,6L3+5Vr2\4&JPX?BF#q$?\=E7G.3D%LB%NOc8Pnp4:AfOC(28VBXE`*ihY3KLPbb;EgD+sN4^@u].?:(Nu$_#l]YjdA5LH8@D77(aiOWH6'I!T)VU"K2kgD.[.06QNc?&^dTMs95E>#kI"p@#N_hu6H"*dqAP+T4PdDL)ce8hoS'GKF4g.d/+iWk-i)f]VpNT<H'5c78b@.o@5nEa#E'7CX-#FZ36gMJ,rIF!df2LUaD[-_u=&J8.b*F$:c%Qq*>(6?&m%6pK\A'>",I>$dq9AWc.>^V[2.L$/c)WfMf~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1247
|
||||
>>
|
||||
stream
|
||||
GauHJ9lo&I&A@[:lm4Hf7mR-:pgA%.1oW?r8Xg3R?@7;P8>8P($40k'r;&%^&(-].A9t1#Mc^Pfn?@/L!!A*#(4Pj<^i(+'H,kGTaO\->P!J^^hIb0%LiCI9,(RV#`[M.U`pFktS&g/B7&E(XTW',=-'2e0Zq`JtoM^VrW%G)h+pLG,WcS;11Ej0=n;iFe*2cPfO>;7Pht,;K]p-U0*Ra;g#_jR+^Sh`%:'bDt$-;foqs`e3"l*)sgZ7+h.cHSpD8,[^caeY^`Tu9U.2m,+n@O+UATcX0kC8'L!OA2<XpKQ?E$CD#;uHBu62H+m``MI9p+Na$S:Pp:o+>In!(l#\<E,/AV@*(W])dt%%#g2[R7Pt#>`c*Kp6?4kK[N7aqRGr`-%m_hV`eK+P=K+)2OJ*XKc7R$*1%!LZ")@E)$p]l3>*=cA]4'i427R9&05E*,C3s!U+qhtS]>;8<h;h9-toXZFBee-?)sM=?:f5lO3*\B9"8Iq#)9Xm(-67LS*UMk?'q6K-hoITVhAF:[u+QBlLkP`Sb6s#X(?#4*`g:uhm\)#FXX<1s%!`"a\`XWVDI1\!,!HD6djDK(UE>?o`f$;$TM:Zr6a5=%XV^NcMk%mS&#Df'Fk1sYccnKH`2t&^2LfW_5bY=D'E'eNAnij=J/MQ$TmZZd4C:3c!aZlkOZjlg>k584tLT+MdIKGO!r78"CQ$#C0B4q0HSUqAcp5n3U:i9ZR8PcnoA,k^)!*sLN1Ei$K+UFATf.'e[7*i_&.AM4'WN%$62uPZqe&!qq%m=`]>?tPS4M0#MfeN(`uE9i/^m$FtV[oq9'MEpT.Tbm%,YKPpBt6@$k`i>h;?_?OGJW/aXqgfN[,*@r:%U;F8'4,DQ)!<Ab%AR*E8OACj-3<K\2Us"P)b.1V_Che]iXd^7-c%5n%s5r]jNARTf30HBB;&L#j20GdtY?;h]4M%bTV>$=$i^`0@+GX1C4o!16^k:(`E+i2kE%Uf\j)L,SG\2C.=F"GY7JGa4/Wc]5^'[Q1-$mPHa*SWUNihPiPYt50:g*5YR&GV^f"1hhXGil5aIr<%jYH1r6XG*H,eJ&PgPiI08`FgQSE,D8!a2%4&)Vm"(V_\j-R_9sK]Pfp_!UFiH9p@>I.lqPOee0lAQ+hr%.F&PtSh$hspka1^N%&1H0?S<pq1jY:'n_@PHa(bKk`1@P#<ONfeR8q,CY,8E@:@(>%@FGP[RAlRINE=g:T2=!^T=u:$6f6^3:^NM~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1795
|
||||
>>
|
||||
stream
|
||||
Gau`SgN)%,&:N_CltH6gA1Rud5nH_fQ_\e&[n5:1Wq-ebJYlhC2D5D/4&%Nq[s&.?+Jm/F44@p*$`2tElriiE@`<lW%l=6L*8np?k"cjK%Z>C9Jd>I@LSKRthBQ:r:]s%27K&#EM>[&Ne]KBbO@2Q#@\@C*<%(![7[/cEE*O36/;-X5q*U2D#pg%G!2<R<Q2G+):F,*?>*)"lBR>0J:UTg=,!0s=e,CO.HE]"NL[#76=V(eAaC4r9Q]kqSZ!?NS\L>W0IW$^,>VpEBGmKIlB92@>XW+s,6[b`9Eh2NjGlWaq=?LD0NUVY[,X?[Fps4)BoDpUf;k@4G?fP-i`#qN/YAd9a+NZ^>+]ZJTBo(*pc40Mqg_f,\.4!sq%u>Z"Cmi=26E*d+)crSs+=rb)C^GE[`,dha_\?T4PVYbl(Bra$I:dmIJ_lJ2_>\lF0tb\aS(%I9(FAV8^`pZ,Nb#WOV(p+TnLcr!WV4@rqYAT@4%?lGW(nD/8&ct9MK'^Tiqb-$k)o2]F!8bFoCd<[0+E@upm:cX,$9N0h8"LR2iuU`FTFNfe&u<+XM3Ge"W:18L%jUE\2c,j1UUqsQ*u@-Nr?(\0Kg>0l=6<>-3LS^HD?\$X?(,-1UW+"X"+)S2fSkJZ+u]&>j&Z#C1sd9)PLaOn38/%crY>l\k7k_Y^"OYl?YlWdbC<8\&E4&c7!QLFpE6!U,QQ%G!`^pf%^pIBKM-YAY)`*>r\3VqiQO=)NHDLpIRJ[$W)/.;/W%QRj+*W$[V:blC8grB0iKT6`]rXWR5QDJC,N@.sX#55R4IJqB;?em7te#&/dpb?&F\6`#6V,nO_qcD?W$\e8U,9r%i2_>f0H,*]Fbr?F+S//`t7f?q,tY%KM(1!>%TSm;@T*%$#[ISu@LnC2dHf2pB^8$$2dZB]=/\M4qKt_T#t17W=t:rRHQ\.<T\IB93nu;D30X`PljB%0R4?(7[/oald$LfW@jtS2UCenX\lYV6fgbl7BqRlV'868f(T^hdD.sn(;q0n1qtBq,!C-E`V&(8ZZb<DV(pVg+M'*'O)-k;*oIc\ZK+,4k8b3HIOm>=^E_'QKdD[q!JD'iHR`Y*%!(%Ei@S-(N+E[7,PS/I$Y4tg6i$6ft)?bK\D@-`LSX[&Rb='m5P(kN\Eql0A<P]/S-7HM.dImY(p`*gQA6<"V5kHpd83A15bQT+Lo[Fn+e:_$E^@k1GtbP$ES"\4#7.Y!B9k9B''ScR6RbD389ct\$eFUp>j_KIRUM!cIn+=H$.u!qE2L;DV9]OamE]3`E7>2R!3?9`8u?5GFlmGq>6S(hG:A#;+ttm>ZYq9n[8iMj'7goRb/$\D)-I:gI7f8H\bn@B3!`/4lge;f(F<6(!#!T?)9KLSD+DN$3)W_PpjV"/7ji?]l3%[KuFM94nD2E+8THkLU+cm)8%ke@V;_SinnNO`s%LqnG;Y;r>h%k3*8eC4oY1qK_Z4ON0,*@,>@#\.B.@J#SXppEX\J7*Cnli2i]j**PgkMlTZF\(rJ!=+gj'uo6"1nnB$]l#PR^4/j@-b33UIE-?2*BqX1nEd=<ngLeY[*Xc#/]0]0q8XBo5XXa)"!jC=dD16Atli=kbad8bHi(,uen@hFcK)ht^7m*8:G/t>Za]=[CioYDSH1*T%mak]Z@#YE3r),g&d*,V4n3u3X&83aK1iMY()LX2>Wr#s'?dF_4aVTB=GrA_F?NrTj0DZCIG]U/G=!kmb*M9JaPDIoU1fEA,$\nEIOlL_pka2Ch=ol`[(b-mM6+2Q]'4?7r6g#Ut4@]qo`rW`-Jl](~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1514
|
||||
>>
|
||||
stream
|
||||
GauHK>>s9G'RnZ;3"LFV1A,6tg0IF:cB*@M*HFP>\BOV]7h-';_/qs/TN(Udk@:'YS[Bmn:9k<"$.AjGn*WrG?nn9tCVNYsM\M]6&e86DD!dDG_E&?`O//eDHin2LtTP^edsS-3(cPfGokL2(2)"DJ]-`\YD-:MZgNZZ,Ni@6h2og.3?@\]Uj&I6eS1\@!Tb;Vn:"A"P3po8%._6Sn$eJtW<_89Ljt'$=JH_BO`f0B\s&i+'@(H#T7M&c>[?2Rk?sRWAoC-hDsQ&+dXjiQsd@./ae5Uj*'$4'9-JpDVs_abbr<4iKXn3LUJ'ac#egLAfY0Ko$?.!9XE3g&`_[(ojPY6;WYSZ.%Zl5fX]bPuJ;Vga2W1[h^u/'2#)=XDu][8]%jo_\KYG$(PlBDjXt7K^+jcPk//DYQ_(=3Wn)i!"Ec5,DY6eXT+H,5d]8lE+j/YKFaW9NH.O!^cZRRSLUMZ=+0^ZNm$(SJl`Q5KZE#l(a[rk/q>+J0WqC89FT4#YhI?kf)`Ucn3+Xtn]Q_r*ldR\r(f4!dL>\#%O41GMn^UuTF4l@GS^U3;NHF-qK/3Jfk"@IPu:$A0bNE1)2JtcY)ST$HZ?UO.FL89<Xp'gQbnEZ4EP1n/.BPIN1m:&=#RA!q\/eHNCU"akQI*fg2%m*/'6?t#2Z^qY-'HBUj8?[nY7&0Yu1Ccg3Y7!H#=:?W(oR4le.doOVYa8?gT#`TH)Z)IiT9nOCIhrp=f^g9F=Y?,P[TV?*8edaj/+"iG'CNK:fMC2VDSAX-VBdZ5GE$fd+gO572IRoQ#hU.NOLp8*_9)Qe%e]VR*Fk5.J]B5?U=?5dh0__X9.]f)!ibbL5W"/h7V&BDgOnOrM:BAe`9d,c[_P0ZW,B-\o5$f]Gl_7lNV#E.utk0j_Cr,DR/KZ\Z!2Qsuo6K:5e?XR2C-8)MXY:JU(^]PpDO#%h\+qaMe'qT)J#XYgcXf9k*]0Y:q#:E#)_hdopW]=kWRCR?.Y95ORP<\S$:<nKR!=l!BN2a8]uAY\b_hSNo0fR'*@q4o1$V<[0p[X10$/L@:2SfpO&hbbrOiuZ!]-+cYXA2a":1)"kN(Zshh[sL9-]=Z>DN)TcRdJp]"3INpMm9\+LrYA]*>=;t:YoZArKXSZc)d+`I`@eSP8G59>njG[>afb6*/AY63s/cEr?r(%XNI-h;\$8be86=>?4/4e8@7TRb[G.a]*N(4#lhN5XUt0[(l..%K)HnWa93qQpKM&8"/#JR<SA%XAjf^&s<`#n<?A7;i[,=D16Ce;9$:=Crjl!;$)15/*0\YtqcL.Rm5^Q]SrLQ`54$(!3Ag!jYUUs#Yc?M>g&Q]<Ug1@accf+9'+!JiOW12p)/;JtqibWWW=^8e*oNg3DVrj0Y>.R*.?-hk=8#Jj!('pDf=B'[/;KKd`nRoI=L<6ILp,ef*^sDaMLFp4O6jq<+%?<*fcQK`'-hmFS/WN"JIfrhVkEgroQgjtbfX,0J7XM7dTUq[E/AV5C8RR2B&,)BT_Z~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1839
|
||||
>>
|
||||
stream
|
||||
Gb!#[D/\/e&H8h>E@Y5.l"66W.#H*n!H-A0"gY!YDS$D`<^gV:-$jPV2/Q9n^O8UEU?";=1DK%b)jYa.4ZnnRF$)1MpgrSPc2\n[iPAFsN5P8jQllI-L[\j>S3)E$4:iPF=&M!<5XJP%TNjH:GWtXb*\rg6+>OKN,U63A&if^k!=PQ"E,siE5Q[P^;?2j'o;04\mh276**Np"pVX'2D1#6:(D=KB%`<+3#_kKE4"6*Q$::-sGVIBMo]#)jF83FNW%)*h71*u!;T6ftAt0)[Gs7g!<Dq@^%L;8cV8Xo_H+aI3;jcFn@h%p]i5EP&f's8NL#E,?Wk(0(?scB],`f__i4Qoam=9T@"r^<0>aGZ)(L%^2EH,E_.t$;Ck&?A<\(6j+4<Tjq3O!MVp(E#^4s*t@Y#"Cd)r<9jR"hoD&Gq/&=DX<]I;AnS+iXMoqp]'LPk>Qd"ep1)fV*^-.5l"(Xg=S<@-4`#F:>)>\(UT_:Gm>)H$pl@H476@;8OX>/hk4dV'jNRM8?n;eh$FClN#g$p`\UcpmMdG7,G**^RdQ?<O,M:P,hk&n3^q>E'Arp2:5gU8CE<Yl8UJTk#X0C2#X57*N@`t1RO$PoGo7R>^_>fa]//56Q*^VUf<pSBsO,nJ]]oVcTeH#b>#,i<Ph0B7HdCq-@#J'nZJMDp'&2dqn`9#h&SXcT@)504c)h4Z\qj#UUX*eNpO/NR8Fd_0>a3,bT%2t9e*KXe.ILiNkNQs4.f>8K:t_WI:B,C'p*tAShO!3/L2JM9^Z`4"4hY$&QfE-oaNLW5g$oQg4Re%/?LY[c[kep[f65AqV,dCk!EaiJ6jTX4Yl&bAj#(b5pLoH#>a:Zb_s!B5*L.RY"Jh_o>V)Er#97nZ;U0J`645;))CTK7k)s,RE@&,X>BS'hKE"]9<=2:*qi0KN#>ruB<"8Mmb_<PqjY/FeUHQT=04[<(noB>o-@[.P/D$STjmAq9C8=k/LA/5$Er"q\lB*SP=.t;5EJ2Cl3!Z)6Rh6"lZ1<D6+Un\)hGD1.G0B?K!irHN:pc[?n$=$nV$3`6_LngQRoV0DQ'0.B8n8GiDQL"Es5]$="-OO"X^_YZqCaq$@?#WGZ[4rA4[])8c8fB'p\;e5HB^mpoL1b%cqS#m$Ua!XfI:3/om_O.*CN<n;i'u3:q_LY<Naj3No,Y_/Y->)I+Ce$[dTVqB@WSQLa4a]jlaB):[&RWj&pjCP^GMV+X\Xb)g7b__VWK2NTkhWuJsff(Jne;JJIEVO_N2=54TTM!t*%GTW\6?IBT$De\B$2,]AL,dsk@9S_c&RQ+eZ_Ya4l482]<hj=cXBdbF]`OM-Rns>$CXu5nqr%U*u!h5Wile($anVl5s!*`#$D;?a@)O>SI$Do6&<%0a@ZhasB9X"N+AVIWtf'$B[e/Oh`Q\h_$8sHt:P'Wla6'3]50$M-V!g>I[.L3\5387CUH"tl,e<VckhSdSc8roaG6ikef\i;9@Y4@T],<X%2lIQ*&DcSs'DG%IB0*_@O^/Y-Be^CHKZ;h@Ab#W4Uh]hN1c:Nh>q![f<_m]n("<6KOL+`bJo3@dZ)87no:,W/hd<XV-Cu2W:g4Re%X<3u,)>/e7]lY111f`3LHO(]7W@jfWMDf0s/[*FLV<X3_fo[mm$oX?p5SHi"F6a,?c$2>"0s\Om"YS=,k"uR[Q"b8b[K@TFa41E^L;188essSg>R0fokf*29T@<c+^hX28h62eP8J\F?UY.'8(QjKf5toKrnf$C;:5^BiNUG#&3W@%YEH?26[eKNucdH#=NUL1L-I;DEmT4f)WTp;#GDn4<s)*%^9jUNN+!&$Yf`1I,RVot;#CkV5"9~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1884
|
||||
>>
|
||||
stream
|
||||
Gb!#[gMYb*&:N/3bX2^s7E^i.78gbR"hL%p"kmk+osQLX!a2C;26u:6Eglq)%Om6tF&Vq@=N@Ye&U?>U*mU0-T_QR_Dn6&GD%84*1LM3bCLD`"R(n8<kJ9SP\E$7M5XJ2_Qii14qgiJq'G*h[@idGQ<"Z58#gJ1VYm-6[P[XtL"7)ic"/7&@=ocr$P$6_2*tl[U/hKU>AUJQJkF]3N`C+n+TptEc_/KPC2W[e'5q%Xj"S3YX55b*8/\EMkVG=U3RDV6`A^YDt$dKL"HMdV??"\:i<q(mT1jQXhRA)]qq)=[*[;1ckL#=4CpQ"e$R2XldI+1m$DIGaVS#f.GNGq'i9R@o(4%C%9,RgZhZZn1^6&:*>To_UX0&R=US)0G9>S0S^*ne'k7L?qrjC:j4O%BFl_ZnSK=X\3GEEjK7`su66`]MNd;KT-]k5iK0Cj8N7&G<*"!?JhoZ/_DA-MqY=G+Jom^i@Nr[7C8V[H3DnY9^]Sa6:S]-!W5I!^ka>8(ntd1ccGig+_E3Nprm_NsWR\D]-B#jR!QYIWK5OVQ:mji:CBL/<ed];*i4O;rM2u"NJH513YV3AYms"oVVRf)jl%)lXj'=&^=<cME36q!cNk%ItC*+1$?AhBGHWk=aCQLg16PI1'K]/nP1_^]W[iFkm5H\DB3Z*J%!:<KZ00@GT3)W+I$WIK=Brnd*tf19Kn/$63UA!MseW]J[[?[LmF2a-uVJM.MY8\cBN2OGn7OkTn\ThXO;FeRLW_I=Nf,n%sst%YMK+b2ad13g4g/#<meN])jAnXR7/*(Pro"K>+)0@_[/]=L8&M2JEa+hQ>o.HS[gkU_"BOB/`6`+n<V'd*\H+lbM5@o4V[':/7oR=0H3(<QC.ba/+B0B[])ud,rt=mCmS,QE,MhN9u,/40H#]WXtC5bC0J+O_r7=d*:eIY-:[8iZ0l&oai<sZAOSC0)(?=j!qS`?3@tV$L?=8>O>.>6CYlmph9rB*=g:8[p&Gmb(56dK3e7qn@_H3`47822B*in6A33^Ka_hV>ZVD"@p$MWc[Q&[P;+*g,Wm+n/dg7(L+]0EGqI(Fb]TuUCa#qM%Q+6s,DbHp`'C27W-4dKne@)<j,6<S@[;e9$971U%b'e^)rA]f4!^VL^Vm20<ajQheG8/*WTWs>nDp&ib[DUcCgY>(<`t"CY;.r&ORO==e2hH.9ba=s^HPe]S<uscP!?KoDcG`<1+_$Z/;lE,WnIa_qg'q_FWUhp"/UVsR7I\50X'9qell7MsM&>P4c,6K\j/DRBp[1%bg9QXJJG$IQQQ4"nFm/Ku=R0hGC73aj(2>7=RWYgb_X,O&cX5M5g%E9XKsQOp3&5iER4>2aD3eWTq2ein)n4_N]-P_]CK]c"R_Z!&^+-<?aWTsXPFt%t<A<*<].>H6F1e`9dOV7(PNSfp<Dm3W75=/N1TQL@"&mc0fMlFO"RT;*f.SGMqo`#1)*NbTr*I;:Tn%%c[\eWP4PT>n;UeM:.BB-3Llr9M,r\GK;%_m-AG/3$d-pY$5\8!:[[@$lnb8gUIub,1g@?fo+,IOp`;LK+(3bb<Uj5(Y4'#$mclhbi`JJs!)8s4tciK(8C]2lE/qj]13'/=`.^RG7PW+)<4+!?-D7Z5OhJp@3E,dS^p"@Shq+->[f*[>a\PXDEoh[9S\3puYj'#naK9#5Oau"mRW4Q"e\b"i"!D<p(1p!d*WO=S\X=kX+::A@:Uro-A=Qa79cbFmPedk\`3N<ndUt?Z'\PZ_p:q%B9C,e6LTjj]R\]a/r#p`_<l^Q]?ID=UuO&ZaAYF]6g4,AGD07Wfj-BAq?f)$V8O"W$aelB8-PkL/2%%LC"/'QDaCNtej%jN0],lfsnUD_O-U@oH@,pt(\V$7)&!hsNPDZ~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1594
|
||||
>>
|
||||
stream
|
||||
GauHKD0+/q&H8h>Z!Z4)W,pabmS0O""pue59I[ns5@?Y\j^O.hP*+gYT``'CH`,I@Fu8T.L_EEBm6:F6hX96T!Q>T@hoQ)\nH/-N0OPk9kJ.34Y]pIM(W".BE5f5H5XIiUL]`K#pB0M,a?_;M+s&Hm<"l#1#fDHQOTCJg=u-VaL%BWl"/6O\7K>/:.\gnGLI?-BV?N)S75smDI`BcnLhbTdc/ohY:kH)Mor+Q,PY!H8Z%:SHYPrnZc\e!iP''W4n4-d<A6m*9Et+2%+$Zd*[&WL24L(52/%-Cdf13(?!/l3Led,Ug:*e9"NrRO.80@>!e\ue9qg5tJo,,[Wk;C8d"&ccXWi7.M82]hSSH06f%#bLH&dgMtQ__1Zf<TpB6E)Z62/ZSH6't]#]HTG*,j<kLL)IuN<50-@>U6#`%Tt%*'Bg2E,hUpc!Q2B_GY*6)g07,M3j?]pTdSMCpMPpP:P*`IF38p"XhTAFElTQ2,,S(qd?ZLZ\7%K&L:Eq>`c%7`m0d>sQu?;$pr_VeK\sdn)8RN@gM?T!XEXJ/R]iBabIG!]e:)=[S*WFi+<UHu<K-\J0N!E9+uPV@0c,8W[F&t<RdUn5OfLllou=C>&nVcCM`XA=^el2`4^h8kpc_TR?%DWM*2A[9`d@5g`;r9-0\J.d3l/Sj4L_a=#u`M1O^eqCjsILV3(Z>C=/aQn;rh,-8L`0.L!?YY8.*>4/[K[`_7.sC@aF(?<mT1KAns3h8,Mt`*^[("`[8='8h[*P[Gn+=/?LEN[]kb7(lj*#cY;f"=%,uX9lOo'*nthB40)Xbmui"+.h>L9)O#=XkB34e&8[#lDVem)O]-/h3NUA4[rnB8ZhT'9Sm9cc_iV^R<K(hLFPOW:Q+auJ%p8)9n.<d"j*;@8PK@3`_4+d6,2oOPR*C`HplrTDQKmf7m9:J`0%W0H8Z.O?jVX$sQZ$)qYr/<l=I[4.Y\+^af0lc!N4gf1nhGoh=am+K#CQ]JRHu[6BK)<-<1#8*ed"e+#p>]GC2Mc2Cg[HLCpM<(\UrL9:?h:F<o2-[eI7bMYDf1]G;@9$0!"*]5r(Ehcu8)eBBroNr2OHLq!+6gmqt5RL5RlQ0-H90:J.++`dE(#$e+6^\c*%$s%eZah#_4G=P:*ifm_iMeCLiqcd5mTK5Y38H+Mi"EW0@rMFd\a<BH]_Nm<R3KAl'sLJmnm`fGM+ip_JhW4t7Fi^Z0p@L6t-MbP>$DH]rQQ3G-%V`67#I7k/%W#i.0:*p;D226/m(P6q2IOua1aq-UQLAU7iH3X<T64FHClI3b3?IJq6%^t0NB*KIA^pV]7]GYV]%Xqi5K[Y?j?UQ)f]!u7).Hrd0G.u.`3\Bp"[4^gDH9&%rqaYk+iOCda2Y&#_hK7VFhu7V@"Fol)LH7e"Lh%aZ(]m6c]W)o4<<rQt*Fc^1pY]p3g^-Z:p;dEY0],gYUD%AL6S7eTCXoT=BkFV7VXP6Y(J/61<P>!t6YbV^ZPEdL+38V,Xda!d.SCG5cuUK+K+HZ+BR)GN:Iq3HD0f;`IFs5M\;Tt^65U=UEo'*0\QbM46G*7C>NsSe]<^8fC;NekFdc[?'f6'n!4!,T>l~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
|
||||
>>
|
||||
stream
|
||||
GasapD/\/e&H8h>E<oU\m8A\7.#Q0o!A3#u`O`J5h!N"l%uO%BC@i95mL+pE\*lj[Pao1*j1qHca60$=+-L7@'L0fjB-k?W-(J6p?lYIO@"JYd^4^.Bi$KROnlS:F6:JeF"%biN#OOFJ+Hf,>4COmr9N3Q.-q9Fn'Q>$.a"2sG_ra8#JAdVj8cV^>P"L0\4sZ;4$nY!qATE"m^eYho$'><OV!7jj$pE7)PXc/dTe)'G=I-2dj43$?i-E#d?$96""ha5,SR(omi]Mt90-3=F]"IA%)P66g\u;)r=8dPL^4YML'5N=U"=Ff0gW+fC4/<D>FFCiBYclMm?DWb-E8KNCN^/a<6t<5f(t'8Qio#Ip+\mt'KbP0BJ4IP#]r_Qb9ILWjB&dXq&qn5ecRmQ.P6Kft*MsZ0:u7G0?P7q1%[lqY"@bb5"NPs'0RV'@Qdc+7+!h<Y`1k$P:N@<=e!O^<"]p9*Pkhhc5J&W'Z/V$:%J#l<)5Dj8`^=-i)lcH^-&ZF%6C=8)]Bp#6>5X'CYORSr=G"-?_gbOcog`]d7Mg?H+g3#hfD:jO"AVl><Fj(a.)P!3nZ<IY'a&>4_HkeL&97&[T!oGc:t,oD9R_=\63WA3(,br!_6f&/*7$%B:u92`aI1`H%NNIQS3-ml6)a%akTNuAp6n`aFGaWTTr&gn`,un]AM\JD.>\TK]41D:MSM77N2+1A+uEhJ&"fCYg,*GVPFprKmEXF3f5&fYh`_<.7OX+-%7FM\FkafHcl`-Cc2&`pE!r/D0&^>B2=&N/01XAKVT!7k#Hk_hleg1VP6XbPUIYfkf;3Z+M,GO2,&B7;d=t<+:K7Ug?'Y).?,>i.Ik=`[<o9FbTDE/t]7+lE4s,g0PTg]Se\V@p^Zp-te8>"<m8J64l%_+anT;MhUN*\Dh*7&6Sdcr;Ph65c$]TZ]%,'op[5]nl';V%>E.QhT^f&h@[g.;kgMCtid$=S1DtBu1O4uSkZs#HW-3?bujb\+:r/YJ%EE$qjT>>1(SP$n9R;$<AE&:>;X46N;j.2pVF6MALL>1bY"KGh6jKJh*T#XDRpK;'/]BZsp::B2m(nRW:;/3h*#A89k`&,#gXHi?;YI<7Lj\ap9"X],IA8qtFdpcs1*LQai[PT1%7cJBq8VkF,U`pVigGS5,g1b"S([B&[6j8^cl?%rp*tF:n@i,Nb0H;JLUe-t4.#\>lhIAik$9;kldBA('*laBM&OIr7nGp.Hf4Om7$*QroI0q4c@B:k(L)QOiq)(.(/]YX%In,Sg6t@,b%s2S4Is=L$OuVNM*-6323F4P>p)Ep6h='&)c^h4)q.T?%X",D,2nbtE8>3)$J+WmV)-_`Bqjjd4.Ni<dYMu&0hPJ54%f5f1%id,AEJkNVA'.mn<T<s)h>Kj;^]uDhUX2-ICZ8/cO,,j/OI7!IT5`(p2gQ#a-Zn]f3#7um5f>8qLJWVf@2%U83]OWfC3aE1"og]8._)CWoiJ\3Kos;9&dd.RMMKSUk>TQ3AG6WW>ZjhhKT5`..C8ApbPeUL?[iAl[Bhhs'+.Kubl6D%2&7tJhlk",nQUuAH=[o<7'nfn<b1V?*R&P!<#0_j4`&Ou]RG0X8'Mi=YG&/'F_/k+020ek5O`LE"`TG5i6+n1Md7n0<f>Cj!\LKP/DkEMiJ@QCYVX#(Y7)3$>-G-q\q-KIX)pZ$Zt7b[`dT'**t.0g^]~>endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1584
|
||||
>>
|
||||
stream
|
||||
GauHKD/\/e&H8h>E<u9Sl!g*b,3>L#>&<R5'\fq)`IN5I'U`sj=>,ftUd^CF^6>KS2bmWE6S63M:?6KB*M4Qj"r&`Ar'h".b6?*r0jkt:(Vh;YY]pPj0"bt\E1^pFJ-\M2^juW_r]ma^;G]RI"!0-M<"l#Q8Ag6<JH:ekZt:3`$Q%,O#"8>f)[XG:jW"69GX6`&dBt7BpOfZi\,8oo0-G_e%V`L-Jmf2@#1]IU8LRM@_'4HPmsf_0"2AD_>=Or^[Xn0Q@AD!QQCM`8nRhWl/E=%A/,7*?fM.YZ2Q`!JnHfuWC0r35.1F$poIg,I>9\L2>?PMR%ee4f*N(&73W_@]Uio9mDjb?TL_*PGA3?9":LIdnOM:?1n[KdGg"SD+f7[%97Q,/<?BR#0o<C`,I3Pl(@=oH;C8fu'@)"rmO"HabdhCpR&JFHa)irr^2S>hK&FI/!*Z\@!n=_5_%dq4O736`@?6cKHEcXHl<K!bPY>7imO3sIP9"8;7!(1""P\K:(N95gJQfbL?,CI8B%g[Z@00)l\5iM.hT5JN9"nFX^RlIeeV\!DiHUnQC$)0;SW>5T2#`Ifnhjl/60jq[,mbB.\eCp0oDqfNbd!FVXlKcnH<)PlfR''6G%94s-_?4nh);>RS9']Z)i*9.2WFsR^&8HHgoq79^]qtlK*IQD#0LH7ji7a25U)BDH,p=CEArQ.V6pLh!3!tTh#cFh]B/9hbHktl7/1du:7CK<LWQP2*5``]?&^18D=^"jF#M30a<cOGB4ZnMh2N0.kaf`a`1/!stjmc+O&1Jog#-ru2]GEAk4B04YA"0"CeLF>G,L;?d3OsbFe[IPiRrG^Ao+B#qKWle[$HsY[^2,(-6YI38i>eT8D0f3B30i5\4:b(XMEG';NfpW(/jHpR7ji8"J<&V;"l7On5TNDcj^TD.Hq;*\[(t?QXlc9O%cBsDMIA,fDo`9g@Y+4`AVK*KWTcAmpflFt'dgge9Th)"=\PP'LmQ0Or9"02W?TPf?2/(4]Z[`PADr<Upd]Kla*U4s`cV7LEgbL>NQ8E;5C4;/&6Mcr4JPTlV^t<N*[(f<VphV\*$*63/f$cF>Wn_'N#rT[NSusFBbE5cA8P7LKU#i9)(#3f>GXpYQ*lH_j?A>M[qQ[h)!ei'b&!NbBoF2Td"568(YZ5mH''C5.d65q%FaT#bDI%o!\-/^Z(BB]9'7hamc!$!lU)8=lZuUCK"F6/i*5+D+?2=;W'cS<F+%;>3nrK(`[kr6/Q2AeQ8thu[d2?^KP>^IMg7)b-f?g\%b827AH1d<<53_/Vak^`h%L]Eo/2Nia@Y3MZX0:PM`<BQ4!)C*L3"AQ1kQ=m]^FAK*g-1efP\>&@6.SQ*jPFDjLZt09^>2?iaG>34PKGA*WeBiJa+XT0R7O&"e6B3RbAGggDh,N(Ws5S8TemrMf!iQ$b/ORh:NpcL`8+5V,5%:$Ca6%&%>pBrK>P'H9;L]?P9@iT3gIhTGe>.VC$iD[a+6^_TH2<gC9%iIa0YSokE5k;QpTBDC\N%ruK#W?WHS*s-\O35O4N=H#=D5A%9@O#L70tnW2)p^E693D(b>W"6$+I2?~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1631
|
||||
>>
|
||||
stream
|
||||
GauHKD/\/e&H8h>EQBN*Mcl4;7*7kUADOg#kgjnL2IJ48-o,;5PseWHPJ/1>I4+;;NDj9_d)_%('D_..]B>22",[n9s-"G\LG_2C`di&2?NCg._"p(h[.`@%KSNfi!/RhW+)rEchu#dJo*Pai&k4umTVs,.$4r(,V'6'_k=Nc@<#Vn\GmoK;WDiqT@R6s0Hh2XuEfNd#&qj]cq>?8FC?S)3m+<K+p4Ha4JosftW4jC="YtUN%ZB4Nr,i%TR%:CD]eLE&Be,b@/SmD5E.5&SYNrR(H4)t&CRpkKf7cAAoc%!tC=\j3$XXM<7Gc"5Y%sgDGcRsWC12UQGA.Wk@hYN%(t9TUj;h/j(%H]%0c=>W\E@@4PV5?86LOAo[#]hko=-'1$8o).eVt2pacK:M5sH^d"rf`[`E34"Qm*Ld"Ya?]X;s).kR@h2m%6(piEJ4SU]V/K<_5D7ZH2OY5dHkA\2!M9nb/Of<HGRYJ,HA[O&Ksf<2YdKc57j41%&O(0[M:G/n`QW08-`Arqnsr%ip*epjIP+8\cMamN)3ic@90S7Vlc>=r.b=mY^;.(;bk;>%m13g<X&aA"]8*>g_!.BH'j-V^B/8i3_^cAoZZY=DpsnS(8@J%7tD3=*IadjWOcXU^EV;)+E_mLkg=-9d`N6HGWnLF&,?[2LrW0Gs]njib07A6qF8LEb\JMR)Bqj$.bLdq9hP<2CrkuR[G`ea.d[."pT\`TuJaF.]$%,f5q.jj'%Ere`[)U\T[R7DoThm_Xo!l$)5I^)3K_2N\Y7;poNj.M(*0I<P3[Tj!ra"'5050=aW9eNWob]M4g![`<'G*NNPlHS!mdi-iY%^[Sc!Y+3MgG>7Ff#*1@qHMj(J#"#HcEJGJV`e"StW=34e3nHqGmF889Q(>6O9K<@BpVV5'bZ.gg+GIO'j3HQ.%+At@>rE^iVX&fka+,U1*g]e(VHj;Gn@%hQ1mEbje@-[YfDmr7EK,NRbBkM]gkfpu\Dbe'<!=h*O?&HsY5J]7d1#*8Koq@<tj'%YH9K%fm4LWB2Fg=4GDsNUn=nVncU=dO.>3B6'r)]XDB@)4!6Xa%-bf(H(DC!-IJ_PIB))ck67llpDLT5;GcV:5:@:@NdFur&Do!*jo&^1p+0&Qs9RkkP(GZ5Ll+dCuMU-d8>QAFcToF7n,Y+?S23nnJ%"d1^#ZHfB'Cj!/f;sGQ%1"Ena/qmHjBp#"2Wa5fRoVLiGl48.1aLa2jK1tV-%.G*3fRq`mE"03j"_SA_p;WJ7$b8a/R7!8'R`lMNmh=r,hQPLPPBqr2*F$`mIQ4dlk!7Mh[ANkKYHb>Tn+(0;^l*NF@!'ugrdH-=a#>.WoUF4>R4\9lei90*>i/ibo\NV'=CT?/JND'!9EX^:&+>233AQt`T^H[SgkY9`%k$Lg\cQ73Hh`4@\d*IU\+C,nnj/:\[L*kCUkZ;F"1Bm.m,UP8MdI!_9p^3*@Vs/n.Bp"c[78IXm8;FT?/TR)!+5Sn*:ZG%27e$o*,`(SlU@IRlT[ksV;#9*I8ctc`qsOW?#$Djgc\Ol^dkKWV/+Fb$Gff&_#FbW"aUm;oNG`9Y0oWI5CDlf60emD*J`1DRQ/:kPVk\!^5'!LQhV3*doKQH*mp>B!q0Kb(1Kc~>endstream
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1621
|
||||
>>
|
||||
stream
|
||||
Gb!;d=``=U&:WfGfS=(?A0Mij5ltkdc6Tf=9Kd)fX3$9,+;0iNo2&!#Y5@Osi9`m(eB)r?*@:pkhsRtc=R;IW`AajtL+0q()'$A!^c9OR^]Y6'4+ZF[o@5#>k>HOi>XdJ!!sr60Jn!tTi;(;%GecRA8X'o3'I-3R$952PP"@&1/=nj1M"IZG!f1<WYP^KJ?/fpcU':(75IeD@ePB?@!og]CTpj42_/KTo8bK$b+I%0"K38PF%tAtU&+B(c.Do/W-h(2;QuH'A)<VW=]q<"gbIFWI"@qV&,l'JY=nNJ@k:h6pAW?25WsEa.2t9[;M<Y1LaspaTYTNNnc8:u4"S<"[g*-:)ePoF8>aJ')`_MFR/m:`T_A>D3%eiM8gQ*WjQ\8817Uo]2ZNO^bkMm/XRhIj](u?scR"ag\!h:'Ocss\hRF<\5%>:ZfGFpi'T<dE!m=Aq(-$8@D7=maM-/6C%N=kd/=ZLV$fluKR_`'JulKIntC?&gb1EW_:E3=^7Y,aZbqi&pb@gHl:I/>r;i7A#3_X@gCS9QU#aLrbTMp=(f$Fhk5@2>=>rd=D4>I!*:g;h(^DeR,dI4VJ4OVMUM9YML=g^uO^g2@^%M31V<5OO?&Q"c6"qlJ/H=+HdSd;sZaDUGf,X>A,^N(LQ[8W4aP@C1u(61`b2c>J5`2C<u'%l\Z)RgdRh^NpUFH3mSXBq:bWh'uU%KcZ93+DWWZ7g$.l;s5bfKtZ%5@L"Pf4WLXkem?!a9rbNL%\Y_j#Zm,L;7o<ceJh(-M*[HiD]Ml?l7-'+F<d[o7sC'&(Yt-@I&Y$LT-;XJL,;D`E<bV%jpI,T_`We%028m5iWCt6:BY#@H%l?gRO!cs-,_+R9qnt*&YIs?e5*PapZ^)CSFrZ*j4iVS&)W]qKhq_5q=-(F"#Hl:nf1>P:CIG2QQU:#eWM74RL?/pjk8+[U03qc4O7hOQIf4jV6DTCFBq&8"M1K3NboC`c+Z9/X>(dr1"gVmIoLTu/$'Sc+m,:-%cR'/aWjH[Hh'>%MG72i$7p@)cBZDOR$au8M\L/P<i1+P.<SF\$ncAh[Dm-<a8Odu(0WAdL1?7SBOu8gom%/fRr[Q34K8h%-f<hPa@td.2!nm,bSoLV)]^^disOW<*&k)b2:K*mC_AA85OSrGs0005mWr3XOZ`_]Q%FH6[qCe/!LIgN"g'@D(r]r']?:-,\>&DRYRMnk.E5:V8D/*\rE/%bXKZ^'0\!S65,!SNnVHJmKMFbOQtL2^^P;$o,k6PQdt0CB'C&0nfo%6nZu,aB92V?7YUAD'^L%34T)CV,%'1YgjA.Cg#>^7b3X8c]5Xj$!Su@K`_)hded%cZrj@Z&juReN,@9e'2(NBY2#RmZ>d^XsYn]/5g0pK*>JHgN']R"eHbHXu=s%\JrHtL_*2K)WYXe'eYaHV?dXDKEF>h,"u\!("U*DQJ'&#lY(8<J;U8%dWLj[`6$8WG)mKdg>-mS/Hu4F]&Rbq'&aG[Q)\*j4gd8+0+8g_=i<aTV$lnU!6-\Am)!"7j[HNqhK=ZsD<'@/kJaZRiPiuIi?ng_J.`B1j-OW]]NYBV!4MoCBa*:jU@4"up1e68nsESCf67s%i\[gRqrPi##3T>K\gH)~>endstream
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1654
|
||||
>>
|
||||
stream
|
||||
Gb"/'D/\/e&H8h>E<A*Al"XCthP^JrOL/^;/8`hbl_f,=,fWk9MloH.gOB)rYEn5&M2]YqJ_>c0PLSDOp[[f%$7<K$q&RZZN^kdN7tEE4h#b!@%ARu()&Zci_Hc6V%M4PfQHt*V<q1c`Ca@rK`:]NoJX[_>SddTUKQEaf_tuQWHB:b/I:5qpSniZ-#/[A"Ed-o"Mn"_>C7@2clT`%s[Z;6eq8BD;6SkbR$H!Uc#1Z>RCgp;_F'phWK+Acjces")Q<>n=.Y^01.YES4CW8Ir3ZB&M=d3V<_HXR4Z's!LU(:[S#GW)hY)[naiWQN2X7FSJL?8U;A6Rei@P>\[8d%tk,Np@5[?9OUg/?8X`3N&IK<`teL4u5XQL3BYbREWqlEVFg%H\8O[!?;CnTpHpW9:JSLsrF3N-5U_.&XE`eIMDbc?GqHD:*)mCHsZeTRk@1Cf$Xub=T#TA@>YC_d$@Wj.FoDqW8P_#C0]NN&%AiG8:#6]-;OClu`W<o:lsT(VT&h`dsjhM`\S1)5Wn>&6eo*iGV5;>:T+K>?>2gi_U^QG=B>ZWkpmFOW:<R9^a:)_cS7HAV81N2B0@m@M`l3fe,+)r(q^ui(=JWO\AX@MUnusF]\3ElBRtNMU;0272MepOdZe#=GR(q1=.M)9StZo^e_/m7UfeWXqiFU3^8*i?tgKh>0L>j['-TEa*0W=RH8N(S^n_1;HIp(a,4^VD(MduY3[@.A>>!i=Ymll;_R<^??+ZV2dj_M+pk#@BUN^:M`gkfaOh/fZ/(Ml;21JGAIR/qDq%1Png,9JSm3?Q822^3fn/]r!*W.u\/W.,9R'6D(uqc]X=<tTWRMoKmp9ao%rlA94?Ke*2>g.'FBYT'bVk)Zl?Ah\@SR[_c@q;.3rZN"[QV''#s7QrjM<_H]T#D?535VrPrcL&K`u78OUC/np_=.E(YE%b&;e+kc3qCM*M"J=qa7jBeZY)+"Kmb[?P_#.161e"TQ33AoeptKpBPK5Wu!=gh1<WOGBl06e`J^%(NH6^Zk_47EUC1RV]rnV@B;^aGN%#-ai4hg\VT3"N><`5qf,Z@\@ih3I#GR=(KcMsRbET5ZK(:@L*:_#)14D*)'Nd*:pT:3QIQj]/<MWnS7W\SdsO7i0b39,Y5>.>]=cu.OhP!s`f7c^l^oRs&0gQ@X5arO%%DnqOrEGL^E@kQr%krKr/lp-F$V>/+=P0c/;$*VGQk@@$5%o,d\H2k0&g!W`)dLgXB@Y6On(4!s5@siLI_%[+!-JA<><T1dpbtAeFT0npYnq_n$/^(Q1\\6UGM];p(-AKBM"X:)J%fkKQ8dU<KIZjs+<S5EnFFB[W<*nHe^VbANdk:b[lA9GR/a>$[o)(),W@H+qghWjb7[qKee"L"6^dbO&fCZ#L(6]Z\3(kN:\C1.H*/umDTj:rZ:fh7TWBUTCTP&&Xe;P^f`h5(?&^-.40f?0.i@r:L;^Q4Q4ujo0f?h=&5n]6n31@iY2t6X.,1Tid@&.bPlF"RdA4FM4UMX*X2%&,oQlVA%-D'FW*h6Dq?huX=9^&aSTcj[#50'ZhQUDGkQ5IT$5+,2$V-c]51SEI:j"0:NS,5E[m;B;Bs1jI$s'GJo-Qg^I9C&'C:u$Mk[u$3][Gt4uKo8m,1Ws,pZA-\mI/j"ej-QA,~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 34
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000536 00000 n
|
||||
0000000741 00000 n
|
||||
0000000946 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001873 00000 n
|
||||
0000002079 00000 n
|
||||
0000002285 00000 n
|
||||
0000002491 00000 n
|
||||
0000002697 00000 n
|
||||
0000002903 00000 n
|
||||
0000003109 00000 n
|
||||
0000003179 00000 n
|
||||
0000003471 00000 n
|
||||
0000003615 00000 n
|
||||
0000005572 00000 n
|
||||
0000006064 00000 n
|
||||
0000007403 00000 n
|
||||
0000009290 00000 n
|
||||
0000010896 00000 n
|
||||
0000012827 00000 n
|
||||
0000014803 00000 n
|
||||
0000016489 00000 n
|
||||
0000018280 00000 n
|
||||
0000019956 00000 n
|
||||
0000021679 00000 n
|
||||
0000023392 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<744a57cef2ee7e8b9f4d67dce5818616><744a57cef2ee7e8b9f4d67dce5818616>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 19 0 R
|
||||
/Root 18 0 R
|
||||
/Size 34
|
||||
>>
|
||||
startxref
|
||||
25138
|
||||
%%EOF
|
||||
BIN
Les14-RAG-Embeddings/Les15-Slides.pptx
Normal file
BIN
Les14-RAG-Embeddings/Les15-Slides.pptx
Normal file
Binary file not shown.
BIN
Les14-RAG-Embeddings/lu43khle3.tmp
Normal file
BIN
Les14-RAG-Embeddings/lu43khle3.tmp
Normal file
Binary file not shown.
Reference in New Issue
Block a user