Gå til innhold

ADR-010: Valg av regelmotor og beslutningstaker⚓︎

Erstattet av ADR-013 (2026-07-13, AB#21271). ADR-013 fastsetter arkitekturen som faktisk ble bygget; denne ADR-en beskriver komponenter som aldri ble implementert.

Kontekst og problemstilling⚓︎

Dagens analyse-pipeline sender alle saksdokumenter og regler fra SharePoint til en LLM (Gemini) som evaluerer regeloppfyllelse i én operasjon. Dette gir ikke-deterministiske resultater på to nivåer:

  1. Fakta-ekstraksjon: LLM trekker ut inkonsistente fakta fra dokumentene fra kjøring til kjøring
  2. Regelanvendelse: LLM tolker regelverket ulikt selv med identiske fakta

Juridiske og faglige krav krever at en saksbehandler kan stå ansvarlig for en vurdering. Det betyr at alle regelkjøringer må være sporbare til konkrete fakta og eksplisitte regler. I tillegg har oppdagede feil i analyser svekket tilliten blant virkemiddelansvarlige, og systemet nærmer seg produksjonssetting.

Regler er i dag lagret som fritekst-prompts i SharePoint (Regel.Beskrivelse). Det gir lav testbarhet og ingen versjonskontroll.

To distinkte konsepter⚓︎

Det er viktig å skille mellom:

  • Regelmotor — evaluerer individuelle regler mot strukturerte fakta og returnerer et RegelResultat (oppfylt/ikke oppfylt + begrunnelse). Kan være deterministisk C# eller LLM-drevet per regel.
  • Beslutningstaker — orkestrerer regelkjøring, aggregerer resultater og produserer en endelig RapportResultat (innstilling). Koordinerer også manglende data, oppfølging og tilstandsstyring gjennom saksgangen.

Et alternativ som C# pipeline/strategi-mønster håndterer disse lagene med separate metoder, men uten eksplisitt rammeverk. MAF Workflow skiller dem eksplisitt: funksjoner er regelmotoren, agenter er beslutningstakere.

Relaterte beslutninger: ADR-006 (købasert analyse), ADR-001 (blob-struktur for dokumenter).

Nøkkeldrivere⚓︎

  • Saksbehandler skal kunne se hvilke regler som er kjørt, hvilke fakta som ble brukt, og om regelen er oppfylt eller ikke
  • Regeldefinisjoner skal kunne deklareres i YAML og være enhetstestbare — ikke lagret som fritekst i SharePoint
  • Hver regel skal kunne konfigurere hvilke data retrievers som henter input — løsrevet fra regellogikken selv
  • Evalueringsinstruksjoner til LLM kan fortsatt uttrykkes som prompts (fleksibilitet for fagansvarlige)
  • Arbeidsflyter er per virkemiddel — ikke en generisk motor
  • Løsningen skal fungere i .NET 10 og passe inn i eksisterende Worker-arkitektur
  • MAF Workflows anses som umoden teknologi — leverandøravhengighet og begrenset community er en risiko

Vurderte alternativer⚓︎

  1. C# pipeline/strategi-mønster — virkemiddelspesifikke arbeidsflyter i ren C#
  2. Microsoft RulesEngine — JSON-drevet regelmotor fra Microsoft, åpen kildekode
  3. NRules — .NET-native forward-chaining regelmotor
  4. MAF Workflows + YAML-deklarert regelmotor — MAF Workflows som beslutningstaker, YAML-deklarerte regler med konfigurerbare data retrievers som regelmotor

Beslutning⚓︎

Valgt alternativ: MAF Workflows + YAML-deklarert regelmotor, fordi det gir et tydelig arkitektonisk skille mellom regelmotor og beslutningstaker, støtter konfigurerbare data retrievers per regel, og lar fagansvarlige deklarere regler i YAML uten å måtte skrive C#. MAF Workflow fungerer som beslutningstaker og orkestrerer kjøring basert på regeldefinisjoner lastet fra YAML.

Konsekvenser⚓︎

  • Positivt: regelkjøringer er deterministiske — samme fakta gir alltid samme konklusjon
  • Positivt: regler kan enhetstestes isolert fra LLM og dokumenter
  • Positivt: fagansvarlige kan deklarere og endre regler i YAML uten å skrive C#
  • Positivt: data retrievers er konfigurerbare per regel — ny datakilde krever ikke endring av regellogikken
  • Positivt: tydelig skille mellom hva som hentes (retriever), hva som evalueres (regel/funksjon), og hva som besluttes (MAF agent)
  • Negativt: YAML-laster og retriever-registry er ny infrastruktur som må bygges og vedlikeholdes
  • Negativt: MAF Workflows er umoden teknologi — leverandøravhengighet er en risiko
  • Nøytralt: LLM brukes fortsatt for fakta-ekstraksjon og LLM-drevne regelsteg — ekstraksjonsfeil er isolert og sporbart

Implementasjonsplan⚓︎

Arkitekturen er tre lag per virkemiddel:

Text Only
1
2
3
Retriever-lag:  IDataRetriever<T> henter data fra kilden (dokument, Mimir, Websak, osv.)
Regel-lag:      YAML-deklarert regel → funksjon evaluerer fakta → RegelResultat
Beslutnings-lag: MAF Workflow (agent) orkestrerer retrievers + regler → RapportResultat

YAML-format for regeldeklarasjon⚓︎

Regler deklareres i YAML under src/Enova.Agent.Kai/Rules/<VirkemiddelId>/. Hver regel angir hvilke data retrievers som skal kjøres og hvilken funksjon som evaluerer resultatet:

YAML
# src/Enova.Agent.Kai/Rules/TungeNullutslipp/maksbelop.yaml
id: R-MAKS-01
navn: Totalkostnad innenfor maksbeløp
beskrivelse: >
  Prosjektets totalkostnad må ikke overstige maksgrensen for virkemidlet.
retrievers:
  - type: DocumentExtractor
    skjema: SluttrapportFakta
    dokumenttype: sluttrapport
  - type: MimirRetriever
    felt: virkemiddel.maksbelopKr
funksjon: SjekkMaksbeløp
YAML
# src/Enova.Agent.Kai/Rules/TungeNullutslipp/teknisk-dokumentasjon.yaml
id: R-TEKN-01
navn: Teknisk dokumentasjon godkjent
beskrivelse: >
  Innsendt teknisk dokumentasjon må oppfylle kravspesifikasjonen.
retrievers:
  - type: DocumentExtractor
    skjema: TekniskDokumentasjonFakta
    dokumenttype: teknisk-rapport
  - type: WebsakRetriever
    arkivKode: TEKN
funksjon: VurderTekniskDokumentasjon
evaluering: llm   # deterministisk (default) eller llm
prompt: >
  Vurder om den tekniske dokumentasjonen oppfyller kravene i § 3.2.
  Fakta: {fakta}

Retriever-grensesnitt⚓︎

C#
// src/Enova.Agent.Kai.Abstractions/Services/IDataRetriever.cs
public interface IDataRetriever
{
    string Type { get; }  // matcher "type" i YAML
    Task<object> HentAsync(SakModel sak, RetrieverKonfig konfig, CancellationToken ct);
}

// Eksempelimplementasjoner:
// DocumentExtractorRetriever : IDataRetriever  (wrapper rundt IDocumentExtractor)
// MimirRetriever             : IDataRetriever  (wrapper rundt IMimirService)
// WebsakRetriever            : IDataRetriever  (wrapper rundt IWebsakService)

Retriever-registry løses via DI: IEnumerable<IDataRetriever> indeksert på Type.

Regelmotor-kjøring⚓︎

MAF Workflow-funksjonen kjører en regel slik:

C#
// Pseudokode — MAF Workflow-funksjon
private async Task<RegelResultat> KjørRegelAsync(RegelDefinisjon regel, SakModel sak, CancellationToken ct)
{
    // 1. Hent data via konfigurerte retrievers
    var fakta = await _retrieverRegistry.HentAlleAsync(regel.Retrievers, sak, ct);

    // 2. Evaluer — deterministisk funksjon eller LLM
    return regel.Evaluering == "llm"
        ? await _llmEvaluator.EvaluerAsync(regel, fakta, ct)
        : _funksjonRegistry.Evaluer(regel.Funksjon, fakta);
}

Berørte filer⚓︎

  • src/Enova.Agent.Kai.Abstractions/Services/IDataRetriever.cs — nytt retriever-grensesnitt
  • src/Enova.Agent.Kai.Abstractions/Models/RegelDefinisjon.cs — C#-modell for YAML-struktur
  • src/Enova.Agent.Kai/Rules/<VirkemiddelId>/ — YAML-filer per regel per virkemiddel
  • src/Enova.Agent.Kai/Retrievers/ — implementasjoner av IDataRetriever
  • src/Enova.Agent.Kai/Rules/RegelLaster.cs — laster og validerer YAML-regler ved oppstart
  • src/Enova.Agent.Kai/Rules/RetrieverRegistry.cs — løser opp IDataRetriever etter type-navn
  • src/Enova.Agent.Kai/Rules/FunksjonRegistry.cs — mapper funksjon-navn til C#-metode
  • src/Enova.Agent.Kai/Workflows/ — MAF Workflow per virkemiddel (beslutningstaker)
  • src/Enova.Agent.Kai.Worker/JobProcessor.cs — erstatt LLM-kall med MAF Workflow-kall
  • tests/Enova.Agent.Kai.Tests/Rules/ — enhetstester for individuelle regeldefinisjoner

Avhengigheter: YamlDotNet for YAML-parsing (eller Microsoft.Extensions.Configuration.Yaml)

Mønstre som skal følges: - YAML-filer er embedded resources — lastes ved oppstart, valideres mot RegelDefinisjon-skjema - Retriever-typer registreres i DI: services.AddScoped<IDataRetriever, DocumentExtractorRetriever>() - MAF Workflow løser opp regler via RegelLaster og kjører dem via RetrieverRegistry + FunksjonRegistry - Følg eksisterende options-mønster for virkemiddelspesifikke grenser og terskelverdier

Mønstre som skal unngås:

  • Ikke hardkod retriever-typer i regellogikk — bruk alltid YAML-konfigurasjonen
  • Ikke send rå dokumenttekst til LLM — kun strukturerte fakta fra retrievers
  • Ikke legg regellogikk i JobProcessor direkte — alt går via MAF Workflow

Migrasjonssti:

  • Fase 1: Bygg IDataRetriever, RegelLaster, RetrieverRegistry — verifiser med én regel
  • Fase 2: Migrer eksisterende SharePoint-regler til YAML per virkemiddel
  • Fase 3: Fase ut RegelRepository og direkte SharePoint-avhengighet

Verifisering⚓︎

  • [ ] YAML-regler lastes og valideres ved oppstart — ugyldig YAML kaster tydelig feilmelding
  • [ ] Retriever-typer fra YAML løses opp korrekt via registry
  • [ ] RapportResultat inneholder RegelResultat per regel med RegelId, Oppfylt, Begrunnelse, og FaktaReferanser
  • [ ] Enhetstester for regeldefinisjoner kjører uten LLM eller nettverkskall (mocket IDataRetriever)
  • [ ] Samme RapportResultat produseres ved to identiske kjøringer med samme inputdokumenter
  • [ ] dotnet test passerer uten endringer i integrasjonstester

Presiseringer ved akseptering (2026-05-21)⚓︎

Avklaringer som dukket opp under sprint 1-planlegging for første reelle bruk (Tunge nullutslippskjøretøy, US #20415). Disse presiseringene overstyrer tilsvarende detaljer i opprinnelig "Implementasjonsplan" ovenfor — den står som historisk intensjon.

Beslutningstaker: native workflow-graf, ikke MAF Workflows⚓︎

Etter ADR-utkastet er det levert en native WorkflowGraphRunner i US #20185 med støtte for Direct/Conditional/FanOut/FanIn-kanter — se src/Enova.Kai.Application/WorkflowEngine/WorkflowGraphRunner.cs. Den fungerer som beslutningstaker uten avhengighet til MAF Workflows. ADR-010s negative punkter knyttet til MAF Workflows ("umoden teknologi", "leverandøravhengighet") er dermed unngått. Valget av beslutningstaker-implementasjon presiseres her til native workflow-graf.

v2-prosjektstruktur⚓︎

Opprinnelig implementasjonsplan ble skrevet før v2-arkitekturen (Application/Domain/Infrastructure-split) var ferdig. v2-stier:

Komponent Plassering
IDataRetriever-grensesnitt src/Enova.Kai.Application/Retrievers/IDataRetriever.cs
RetrieverRegistry src/Enova.Kai.Application/Retrievers/RetrieverRegistry.cs
Retriever-implementasjoner src/Enova.Kai.Infrastructure/Retrievers/ (DocumentExtractorRetriever, MimirRetriever, WebsakRetriever)
YAML-regelfiler src/Enova.Kai.Application/Rules/<VirkemiddelId>/ som embedded resources
RegelLaster src/Enova.Kai.Application/Rules/RegelLaster.cs
FunksjonRegistry src/Enova.Kai.Application/Rules/FunksjonRegistry.cs
Regel-funksjoner src/Enova.Kai.Application/Rules/<VirkemiddelId>/Functions/*.cs
Fact-modeller per virkemiddel src/Enova.Kai.Domain/Facts/<VirkemiddelId>/
Workflow-orkestrering Eksisterer: WorkflowGraphRunner.cs

Content Understanding-skjema per virkemiddel⚓︎

Fakta-modeller er per-virkemiddel, ikke felles katalog. Hvert virkemiddel har forskjellig kombinasjon av dokumenttyper og felter; en felles modell hadde drukknet i optional-felter. Schema-evolusjon styres per virkemiddel ved at Content Understanding-analyzeren versjoneres sammen med fact-modellen.

C#
1
2
3
4
5
6
7
8
// src/Enova.Kai.Domain/Facts/TungeNullutslipp/SøknadFakta.cs
public sealed record SøknadFakta(
    DateOnly? Søknadsdato,
    decimal? TotalKostnadEksMva,
    decimal? TotalKostnadInkMva,
    string Valuta,
    int? AntallKjøretøy,
    IReadOnlyList<TilbudFakta> Tilbud);

Funksjon-konvensjon⚓︎

Hver regel-funksjon implementerer:

C#
1
2
3
4
5
public interface IRegelFunksjon
{
    string Navn { get; }                 // matcher "funksjon"-feltet i YAML
    Task<RuleResult> EvaluerAsync(RuleFacts facts, CancellationToken ct);
}

DI-registrering: services.AddScoped<IRegelFunksjon, SjekkSøknadsdatoFunksjon>(). FunksjonRegistry indekserer på Navn. Asynk-signatur valgt fordi enkelte funksjoner kan trenge ekstra retriever-kall under evaluering.

HITL — WaitingForHuman⚓︎

WorkflowGraphRunner.cs:41 og :120 støtter allerede pause på RuleResult.WaitingForHuman = true. For full HITL-flyt:

  • CaseEvaluation får felt Status: Completed | WaitingForHuman | Failed og PausedAtRuleId: string?
  • Resume via POST /cases/{caseKey}/evaluations/{evalId}/resume med saksbehandlers utfall som input
  • Read-API (under US #20124) inkluderer pause-status og pauserende regel slik at webflaten kan vise saken i "venter på meg"-kø

Detaljer i Task #20423.

Regelkilde og versjonering — utstår⚓︎

Strategi for hvordan SharePoint-regler (kilde i dag) skal speiles til v2 (snapshot-import vs runtime-fetch) er utskutt til egen ADR. Sprint 1-arbeidet bruker manuelt respesifiserte YAML-filer per regel. Når snapshot-modellen er bestemt vil fact-modellen, regel-YAMLene og evt. Rule.Version knyttes til en RuleSetRevision for sporbarhet (jf. US #20124).

Oppdatert migrasjonssti⚓︎

Erstatter opprinnelig migrasjonssti (som forutsatte v1-prosjekter):

  • Fase 1IDataRetriever + RegelLaster + RetrieverRegistry + FunksjonRegistry i Enova.Kai.Application. Verifiseres med én deterministisk regel.
  • Fase 2 — Respesifiser SharePoint-regler per virkemiddel til YAML + funksjoner. Første virkemiddel: Tunge nullutslippskjøretøy.
  • Fase 3 — Fas ut v1 RegelRepository.cs og direkte SharePoint-runtime-avhengighet i Enova.Agent.Kai. v2 leser kun fra YAML/DB.
  • Fase 4 — Egen ADR + implementasjon av snapshot-import fra SharePoint (jf. "Regelkilde og versjonering" ovenfor).

Fordeler og ulemper per alternativ⚓︎

C# pipeline/strategi-mønster⚓︎

Virkemiddelspesifikke arbeidsflyt-klasser implementerer IRegelWorkflow. Fakta ekstraheres strukturert, regler evalueres i C#-kode eller via LLM med minimalt fakta-kontekst.

  • Positivt: ingen nye avhengigheter
  • Positivt: full testbarhet — enhetstester per regel uten LLM
  • Positivt: versjonskontroll av regler via git
  • Positivt: virkemiddelspesifikk logikk er eksplisitt
  • Negativt: mer standardkode per virkemiddel enn deklarative alternativer

Microsoft RulesEngine⚓︎

JSON-drevet regelmotor fra Microsoft (åpen kildekode). Regler uttrykkes som lambda-uttrykk i JSON.

  • Positivt: regler kan i prinsippet redigeres uten rekompilering
  • Positivt: aktivt vedlikehold fra Microsoft
  • Negativt: regler i JSON gir dårligere IDE-støtte og vanskeligere enhetstesting enn C#-kode
  • Negativt: ekstra avhengighet for et problem C# løser like godt
  • Negativt: lambda-syntaks i JSON er uleselig for fagansvarlige

NRules⚓︎

Forward-chaining regelmotor med fluent API i C#. Brukes i domenedrevne systemer med mange sammenkoblede regler.

  • Positivt: kraftig for komplekse regelnettverk med avhengigheter mellom regler
  • Negativt: overkill for sekvensielle, uavhengige regelsjekker per virkemiddel
  • Negativt: bratt læringskurve — begrenset community i .NET-verdenen
  • Negativt: ekstra avhengighet uten klar fordel over ren C#

MAF Workflows⚓︎

Microsoft Agents Framework Workflows skiller eksplisitt mellom regelmotor og beslutningstaker:

  • Funksjoner = regelmotoren — deterministiske beregninger og strukturerte sjekkpunkter per regel
  • Agenter = beslutningstakeren — LLM-drevne vurderinger, orkestrering av manglende data, og produksjon av endelig innstilling

En søknad for et virkemiddel modelleres som en arbeidsflyt der hvert steg fra innsendt søknad til innstilling er et eget trinn med funksjoner og agenter som utførere.

Eksempel: Tunge nullutslippskjøretøy — søknad til innstilling⚓︎

flowchart TD
    A([Søknad mottatt]) --> B[Funksjon: Ekstraher søknadsfakta\nAzure Content Understanding]
    B --> C{Fakta komplett?}
    C -- Nei --> D[Agent: Be om manglende opplysninger\nLLM genererer oppfølgingsbrev]
    D --> B
    C -- Ja --> E[Funksjon: Sjekk formelle krav\nOrgnr, søknadsdato, virkemiddel-ID]
    E --> F{Formelle krav\noppfylt?}
    F -- Nei --> G([Avvis søknad\nautomatisk])
    F -- Ja --> H[Funksjon: Beregn støttebeløp\nAntall kjøretøy × sats]
    H --> I[Funksjon: Sjekk budsjettramme\nGjenstående ramme ≥ beregnet beløp?]
    I --> J[Agent: Vurder teknisk dokumentasjon\nLLM verifiserer samsvar med krav]
    J --> K[Agent: Vurder risiko\nLLM vurderer prosjektrisiko mot risikomatrise]
    K --> L[Funksjon: Aggreger regelresultater\nSamle RegelResultat per regel]
    L --> M{Alle regler\noppfylt?}
    M -- Ja --> N([Innstilling: Innvilg\nmed beregnet beløp])
    M -- Nei --> O([Innstilling: Avslå\nmed begrunnelse per regel])

    style A fill:#e8f5e9
    style G fill:#ffebee
    style N fill:#e8f5e9
    style O fill:#ffebee
    style D fill:#fff3e0
    style J fill:#fff3e0
    style K fill:#fff3e0

Oransje bokser = agenter (beslutningstakere, LLM-drevne). Hvite = funksjoner (regelmotor, deterministiske). Grønne/røde = terminalsteg.

Vurdering av MAF Workflows for dette mønsteret:

  • Positivt: native integrasjon med MAF som allerede brukes i prosjektet
  • Positivt: eksplisitt skille mellom regelmotor (funksjoner) og beslutningstaker (agenter) — to distinkte konsepter i samme rammeverk
  • Positivt: agenter håndterer kompleks orkestrering med innebygd tilstandsstyring
  • Positivt: arbeidsflyt-diagrammet kan representeres direkte i MAF-kode
  • Negativt: MAF Workflows er umoden teknologi med uklar langsiktig retning
  • Negativt: begrenset community og dokumentasjon — feilsøking er kostbart
  • Negativt: leverandøravhengighet — bruddendringer i rammeverket kan tvinge refaktorering
  • Negativt: høy kompleksitet for et problem som løses enklere i ren C# — det samme flydiagrammet kan implementeres som en sekvens av metodekall

Det samme arbeidsflyten i C# pipeline/strategi-mønster ville vært en enkel sekvens av await SjekkFormellKravAsync(), await BeregneStøttebeløpAsync() osv. — uten rammeverksoverhead.

Tilleggsinformasjon⚓︎

  • Fakta-ekstraksjon er avhengig av at Azure Content Understanding støtter virkemiddelspesifikke skjemaer — dette bør verifiseres i en spike før implementasjon starter
  • Revisit-trigger: hvis antall virkemidler overstiger ~10 og standardkode per arbeidsflyt blir uhåndterlig, vurder Microsoft RulesEngine på nytt
  • Relatert funksjonalitet: Deterministiske resultater, Korrekt regelverk
  • ADR-013 — utvider regelmotoren med workflow-orkestrering og facts-pipelines.