Skip to content

Latest commit

 

History

History
356 lines (287 loc) · 22.9 KB

File metadata and controls

356 lines (287 loc) · 22.9 KB

Digitale Assistent

De Digitale Assistent biedt ondernemers hulp met regelgeving, subsidies en bedrijfsregistratie. Twee LLM-backends (VLAM en Claude) raadplegen overheidsbronnen via twee transportmechanismen: MCP en CLI. Beide zijn API-wrappers die dezelfde externe API's aanspreken — het verschil zit in hoe ze worden aangeroepen (zie PDR-005).

Quickstart, endpoints, .env-configuratie en Docker-instructies staan in de root README. Dit document beschrijft de architectuur, routering en scenario's.

Zie Product Decision Records voor gemaakte keuzes in de opzet.

  • PDR-001 — dual-backend keuze (VLAM + Claude)
  • PDR-005 — CLI vs MCP als transport
  • PDR-006 — feasibility-onderzoek en conclusie
  • PDR-007 — demo-persona's, netbeheerder/Business Wallet en EML-maatregelen
  • PDR-008 — generieke RegelRecht-tool en energiegegevens via de Business Wallet

Architectuur

                     ┌──────────────────────────────────────────┐
  moza-portaal ─────▶│  host (poort 8000)                       │
  /chat endpoint     │  VLAM (Mistral) of Claude                │
                     │  + tools via MCP of CLI (instelbaar)      │
                     └──────┬───────┬───────┬───────┬───────────┘
                            │       │       │       │
                   MCP:  server   server  server  server  (Python, persistent)
                   CLI:  kvk-cli koop-cli rr-cli  rvo-cli (Bash, on-demand)
                            │       │       │       │
                            ▼       ▼       ▼       ▼
                          kvk.nl  koop    regelrecht rvo
                         (API)   (API)    (API)    (API)
Bron MCP-server CLI-tool Type Externe API
KvK Resource + Tool kvk-cli Bedrijfsgegevens (sessie-gebonden) KvK Test API + BAG (Kadaster)
KOOP Resource + Tool koop-cli Regelingenbank wetten.overheid.nl
RegelRecht Tool (non-muterend) regelrecht-cli Generieke regel-executie (execute_law) poc-machine-law API
RVO Tool (muterend) rvo-cli Subsidies en rapportages RVO API (mock)
Business Wallet (netbeheerder) Tool (geen) Energiegegevens als credential, met toestemming gedeeld netbeheerder (mock)

De host werkt ook zonder MCP-servers of CLI-tools; de assistent antwoordt dan op basis van eigen kennis.

RegelRecht-tool (MCP) en CLI-divergentie: op MCP biedt RegelRecht één generieke tool regelrecht__execute_law(law, parameters, overrides) waarmee elke wet (informatieplicht, EML-maatregelen) wordt uitgevoerd (zie PDR-008). Het CLI-transport loopt bewust achter en houdt regelrecht__check (geen netbeheerder/Business Wallet); de demo draait daarom op MCP (vlam/claude). Business Wallet: de energiegegevens komen als een door de Business Wallet gepresenteerde credential (uitgever = netbeheerder), demo-presentatielaag, geen echte Business Wallet-koppeling (PDR-008). De woonfunctie/gebruiksdoel komen live uit de BAG (Kadaster, met BAG_API_KEY) met demo-fallback.

MCP vs CLI: MCP-servers draaien als permanente processen en ondersteunen zowel tools als resources. CLI-tools zijn Bash-scripts die on-demand worden aangeroepen en alleen tools ondersteunen (geen resources). Zie PDR-005 voor een uitgebreide vergelijking.

KvK testomgeving: De KvK-server haalt bedrijfsgegevens op via de KvK Test API (api.kvk.nl/test/api/v1/basisprofielen) voor het KvK-nummer dat de host per aanroep meegeeft. De host bepaalt dat nummer server-side uit de sessie (zie PDR-009); het LLM en de gebruiker kunnen het niet kiezen. Er is geen hardcoded demo-bedrijf meer. In de Beta wordt het KvK-nummer bepaald door echte authenticatie (eHerkenning/DigiD, BETA-02).

Testgebruiker toevoegen (gesloten testgroep)

