From 24d9a015541b1b328009f02474ccef2b33d46e06 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 25 Feb 2026 23:12:05 -0500 Subject: [PATCH] Update summary.md for Phase 19 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- summary.md | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/summary.md b/summary.md index e192d34..835167b 100644 --- a/summary.md +++ b/summary.md @@ -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)