You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(adminapi): streaming chat with multimodal, regenerate, routing override
Rebuild the Chat tab on top of Server-Sent Events so tokens render live,
tool calls appear as chips the moment the agent invokes them, and the
input stays free while the assistant is thinking. Reuses the existing
agent.ProcessStream + router.ChatStream stack; no provider changes
needed.
- Admin API: POST /chat/stream (SSE: token/tool_call/done/error +
15s heartbeat), POST /chat/pop (drop last user turn, return text);
per-user chat_id from X-authentik-username (FNV-64 hash, falls back
to -1 under bearer auth); 25 MB body cap for base64 attachments.
- Routing: llm.WithForcedRole ctx override, consulted by Router.pick
before the global override so per-request role pins don't mutate
shared state.
- Store: TruncatableStore interface + SQLite/Postgres impls for
LastUserMessage + TruncateAfter; agent.PopLastUserTurn helper.
- Client: fetch + ReadableStream SSE consumer (no htmx/EventSource),
typing dots, tool chips, Stop/Esc abort, drag-drop/paste images,
Edit/Regenerate on last bubbles, code-block copy, localStorage
draft, role dropdown, smart scroll.
-`GET /chat` — chat page; history scoped to the caller's per-user chat id (FNV-64 hash of `X-authentik-username`, mapped to negative range; falls back to `-1` under bearer-token auth)
325
+
-`POST /chat/stream` — SSE endpoint. Body: `message`, optional `image[]` (base64 data URIs), optional `role` (simple/default/complex). Drives `agent.ProcessStream`; emits events `token` (`{delta}`), `tool_call` (`{name}`), `done` (`{model, tools, text}`), `error` (`{message}`), plus `:ping` comments every 15 s. Body capped at 25 MB; per-request routing override via `llm.WithForcedRole(ctx, role)`
326
+
-`POST /chat/pop` — drops the last user turn (via `agent.PopLastUserTurn` → `store.TruncatableStore`) and returns `{ok, text}`. UI uses this for Regenerate (pop + re-stream with returned text) and Edit (pop + put text back into the input)
327
+
-`POST /chat/clear` — resets session via `store.ClearHistory` for the caller's chat id; returns empty message container
324
328
-`GET /healthz` — unauthenticated liveness probe
325
329
330
+
**Chat tab notes:**`WriteTimeout: 0` on the admin HTTP server (long agent chains). Client is a fetch + `ReadableStream` SSE consumer (not htmx / `EventSource`) so the same endpoint can drive a future native app. Typing indicator is three bouncing dots until the first `token` event; tool-call chips (🔧) appear inline in the bot bubble meta; `Esc` or the Stop button abort via `AbortController`. Markdown rendered client-side via `marked.js` (CDN) on each `done` event, with copy-buttons hydrated on `<pre>` blocks. Draft persisted in `localStorage`. Hover actions (Edit on last user bubble, Regenerate on last bot bubble) call `/chat/pop`. Images pasted or dropped into the form are attached as `image_url` parts in a multimodal `llm.Message`.
331
+
326
332
The admin API never imports the Authentik library — it only _trusts_ the forwarded header. All actual authentication happens upstream in Traefik via the `authentik-auth` middleware. When running without authentik (local dev), the bearer token flow is the default.
0 commit comments