De identiteit wordt server-side vastgesteld en afgedwongen (PDR-009). De frontend stuurt per request het KvK-nummer van de gekozen persona mee in de header X-Test-User; de host controleert dat tegen de allowlist TEST_KVK_NUMMERS in de backend-env en injecteert het nummer vervolgens bij elke bron-aanroep. Een testgebruiker voeg je toe door het KvK-nummer aan die lijst toe te voegen en de host te herstarten. Staat een nummer er niet in, of ontbreekt de header, dan blokkeert de host met een nette "log eerst in"-melding en raadpleegt geen bron. Wisselen van persona kan zonder herstart.

De allowlist is geen geheim — de KvK-nummers van de persona's staan al publiek in _data/personas.json van de frontend. Ze is wél een harde grens: zonder die grens zou de KvK-server voor een willekeurig meegestuurd nummer de echte KvK Test API gaan bevragen. De garantie die er wél toe doet staat hier los van: het LLM ziet kvk_nummer niet (het is uit alle tool-schema's gestript) en kan de identiteit dus niet kiezen, ook niet als de gebruiker in het gesprek een ander nummer noemt.

De gesloten testgroep telt vier profielen. Ze zijn zo gekozen dat dezelfde vraag over de informatieplicht energiebesparing verschillende kanten op loopt, zodat een gebruikerstest niet één tak van de regel test:

KvK Bedrijf Persona (frontend) Verbruik Uitkomst informatieplicht
85234567 Koffiezaak Noon koffiezaak 61.250 kWh / 9.800 m³ geldt, elektriciteit boven de drempel
62345681 Kwekerij De Bloesem bloemenkweker 420.000 kWh / 140.000 m³ geldt, gas boven de drempel, onder de onderzoeksdrempel
56789012 Roots & Locks haarstylist 14.800 kWh / 1.900 m³ geldt niet, onder beide drempels
61234570 Vogel Bouwregie B.V. bouwmanagement 88.400 kWh / 12.600 m³ geldt, elektriciteit boven de drempel

Alle vier zijn volledig mock-bediend in de KvK-server (MOCK_PROFIELEN, MOCK_VESTIGINGEN, MOCK_VESTIGINGSPROFIELEN, MOCK_EIGENAREN, _BAG_DEMO_FALLBACK) en de netbeheerder-server (MOCK_VERBRUIK), dus ze werken zonder netwerk of API-key. De persona-id's corresponderen met _data/personas.json in MinBZK/moza-poc. De overige persona's daar staan bewust niet in de allowlist: de assistent antwoordt dan "log eerst in" in plaats van gegevens van een bedrijf te tonen dat de backend niet kent.

De frontend bepaalt welk profiel nodig is, niet deze repo. Zet MinBZK/moza-poc een persona op actief die hier ontbreekt, dan leest de respondent zijn bedrijf op het scherm en krijgt hij van de assistent "log eerst in" — de sessie is voorbij voordat er één vraag is gesteld. services/host/tests/test_personas_frontend_pariteit.py leest _data/personas.json van een checkout naast deze repo (of via MOZA_POC_PERSONAS) en faalt op precies dat gat, en op elk veld dat uiteenloopt met het scherm. Staat die checkout er niet, dan slaat de test over met een luide reden: de pariteit is dan ongetoetst, niet in orde.

Welk veld op welk niveau hoort, volgt de echte KvK-API en niet wat handig uitkomt. Het basisprofiel draagt totaalWerkzamePersonen; de uitsplitsing naar voltijd en deeltijd staat in het vestigingsprofiel (/v1/vestigingsprofielen/<nr>), dat kvk__mijn_bedrijf erbij haalt en in de hoofdvestiging mengt. Het RSIN komt van kvk__eigenaar. Zet je zulke velden op de profielwortel, dan werkt de mock wél en een echt KvK-nummer niet.

Routering: welke bron bij welke vraag?

Het LLM kiest op basis van de systeemprompt welke MCP-server wordt aangesproken. De routeringsregels zijn gedefinieerd in host/prompts/blocks/shared/tool_usage.md en volgen onderstaande beslisboom:

