better-future-llm-kit
v0.6.0
Published
Voorstel om de chat-+-LLM-functionaliteit uit **Organisatie-Anatomie** los te trekken tot een set herbruikbare bouwstenen, zodat elke app er een AI-assistent in kan hangen door alleen **een handvol dingen** te configureren in plaats van het hele apparaat
Readme
LLM-integratie — herbruikbaar chat-+-LLM-fundament
Voorstel om de chat-+-LLM-functionaliteit uit Organisatie-Anatomie los te trekken tot een set herbruikbare bouwstenen, zodat elke app er een AI-assistent in kan hangen door alleen een handvol dingen te configureren in plaats van het hele apparaat opnieuw te bouwen.
Status: geïmplementeerd, behalve de payment-gate. De vier pakketten leven in
packages/*(contract, gateway, chat-core, chat-vue) met een draaiend voorbeeld inexamples/demo-app/. De org-builder-migratie staat als config inexamples/org-builder/(US-038) en een tweede consument valideert de naden inexamples/second-app/(US-040). Fan-out + reducers (US-033) en de provider-conformance-suite (US-032) zijn af. Nog niet gebouwd: het policy-checkpoint / de paywall (US-015, US-019, US-022–US-026, US-031, US-035) — bewust uitgesteld tot er een echt betaalmodel is. De originele werkende implementatie leeft nog inorganisatie-anatomie/(zie referenties hieronder).
1. Waarom
De org-builder is feitelijk een generiek apparaat met vier app-specifieke knoppen. Het transport, de UI, de conversatie-orchestratie en een paar terugkerende patronen (gestructureerde output via tools, valideer-en-herprobeer, context-injectie) zijn domein-agnostisch. Wat per app verandert is klein en scherp af te bakenen.
Door die scheiding expliciet te maken krijg je:
- Eén chat-webcomponent die je overal inhangt.
- Eén serverless gateway die de API-sleutel verbergt, provider-/modelkeuze centraliseert én het policy-checkpoint (entitlement/payment + metering) huisvest.
- Vijf configpunten per app: systeemprompt, context-builder, output-schema, apply-callback, en (optioneel) entitlement/payment.
2. De naden (wat is generiek, wat verandert per app)
Huidige verdeling in Organisatie-Anatomie en het hergebruik-oordeel:
| # | Laag | Nu in de code | Herbruikbaar? | Wat per app verandert |
|---|------|---------------|---------------|------------------------|
| 1 | Chat-UI (webcomponent) | components/OrgBuilderChat.vue | ✅ generiek | titel, intro, theme-tokens |
| 2 | Conversatie-orchestratie | composables/useOrgBuilder.ts | ✅ generiek | — |
| 3 | LLM-proxy / gateway | netlify/functions/org-builder.ts | ✅ generiek | model-id, max_tokens, provider |
| 4 | Systeem-instructie | SYSTEM_PROMPT (hardcoded) | ❌ per app | de hele prompt |
| 5 | Context-injectie (input) | roleLibrary inline in prompt | ⚠️ patroon generiek | wélke state je meestuurt |
| 6 | Output-contract (tool/schema) | SUBMIT_TOOL + SNAPSHOT_SCHEMA | ⚠️ patroon generiek | het schema |
| 7 | Validatie + retry | client-side validateImport | ✅ patroon generiek | het schema |
| 8 | Apply-to-state adapter | store.importAsNewDataset() + view='graph' | ❌ per app | het hele koppelstuk |
| 9 | Feature-gating (cosmetisch) | ?ai-enabled=true | ✅ generiek | — (mag UI tonen/verbergen, géén poort) |
| 10 | Entitlement / payment gate (enforcement) | — (nog niet aanwezig) | ✅ patroon generiek | wie + welk plan/quota |
| 11 | Metering / observability | alleen prompt-caching, geen meting | ✅ generiek | — (voedt billing + quota) |
| 12 | Paywall-state (UI) | — | ✅ generiek | upgrade-CTA / tekst |
Kernobservatie: de drie naden die jij benoemde (UI, LLM-interface, in/output-contract) zijn correct, maar het "in/output-contract" valt in de praktijk uiteen in drie aparte naden (5, 6, 7) en er is een vierde die makkelijk over het hoofd wordt gezien: de apply-to-state adapter (8) — het stukje dat de LLM-output op de app-state toepast. Dat is het meest app-specifieke deel en hoort als callback naar buiten getrokken, niet ingebakken.
Over de payment gate (10–12): een betaalpoort is géén peer-laag naast UI en LLM —
het is een policy-checkpoint ín de gateway, want enforcement kan alleen server-side,
achter de sleutel. Client-gating (rij 9, ?ai-enabled=true) is triviaal te omzeilen en
mag daarom uitsluitend cosmetisch zijn — de echte poort staat op de enige plek waar
elke call langskomt en die de gebruiker niet kan vervalsen. Het splitst in twee helften
die op de metering-naad (11) aansluiten: pre-call authorize (mag dit, quota over?)
en post-call meter (registreer tokens/kosten → billing + volgende quota-check). Een
geweigerde call levert een nieuw antwoordtype payment_required op, zodat de chat (12)
een paywall toont i.p.v. een foutbanner.
De scheidslijn in één zin
Generiek = transport + UI + orchestratie + de patronen + het policy-checkpoint. Per app = vijf configs:
systemPrompt,buildContext,outputSchema,onResult,entitlement.
3. Pakketindeling
Vier pakketten, van server naar UI. Elk is los bruikbaar; samen vormen ze de volledige stack.
@llm-kit/gateway server provider-agnostische serverless proxy + tool-loop + retry + policy-checkpoint
@llm-kit/chat-core client framework-agnostische conversatie-orchestratie (geen Vue)
@llm-kit/chat-vue client de Vue 3 chat-webcomponent (UI + animaties + paywall-slot)
@llm-kit/contract shared schema-typen + validatiehelpers (door gateway én client gedeeld)
@llm-kit/billing server OPTIONEEL — kant-en-klare adapters (Stripe, usage-meter) voor het policy-checkpointDe gateway krijgt een middleware-keten rond de LLM-call: identify → authorize →
[LLM] → meter. De hooks zijn generiek; de app prikt er zijn billing-systeem in (zelf
geschreven, of via een @llm-kit/billing-adapter). @llm-kit/billing is optioneel —
een app zonder betaalmuur laat de hooks gewoon weg.
Afhankelijkheden (pijl = "hangt af van"):
chat-vue ──► chat-core ──► (fetch naar) gateway ──► provider-SDK
│ │ │
└────────► contract ◄──────┘ └──► billing-adapter (optioneel)Waarom vier en niet één: de gateway draait server-side (heeft de SDK + sleutel),
chat-core is puur client-mechaniek zonder Vue zodat het ook in React/vanilla kan,
en chat-vue is de presentatielaag. contract is bewust apart zodat hetzelfde
JSON-Schema zowel de tool-definitie (server) als de client-validatie voedt — één bron.
4. De vier configpunten
Alles wat een app moet leveren om een werkende AI-assistent te krijgen:
| Config | Type | Org-builder-invulling |
|--------|------|------------------------|
| systemPrompt | string \| (ctx) => string | de interviewregels uit SYSTEM_PROMPT |
| buildContext | () => string \| object | JSON.stringify(roleLibrary) + huidige org-state |
| outputSchema | { name, description, schema } | submit_snapshot + SNAPSHOT_SCHEMA |
| onResult | (output) => void \| Promise | store.importAsNewDataset(...) + view='graph' |
| entitlement | { identify, authorize, meter } | server-side; koppelt aan Stripe/intern plan (nog te bouwen) |
entitlement is optioneel: laat je het weg, dan is er geen poort (huidig gedrag).
Plus optionele presentatie-config (titel, intro-bubble, theme) en transport-config
(model-id, endpoint). Zie interfaces.ts voor de volledige typen.
Het policy-checkpoint (de payment gate) in detail
De gateway draait een middleware-keten rond de LLM-call. Alleen de gateway is een betrouwbaar handhavingspunt — server-side, achter de sleutel, op de route waar elke call hoe dan ook langskomt.
request ─► identify(req) wie is dit? (JWT/cookie/header → Principal)
─► authorize(principal) mag dit, quota over? ── nee ─► { type: 'payment_required', … }
─► [ LLM-call + tool-loop + retry ]
─► meter(principal, usage) registreer tokens/kosten → billing + quota-teller
─► responseDe app levert drie kleine functies (net als onResult app-specifiek):
identify(req)— haal de gebruiker/tenant uit het verzoek. Geen identiteit en de route is betaald? → meteenpayment_required.authorize(principal, ctx)— check abonnement/quota tegen je billing-systeem. Retourneert toestaan / weigeren-met-reden.meter(principal, usage)— post-call: leg verbruik vast. Dit is dezelfde data als de observability-naad (11), maar nu load-bearing voor usage-based billing.
Aan de clientkant is payment_required een terminale state naast done en error:
chat-core zet 'm, chat-vue rendert een paywall-slot (upgrade-CTA) i.p.v. de rode
foutbanner. De bestaande ?ai-enabled=true-gating (rij 9) blijft puur cosmetisch —
hij bepaalt of de knop zichtbaar is, niet of de call mag.
Per-seat én usage-based — dezelfde hooks
De interface legt geen betaalmodel vast; het zit in wat authorize/meter lezen en
schrijven. Beide modellen — en de combinatie — vallen uit dezelfde drie functies:
| Model | authorize checkt | meter doet | Scope (identify) |
|-------|--------------------|--------------|--------------------|
| Per-seat abonnement | "plan actief / seat geldig?" | (alleen loggen voor inzicht) | meestal tenantId |
| Usage-based / credits | "saldo / quota > 0?" | saldo aftrekken o.b.v. tokens | userId of tenantId |
| Hybride | "plan actief én maand-bundel niet op?" | bundel-teller bijwerken, daarna pay-as-you-go | tenantId + userId |
Een app kan zelfs per route/feature verschillen (gratis chat, betaalde bouwer) door
gewoon een andere entitlement aan die gateway-handler te hangen. De Principal draagt
zowel userId als tenantId, dus per-gebruiker én per-organisatie gaten zijn mogelijk
zonder interface-wijziging. Conclusie: we hoeven nu niet te kiezen — het ontwerp houdt
beide open; de keuze zit puur in de billing-backend achter de hooks.
5. Hoe org-builder inkrimpt
Na extractie is org-builder geen eigen apparaat meer maar een dunne configuratie
van het fundament. Zie examples/org-builder.config.md
voor de volledige before/after. Kort:
OrgBuilderChat.vue(≈360 regels) →<LlmChat :config="orgBuilderConfig" />(≈10 regels)useOrgBuilder.ts(≈150 regels) → verdwijnt; orchestratie zit in@llm-kit/chat-corenetlify/functions/org-builder.ts(≈280 regels) →createGatewayHandler(orgBuilderGatewayConfig)(≈15 regels) + de prompt/het schema als data
De prompt, het schema, de context-builder en de apply-callback blijven; al het mechaniek eromheen wordt geïmporteerd.
6. Patronen die we meteen beter doen dan nu
De extractie is ook het moment om drie zwakke plekken in de huidige implementatie op te lossen — ze zijn generiek, dus ze horen in het fundament:
- Validate-and-retry loop. Nu krijgt het model één kans; een ongeldig snapshot
wordt client-side gevangen en als rode banner getoond (
useOrgBuilder.ts:121-127). Het fundament stuurt de validatiefout terug naar het model voor een herkansing (N pogingen) vóór het opgeeft. Dit is puur generiek. - Streaming. De huidige call is blocking (
anthropic.messages.create). De gateway krijgt een streaming-variant zodat de chat token-voor-token kan tonen; de paragraaf-animatie in de UI wordt dan echte streaming i.p.v. gesimuleerd. - Observability + metering. Tokens/kosten/latency loggen en provider kunnen wisselen
via één env-var. Nu zit alleen prompt-caching erin (
org-builder.ts:248-253), geen meting. Dezelfde meet-hook is demeter-helft van de payment gate — bouw 'm één keer, gebruik 'm voor zowel inzicht als billing.
7. Migratiepad
Incrementeel, elke fase levert werkende code op:
@llm-kit/contract— trekSNAPSHOT_SCHEMA-stijl typen + validatiehelper los. Geen gedragsverandering.@llm-kit/gateway—createGatewayHandler(config)factory;org-builder.tswordt de eerste consument. Voeg de retry-loop toe.@llm-kit/chat-core— generaliseeruseOrgBuilder.tsnaar een config-gedreven conversatie-engine metonResult-callback.@llm-kit/chat-vue— generaliseerOrgBuilderChat.vuetot<LlmChat>; org-builder wordt een config-object. Voeg depayment_required-state + paywall-slot toe.- Policy-checkpoint — voeg de
identify → authorize → meter-middleware aan de gateway toe (demeter-hook bestaat dan al uit fase 2's observability). Pas wanneer er écht een betaalmodel is; org-builder draait tot dan metentitlementweggelaten. - Tweede app — pas het fundament toe op een nieuwe usecase om de naden te valideren (de echte test of de abstractie klopt).
Vuistregel: pas abstraheren als de tweede consument er is. Fase 1–2 zijn veilig (puur losmaken), fase 3–5 wachten idealiter tot er een concrete tweede usecase of een echt betaalmodel is, zodat we niet de verkeerde naden vastleggen.
8. Open vragen
- Webcomponent of Vue-component? ✅ Beide bestaan nu. De Vue-component (
/vue) blijft voor Vue/Nuxt-apps; daarnaast is er een framework-vrije custom element<llm-chat>(/element, vanilla + Shadow DOM, volledig getokeniseerd) als portable variant — zieFRONTEND.md. - Waar leeft de systeemprompt? Org-builder houdt
SKILL.mdals bron van waarheid en synctSYSTEM_PROMPThandmatig (org-builder.ts:18-19). Wil je dat patroon (skill = bron, prompt = afgeleide) onderdeel van het fundament maken, of per app vrijlaten? - Monorepo of losse repo's? Vijf pakketten met onderlinge afhankelijkheden schreeuwen om
een workspace. Past dit in een bestaande monorepo onder
for-the-money/? - Stripe of intern saldo-systeem? De
authorize/meter-implementaties praten met je billing-backend; die keuze staat los van de interface maar moet wel gemaakt worden.
Referenties (huidige implementatie)
| Onderdeel | Pad |
|-----------|-----|
| Chat-UI | organisatie-anatomie/components/OrgBuilderChat.vue |
| Orchestratie | organisatie-anatomie/composables/useOrgBuilder.ts |
| Gateway | organisatie-anatomie/netlify/functions/org-builder.ts |
| Output-schema | org-builder.ts:37-138 (SNAPSHOT_SCHEMA, SUBMIT_TOOL) |
| Systeemprompt | org-builder.ts:145-201 |
| Apply-to-state | useOrgBuilder.ts:104-127 |
| Feature-gating | organisatie-anatomie/components/TopHeader.vue:52-68 |
