Skip to content

Klienten fra kunnskapsassistenten-frontend erstatter src/apps/web - #129

Draft
larsekhansen wants to merge 22 commits into
mainfrom
feat/ny-klient
Draft

larsekhansen wants to merge 22 commits into
mainfrom
feat/ny-klient

Conversation

@larsekhansen

Copy link
Copy Markdown

Gjelder #127.

Klienten fra larsekhansen/kunnskapsassistenten-frontend erstatter src/apps/web, bak BFF-en som står. Utkast, så klientene kan sammenlignes.

Klienten kan prøves i testmiljøet: https://ka-frontend-test.bravecoast-68900303.norwayeast.azurecontainerapps.io (krever tilgangslenke). Der går den rett mot headless-rag, ikke gjennom BFF-en.

Commitene

  • Kopi av klienten fra 07ccee2, i én commit og uten historikken.
  • Klienten i workspace-et: @ka/web, én låsefil, hooks og Prettier fra src/, tsc -b i typecheck, CI for klienten (lint, tokens og e2e i mock) og bildet bygget mot BFF-en.
  • BFF-en, 19 commits:
    • svarteksten sendes, ikke agentens plan
    • filterfelt og datasettnavn fra oppsett, alle verdiene, og 400 for ukjente felt
    • proben for filtre, hurtigbuffer på /assets/ og 404 for ukjente /api/-stier
    • kildene nummereres per bit, feilkoden følger med, og dokumentlenker for begge formene av doc_num
    • agentens steg og søk sendes som egne hendelser

Kontrakten i packages/contract har fått feltene BFF-en sender.

larsekhansen and others added 21 commits October 5, 2026 17:10
Kopi av larsekhansen/kunnskapsassistenten-frontend på
07ccee23b55b48564c905ea121c5ec1aa5dc8e72, i én commit og uten historikken.
Historikken står i det repoet. README-en sier det samme øverst.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… BFF-en

- Klienten heter @ka/web, og låsefila står bare i src/.
- typecheck kjører klientens egen tsc -b.
- Hookene finner verktøyene npm løfter til src/node_modules.
- Prettier i src/ hopper over det klienten genererer.
- CI får en jobb for klienten (lint, tokens:verify og e2e i mock) og en som
  bygger bildet og spør det.
- Bildet bygger klienten mot BFF-en (VITE_API_MODE=bff), ikke mot mocken.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Against this backend every response/chunk is the agent's plan, followed by
an agent/thinking with the same words, and the answer arrives whole in the
final frame. The deltas were shown as the answer and the final frame was
skipped, so /api/ask showed the plan and lost the answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lent cut

The fields were code (type, orgs_long, concerned_years), so the BFF only
worked against Kudos, and the client had to be built with the same list.
KA_FILTER_FIELDS and KA_DATASETS now say it per dataset, in the client's own
grammar, and /api/facets and /api/capabilities hand it on: id, field,
value type and label per field, and the dataset's name.

- Every value: max_facet_values 2000 and no CAP. Kudos has 457 organisations
  and 46 years; 200 and 22 came through.
- Years: value-type integer on the filter. Without it 2024 finds nothing
  (0 chunks against 4, measured 29.09). The year span now follows the id.
- A field with every known value selected is no filter on it.
- More than 100 values in one field is a 400 with a code and a Norwegian
  sentence, not a quiet cut to 100.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From KA CC's review of dd3b3c1.

- /api/ask reads the facets only from the cache, and warms a cold one for
  the next question. A Typesense that never answered held every question
  for 10.5 s. The call to Typesense also has a 5 s timeout now.
- The cache is keyed by the fields it was fetched for.
- A value longer than 256 characters, or with a backtick, a backslash or a
  control character, is refused with 400 filter-invalid-value, as #15 does,
  instead of being dropped.
- Tests against a Typesense that returns only max_facet_values values, as
  the real one does: 8 types, 457 organisations and 46 of 1006 year values
  come through. And a test that /api/capabilities names the dataset.

The grammar of KA_FILTER_FIELDS is the client's VITE_KA_FILTER_FIELDS plus
an optional fourth part, the label. The client does not read that part, so
a label set in VITE_KA_FILTER_FIELDS does nothing there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From KA CC's second review. cachedFacets gave nothing once the cache was
ten minutes old, so «all 457 selected» became a 400 on the first question
after a quiet spell. It now gives the old cache and fetches a fresh one
behind it. The filter handling of /api/ask is askFilter, synchronous so it
cannot wait on Typesense, and tested as such.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From KA CC's third review: each call to cachedFacets on an expired cache
started a fetch of its own, so ten questions made ten calls to Typesense.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plan or an allocation letter names the year it runs to, so
concerned_years holds years no document is from yet. With 2035 as the
fixed end, the filter offered 2027–2035 (measured against Kudos 30.09,
27 to 88 documents each). The end is now the current year in Norway,
read on every fetch, so it moves on New Year without a deploy. Oslo and
not the server's clock, because the container runs in UTC.

