166 lines
7.4 KiB
Markdown
166 lines
7.4 KiB
Markdown
# Logboek
|
|
|
|
Bijgehouden tijdens de bouw. Per blok: 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/abbonement 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.
|