flowchart TD
    Start([Gebruikersvraag]) --> Q1{Vraagt naar eigen\nbedrijfsgegevens?}

    Q1 -- ja --> KVK_TOOL[/Tool: kvk__mijn_bedrijf/]
    Q1 -- nee --> Q5{Vraagt of een verplichting\nvan toepassing is?}

    Q5 -- ja --> KVK_TOOL
    KVK_TOOL --> Q5b{KvK-nummer\nverkregen}
    Q5b --> RR_TOOL[/Tool: regelrecht__execute_law/]
    RR_TOOL --> Q5c{Wil gebruiker\nde wettekst lezen?}
    Q5c -- ja --> KOOP_RES[/Resource: koop://regeling/bwb_id/]
    Q5c -- nee --> Antwoord
    Q5 -- nee --> Q4{Vraagt naar een\nspecifieke wet of regel?}

    Q4 -- ja --> KOOP_TOOL[/Tool: koop__zoek_regelgeving/]
    KOOP_TOOL --> Q4b{Wil gebruiker\nde inhoud lezen?}
    Q4b -- ja --> KOOP_RES
    Q4b -- nee --> Antwoord
    Q4 -- nee --> Q3{Bevat een\nBWB-ID?}

    Q3 -- ja --> KOOP_RES
    Q3 -- nee --> Q6{Vraagt naar subsidies\nof rapportage?}

    Q6 -- ja --> Q6b{Wil indienen\nof alleen informatie?}
    Q6b -- informatie --> RVO_ZOEK[/Tool: rvo__zoek_regeling/]
    Q6b -- indienen --> Q6c{Gebruiker\nbevestigt?}
    Q6c -- ja --> RVO_IND[/Tool: rvo__indienen/]
    Q6c -- nee --> Antwoord
    Q6 -- nee --> Q7{Algemene vraag\nover regelgeving?}

    Q7 -- ja --> KOOP_TOOL
    Q7 -- nee --> Q8{Binnen het taakgebied?\nbedrijf, verplichting,\nregelgeving, subsidie}

    Q8 -- ja --> EIGEN[Eigen kennis\n+ disclaimer]
    Q8 -- nee --> AFWIJZING[Afwijzing in 3 delen:\nonderwerp benoemen,\nbuiten taakgebied,\nvoorbeeldvraag die wel kan]

    KOOP_RES --> Antwoord([Antwoord aan gebruiker])
    RVO_ZOEK --> Antwoord
    RVO_IND --> Antwoord
    EIGEN --> Antwoord
    AFWIJZING --> Antwoord

    style KVK_TOOL fill:#4A90D9,color:#fff
    style KOOP_RES fill:#48BB78,color:#fff
    style KOOP_TOOL fill:#4A90D9,color:#fff
    style RR_TOOL fill:#4A90D9,color:#fff
    style RVO_ZOEK fill:#4A90D9,color:#fff
    style RVO_IND fill:#ED8936,color:#fff
    style EIGEN fill:#A0AEC0,color:#fff
    style AFWIJZING fill:#A0AEC0,color:#fff
Loading

Legenda: groen = Resource (read-only ophalen) blauw = Tool (read-only zoeken/berekenen) oranje = Tool (muterend, vereist bevestiging) grijs = geen bron geraadpleegd (eigen kennis of een afwijzing met brug)

Bij gecombineerde vragen geldt de volgorde: KvK (wie?) → RegelRecht (wat geldt er?) → KOOP (verdieping wettekst) → RVO (actie ondernemen).

Informatieplicht-flow (demo, persona Claudia/Noon): deze flow voegt stappen toe die de beslisboom hierboven niet apart toont: (1) eerst toestemming vragen voordat er bronnen worden geraadpleegd, (2) energieverbruik uit de Business Wallet (netbeheerder__verbruik, met toestemming), (3) regelrecht__execute_law voor de informatieplicht, (4) regelrecht__execute_law voor de geldende EML-maatregelen, (5) rvo__indienen met geautomatiseerde toets. Zie PDR-007 en PDR-008.

Voorbeeldscenario's

Scenario 1: Eigen bedrijfsgegevens opvragen (KvK)

Gebruiker: "Wat zijn mijn bedrijfsgegevens?"

sequenceDiagram
    actor Gebruiker
    participant Host as AI-platform (Host)
    participant KvK as MCP Server (KvK)

    Gebruiker->>Host: "Wat zijn mijn bedrijfsgegevens?"
    Note over Host: Routeringsregel: eigen gegevens<br/>→ kvk__mijn_bedrijf
    Host->>KvK: tools/call [mijn_bedrijf]
    Note over KvK: Sessie-gebonden:<br/>retourneert profiel van<br/>de ingelogde gebruiker
    KvK-->>Host: basisprofiel (KvK API-formaat) + provenance
    Host->>Gebruiker: "Uw bedrijf Test BV Donald (KvK 68750110)<br/>is een Besloten Vennootschap gevestigd<br/>in Lollum. SBI-code: 01241."
Loading

Scenario 2: Regelgeving zoeken (KOOP)

Gebruiker: "Welke regels gelden er voor energiebesparing?"

sequenceDiagram
    actor Gebruiker
    participant Host as AI-platform (Host)
    participant KOOP as MCP Server (KOOP)
    participant SRU as SRU zoekservice

    Gebruiker->>Host: "Welke regels gelden er voor energiebesparing?"
    Note over Host: Routeringsregel: vraag over regelgeving<br/>→ koop__zoek_regelgeving
    Host->>KOOP: tools/call [zoek_regelgeving, trefwoord="energiebesparing"]
    KOOP->>SRU: GET zoekservice.overheid.nl/sru/Search<br/>?x-connection=BWB&query=overheidbwb.titel any "energiebesparing"
    SRU-->>KOOP: XML met regelingen
    KOOP-->>Host: resultaten + provenance
    Host->>Gebruiker: "Er zijn 86 regelingen gevonden, waaronder..."

    Gebruiker->>Host: "Wat staat er in de Subsidieregeling energiebesparing?"
    Note over Host: Routeringsregel: BWB-ID bekend<br/>→ resource koop://regeling
    Host->>KOOP: resources/read [koop://regeling/BWBR0038472]
    KOOP->>SRU: SRU lookup → resolve naar XML-URL
    SRU-->>KOOP: locatie_toestand URL
    KOOP->>SRU: GET repository XML
    SRU-->>KOOP: volledige wettekst
    KOOP-->>Host: artikelen + provenance
    Host->>Gebruiker: "De regeling bevat X artikelen. Artikel 1 bepaalt..."
Loading

Scenario 3: Gecombineerde vraag (KvK + RegelRecht)

Gebruiker: "Moet mijn bedrijf voldoen aan de Informatieplicht Energiebesparing?"

sequenceDiagram
    actor Gebruiker
    participant Host as AI-platform (Host)
    participant KvK as MCP Server (KvK)
    participant RR as MCP Server (RegelRecht)
    participant Engine as RegelRecht Engine

    Gebruiker->>Host: "Moet mijn bedrijf voldoen aan de<br/>Informatieplicht Energiebesparing?"

    Note over Host: Stap 1: bedrijfsgegevens ophalen<br/>via sessie-gebonden KvK
    Host->>KvK: tools/call [mijn_bedrijf]
    KvK-->>Host: Test BV Donald, KvK 68750110,<br/>SBI 01241 + provenance

    Note over Host: Stap 2: verplichting checken<br/>met verkregen KvK-nummer
    Host->>RR: tools/call [check, kvk_nummer="68750110"]
    RR->>Engine: POST /mcp/rpc [execute_law,<br/>service=RVO, law=informatieplicht]
    Engine-->>RR: beslisboom-resultaat + wettelijke grondslag
    RR-->>Host: resultaat + provenance

    Host->>Gebruiker: "Op basis van art. 5.15d Bal: de informatieplicht<br/>is van toepassing als uw energieverbruik boven<br/>50.000 kWh of 25.000 m³ gas per jaar ligt.<br/>Wat is uw jaarlijks energieverbruik?"
Loading

Scenario 4: Subsidie zoeken en rapportage indienen (RVO)

Gebruiker: "Welke subsidies zijn er voor energiebesparing?"

sequenceDiagram
    actor Gebruiker
    participant Host as AI-platform (Host)
    participant KvK as MCP Server (KvK)
    participant RVO as MCP Server (RVO)

    Gebruiker->>Host: "Welke subsidies zijn er voor energiebesparing?"
    Note over Host: Routeringsregel: vraag over subsidies<br/>→ rvo__zoek_regeling
    Host->>RVO: tools/call [zoek_regeling, trefwoord="energiebesparing"]
    Note over RVO: Zoekt in mock-regelingen<br/>op naam en beschrijving
    RVO-->>Host: 3 resultaten (EBR-2026, EIA-2026, ISDE-2026) + provenance
    Host->>Gebruiker: "Er zijn 3 regelingen gevonden:<br/>1. Informatieplicht Energiebesparing (open, deadline 2026-12-01)<br/>2. Energie-investeringsaftrek EIA (open)<br/>3. Investeringssubsidie Duurzame Energie ISDE (open)"

    Gebruiker->>Host: "Ik wil mijn energiebesparingsrapportage indienen"
    Note over Host: Muterende actie →<br/>eerst bedrijfsgegevens ophalen,<br/>dan bevestiging vragen
    Host->>KvK: tools/call [mijn_bedrijf]
    KvK-->>Host: Test BV Donald, KvK 68750110 + provenance
    Host->>Gebruiker: "Ik kan de rapportage voor Test BV Donald (KvK 68750110)<br/>indienen bij regeling EBR-2026.<br/>Welke maatregelen heeft u genomen?"

    Gebruiker->>Host: "LED-verlichting en HR++ beglazing"
    Host->>Gebruiker: "Ter bevestiging: ik dien de volgende rapportage in:<br/>• Bedrijf: Test BV Donald (68750110)<br/>• Regeling: Informatieplicht Energiebesparing (EBR-2026)<br/>• Maatregelen: LED-verlichting, HR++ beglazing<br/>Wilt u doorgaan?"

    Gebruiker->>Host: "Ja"
    Host->>RVO: tools/call [indienen, kvk_nummer="68750110",<br/>regeling_id="EBR-2026",<br/>maatregelen=["LED-verlichting","HR++ beglazing"]]
    Note over RVO: Mock: simuleert succesvolle<br/>indiening en genereert<br/>referentienummer
    RVO-->>Host: status=INGEDIEND,<br/>ref=RVO-EBR-2026-68750110-001 + lopende_zaak + provenance
    Host->>Gebruiker: "Uw rapportage is ingediend (ref. RVO-EBR-2026-68750110-001)<br/>en in behandeling genomen. U vindt de status terug onder 'Lopende zaken',<br/>u hoort het zodra er een vervolgactie nodig is."
Loading

Let op: De indienen-tool is muterend. Het AI-platform vraagt daarom altijd om expliciete bevestiging van de gebruiker voordat de tool wordt aangeroepen. Dit is afgedwongen via de ToolAnnotations (readOnlyHint=False) én de systeemprompt.

Scenario 5: Bron niet beschikbaar

Gebruiker: "Welke regels gelden voor voedselveiligheid?"

sequenceDiagram
    actor Gebruiker
    participant Host as AI-platform (Host)
    participant KOOP as MCP Server (KOOP)
    participant SRU as SRU zoekservice

    Gebruiker->>Host: "Welke regels gelden voor voedselveiligheid?"
    Host->>KOOP: tools/call [zoek_regelgeving, trefwoord="voedselveiligheid"]
    KOOP->>SRU: GET zoekservice.overheid.nl/sru/Search...
    SRU--xKOOP: timeout / 503
    KOOP-->>Host: error: SOURCE_UNAVAILABLE
    Host->>Gebruiker: "De regelgeving-database (KOOP Regelingenbank)<br/>is op dit moment niet bereikbaar. Probeer het<br/>over een minuut opnieuw, of kijk rechtstreeks<br/>op wetten.overheid.nl."
Loading

De melding komt uit de foutcatalogus in services/host/errors.py (PDR-011) en bestaat altijd uit twee delen: wat er gebeurde en wat de gebruiker kan doen. De host vertaalt de foutcode van de bron; het LLM krijgt de melding mét de instructie om niets te verzinnen, en de technische oorzaak blijft in de log.

Wat de gebruiker per situatie ziet:

Situatie Foutcode Wat de gebruiker ziet
Bron reageert niet of geeft 5xx SOURCE_UNAVAILABLE naam van de bron + het alternatief (bijv. wetten.overheid.nl)
Bron geeft een 4xx op de aanvraag API_FOUT idem; de bron werkt wel, maar wees deze aanvraag af
Antwoord afgekapt op max_tokens LLM_ANTWOORD_AFGEKAPT dat het antwoord niet compleet is, mét de deeltekst erbij
Model gaf niets terug LLM_LEEG_ANTWOORD dat er geen antwoord kwam, met het advies anders te formuleren
Bron kwam bij het starten niet op BRON_NIET_GESTART welke bron ontbreekt, en dat wachten daar niet bij helpt
Tool bestaat niet in dit transport TOOL_NIET_IN_TRANSPORT dat de mogelijkheid hier ontbreekt (CLI kent minder tools dan MCP, PDR-005)
Bron reageert niet binnen TOOL_TIMEOUT SOURCE_UNAVAILABLE idem als hierboven; zonder deze grens bleef de stream hangen
Bron vindt niets NIET_GEVONDEN waar niets is gevonden, met het advies een algemener trefwoord te proberen
Bron mist een gegeven van de gebruiker ONTBREKEND_VELD / ONTBREKENDE_VELDEN welk gegeven ontbreekt, in woorden die de ondernemer herkent (niet de tekst van de bron)
Bron mist een gegeven van de assistent ONTBREKEND_INTERN_VELD dat de gebruiker er zelf niets aan kan doen (bv. het KvK-nummer uit de sessie)
LLM te traag LLM_TIMEOUT hoelang er is gewacht, met het advies de vraag korter te stellen
LLM-sleutel geweigerd LLM_SLEUTEL_ONGELDIG dat de sleutel niet wordt geaccepteerd (opnieuw proberen heeft geen zin)
Geen sleutel, override uit LLM_NIET_INGESTELD dat de beheerder aan zet is (een sleutel invullen wordt tóch genegeerd)
Fout in de assistent zelf HOST_FOUT dat de vraag niet kon worden afgerond, zonder het aan het model toe te schrijven
LLM overbelast of rate limit LLM_OVERBELAST / LLM_TE_DRUK dat het tijdelijk is, met het advies het over een minuut te proberen
Gesprek te lang voor het model LLM_GESPREK_TE_LANG het advies een nieuw gesprek te beginnen
Te veel stappen nodig LLM_MAX_STAPPEN het advies de vraag op te splitsen, met een voorbeeld
Lege of te lange vraag LEGE_VRAAG / VRAAG_TE_LANG wat er mis is met de invoer, vóór er een bron of LLM wordt geraakt
Geen geldige sessie GEEN_SESSIE dat er eerst ingelogd moet worden (PDR-009)

Ligt een bron er al bij het starten uit, dan komt dat ook in de systeemprompt te staan (prompts/blocks/shared/bronnen_status.md), zodat de assistent er niet overheen praat en niet terugvalt op eigen kennis. Liggen alle bronnen eruit, dan vervangt geen_bronnen.md het no_tools.md-blok: dat laatste zegt juist "antwoord op eigen kennis", en twee tegengestelde instructies in één prompt is slechter dan één.

Het SSE-error-event draagt naast message (de volledige zin) ook code, bericht, actie, bron en herstelbaar, zodat de frontend een retry-knop of een bron-vermelding kan tonen. Valt een bron uit terwijl het gesprek doorloopt, dan stuurt de host een bron_fout-event; het antwoord zelf volgt daarna gewoon.

Mappenstructuur

moza-poc-digitale-assistent/
  pyproject.toml            uv-project (dependencies + ruff/pytest config)
  uv.lock
  compose.yaml              Docker Compose (host + MCP via stdio-subproc)
  docs/
    architecture.md         (dit document)
    ai-verantwoording.md    Verantwoording inzet Claude Code in ontwikkeling
    test-vragen.md          Handmatige testvragen
    decisions/              Product Decision Records
  scripts/
    validate-mcp-servers.sh Validatie tegen mcp-standaard
  services/
    host/
      api.py                FastAPI REST-server
      vlam_host.py          LLM-orchestrator (VLAM + Claude, MCP + CLI)
      mcp_client.py         MCP-server verbindingen
      cli_executor.py       CLI tool-aanroepen via subprocess
      config.py             Configuratie
      errors.py             Foutcatalogus (melding + actie per foutcode)
      prompts/              Modulaire systeemprompts
        composer.py         Stelt blokken samen tot system prompt
        blocks/
          identity/         Per-model identiteit (vlam.md, claude.md)
          shared/           Gedeelde blokken (tone, format, guardrails, ...)
            domain/         Domeinkennis per onderwerp
          model_specific/   Fijnsturing per model
        examples/           Few-shot voorbeelden (naast blocks/, niet eronder)
      tests/                Pytest smoke-tests
      scripts/              Handmatige integratie-scripts (vereisen API-keys)
      Dockerfile
      .env.example
    mcp/                    MCP-servers (Python, persistent)
      kvk/                  Resource + Tool — Bedrijfsgegevens (KvK Test API + BAG)
      koop/                 Resource + Tool — Regelingenbank
      regelrecht/           Tool — generieke regel-executie (execute_law)
      rvo/                  Tool — subsidies en rapportages
      netbeheerder/         Tool — energiegegevens als Business Wallet-credential (mock)
    cli/                    CLI-tools (Bash, on-demand)
      kvk-cli               API-wrapper KvK
      koop-cli              API-wrapper KOOP
      regelrecht-cli        API-wrapper RegelRecht
      rvo-cli               API-wrapper RVO
      lib/                  Gedeelde modules (output, provenance, audit)