Update summary.md for Phase 19

Documents changes from the Phase 19 session:
- Light mode indigo theme: #6366f1 primary, #f5f5fb/#ededf5 off-white backgrounds
- OLLAMA_NUM_CTX configurable env var (default 8192, replaces hardcoded 32768)
- OLLAMA_MODEL default → qwen3:latest, OLLAMA_INTENT_MODEL default → qwen2.5:1.5b
- GET /api/settings/models endpoint: installed models + configured defaults
- Chat/intent model dropdowns in Settings UI (populated from installed models)
- Optimistic streaming in generation_task.py: races intent classification against
  stream start via asyncio.Queue + asyncio.wait; user sees first token immediately
  for pure-chat messages, tool path unchanged when intent wins the race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 23:12:05 -05:00
parent 98d3fca277
commit 24d9a01554
+28 -19
View File
@@ -12,7 +12,7 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-02-25 — Phase 18b: Favicon contrast fix + email template redesign
2026-02-26 — Phase 19: Light mode indigo theme, OLLAMA_NUM_CTX, model dropdowns, optimistic streaming
## Project Overview
Fabled Assistant is a self-hosted note-taking and task-tracking application with
@@ -255,7 +255,7 @@ fabledassistant/
│ ├── __init__.py
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API, request logging, security headers (after_request)
│ ├── auth.py # Auth decorators: login_required, admin_required, get_current_user_id — shared _check_auth() helper
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret) + SECURE_COOKIES + TRUST_PROXY_HEADERS + OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES + LOCAL_AUTH_ENABLED + oidc_enabled() classmethod
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret) + SECURE_COOKIES + TRUST_PROXY_HEADERS + OLLAMA_NUM_CTX (KV cache window, default 8192) + OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES + LOCAL_AUTH_ENABLED + oidc_enabled() classmethod
│ ├── rate_limit.py # In-memory sliding-window rate limiter (asyncio.Lock + defaultdict); is_rate_limited(key, max, window)
│ ├── models/
│ │ ├── __init__.py # async_session factory, Base, imports all models
@@ -273,16 +273,16 @@ fabledassistant/
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks + tag suggestions (all @login_required)
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
│ │ └── settings.py # /api/settings GET/PUT — per-user settings (@login_required)
│ │ └── settings.py # /api/settings GET/PUT, GET /models — per-user settings + installed model list (@login_required)
│ ├── services/
│ │ ├── auth.py # Auth business logic: hash_password, verify_password, create_user (password optional, oauth_sub kwarg), authenticate (returns None for OAuth-only users), get_user_by_id/username/email/oauth_sub, update_user_email, link_oauth_sub, is_registration_open, list_users, delete_user, password reset tokens, invitation tokens
│ │ ├── oauth.py # OIDC/OAuth2 service: get_oidc_config (discovery, cached), build_auth_url (PKCE), exchange_code, get_userinfo, find_or_create_oauth_user (sub lookup → email auto-link → create)
│ │ ├── backup.py # Backup/restore: export_full_backup, export_user_backup, restore_full_backup
│ │ ├── notes.py # CRUD with user_id isolation, is_task filter, convert, backlinks, search_notes_for_context
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching; uses Config.OLLAMA_NUM_CTX for KV cache window
│ │ ├── chat.py # Conversation CRUD with user_id isolation, add_message, save/summarize as note (LLM-titled, chat-tagged)
│ │ ├── generation_buffer.py # In-memory SSE event buffer with cancel_event, reconnect support, auto-cleanup; supports chat (int keys) and assist (string keys)
│ │ ├── generation_task.py # Background asyncio tasks: run_generation (chat, DB flush, titles, intent routing + tool loop) + run_assist_generation (lightweight, no DB)
│ │ ├── generation_task.py # Background asyncio tasks: run_generation (chat, DB flush, titles, optimistic streaming + intent racing + tool loop) + run_assist_generation (lightweight, no DB); _drain_queue() async generator helper
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call to detect tool intent before streaming
│ │ ├── tools.py # LLM tool definitions (create/delete note+task, update_note w/tag management, get_note, list_notes, search_notes w/type filter, list_tasks, full CalDAV suite incl. search_todos) + execute_tool dispatcher
│ │ ├── tag_suggestions.py # LLM-powered tag suggestions: suggest_tags() builds prompt with existing tags, calls generate_completion, parses JSON response
@@ -303,7 +303,7 @@ fabledassistant/
│ ├── App.vue # Shell: .app-shell (100dvh flex column) with AppHeader + .app-content (flex:1, scrollable) wrapping router-view; starts status polling + loads settings on mount
│ ├── main.ts # App init, imports theme.css
│ ├── assets/
│ │ ├── theme.css # CSS custom properties: light/dark themes, body reset, design tokens, responsive breakpoints (480/768/1024), utility classes (.hide-mobile/.hide-desktop), mobile touch targets
│ │ ├── theme.css # CSS custom properties: light/dark themes, body reset, design tokens, responsive breakpoints (480/768/1024), utility classes (.hide-mobile/.hide-desktop), mobile touch targets; light mode uses indigo brand color (#6366f1) + indigo-tinted off-white backgrounds (#f5f5fb/#ededf5)
│ │ └── editor-shared.css # Shared editor styles for NoteEditorView + TaskEditorView: layout, toolbar, tag suggestions, assist panel, diff view, modal, responsive, .inline-assist-btn (global — required for teleported elements)
│ ├── api/
│ │ └── client.ts # ApiError class, apiGet/apiPost/apiPut/apiPatch/apiDelete + apiSSEStream (fetch+ReadableStream), apiStreamPost (legacy), auto 401→login redirect
@@ -317,7 +317,7 @@ fabledassistant/
│ │ ├── notes.ts # CRUD + tag filter, resolveTitle, convertToTask, convertToNote, fetchBacklinks, fetchAllTags (with toast errors)
│ │ ├── tasks.ts # CRUD + status/priority filter, patchStatus (with toast errors)
│ │ ├── chat.ts # Conversation CRUD, sendMessage (SSE streaming), status polling (memory-leak-safe _pollUntilLoaded), running models, model warming, updateConversationModel (with toast errors)
│ │ ├── settings.ts # App settings: assistantName, defaultModel, pullModel, deleteModel (with toast errors)
│ │ ├── settings.ts # App settings: assistantName, defaultModel, installedModels, defaultChatModel, defaultIntentModel, pullModel, deleteModel (with toast errors)
│ │ └── toast.ts # Toast notification state (success/error/warning), 4s auto-dismiss, dismiss(id)
│ ├── types/
│ │ ├── auth.ts # User interface (incl. has_password bool), AuthStatus interface (incl. oauth_enabled, local_auth_enabled)
@@ -343,7 +343,7 @@ fabledassistant/
│ │ ├── UserManagementView.vue # Admin user management: registration toggle, invitations (send/revoke), user list with delete
│ │ ├── ChatView.vue # Dedicated /chat page: responsive sidebar (overlay on mobile), bubble messages, note picker, persistent context sidebar (right panel, hidden mobile), model selector in header
│ │ ├── HomeView.vue # Chat-first dashboard: quick actions + chat widget (top, full-width), inline response panel, two-column grid (3fr tasks / 2fr notes); task sections: Overdue, Due Today, Due This Week, High Priority, In Progress, Other (capped 10, due-dated first); 8 recent notes; model warming on mount
│ │ ├── SettingsView.vue # Settings page: assistant name, email change (with password confirmation for local-auth users), change password, notifications, CalDAV, SMTP (admin), base URL (admin), data export/restore (admin)
│ │ ├── SettingsView.vue # Settings page: assistant name, chat/intent model dropdowns (populated from installed models), email change (with password confirmation for local-auth users), change password, notifications, CalDAV, SMTP (admin), base URL (admin), data export/restore (admin)
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
│ │ ├── NoteEditorView.vue # Create/edit: Tiptap editor, sticky toolbar, AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions, Ctrl+S, auto-save (5min), unsaved guard; styles from editor-shared.css
│ │ ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task, backlinks, table of contents sidebar
@@ -446,6 +446,7 @@ fabledassistant/
| POST | `/api/admin/smtp/test` | Send test email (admin only, body: `{recipient}`) |
| GET | `/api/settings` | Get all app settings as `{key: value}` dict |
| PUT | `/api/settings` | Update settings (body: `{key: value, ...}`) |
| GET | `/api/settings/models` | Get installed Ollama models + configured defaults (`{models, default_chat_model, default_intent_model}`) |
## Alembic Migrations
@@ -558,7 +559,7 @@ When adding a new migration, follow these conventions:
- **Keyboard shortcuts overlay:** Press `?` or click `?` in nav bar. State shared via `useShortcuts` composable.
### LLM Chat
- Ollama integration via async HTTP (httpx), default model qwen3 (better tool support than mistral), auto-pull on startup
- Ollama integration via async HTTP (httpx), default model `qwen3:latest` (better tool support than mistral), auto-pull on startup
- Background generation with `GenerationBuffer` (in-memory SSE fan-out, `Last-Event-ID` reconnect, 60s cleanup)
- Stop generation with partial content preservation
- Note-aware context building: current note + keyword search for related notes + URL fetching
@@ -611,21 +612,29 @@ When adding a new migration, follow these conventions:
(`search_todos` keyword-filters the todo list — companion to `list_todos`)
- **Streaming status transparency:** The backend emits `status` SSE events at each pipeline stage
so the user always sees what's happening instead of a blank progress dot. Stages:
(1) `"Analyzing your request..."` before intent classification; (2) human-readable tool label
(e.g. `"Creating calendar event..."`) before each tool executes (both intent-routed and native);
(3) `"Generating response..."` / `"Composing response..."` before each LLM streaming round.
(1) `"Generating response..."` immediately after context is built (no blocking wait for intent);
(2) human-readable tool label (e.g. `"Creating calendar event..."`) before each tool executes
(both intent-routed and native); (3) `"Composing response..."` before tool follow-up rounds.
Frontend: `chat.ts` stores `streamingStatus` ref, cleared on first content chunk or on done/error.
`ChatView.vue` shows a pulsing dot + italic label above the content while status is active, then
falls back to the blinking cursor when content streams in. `HomeView.vue` dashboard panel shows
the status label in place of `...` before any content arrives.
- **Intent routing:** Before streaming, a fast non-streaming LLM call classifies user intent and
extracts tool parameters (`services/intent.py`). If a tool call is detected, it executes directly
— bypassing the model's native (sometimes unreliable) tool calling API. Falls through to normal
streaming when no tool is detected or classification fails. Only runs on first round of tool loop.
- **Intent routing (optimistic streaming):** On the first round, the backend races intent
classification against the start of the LLM stream using `asyncio.wait(FIRST_COMPLETED)`.
The LLM stream is immediately started into an `asyncio.Queue` while `classify_intent()` runs
concurrently. If the stream produces its first token before classification completes, the intent
task is cancelled and the user sees tokens immediately (zero blocking for pure chat). If
classification finishes first and detects a tool call, the stream is cancelled and the tool
executes directly — bypassing the model's native (sometimes unreliable) tool calling API. Falls
through to normal streaming when no tool is detected or classification fails.
Dedicated intent model configurable via `OLLAMA_INTENT_MODEL` env var (default `qwen2.5:1.5b`)
or per-user `intent_model` setting — smaller/faster model for routing. Main model default
is `qwen3:latest` (configurable via `OLLAMA_MODEL` env var or per-user `default_model` setting
— both settable as dropdowns in the Settings UI populated from installed models).
`OLLAMA_NUM_CTX` env var (default 8192) controls the KV cache context window for all generation
calls; reducing from 32768 cuts VRAM usage ~4x without affecting most conversations.
Supports confidence levels ("high"/"medium"/"low") — low-confidence intents fall through to streaming.
Passes last 6 user/assistant turns as history for anaphora resolution ("move it", "cancel that").
Dedicated intent model configurable via `OLLAMA_INTENT_MODEL` env var or per-user `intent_model`
setting — allows a smaller/faster model for routing while the main model handles responses.
Intent router rules cover: update/delete events, CalDAV todos, time-period → list_events (not
search_events), update_note vs create_note disambiguation, reminder_minutes conversion,
delete_note vs delete_task disambiguation, get_note for "read/show me this note", list_notes for
@@ -684,7 +693,7 @@ When adding a new migration, follow these conventions:
- Configurable base URL for email links (admin setting, env var fallback)
### UI & Theming
- Dark/light theme with CSS custom properties, design tokens, `prefers-color-scheme` detection
- Dark/light theme with CSS custom properties, design tokens, `prefers-color-scheme` detection; light mode uses indigo brand color (`--color-primary: #6366f1`) and indigo-tinted off-white backgrounds (`--color-bg: #f5f5fb`, `--color-bg-secondary: #ededf5`) with white cards to pop content
- Responsive design: breakpoints (480/768/1024), hamburger menu, mobile touch targets (44px), sidebar overlays
- App shell: navbar always visible, 100dvh flex layout, all views fit within viewport
- Toast notifications (success/error/warning, 4s auto-dismiss)