The Kudos-shaped test lays its years out from this year, so it says the
same thing next year.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From KA CC's review of ca5a158: on a Mac in Norway the test passed with
or without the Oslo time zone in currentYear, because the machine's own
zone was already Oslo. It now sets TZ to UTC while it runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found by #4 in the pod: a filter keyed by the facet ids (year,
documentType) instead of the field names got 200 and an answer from the
whole corpus, with nothing to say the filter had gone. cleanFilter now
refuses any key that is not in KA_FILTER_FIELDS with 400
filter-unknown-field, naming the key and the fields there are. Our
client sends the field names, so this is for other clients.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A timeout or a backend error settled the probe on filters: false for the
life of the process. Measured 101 s for the two searches in a row against a
60 s cap. The two now run together, and an attempt that gives no answer is
tried again after 1 and 5 minutes before the probe settles on no.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
/assets/ had no Cache-Control, ETag or Last-Modified, so the 1.6 MB bundle
could not be cached. Files there now get a year and immutable, index.html
gets no-cache, and a name that is not there is 404 instead of index.html.
An /api/ path no route takes was 200 with index.html; it is 404 with JSON.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
KA_CAPABILITIES was read by the server and set nowhere. With the probe
slower than its cap, filters stayed off in a deployment until someone
redeployed; kaCapabilities=filters now says so from the template.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The impossible filter named Kudos' type field, so the probe only meant
something against Kudos. It now takes a configured text field, or a year
with its value type, and with no fields configured there is nothing to
filter on: filters are off without asking the backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#15 refuses a filter it cannot serialise with a JSON-RPC error and no
result, not with isError (measured on 8090). The probe read that as zero
chunks, so a refused impossible filter would have said filters: yes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
refused() had tests, but nothing showed the probe used it: with the old
isError check back in retrieveOnly all 90 stayed green (KA CC). This one
runs probe() over its own fetch with a refused filtered search, and is red
with the old check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent's [N] is a 1-based index into its flat chunk list. toSources
grouped the chunks by document and numbered the documents, so the two
agreed only when every document gave exactly one chunk.

Measured against kudos-full on 2026-09-29: one question returned eight
chunks, all from document 347922, and the answer cited [1] through [8].
The grouped form offered a single source with marker 1, so [2] through
[8] resolved to nothing and were drawn as plain text, and [1] pointed at
all eight passages joined with a blank line.

A chunk with neither doc_num nor url is no longer dropped: dropping one
shifts every marker after it. It keeps its place with an empty docNum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed one

The client writes the reader's two sentences from a code, so without one
it has to guess from English prose and everything it cannot place becomes
the same catch-all sentence. Three things kept a code from reaching it.

The backend names the condition in the result's _meta.code
(digdir/mcp/tools.clj, error->tool-result). It was read for its text only
and the code was dropped.

A tools/call that named something nonexistent comes back as a JSON-RPC
error with the code in data.code (digdir/mcp/transport.clj,
invalid-params). Nothing read those frames at all, so the loop ran out
and the reader was told the connection broke.

A backend that could not be reached threw out of the generator and became
the route's catch-all «Uventet feil mot backend.», which says nothing
about what happened. It is now its own event with backend_unreachable,
while an abort stays an exception so the route can still tell the two
apart.

Measured 2026-09-29: with the backend down the reader saw «Svaret kom
ikke fram — Tjenesten sa ikke hva», and a faked 404 said the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kudos answers them at different addresses (measured 30.09):
/documents/<number> is a 301 to the document, /documents/<uuid> is a
404, and /dokument/<uuid> is the document. The Kudos API now gives only
UUIDs (headless-rag #25), so a corpus loaded again after that fix has
them. A number in neither shape gets no link rather than a guess, which
also keeps anything but digits or a UUID out of the path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`stage` says which phase the agent is in and nothing about what it did.
That was all this server sent, so the client drew four fixed sentences,
while the live client — reading the same backend frames directly — showed
the agent's reasoning in its own Norwegian, what each tool call found, and
how long it took.

Measured against the backend 2026-09-29 with one question: four
`agent/thinking` frames with reasoning, and eight tool calls across five
`agent/turn-completed` frames, each with `result-summary`, `duration-ms`
and either queries or chunk ids. None of it left this server.

One `tool-call` event per call and not per frame: a single frame carries
several, and three `read_chunks` in a row is ordinary.

Nothing here is turned into display text. The sentences the reader sees
are Norwegian and belong to the client, which already writes them for the
live path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/apps/web/server/static.ts Fixed
Comment thread src/apps/web/server/static.ts Fixed
Comment thread src/apps/web/src/api/live/LiveChatClient.ts Fixed
Comment thread src/apps/web/src/routes/info/InfoPage.test.tsx Fixed
Comment thread src/apps/web/src/views/sources/readableText.ts Fixed
Comment thread src/apps/web/src/views/sources/readableText.ts Fixed
Comment thread src/apps/web/src/views/sources/readableText.ts Fixed
Comment thread src/apps/web/server/excerpts.ts Fixed
Comment thread src/apps/web/server/facets.ts Fixed
Comment thread src/apps/web/server/facets.ts Fixed
Endringene fra 07ccee23b55b48564c905ea121c5ec1aa5dc8e72 til
efe51d9bbdd29ece3cc39813848d913117101948. Det meste rydder i varslene CodeQL
fant i denne PR-en:

- stisjekken i static.ts med path.relative, med samme oppførsel
- verdier fra brukeren skrives med JSON.stringify i loggen
- reserven til bruker-id-en bruker crypto.getRandomValues
- fjerningen av tagger i utdragene gjentas til teksten står stille
- testen for videolenka leser vertsnavnet

I tillegg tester for like titler og oppskriften for backenden. README-en
peker på den nye commiten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants