Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
pull_request:
paths: ['src/**', '.github/workflows/ci.yml']
push:
branches: [main]
paths: ['src/**', '.github/workflows/ci.yml']

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

defaults:
run:
working-directory: src

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: src/package-lock.json
- run: npm ci
- run: npm run format:check
- run: npm run typecheck
- run: npm test
- run: npm run build
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
dist/
.env
.env.*
!.env.example
*.local
.DS_Store
*.log
mise.local.toml
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Mappe | Innhold |
| --- | --- |
| [`src/`](src/) | Applikasjonen: frontend, server og kontrakt, bygd på `digdir-headless-rag`. Se [`src/README.md`](src/README.md). |
| [`docs/`](docs/) | Dokumentasjon av designarbeidet. |
| [`evals/`](evals/) | Evalueringer og golden questions. |

Expand Down
12 changes: 12 additions & 0 deletions src/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
**/node_modules
**/dist
.git
.github
docs
decisions
**/*.test.ts
**/*.test.tsx
**/.env
**/.env.*
!**/.env.example
12 changes: 12 additions & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions src/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
4 changes: 4 additions & 0 deletions src/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
package-lock.json
docs/fixtures/*.sse
docs/backend-patches/*.patch
5 changes: 5 additions & 0 deletions src/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"printWidth": 96,
"trailingComma": "all"
}
35 changes: 35 additions & 0 deletions src/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Node 22.18+ runs the server's TypeScript directly, so only the SPA is built.
FROM node:22.23.2-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
COPY packages/contract/package.json packages/contract/
COPY apps/server/package.json apps/server/
COPY apps/web/package.json apps/web/
RUN npm ci
COPY . .
RUN npm run build

FROM node:22.23.2-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
COPY packages/contract/package.json packages/contract/
COPY apps/server/package.json apps/server/
COPY apps/web/package.json apps/web/
RUN npm ci --omit=dev --workspace apps/server --include-workspace-root \
&& npm cache clean --force
COPY packages/contract/src packages/contract/src
COPY apps/server/src apps/server/src
COPY --from=build /app/apps/web/dist apps/web/dist

ENV WEB_ROOT=./apps/web/dist
ENV PORT=8787
EXPOSE 8787

RUN addgroup -S ka && adduser -S ka -G ka && chown -R ka:ka /app
USER ka

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD node -e "fetch('http://localhost:'+(process.env.PORT||8787)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

CMD ["node", "apps/server/src/server.ts"]
7 changes: 7 additions & 0 deletions src/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2026 Digitaliseringsdirektoratet (Digdir)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
177 changes: 177 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Kunnskapsassistenten, applikasjonen

**Frontenden til Kunnskapsassistenten, bygd på `digdir-headless-rag`.**

Samme produkt, samme brukeropplevelse, ny teknologi og en annen backend. Den
skal kunne kjøres ved siden av den "gamle"
[Kunnskapsassistenten](https://test.kunnskap.digdir.cloud) og sammenlignes med
den. Selve poenget er byttet: hold frontenden konstant, bytt motoren under, og
finn ut hva den nye backenden faktisk klarer.

**Porten er gjennomført.** Klienten er skrevet om fra bunnen i Preact, snakker
med `digdir-headless-rag` over det offentlige API-et, og gir svar med kilder
mot Kudos-korpuset: tråder, strømmede svar med statusetiketter, kildepanel med
utdrag, filtre og innlogging. Underveis ga det elleve dokumenterte hull i
backendens offentlige kontrakt, og noe for [`../evals`](../evals) å kjøre mot.

## Utrullet

<https://ka-app.thankfulpebble-9bb35590.norwayeast.azurecontainerapps.io>

Logg inn med @digdir.no-adressen din. Du får en engangskode på e-post, ingen
passord å opprette. Andre domener slipper ikke inn.

To ting er ikke på plass i det utrullede miljøet, og begge ligger i backenden,
ikke her:

- **Svar.** `test.rag.digdir.cloud` tilbyr bare `fact-checker` og
`retrieve-only`, og avviser de agentiske RAG-modusene med `mode_not_allowed`.
Appen er koblet opp og klar; den dagen agenten er slått på der, svarer den.
- **Filtre.** Den utrullede backenden tar ikke imot filteret fra klienten, så
chipsene vises deaktivert med en forklaring. De slår seg på av seg selv når
backenden begynner å ta imot dem, uten ny utrulling.

Mot en lokal backend med de foreslåtte endringene virker begge deler, og det
er der sammenligningen mot dagens Kunnskapsassistent gjøres.

```
apps/web Vite + Preact + Designsystemet (-css / -web). Ingen hemmeligheter.
apps/server Hono. Holder API-nøkkelen. Det eneste som snakker med backenden.
packages/contract Typene som går mellom de to.
```

```
Preact SPA ──► apps/server ──► digdir-headless-rag
(BFF-en) /api/mcp samtale + strømming
/v1/models oppslag
──► Typesense fasetter, utdrag fra kilder
(ingen av delene finnes i API-et)
```

Nettleseren får aldri API-nøkkelen. Det er et krav fra backenden, ikke en
preferanse: den autentiserer applikasjoner og ikke personer, og `X-User-Id`
godtas uten verifisering. Derfor må noe på serversiden _være_ identiteten. Se
[`decisions/0002`](decisions/0002-backend-for-frontend.md).

## Komme i gang

```sh
mise install && mise trust # node 22.18+, kjører TypeScript direkte
npm install
cp apps/server/.env.example apps/server/.env # sett DIGDIR_API_BASE og DIGDIR_API_KEY
npm run doctor # sjekker backenden før du starter
npm run dev # server :8787, SPA :5173
```

`npm run doctor` er den raske måten å finne ut hvorfor ingenting virker.
Den sjekker at nøkkelen autentiserer, at den gir tilgang til agenten som er
satt opp, at samtale-API-et svarer, og at Typesense er tilgjengelig. Feiler
noe, sier den hva du skal endre.

To backender, styrt av `DIGDIR_API_BASE`:

| | |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| `https://test.rag.digdir.cloud` | utrullet testmiljø. Ingenting å kjøre selv, men du trenger en nøkkel for det miljøet. |
| `http://localhost:8099` | lokalt. Oppsett ligger i `digdir-headless-rag`. |

Nøkler gjelder per miljø: en som er laget lokalt autentiserer ikke mot den
utrullede backenden. Typesense er valgfritt. Uten det kjører appen fint, men
filterraden skjules og kildekortene viser ingen utdrag.

## Kommandoer

| | |
| ------------------- | ---------------------------------- |
| `npm run dev` | begge appene, med watch |
| `npm run doctor` | sjekk forbindelsen til backenden |
| `npm run build` | produksjonsbygg av SPA-en |
| `npm test` | server (node:test) og web (vitest) |
| `npm run typecheck` | alle tre prosjektene |
| `npm run format` | prettier |

## Status

Den portede brukeropplevelsen virker ende til ende mot Kudos-korpuset: tråder,
strømmede svar med statusetiketter, og kilder med utdrag.

Den samme builden kjører mot begge versjoner av backenden. Se [Hva backenden
støtter](#hva-backenden-støtter).

| | |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Samtale over `/api/mcp`, SSE-strømming, statusetiketter | ferdig |
| Trådliste: søk, endre navn, slett | ferdig |
| Flere turer i samme tråd, startskjerm | ferdig |
| Kildepanel: snarveier, kort, utdrag, husket per tråd | ferdig |
| Kopier svar og lenke, tilbakemeldingslenke, nøkkelord | ferdig |
| Innlogging | ferdig. Engangskode i drift, Entra ID venter på samtykke. Av lokalt. Se [Innlogging](#innlogging) |
| Utrulling til Azure Container Apps | utrullet og i bruk. Se [`deploy/`](deploy/README.md) |
| Filterpanel: fasetter, chips med antall, låst per tråd | vises alltid. Deaktivert med forklaring når backenden ikke tar imot filtre, og slår seg på av seg selv når den gjør det |
| Genererte trådtitler | faller tilbake på spørsmålet til backenden lager en |
| `Vis andres tråder` | deaktivert, API-et kan ikke liste tråder du ikke eier |
| Mapper | droppet. Backenden har tagger, ikke mapper |

## Hva backenden støtter

Serveren måler det ved oppstart i stedet for å anta det, og eksponerer
resultatet på `/api/capabilities`. Da virker den samme builden mot begge
versjoner av backenden, og filtrene slår seg på uten ny utrulling den dagen
endringene er inne.

Filtermålingen er en faktisk test, ikke en erklæring: den sender et filter som
ikke kan treffe noe, og sammenligner med samme søk uten filter. Kommer det
treff uten filter og null med, kom filteret fram. Uten den kontrollen ville en
backend som er nede sett ut som en som støtter filtre.

Tvinges med `KA_CAPABILITIES="filters"` eller `"no-filters"`.

## Innlogging

`AUTH_MODE` velger mekanisme. Alle tre ender i den samme signerte
øktinformasjonskapselen, så å bytte er ren konfigurasjon.

| | |
| ---------- | ----------------------------------------------------------------- |
| `entra` | Entra ID. Krever at en administrator har gitt samtykke for appen. |
| `supabase` | Engangskode på e-post. Mellomløsning mens man venter på samtykke. |
| `off` | Kun lokalt. Usignert id, hvem som helst kan bli hvem som helst. |

Utelater du `AUTH_MODE` utledes den: `entra` hvis Azure-variablene er satt,
`supabase` hvis Supabase-variablene er satt, ellers `off`. Serveren skriver hvilken
modus den kjører i ved hver oppstart.

Uansett modus når ingen token nettleseren, bare en signert
informasjonskapsel. `/api/*` svarer 401 uten innlogging, `/api/health` er
åpen, og `ALLOWED_EMAIL_DOMAINS` gjelder i begge innloggingsmodusene.

`supabase` har ingen brukerliste og ingen passord: alle med en adresse på et
tillatt domene får en engangskode på e-post, og det å motta den er det som
beviser at de eier adressen. Koden løses inn på serveren, så ingen token
havner i nettleseren. `ALLOWED_EMAIL_DOMAINS` er påkrevd i den
modusen, ellers nekter serveren å starte. Oppsett og bytte til Entra ID i
[`deploy/README.md`](deploy/README.md).

## Dokumentasjon

[`decisions/`](decisions/) er på engelsk, som resten av koden og som
`digdir-headless-rag`: hvorfor Preact, hvorfor en BFF, hvorfor `/api/mcp`
framfor `/v1`.

[`deploy/README.md`](deploy/README.md) dekker utrulling til Azure og
innlogging.

## Hvor ting ligger

| Hvor | Hva |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `src/` | **Dette.** Applikasjonen: SPA, server og kontrakt. |
| [`../docs/`](../docs) | Designarbeid, akseptansekriterier, innsikt. |
| [`../evals/`](../evals) | Evalueringene, som denne gir noe å kjøre mot. |
| `digdir/digdir-headless-rag` | **Backenden.** Brukes kun over HTTP. |
| `digdir/digdir-rag` | **Arkivert.** Den opprinnelige Clojure-monolitten. Kilde til norsk tekst, ikke en avhengighet. |
| `digdir/digdir-rag-frontend` | **Egen alfa-demo.** Uten sammenheng med dette, tross navnet. |

Arbeidsmateriale som hører til prosjektet men ikke til repoet, som den
opprinnelige planen, presentasjonene og det fangede stilarket, ligger utenfor i
`_notes/` ved siden av utsjekkene.
60 changes: 60 additions & 0 deletions src/apps/server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ── Backend ───────────────────────────────────────────────────────────────
# Pick one. Keys are per environment: a key minted against a local backend
# will not authenticate against a deployed one.
#
# https://test.rag.digdir.cloud deployed test (no setup, needs a key)
# http://localhost:8099 local (see docs/local-backend.md)
DIGDIR_API_BASE=https://test.rag.digdir.cloud

# Server-side only. It must never reach the browser.
DIGDIR_API_KEY=rag_...

# ── Pinned server-side on purpose; the browser cannot override these ──────
DIGDIR_TOOL=builtin.agent-rag-agent__agent-rag-graph-bundled
DIGDIR_TENANT=public-sector-knowledge
DIGDIR_DATASET_CONFIG_KEY=default
DIGDIR_AGENT_ID=builtin/agent-rag-agent

# ── Typesense: optional, read-only ────────────────────────────────────────
# Fills the filter panel and the source excerpts, both of which the backend
# does not expose over HTTP (docs/contract-gaps.md, gaps 1 and 5). Leave these
# unset and the app still runs; the filter row hides and cards show no excerpt.
TYPESENSE_API_HOST=
TYPESENSE_API_KEY_ADMIN=
KUDOS_DOCS_COLLECTION=

# Where "Åpne" on a source points.
KUDOS_BASE=https://kudos.dfo.no

# ── Innlogging (Entra ID) ─────────────────────────────────────────────────
# Leave AZURE_CLIENT_ID empty and the server runs with an unsigned development
# identity where anyone can be anyone. It logs which mode it is in on boot.
# Set all three and /api/* requires a signed-in user. See deploy/README.md.
AZURE_TENANT_ID=
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_REDIRECT_URI=http://localhost:8787/auth/callback

# Required when the three above are set. 32+ chars: openssl rand -hex 32
SESSION_SECRET=

# Optional. Empty means anyone the tenant already admits.
# ALLOWED_EMAIL_DOMAINS=digdir.no

# ── Hvilken innlogging ────────────────────────────────────────────────────
# entra | supabase | off. Utelatt: utledes av hva som er satt opp.
# `supabase` sender en engangskode på e-post, som mellomløsning mens Entra ID
# venter på samtykke. Krever ALLOWED_EMAIL_DOMAINS og egen SMTP.
# AUTH_MODE=supabase
#
# Kun for AUTH_MODE=supabase. publishable-nøkkelen er ikke hemmelig, men holdes
# serverside her uansett. Se deploy/README.md for e-postmalen som må endres.
# SUPABASE_URL=https://<prosjekt>.supabase.co
# SUPABASE_PUBLISHABLE_KEY=

# ── Overrides ─────────────────────────────────────────────────────────────
# The server measures what the backend supports at boot. Force it instead
# with a space-separated list, e.g. "filters" or "no-filters".
# KA_CAPABILITIES=

PORT=8787
Loading
Loading