discovery-by-llm/docs/logboek.md
2026-08-10 14:46:10 +02:00

203 lines
9.1 KiB
Markdown

# Logboek
Bijgehouden tijdens de bouw. Per stap: wat ik deed, waarom, wat ik heb laten
vallen.
## Dag 1 - korte sessie in de avond
### Opzet
Wat ik deed:
- Afspraken vastgelegd in AGENTS.md (architectuur, naamgeving, wanneer
commentaar, wat de linters afdwingen).
- Backend-skelet: Python 3.13 + FastAPI via uv, mappenstructuur volgens
ports-and-adapters (domain / ports / adapters / pipeline / api), config
met alleen de app-mode (demo of live), health-endpoint met een smoke test.
- Frontend-skelet: Vue 3 + TypeScript via Vite, met eslint, prettier en
vue-tsc.
- docker-compose die 1 image bouwt: frontend-build en API samen op dezelfde
poort. De poort ligt vast omdat de geregistreerde Spotify redirect-URI
moet kloppen.
- CI op elke push: ruff, mypy, pytest voor de backend; typecheck, lint en
build voor de frontend. Groen vanaf de eerste commit.
- Repo op eigen Forgejo met een mirror naar GitHub.
Waarom:
- Structuur eerst, dan blijft elke volgende stap klein en controleerbaar.
- `docker compose up` is het opleverbare, dus dat werkt vanaf het begin van development.
- Alles Engels behalve dit logboek; ik volg de taal van de opdracht.
Wat ik heb laten vallen of uitgesteld:
- Domainmodellen, protocols, prompts, pipeline-instellingen en het
frontend-typecontract bewust nog niet neergezet. Die ontstaan in de stap
waar ze horen. Ik probeer op die manier bewust vroeg drift en dode code te voorkomen.
- Geen apart beslisdocument. De motivering staat in de README en hier.
## Dag 2
### Spotify-koppeling
Wat ik deed:
- Login via Spotify met PKCE en cookie-sessies: tokens blijven server-side, de browser krijgt alleen een opaque HttpOnly cookie.
- Dunne async client op de Spotify Web API met per-endpoint retry policy:
leesacties herhalen maximaal 1 keer en alleen binnen een grens
(Retry-After), schrijfacties op playlists nooit.
- Token-refresh is single-flight: parallelle requests delen 1 refresh in
plaats van er allemaal zelf een te starten.
- Mapping van Spotify-JSON naar eigen modellen op 1 plek; kapotte items
vallen weg in plaats van dat ze de app breken.
- Getypeerde errors en 12 transport- en routetests op een mock transport.
- Na een eerste review de login-flow uit de routes getrokken naar een eigen
module (routes zijn nu dunne passthroughs) en de retry policy herschreven
naar een lineaire keten van losse regels in plaats van een loop met flags;
foutdetails uit de Spotify-body worden meegenomen in de getypeerde errors
en quota exhaustion (QUOTA_EXCEEDED) wordt apart herkend en nooit opnieuw geprobeerd.
- Daarna het API-contract vastgelegd: request-schema en de gestreamde
events (metadata / track / warning / error / done), gespiegeld in
TypeScript.
Waarom:
- Schrijfacties blind herhalen kan dubbele playlist-items opleveren; dat
risico sluit ik structureel uit in de retry policy.
- Het contract eerst bevriezen maakt parallel werken aan frontend en
pipeline mogelijk zonder elkaar te breken.
Wat ik heb laten vallen of uitgesteld:
- De naam-naar-ID cache en de port-interfaces doorgeschoven naar de
pipeline-stap; daar bestaat het ontwerp pas echt.
- OpenAPI-codegen voor het contract overwogen en afgewezen: de kern van dit
contract is de event-stream en die modelleert OpenAPI niet.
### Pipeline
Wat ik deed:
- Domeinbasis: titel/artiest-matching (normalisatie, met tolerantie voor
Spotify's versie-suffixen zoals "- Remaster 2023"), compressie van het
Spotify-smaakprofiel naar prompttekst plus een set bekende track-ids,
prompts als data in een eigen module, en alle instelbare waarden
gesectioneerd in de config met per waarde het waarom.
- Twee LLM-calls achter een eigen interface: call 1 interpreteert de
vraag (stemming, activiteit, taal, bekendheid) en stelt 30-40 echte
nummers voor als gestructureerde output; call 2 herordent uitsluitend
geverifieerde nummers en streamt per nummer een eerlijke onderbouwing.
Elke output wordt gevalideerd, met hooguit 1 herstelpoging.
- Grounding: begrensde parallelle search fan-out met early stop, een deadline,
een naam-naar-id cache en twee aparte metrieken: niet gevonden versus
wel gevonden maar afgekeurd door de match-check. Een track-id dat niet in
de geverifieerde pool zit kan nooit bij de gebruiker terechtkomen.
- Na review de orkestratie herschreven: de stream-functie leest nu als de
pipeline-stappen zelf, en de selectie-logica (alleen pool-ids, geen
duplicaten, begrensd aantal, ranking) zit in 1 kleine klasse die zowel
het normale pad als de fallback bedient.
Waarom:
- De LLM is hier de aanbeveler, maar mag alleen creatief zijn tussen twee
deterministische muren: alles wat hij ziet is echte data, alles wat de
gebruiker ziet is geverifieerd op Spotify. Een verzonnen nummer valt
stilletjes af en verschijnt nooit.
- Zoeken geeft maximaal 10 resultaten per call, dus resolutie is per
definitie een fan-out; liever een kandidaat laten vallen dan het
verkeerde nummer aanbevelen.
Wat ik heb laten vallen of uitgesteld:
- Refinements doen geen nieuwe search-ronde: turn 2 herordent de bestaande
geverifieerde pool. Sneller en consistent, maar een refinement haalt
geen nieuwe nummers op. Dit is een bewuste afweging, mocht er tijd over
zijn is dit 1 van de uitbreidingen die ik op zou kunnen pakken.
### Streaming API
Wat ik deed:
- De pipeline als streamend endpoint: POST /api/recommendations levert
NDJSON events (metadata / track / warning / error / done); een
client-disconnect stopt de stream en de grounding.
- Playlist-endpoint dat schrijft met een vaste naam-prefix, zodat
aangemaakte playlists later in bulk op te ruimen zijn (ik gebruik mijn
persoonlijke spotify account/abonnement voor de demo).
- Request-timing middleware met per-request counters (Spotify calls, cache
hits, LLM tokens) in gestructureerde logs.
- Seed session voor gehost draaien: in live mode installeert een refresh
token bij startup een sessie, zodat een publieke instantie werkt zonder
interactieve login.
Waarom:
- Streamen maakt de wachttijd eerlijk: de eerste kaart telt. Een afgebroken request mag geen werk laten doorlopen.
### Review-fixes en de account-quota
Wat ik deed:
- Twee fixes uit de live-test: gebruikers-id key op het stabiele account_id
veld (met id als fallback), en de intent-call accepteert nu een
afwijkend kandidaten-aantal in plaats van hard te falen als het model er
34 in plaats van 35 teruggeeft.
- Tijdens het opnemen van demo-fixtures de dagelijkse development-quota
van de Spotify-app geraakt: honderden searches in enkele minuten, daarna
QUOTA_EXCEEDED met een Retry-After van bijna 7 uur. Het systeem
degradeerde zoals ontworpen: een eerlijke foutmelding, geen stille
fallback naar verzonnen resultaten.
Waarom:
- De quota is per developer-account en per dag; bulk-werk zoals fixtures
opnemen moet dus gebudgetteerd, en het cache-ontwerp (naam-naar-id,
smaakprofiel) is noodzakelijk om binnen de quota te blijven.
### Meer live-test fixes
Wat ik deed:
- Matching accepteert nu ook nummers die alleen als release met een
versie-suffix bestaan ("Immunity - Remaster 2023"), met een lichte
voorkeur voor de originele release bij gelijke score; de live-test liet
zien dat echte nummers hierdoor onterecht afvielen.
- Mislukte rerank-pogingen worden met reden gelogd, en de token-ceilings
van beide LLM-calls zijn verhoogd: adaptive thinking telt mee in
max_tokens en kon de gestructureerde output afkappen.
- Per unresolved kandidaat wordt titel, artiest en status gelogd, zodat
zichtbaar is WAT het model verzon in plaats van alleen hoeveel.
### Eerlijke gedeeltelijke resultaten
Wat ik deed:
- Onder de grounding-floor geeft de pipeline nu een warning plus de nummers
die wel geverifieerd zijn, in plaats van een lege foutmelding; alleen een
volledig lege pool is nog een terminal error.
Waarom:
- Vijf goede, geverifieerde nummers zijn een bruikbaar antwoord; een
foutmelding die echte resultaten verbergt is dat niet. Bij "verras me
met iets nieuws" vragen valt een groot deel van de kandidaten af bij de
verificatie, dus juist daar telt dit. Een weg om dit potentieel te voorkomen/verbeteren in de toekomst is het verbeteren van de prompt, of de LLM met behulp van een derde partij API die zonder de Spotify API te overbelasten gebruikt kan worden om echte nummers te vinden.
### Frontend: fundament
Voor de frontend heb ik gedurende de eerste paar uur op de achtergrond Open Design (een
design-tool) laten lopen. Ik ben geen visual designer, maar op deze manier lukt het mij om met minimale
effort een geschikt UI bouwpakket te ontwikkelen. De oplevering heeft zo'n vorm dat ik het
gemakkelijk door een cli agent kan laten uitbouwen in Vue.
Wat ik deed:
- Design tokens uit het ontwerppakket (donker thema, typografie, spacing,
motion) als basis voor alle componenten.
- Typed stream client: fetch met een ReadableStream, regelgebaseerde
NDJSON-parsing naar de contract-events, en AbortController-cancellation
zodra een nieuwe vraag start.
- API client voor sessie-status, logout en playlist-save.
Waarom:
- De frontend is de tweede consument van het bevroren contract: elke event
wordt tegen de TypeScript-union gevalideerd in plaats van los geparst;
wat niet valideert is een transport failure, geen gok.