Files
Guido van Dijk 909eca8067 feat(presentaties): QR-code en link naar git.lexconsultancy.nl/guido op de slotdia
- QR (segno) in beelden/, met bijschrift Alle vier de reeksen
- BRON wijst naar de overzichtspagina in plaats van een enkele repository
- pptx en notitieversies opnieuw gebouwd

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 22:17:24 +02:00

407 lines
34 KiB
Python
Vendored

"""Bouwt de dia's van de workshop (22 stuks) als HTML-secties van 1920x1080 plus deck.json.
Standaard verwijzen de beelden naar de map beelden/ naast dit script, zodat bouw_pdf.py er een
PDF van kan maken. Met --artifact krijgen de beelden de paden van de online presentatie (asset store).
Licentie: CC BY-SA 4.0, zie ../../LICENSE.md. Logo van AI Netwerk Limburg uitgezonderd.
"""
import json, sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
P = HERE / "project"; S = P / "slides"; S.mkdir(parents=True, exist_ok=True)
ARTIFACT = "--artifact" in sys.argv
# Uitgebreide begeleidersnotities (privé, niet in git): notities-begeleider.json naast dit script,
# per dia-id een tekst die de sprekersnotitie vervangt. Alleen gebruikt met --notities.
NOTITIES = {}
if "--notities" in sys.argv:
NOTITIES = json.loads((HERE / "notities-begeleider.json").read_text(encoding="utf-8"))
BEELD = {
"logo": ("../beelden/logo-ai-netwerk-limburg.png", "/_blob/120fdff17b59f8e1cc380d9086a1ad9f"),
"afgekeurd": ("../beelden/schoolnieuws-afgekeurd.jpg", "/_blob/9b09f7c211d54f2d5c5b5bdaa2b69fae"),
"flow": ("../beelden/studiecoach-flow.jpg", "/_blob/1c5f3486aa7ea647a8ee49269f3f95bf"),
"docent": ("../beelden/studiecoach-docent-antwoord.jpg", "/_blob/244acf99a4fa9215357a5e95e1eec2f4"),
"lex": ("../beelden/logo-lex-wit.png", "/_blob/a056bd447e5dc103fd01f7528f888bb9"),
"lexkleur": ("../beelden/logo-lex.png", "/_blob/042fe916c9a2622daf445ddb6d0bd222"),
"qr": ("../beelden/qr-git.png", "../beelden/qr-git.png"),
}
def beeld(naam): return BEELD[naam][1 if ARTIFACT else 0]
EIGENAAR = "Guido van Dijk, LeX Consultancy B.V. en AI Netwerk Limburg"
CONTACT = "guido@l-e-x.nl · guido@ai-netwerk-limburg.nl"
BRON = "git.lexconsultancy.nl/guido"
DARK="#1E2A3A"; LIGHT="#F7F4EC"; LIGHT2="#EFEAE0"; ACC="#D9480F"; BLUE="#2563EB"; BODY="#3D4756"; BODYD="#D9DEE6"; LINE="#D8D2C4"
H="'Rubik', Arial, sans-serif"; T="'Nunito Sans', Verdana, sans-serif"
LOGO=beeld("logo")
order=[]; sections={}
def slide(id_, body, bg=LIGHT, color=DARK, pad="128px 128px 160px", extra="display:flex; flex-direction:column; gap:40px", notes="", section=None, transition="fade", footer=True):
fcol = "#8A8F98" if bg in (LIGHT, LIGHT2) else "#9AA3B1"
notes = NOTITIES.get(id_, notes)
foot = f'<p style="position:absolute; left:128px; bottom:64px; font-size:24px; color:{fcol}">Workshop AI Agents met Dify · LeX Consultancy · AI Netwerk Limburg · CC BY-SA 4.0</p>' if footer else ""
# Klein LeX-logo rechtsonder op elke dia; wit op donkere dia's, kleur op lichte.
lexlogo = beeld("lexkleur") if bg in (LIGHT, LIGHT2) else beeld("lex")
if footer: # cover en slot hebben het logo al bovenaan
foot += f'<img src="{lexlogo}" alt="LeX Consultancy" style="position:absolute; right:128px; bottom:52px; width:90px; height:52px; object-fit:contain">'
html = (f'<section id="{id_}" data-transition="{transition}" style="background:{bg}; color:{color}; font-family:{T}; padding:{pad}; {extra}">\n'
f'{body}\n{foot}\n<aside>{notes}</aside>\n</section>\n')
(S / f"{id_}.html").write_text(html, encoding="utf-8"); order.append(id_)
if section: sections[id_]={"description":section,"start":id_}
def h2(t): return f'<h2 style="font-family:{H}; font-size:64px; font-weight:600; line-height:1.1">{t}</h2>'
def eyebrow(t, c=ACC): return f'<p style="font-family:{H}; font-size:26px; font-weight:600; letter-spacing:2px; text-transform:uppercase; color:{c}">{t}</p>'
def p(t, size=32, c=BODY): return f'<p style="font-size:{size}px; color:{c}; line-height:1.4">{t}</p>'
def card(title, text, w="flex:1", bg="#FFFFFF", tc=DARK, bc=BODY, border=LINE):
return (f'<div style="{w}; display:flex; flex-direction:column; gap:16px; background:{bg}; padding:36px; border:1px solid {border}; border-radius:16px">'
f'<h3 style="font-family:{H}; font-size:34px; font-weight:600; color:{tc}; line-height:1.2">{title}</h3>'
f'<p style="font-size:27px; color:{bc}; line-height:1.4">{text}</p></div>')
def num(n, label, w="flex:1"):
return (f'<div style="{w}; display:flex; flex-direction:column; gap:8px"><p style="font-family:{H}; font-size:120px; font-weight:600; color:{ACC}; line-height:1">{n}</p>'
f'<p style="font-size:28px; color:{BODY}; line-height:1.3">{label}</p></div>')
def ul(items, size=30, c=BODY):
return f'<ul style="font-size:{size}px; color:{c}; line-height:1.5">' + "".join(f"<li>{i}</li>" for i in items) + "</ul>"
def table(rows, widths, size=27, head=True):
out=[f'<table style="font-size:{size}px; color:{BODY}; border:1px solid {LINE}; background:#FFFFFF">']
for i,r in enumerate(rows):
tag = "th" if (head and i==0) else "td"
bg = f' style="background:{LIGHT2}"' if (head and i==0) else ""
cells="".join(f'<{tag} style="width:{widths[j]}%; color:{DARK if tag=="th" else BODY}">{c}</{tag}>' for j,c in enumerate(r))
out.append(f"<tr{bg}>{cells}</tr>")
out.append("</table>"); return "".join(out)
def interactie(label): return f'<p style="font-family:{H}; font-size:26px; font-weight:600; letter-spacing:2px; text-transform:uppercase; color:{BLUE}">Interactie · {label}</p>'
# 1 cover
slide("cover", f'''
<img src="{LOGO}" alt="AI Netwerk Limburg" style="position:absolute; right:128px; top:96px; width:260px; height:210px; object-fit:contain">
<img src="{beeld("lex")}" alt="LeX Consultancy" style="position:absolute; right:148px; top:330px; width:150px; height:86px; object-fit:contain">
<div style="flex:1"></div>
{eyebrow("Workshop · 2 uur · LAI Kennisfestival", "#F4B183")}
<h1 style="font-family:{H}; font-size:112px; font-weight:600; line-height:1.05; color:{LIGHT}; width:1300px">AI Agents bouwen<br>met Dify</h1>
{p("Van één prompt naar een app die je collega's kunnen gebruiken. Zelf bouwen, zelf testen, zelf breken.", 34, BODYD)}
<div style="flex:1"></div>
{p("Guido van Dijk · LeX Consultancy B.V. · AI Netwerk Limburg", 28, "#9AA3B1")}
''', bg=DARK, color=LIGHT, footer=False, section="Opening: wat gaan we doen en wie zit er in de zaal",
notes="Welkom. Laptops open, cloud.dify.ai ingelogd? Wie nog geen account heeft: sandbox-account maken duurt 2 minuten, buddy naast je helpt. Doel van vandaag: iedereen gaat naar huis met één werkende app en een ontwerp voor de volgende.")
# 2 programma
slide("programma", h2("Tweeënhalf uur, twee bouwblokken") + table([
["Tijd","Blok","Wat je doet"],
["0:00 - 0:20","Het landschap","Peiling, drie lagen, wat een agent wel en niet is, vijf patronen"],
["0:20 - 0:50","Bouwblok 1: workflow","Tekst op niveau, in duo's, eigen niveaus"],
["0:50 - 1:00","Demo en sabotage","Agent met kennisbank; laat hem struikelen"],
["1:00 - 1:15","Pauze",""],
["1:15 - 1:45","Bouwblok 2: kies je spoor","A kennisbank-agent · B poortwachter"],
["1:45 - 2:05","Ontwerpkaart en pitch","Waar hoort de mens, jouw app in vijf vakken, pitch"],
["2:05 - 2:15","Vier omgevingen","Dezelfde agent in Dify, Copilot, ChatGPT en Gemini"],
["2:15 - 2:30","Afsluiting","Wat mag, wat mee naar huis, exit ticket"],
],[22,30,48], size=25),
notes="Strak op tijd. Bouwblokken zijn heilig; als de demo uitloopt, kort de demo in, niet het bouwen. Op het festival zit de wisseltijd tussen bouwblok 1 en 2, en 15:30 is hard.")
# 3 peiling
slide("peiling", interactie("2 minuten") + h2("Sta op als...") + f'''
<div style="display:flex; gap:32px">
{card("1", "...je deze week een AI-chatbot hebt gebruikt voor je werk", bg="#FFFFFF")}
{card("2", "...je collega's een tool gebruiken waarvan je niet zeker weet of het mag", bg="#FFFFFF")}
{card("3", "...je ooit een prompt hebt gedeeld met een collega die daarna niet werkte", bg="#FFFFFF")}
</div>
{p("Blijf staan bij 3. Kijk om je heen: dat is het probleem dat we vandaag oplossen. Een prompt is van jou; een app is van het team.", 30)}
''', notes="Laat mensen echt staan. Bij vraag 3 staat meestal driekwart. Benoem: prompts zijn persoonlijk en breekbaar; vandaag bouwen we iets wat blijft staan als jij er niet bent.")
# 4 drie lagen
slide("lagen", h2("Drie lagen, en waar wij vandaag zitten") + f'''
<div style="display:flex; gap:32px">
{card("1 · Chatbot", "Je typt, je krijgt antwoord, elke keer opnieuw uitleggen. ChatGPT, Gemini, Copilot Chat, Claude, Le Chat.")}
{card("2 · Assistent in je pakket", "AI in de tools die je al hebt. Microsoft 365 Copilot, Gemini in Workspace. De school beslist via licenties.")}
{card("3 · Bouwplatform", "Jij bouwt een app met vaste stappen, eigen regels en eigen documenten, en deelt hem. Dify, Copilot Studio, Gems, GPT's.", bg=DARK, tc=LIGHT, bc=BODYD, border=DARK)}
</div>
{p("Het verschil zit niet in het model maar in het account: gratis privé, of school met verwerkersovereenkomst.", 30)}
''', notes="Laag 3 is vandaag. Dify is geen model: het gebruikt de modellen van OpenAI, Anthropic, Google via hun API. Komt de vraag nu al (wij hebben Copilot, wij hebben Google): zeg dat daar aan het eind een blok over komt, met dezelfde agents in vier omgevingen naast elkaar.")
# 4b wat is een agent
slide("watiseenagent", h2("Wat is een agent?") + f'''
{p("<b>Een agent krijgt een doel en bepaalt zelf welke stappen hij zet</b>, met de middelen die jij hem geeft. Een chatbot wacht op je volgende vraag; een agent werkt een opdracht af.", 32)}
<div style="display:flex; gap:32px">
{card("1 · Een doel en regels", "Wie ben je, wat doe je, en wat doe je nooit. Dat is de instructie die je vandaag schrijft.")}
{card("2 · Middelen", "Kennis uit jouw documenten, gereedschap om te zoeken, te rekenen of een bestand te maken, en geheugen van het gesprek.")}
{card("3 · Ruimte om te kiezen", "Hij bepaalt de volgorde, kijkt na elke stap wat eruit kwam, en probeert het opnieuw als het misgaat.")}
</div>
{p("De Toetsweekplanner kreeg één opdracht: maak een studieplan. Niemand zei 'schrijf een programma'. Hij schreef er zelf een, voerde het uit, en gaf het Excel-bestand terug.", 28)}
''', notes="Dit is de dia waar de hele middag op rust. Zeg de eerste zin langzaam. Het voorbeeld van de Toetsweekplanner is het verschil met een workflow: jij vroeg om een plan, niet om een programma. Nog niet over patronen beginnen, dat is de volgende dia.")
# 4c wat een agent niet is
slide("nietagent", h2("Wat een agent niet is") + f'''
<div style="display:flex; gap:24px">
{card("Geen mens", "Geen oordeel, geen zorgplicht, geen verantwoordelijkheid. Een leerling die schrijft dat het thuis misgaat, hoort bij de mentor. Daarom bouwen we een mens in de lus.")}
{card("Geen feitenmachine", "Hij weet niets, hij voorspelt tekst. Zonder kennisbank en zonder de regel 'zeg dat je het niet weet' verzint hij een antwoord dat er goed uitziet.")}
{card("Geen programma", "Hij doet niet twee keer precies hetzelfde. Moet iets zeker zijn, zoals een datum of een woordlimiet, dan doet code dat, of een mens.")}
{card("Geen beslisser", "Niet over toelating, cijfers of toezicht bij toetsen. Dat is hoog risico onder de AI Act, en dat bouw je niet op een woensdagmiddag.", bg=DARK, tc=LIGHT, bc=BODYD, border=DARK)}
</div>
{p("Een agent is geen collega en geen orakel. Het is gereedschap met een instructie, en jij blijft de eigenaar.", 30)}
''', notes="De belangrijkste dia voor AI-geletterdheid. Vraag na de tweede kaart: wie heeft wel eens een antwoord gekregen dat klopte qua vorm maar niet qua feiten? Bijna iedereen. Koppel de vier kaarten aan wat ze straks bouwen: kennisbank (twee), poortwachter (drie), mens in de lus (een), en de AI Act komt terug aan het eind.")
# 5 patronen
slide("patronen", h2("Vijf patronen: hoeveel mag het model zelf beslissen?") + table([
["Patroon","Wie beslist de stappen","Vandaag"],
["1 · Eén aanroep","Jij, vooraf; het model doet één ding","Tekst op niveau"],
["2 · Redeneren met gereedschap","Het model, stap voor stap, met kennisbank","Schoolgids-assistent"],
["3 · Plannen en uitvoeren","Eerst een plan, dan losse uitvoerders","Schoolnieuws: drie kanalen"],
["4 · Zelfkritiek","Het model kijkt zijn eigen werk na","Controleur"],
["5 · Poortwachter","Code of een mens laat het wel of niet door","Poortwachter, docent in de lus"],
],[34,40,26], size=28) + p("Vuistregel: kies het laagste patroon dat werkt. Elk stapje omhoog kost tijd, credits en verrassingen.", 28),
section="Het landschap: lagen, patronen en het eerste voorbeeld",
notes="Niet uitputtend behandelen; de bijlage Ontwerppatronen gaat mee naar huis. Twee zinnen: hoeveel ruimte geef je het model, en de vuistregel kies het laagste patroon dat werkt. De vorige twee dia's hebben het zware werk al gedaan.")
# 6 demo 01
slide("demo01", eyebrow("Demo · 3 minuten") + h2("Tekst op niveau") + f'''
<div style="display:flex; gap:48px">
<div style="flex:1; display:flex; flex-direction:column; gap:20px">
{p("Plak een tekst, kies een niveau (groep 7, brugklas, 3 havo, 5 vwo), en krijg een herschreven versie plus drie begripsvragen.", 30)}
{ul(["Twee LLM-stappen achter elkaar: herschrijven, dan vragen maken","De tweede stap werkt met wat de eerste maakte","Gepubliceerd als webapp: link delen, klaar"], 28)}
</div>
<div style="width:640px; display:flex; flex-direction:column; gap:20px; background:{LIGHT2}; padding:36px; border-radius:16px; border:1px solid {LINE}">
{interactie("roep het maar")}
<p style="font-family:{H}; font-size:40px; font-weight:600; color:{DARK}; line-height:1.2">Welk patroon is dit?</p>
{p("En: wat gebeurt er als ik een tekst met een leerlingnaam plak?", 28)}
</div>
</div>
''', notes="Live demo op udify.app/workflow/eINp9Jdd6ICodDZF. Antwoord: patroon 1, ook al zijn het twee stappen; jij bepaalt de volgorde. De naam-vraag leidt naar de checklist eigen materiaal.")
# 7 bouwblok 1
slide("bouw1", eyebrow("Bouwblok 1 · 25 minuten · in duo's") + h2("Bouw Tekst op niveau, met jouw niveaus") + f'''
<div style="display:flex; gap:32px">
<div style="flex:1; display:flex; flex-direction:column; gap:16px">
{ul(["Handleiding 1, stap 1 tot 8 (blz. 3 tot 15)","Model: <b>gpt-5-mini</b>, niet gpt-5","Vervang de vier niveaus door jouw groepen","Plak een tekst uit je eigen lesmateriaal, zonder namen","Klaar? Publiceer en stuur de link naar je buurduo"], 29)}
</div>
<div style="width:600px; display:flex; flex-direction:column; gap:16px; background:#ECFDF5; padding:32px; border-radius:16px; border:1px solid #A7D9BF">
<h3 style="font-family:{H}; font-size:32px; font-weight:600; color:#14532D">Zelf kiezen</h3>
{p("Klaar voor de tijd? Verander de tweede stap: woordenlijst, samenvatting, of een vertaling voor een nieuwkomer. Eén ding per keer, dan testen.", 27, "#14532D")}
</div>
</div>
''', section="Bouwblok 1: iedereen bouwt een workflow", transition="push",
notes="Loop rond. Meest voorkomende fouten: variabele niet via / ingevoegd; Publish vergeten; gpt-5 laten staan. Na 20 minuten: wie heeft een link? Laat twee duo's elkaars app proberen.")
# 8 demo agent + sabotage
slide("demo02", eyebrow("Demo · 10 minuten") + h2("Een agent met een kennisbank") + f'''
<div style="display:flex; gap:48px">
<div style="flex:1; display:flex; flex-direction:column; gap:20px">
{p("De Schoolgids-assistent beantwoordt vragen van ouders en leerlingen uit een schoolgids van twee pagina's. De Ouderbrief-assistent herschrijft een wollige brief volgens de schrijfwijzer en zegt wat hij veranderde.", 29)}
{ul(["Een kennisbank knipt je document in stukken; de knip bepaalt de kwaliteit","De beschrijving van de kennisbank is voor de agent, niet voor jou","Used Knowledge boven het antwoord: hij heeft echt gezocht"], 27)}
</div>
<div style="width:640px; display:flex; flex-direction:column; gap:20px; background:{LIGHT2}; padding:36px; border-radius:16px; border:1px solid {LINE}">
{interactie("sabotage")}
<p style="font-family:{H}; font-size:40px; font-weight:600; color:{DARK}; line-height:1.2">Laat hem struikelen</p>
{p("Bedenk in je duo één vraag waarop de assistent het antwoord niet mag weten, en één waarop hij een fout antwoord zou kunnen verzinnen. We proberen er drie live.", 27)}
</div>
</div>
''', section="Demo: agent, kennisbank en de sabotage", notes="udify.app/chat/cvLAmpk60m9HUXUy en udify.app/chat/OJCfxjBeBHCEbBPx. De sabotage laat zien waarom de regel 'zeg dat je het niet weet' in de instructie staat. Als hij toch verzint: perfect, dat is de les.")
# 9 pauze
slide("pauze", f'''
<div style="flex:1"></div>
<h1 style="font-family:{H}; font-size:140px; font-weight:600; color:{LIGHT}; line-height:1">Pauze</h1>
{p("10 minuten. Laat je app open staan; straks kies je een spoor.", 34, BODYD)}
<div style="flex:1"></div>
''', bg=ACC, color=LIGHT, footer=False, notes="Tijdens de pauze: check bij twijfelende duo's of de kennisbank van handleiding 2 al klaarstaat, dat scheelt in blok 2.")
# 10 bouwblok 2 keuze
slide("bouw2", eyebrow("Bouwblok 2 · 30 minuten · kies je spoor") + h2("Spoor A of spoor B") + f'''
<div style="display:flex; gap:32px">
<div style="flex:1; display:flex; flex-direction:column; gap:16px; background:#FFFFFF; padding:36px; border:2px solid {BLUE}; border-radius:16px">
<h3 style="font-family:{H}; font-size:38px; font-weight:600; color:{BLUE}">A · Agent met jouw document</h3>
{p("Handleiding 2. Upload een document van je school (protocol, studiewijzer, reglement), knip het goed, schrijf de instructie, test met vijf echte vragen en één vraag die er niet in staat.", 27)}
{p("Voor wie: iedereen die documenten heeft waar vragen over komen.", 26, "#6B7280")}
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:16px; background:#FFFFFF; padding:36px; border:2px solid {ACC}; border-radius:16px">
<h3 style="font-family:{H}; font-size:38px; font-weight:600; color:{ACC}">B · Poortwachter op je workflow</h3>
{p("Handleiding 7, deel B en C. Zet een Code-blok achter je Tekst op niveau: te lang, verboden woorden, een naam erin? Dan gaat de tekst niet door maar naar een mens, met de reden.", 27)}
{p("Voor wie: wie blok 1 vlot afhad en wil zien wat code hier doet. Plakken, niet typen.", 26, "#6B7280")}
</div>
</div>
''', section="Bouwblok 2: twee sporen, zelf kiezen", transition="push",
notes="Spoor B is pittiger; zet duo's met een programmeer-affiniteit bij elkaar. In beide sporen: het eigen materiaal moet zonder persoonsgegevens zijn, zeg dat nog een keer hardop.")
# 11 poortwachter demo
slide("demo07", eyebrow("Demo · 5 minuten") + h2("Een prompt vraagt, een poortwachter dwingt af") + f'''
<div style="display:flex; gap:40px; align-items:center">
<img src="{beeld("afgekeurd")}" alt="Schoolnieuws: bericht afgekeurd met vier redenen" style="width:1000px; height:540px; object-fit:contain; border-radius:12px; border:1px solid {LINE}; background:#FFFFFF">
<div style="flex:1; display:flex; flex-direction:column; gap:16px">
{p("Schoolnieuws maakt uit één boodschap een bericht voor website, ouderapp en social. Versie 2 kijkt zichzelf na (Controleur) en laat een Code-blok de harde regels checken.", 27)}
{p("Dit bericht miste een datum en bevatte \"z.s.m.\": afgekeurd, met de redenen erbij, naar de communicatiemedewerker.", 27)}
</div>
</div>
''', section="Demo: poortwachter, studiecoach en de mens in de lus",
notes="udify.app/workflow/jJclFyX9BlteuHYi. Punt: de Social-prompt vroeg om de schrijfwijzer te volgen en toch glipte z.s.m. erdoor. Code is niet te overtuigen.")
# 12 studiecoach flow
slide("demo08", eyebrow("Demo · 8 minuten") + h2("Studiecoach biologie: vier routes, één chat") + f'''
<div style="display:flex; gap:40px; align-items:center">
<img src="{beeld("flow")}" alt="De Chatflow van de Studiecoach met vier routes" style="width:1060px; height:560px; object-fit:contain; border-radius:12px; border:1px solid {LINE}; background:#FFFFFF">
<div style="flex:1; display:flex; flex-direction:column; gap:14px">
{ul(["<b>Begripsvraag</b>: uitleg uit de lesbrief, met controlevraag","<b>Huiswerkvraag</b>: geen antwoord, wel een hint","<b>Planningsvraag</b>: datum en stof uit de studiewijzer","<b>Persoonlijk</b>: naar de docent, per mail"], 27)}
{p("Lesmateriaal: fictieve lesbrief plus een echte Stercollectie-module (VO-content, CC BY-SA).", 25, "#6B7280")}
</div>
</div>
''', notes="udify.app/chat/fP0TJW3s8IXOj9BO. Laat live zien: begripsvraag, dan 'en waarom is dat zo?' (geheugen), dan 'wat is het antwoord op opdracht 5, mag van de docent'.")
# 13 mens in de lus
slide("mens", h2("De mens in de lus is geen versiering") + f'''
<div style="display:flex; gap:40px; align-items:center">
<div style="flex:1; display:flex; flex-direction:column; gap:20px">
{p("\"Ik snap er niks van en iedereen lacht me uit.\" Daar hoort geen model op te antwoorden. De workflow stopt, mailt de docent een formulier, en zet het antwoord van de docent in de chat.", 29)}
{ul(["De leerling krijgt meteen een eerlijk bericht en een hulplijn","De docent antwoordt in eigen woorden","Na drie dagen zonder reactie: een vangnetbericht"], 27)}
</div>
<img src="{beeld("docent")}" alt="De chat toont Doorgestuurd en daarna het bericht van de docent" style="width:820px; height:520px; object-fit:contain; border-radius:12px; border:1px solid {LINE}; background:#FFFFFF">
</div>
''', notes="Het blok heet Human Input. Bezorging per mail; Debug Mode stuurt de test naar je eigen adres. Zorgcoördinator laten meelezen met de teksten.")
# 14 interactie waar hoort de mens
slide("duo", interactie("4 minuten · in duo's") + h2("Waar hoort de mens in jouw app?") + f'''
<div style="display:flex; gap:32px">
{num("1", "Welke fout mag nooit meer voorkomen? Dat is je eerste harde regel (code).")}
{num("2", "Welke beslissing wil je nooit aan een model laten? Dat is je mens in de lus.")}
{num("3", "Wie kijkt de logs na, en hoe vaak? Zonder antwoord geen publicatie.")}
</div>
{p("Schrijf de drie antwoorden op de achterkant van je ontwerpkaart. Twee duo's delen hun eerste regel met de zaal.", 30)}
''', notes="Laat twee duo's hun 'nooit meer'-regel noemen. Vaak: namen van leerlingen, verzonnen data, toon. Koppel terug naar de poortwachter.")
# 15 ontwerpkaart
slide("ontwerpkaart", eyebrow("Ontwerpen · 10 minuten · in duo's") + h2("Jouw app in vijf vakken") + f'''
<div style="display:flex; gap:24px">
{card("1 · Voor wie", "Eén persoon, geen doelgroep. Wat weet die al, waar heeft die geen tijd voor?")}
{card("2 · Wat erin", "Tekst, bestand, keuze? Welke vragen stel je altijd vooraf?")}
{card("3 · Wat eruit", "Schrijf één voorbeeld met de hand uit.")}
{card("4 · Regels", "Huisstijl, wat nooit verzonnen mag worden, welk document de regels bevat.")}
{card("5 · Werkt het?", "Drie tests: twee gewone, één waarbij hij moet weigeren.")}
</div>
{p("Pen en papier. Pas als vak 3 een voorbeeld heeft en vak 5 drie tests, klik je op Create.", 30)}
''', section="Ontwerpen en pitchen: jouw volgende app", transition="push",
notes="Ontwerpkaart uitdelen (A4). Loop rond en stel bij elk duo de vraag: wie is die ene persoon?")
# 16 pitch
slide("pitch", interactie("5 minuten") + h2("Pitch in 30 seconden") + f'''
<div style="display:flex; gap:32px">
{num("30", "seconden per duo: voor wie, wat erin, wat eruit, welk patroon.")}
{num("3", "duo's pitchen; de zaal kiest de app die ze zelf zouden gebruiken.")}
{num("1", "vraag uit de zaal per pitch: waar hoort de mens?")}
</div>
''', notes="Drie duo's, niet meer: het vergelijkingsblok komt hierna. Kies verschillende doelgroepen: docent, ondersteuning, communicatie, directie. Applaus, geen jury.")
# 16b dezelfde agent, vier omgevingen
slide("vieromgevingen", eyebrow("Vier omgevingen · 7 minuten") + h2("Dezelfde zes agents, vier omgevingen") + table([
["Wat je wilt","Dify","Copilot Chat","ChatGPT","Gemini"],
["Wie mag bouwen","iedereen met een account","elke collega met schoolaccount","alleen met betaald abonnement","elke collega met schoolaccount"],
["Kennis meegeven","kennisbank, meerdere documenten","in de instructie, 8.000 tekens","tot 20 kennisbestanden","bestanden, Drive, NotebookLM"],
["Bronvermelding","citaten","nee","Bronnen","label per antwoord"],
["Bestanden eruit","sandbox: xlsx, ics, md","Word en Excel op OneDrive","Word en Excel als download","Export naar Docs en Sheets"],
["Eigen tool of API","ja: Swagger, MCP","nee","Handelingen","nee"],
["Mens in de lus","ja: Human Input","nee","nee","nee"],
["Kosten","credits of eigen sleutel","in de M365-licentie","per gebruiker per maand","in de Workspace-licentie"],
],[16,21,21,21,21], size=24) + p("<b>De instructie die je vandaag schreef, werkt in alle vier.</b> De omgeving bepaalt wat eromheen kan: kennis, bestanden, tools, en of een mens ertussen past.", 27),
section="Vier omgevingen: dezelfde agent, andere plek",
notes="Niet voorlezen. Wijs drie rijen aan: kennis meegeven (Copilot moet inkorten tot 8.000 tekens, de rest niet), mens in de lus (alleen Dify), en kosten. Zeg erbij dat van elke reeks een handleiding en een video bestaat met dezelfde zes agents.")
# 16c welke heb jij
slide("welkeheb", interactie("handen omhoog") + h2("Welke heeft jouw school, en wat bouw je daar?") + f'''
<div style="display:flex; gap:24px">
{card("Microsoft 365", "Copilot Chat zit in de licentie die je al hebt. Kennis in de instructie, Word en Excel eruit. Begin met de Schoolgids-assistent.")}
{card("Google Workspace", "Gems zitten in Workspace for Education, ook Fundamentals. Bestanden als kennis, export naar Docs en Sheets.")}
{card("ChatGPT-werkruimte", "Bouwen kan alleen met een betaald abonnement of een werkruimte van school. Tot 20 kennisbestanden, downloads.")}
{card("Meer nodig", "Eigen zoektool, een mens die goedkeurt, of ouders zonder schoolaccount: dan Dify, en een gesprek met je bestuur.", bg=DARK, tc=LIGHT, bc=BODYD, border=DARK)}
</div>
{p("Pak je ontwerpkaart erbij: in welke van de vier bouw je hem maandag?", 30)}
''',
notes="Handen omhoog per kolom; meestal is dat Microsoft en Google door elkaar. Laat twee mensen zeggen waar ze hun ontwerpkaart gaan bouwen en waarom. Sluit af met: de vier reeksen staan op dezelfde plek, met dezelfde zes agents.")
# 17 wat mag
slide("watmag", h2("Wat mag: vier vragen voor elke tool") + f'''
<div style="display:flex; gap:24px">
{card("Onder welk account?", "Privé, gratis, betaald of school met verwerkersovereenkomst. Alleen in het laatste geval heeft de school iets te zeggen.")}
{card("Welke gegevens gaan erin?", "Geen persoonsgegevens van leerlingen, ouders, collega's. Ook geen ongepubliceerd toetsmateriaal.")}
{card("Wordt erop getraind?", "Consumentenaccounts vaak wel, zakelijke accounts en API-verkeer meestal niet. Controleer het.")}
{card("Wie beslist en kijkt na?", "Voor jezelf: jij. Voor leerlingen: een schoolbesluit, toetsingskader, iemand die de uitvoer leest.")}
</div>
{p("Copilot en Gemini zitten al in school; of ze aan mogen is een bestuursbesluit op basis van de DPIA's van SURF en SIVON. Dify-cloud is voor oefenen met fictief materiaal; echt gebruik hoort bij het bestuur.", 27)}
''', section="Afsluiting: wat mag, wat mee naar huis, exit ticket",
notes="Bijlage AI-tools in het onderwijs gaat mee. AI Toetsingskader Funderend Onderwijs 1.0 (SIVON/Kennisnet, april 2026). Sandbox is oefenen; Experimenteerruimte ICT SOML is de plek voor echt gebruik.")
# 17b AVG en AI Act
slide("avgaiact", h2("AVG en AI Act: wat het betekent voor de apps van vandaag") + f'''
<div style="display:flex; gap:32px">
<div style="flex:1; display:flex; flex-direction:column; gap:14px; background:#FFFFFF; padding:32px; border:1px solid {LINE}; border-radius:16px">
<h3 style="font-family:{H}; font-size:34px; font-weight:600; color:{BLUE}">AVG</h3>
{ul(["De school is verantwoordelijke; Dify en de modelaanbieder zijn verwerkers, buiten de EU",
"Geen verwerkersovereenkomst (sandbox) = geen persoonsgegevens erin",
"Wél persoonsgegevens: het bericht van een leerling aan de docent (Studiecoach). Dan: grondslag, DPIA, bewaartermijn van de logs"], 26)}
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:14px; background:#FFFFFF; padding:32px; border:1px solid {LINE}; border-radius:16px">
<h3 style="font-family:{H}; font-size:34px; font-weight:600; color:{ACC}">EU AI Act</h3>
{ul(["<b>Art. 4</b>: AI-geletterdheid verplicht voor wie AI inzet (sinds feb 2025). Deze workshop is zo'n maatregel",
"<b>Verboden</b>: emotieherkenning in het onderwijs",
"<b>Hoog risico</b>: toelating, beoordelen van leerresultaten, toezicht bij toetsen. Onze apps beslissen niets over personen; een coach die leerlingen beoordeelt, wel",
"<b>Art. 50</b>: een chatbot zegt dat hij AI is"], 26)}
</div>
</div>
{p('Vuistregel: zodra een app iets beslist of vastlegt over een persoon, verandert de vraag van "werkt het?" in "mag het, en wie is verantwoordelijk?" Stand september 2026; toetsingskader SIVON/Kennisnet voor het actuele overzicht.', 26)}
''', notes="Niet als juridische les brengen. Twee zinnen per kant. De Studiecoach is het voorbeeld dat beide kanten laat zien: de persoonlijke route verwerkt persoonsgegevens (AVG), en een coach die zou beoordelen wie het niet snapt, schuift naar hoog risico (AI Act). Dat is een ontwerpkeuze.")
# 17c EU-kaders
slide("eukaders", h2("De EU-kaders: waar deze workshop in past") + f'''
<div style="display:flex; gap:24px">
{card("Engage with AI", "Begrijpen hoe het werkt: drie lagen, vijf patronen, de sabotage van de agent.")}
{card("Create with AI", "Zelf maken: bouwblok 1 en 2, de app met eigen materiaal.")}
{card("Manage AI", "Verantwoord inzetten: poortwachter, mens in de lus, wat mag, logs.")}
{card("Shape AI", "Keuzes maken en beïnvloeden: ontwerpkaart, pitch, de vraag waar de mens hoort.")}
</div>
{p("De vier domeinen van het <b>AI Literacy Framework</b> (OESO en Europese Commissie, 2026) voor leerlingen; voor docenten is <b>DigCompEdu</b> het kader. Elk domein combineert kennis, vaardigheden en houding. Zet deze workshop zo in je professionaliseringsplan: art. 4 AI Act vraagt erom.", 27)}
''', notes="Voor schoolleiders: dit is de zin die ze nodig hebben om de workshop te verantwoorden. Bronnen: ailiteracyframework.org en education.ec.europa.eu. Niet uitweiden; de bijlage AI-tools heeft de koppeling per blok.")
# 18 mee naar huis
slide("mee", h2("Wat je meeneemt") + table([
["Nr","Handleiding","Wat je ermee bouwt"],
["1","Van workflow naar agent","Tekst op niveau"],
["2","Agent met eigen kennisbank","Schoolgids-assistent"],
["3","Schoolcommunicatie","Ouderbrief-assistent, Schoolnieuws, Balie-stand"],
["4","Agent Console","Toetsweekplanner"],
["5","Transcriptietool","Van opname naar verslag"],
["7","Schoolnieuws met controle","Zelfkritiek en poortwachter"],
["8","Studiecoach voor je vak","Chatflow met routes, geheugen, docent in de lus"],
["Bijlagen","Ontwerpkaart · Ontwerppatronen · AI-tools in het onderwijs","Elke handleiding eindigt met Maak hem van jou"],
],[10,40,50], size=24) + p("En dezelfde zes agents in drie andere omgevingen: <b>Copilot</b>, <b>ChatGPT</b> en <b>Gemini</b>, elk zes handleidingen. Bij alle reeksen samen 28 uitlegvideo's met ondertiteling. Alles staat op <b>git.lexconsultancy.nl/guido</b>.", 26),
notes="Alle apps staan live op udify.app-links in de README. Vier reeksen, 26 handleidingen, 28 video's, alles CC BY-SA op git.lexconsultancy.nl. Zeg erbij dat de video's ook voor collega's zijn die er vandaag niet bij waren.")
# 19 exit ticket
slide("exit", interactie("2 minuten") + h2("Exit ticket") + f'''
<div style="display:flex; gap:32px">
<div style="flex:1; display:flex; flex-direction:column; gap:16px; background:#FFFFFF; padding:40px; border:1px solid {LINE}; border-radius:16px">
<h3 style="font-family:{H}; font-size:36px; font-weight:600">Maak de zin af</h3>
{p("\"Morgen bouw ik ... voor ... en de eerste harde regel is ...\"", 32)}
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:16px; background:#FFFFFF; padding:40px; border:1px solid {LINE}; border-radius:16px">
<h3 style="font-family:{H}; font-size:36px; font-weight:600">Eén vraag die open bleef</h3>
{p("Op een post-it bij de deur. We beantwoorden ze in de LAI-community.", 32)}
</div>
</div>
''', notes="Post-its meenemen. De open vragen zijn de agenda voor de vervolgbijeenkomst.")
# 20 slot
slide("slot", f'''
<img src="{LOGO}" alt="AI Netwerk Limburg" style="position:absolute; right:128px; top:96px; width:220px; height:180px; object-fit:contain">
<img src="{beeld("lex")}" alt="LeX Consultancy" style="position:absolute; right:148px; top:296px; width:130px; height:74px; object-fit:contain">
<img src="{beeld("qr")}" alt="QR-code naar git.lexconsultancy.nl/guido" style="position:absolute; right:128px; top:404px; width:230px; height:230px; border-radius:12px">
<p style="position:absolute; right:128px; top:644px; width:230px; text-align:center; font-size:21px; color:#9AA3B1">Alle vier de reeksen</p>
<div style="flex:1"></div>
<h1 style="font-family:{H}; font-size:96px; font-weight:600; line-height:1.05; color:{LIGHT}; width:1400px">Kies het eenvoudigste patroon dat werkt.</h1>
{p("En ontwerp voor de fout: wat gebeurt er als het misgaat, en wie ziet dat?", 34, BODYD)}
<div style="flex:1"></div>
{p("Handleidingen, dia's en links: " + BRON + " · ai-netwerk-limburg.nl · Experimenteerruimte ICT SOML", 28, "#9AA3B1")}
<div style="display:flex; gap:40px; align-items:flex-start; border-top:1px solid #3A4757; padding-top:24px; margin-top:-16px">
<div style="flex:0 0 520px; display:flex; flex-direction:column; gap:6px">
<p style="font-family:{H}; font-size:28px; font-weight:600; color:{LIGHT}; letter-spacing:1px">CC BY-SA 4.0</p>
<p style="font-size:22px; color:#9AA3B1; line-height:1.4">Eigenaar en contact:<br>{EIGENAAR}<br>{CONTACT}</p>
</div>
{p("Deze presentatie en de handleidingen mag je kopiëren, bewerken en hergebruiken, ook op je eigen school. Noem de bron (" + EIGENAAR + ", " + BRON + ") en deel je bewerking onder dezelfde licentie. Het logo van AI Netwerk Limburg valt buiten de licentie; namen van producten zijn merken van hun eigenaren.", 22, "#9AA3B1")}
</div>
''', bg=DARK, color=LIGHT, footer=False, notes="Bedankt. Wijs op de community en de Experimenteerruimte voor wie echt wil bouwen. Noem de licentie: alles mag mee naar school, met bronvermelding.")
deck = {"v":4,"createdOnFiles":{"v":1,"at":"2026-09-20T18:00:00Z"},"title":"Workshop AI Agents met Dify","order":order,"sections":sections,"cover":"cover",
"faces":{"rubik":{"family":"Rubik","href":"https://fonts.googleapis.com/css2?family=Rubik:wght@400;600&display=swap"},
"nunito-sans":{"family":"Nunito Sans","href":"https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap"}},
"designSystems":[]}
(P/"deck.json").write_text(json.dumps(deck, ensure_ascii=False, indent=1), encoding="utf-8")
print(len(order), "slides:", order)