Removes the entire RSS feature surface — feeds, items, embeddings, reactions,
discussion-note flow, briefing news context, settings, env-vars, and DB
tables. Keeps the URL-generic article-reader (the read_article LLM tool)
under a clean module so the LLM can still fetch arbitrary article content
from URLs the user provides.
Backend:
- New services/article_fetcher.py — single source of trafilatura URL→text
- New services/tools/article.py — read_article tool (was nested under tools/rss)
- Delete services/rss.py, rss_classifier.py, rss_filtering.py, article_context.py
- Delete services/tools/rss.py
- Delete models/rss_feed.py (RssFeed, RssItem), models/rss_item_embedding.py
- services/embeddings.py: drop upsert/semantic_search/backfill RSS helpers
- services/llm.py: remove _build_briefing_article_context, briefing-conv branch,
ARTICLE_DISCUSS_SEED skip-RAG branch; drop get_rss_items / add_rss_feed from
the actions list
- services/generation_task.py: drop _maybe_save_article_discussion_note + caller
- routes/chat.py: drop /api/chat/from-article/<id> endpoint
- routes/journal.py: re-import via web.py refactor (article_fetcher path)
- services/tools/__init__.py: register `article`, drop `rss`
- services/tools/_registry.py: drop the requires=='rss' check
- app.py: drop backfill_rss_item_embeddings + backfill_rss_article_content tasks
- config.py: prose-only edit (no env var change — RSS env vars were never first-class)
Frontend:
- stores/settings.ts: drop rssEnabled
- SettingsView.vue: drop the RSS-classification mention
- api/client.ts: drop openArticleInChat (the from-article endpoint is gone)
Tests:
- Delete tests/test_rss_service.py, test_news_api.py, test_article_reading.py
Migration:
- 0042_drop_rss: DROP TABLE rss_item_embeddings, rss_item_reactions, rss_items,
rss_feeds; DELETE settings rows for rss_enabled / briefing_*_topics
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates user-visible branding across frontend (PWA manifest, page title,
Settings, push fallback), backend (email templates and subjects, LLM
system prompt, CalDAV displayname, SMTP from-name default), README,
quickstart compose, and MCP server description.
Also updates the CI image path and quickstart image reference to
git.fabledsword.com/bvandeusen/fabledscribe in preparation for the
Forgejo repo rename. Internal Python package, env vars, and database
schema unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
qwen2.5:3b produced broken auto-summaries (misspellings, token repetition,
hallucinated topics) — its synthesis ceiling is too low for free-form
summarization. Gemma 3 4B is stronger on summarization at similar size
and still fits comfortably alongside the main chat model in VRAM, so it
preserves the KV-cache-separation strategy that keeps chat TTFT fast.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded "2h" keep_alive everywhere with a helper that
returns OLLAMA_KEEP_ALIVE_MAIN (default 30m) for the interactive model
and OLLAMA_KEEP_ALIVE_BACKGROUND (default 10m) for the background
model. Lets the main model release VRAM during long idle periods
while keeping it warm enough for bursty chat use, and stops the
sporadic background model from camping VRAM it rarely needs.
Seven call sites updated to route through llm.keep_alive_for(model):
the streaming helpers, generate_completion, the two startup warmers,
the settings KV-cache primer, and the chat warmer endpoint.
Override via env vars: OLLAMA_KEEP_ALIVE_MAIN, OLLAMA_KEEP_ALIVE_BACKGROUND.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Background tasks (title generation, tag suggestions, project summaries,
RSS classification) were using qwen3:8b and wiping its KV cache after
every response, preventing prefix cache hits on subsequent user messages.
Adds OLLAMA_BACKGROUND_MODEL (default: qwen2.5:0.5b) config var and
routes all background LLM calls to it, keeping qwen3:8b's KV cache
warm between user messages for consistent sub-second TTFT.
Also adds infinite scroll to KnowledgeView (replaces load-more button)
and bakes spaCy en_core_web_sm into the Docker image to eliminate the
pip install on every startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
65536 was causing Ollama to allocate a ~50GB KV cache, spilling 77% of
the model to CPU RAM and making prefill extremely slow (35-125s TTFT).
16384 covers 30+ message conversations comfortably while keeping the KV
cache small enough to stay on GPU.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NoteEditorView: two-column sidebar layout (project/milestone/tags/assist
always visible), removed assist toggle button, InlineAssistPanel removed.
Writing assist: whole_doc mode rewrites entire document; DiffView.vue
replaces editor during review showing full-document diff. Scope dropdown
in sidebar switches between whole-document and section modes.
Persistent drafts: migration 0022 adds note_drafts (UNIQUE per note+user)
and note_versions (max 20, auto-pruned) tables. Draft saved after generation
completes, restored on editor mount, cleared on accept/reject. Version
snapshot created automatically whenever note body changes on save.
HistoryPanel.vue: version list + DiffView modal, restore button writes
body back to editor.
Config: OLLAMA_NUM_CTX default raised to 65536; assist num_predict now
tracks Config.OLLAMA_NUM_CTX instead of a hardcoded 4096.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The separate intent model (OLLAMA_INTENT_MODEL / qwen2.5:7b) is removed
from every part of the system. All classification now uses the primary model.
Changes:
- config.py: remove OLLAMA_INTENT_MODEL
- intent.py: remove classify_intent() and all supporting infrastructure
(_SYSTEM_PROMPT_TEMPLATE, _RESEARCH_PREFIX, _PRIOR_WORK_REFS); file now
only contains the quick-capture classifier
- quick_capture.py: classify_capture_intent() now called with Config.OLLAMA_MODEL
- generation_task.py: remove intent_model_setting DB lookup and get_setting import;
history summarization and research pipeline use the primary model directly
- research.py: remove intent_model parameter from run_research_pipeline() and
_generate_sub_queries(); both use the model param throughout
- routes/settings.py: remove intent_model from model-key validation and response
- app.py: remove intent model pre-warming at startup
- SettingsView.vue: remove Intent Model selector and related refs/state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The intent classifier (Phase 21) is removed from the main chat generation
path. The main model now handles all tool routing natively via Ollama's
structured tool-calling API, eliminating misidentification issues caused
by the small intent model.
Changes:
- generation_task.py: remove classify_intent call, intent_task, _WRITE_TOOLS,
_TOOL_ACTIONS, _INTENT_TRIGGER_WORDS, _should_skip_intent(), and the entire
round-0 intent-first + write-tool confirmation block (~315 lines removed)
- research_topic tool calls are now handled inline in the streaming loop:
runs run_research_pipeline, streams synthesis to buf, then breaks the round
loop (research is still the full response, no model follow-up)
- config.py: raise OLLAMA_NUM_CTX default from 8192 to 16384
The quick-capture dedicated classifier (classify_capture_intent) is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Images found via SearXNG are fetched server-side, stored on disk, and
served from /api/images/<id> — the user's browser never contacts the
original image host. Original URLs are preserved for citation.
New files:
- alembic/versions/0016_add_image_cache.py — image_cache table
- src/fabledassistant/models/image_cache.py — SQLAlchemy model
- src/fabledassistant/services/images.py — fetch/store/serve logic
- src/fabledassistant/routes/images.py — GET /api/images/<id>
Modified:
- config.py: IMAGE_CACHE_DIR (/data/images), IMAGE_MAX_BYTES (5 MB)
- research.py: _search_searxng_images() — SearXNG categories=images
- tools.py: _IMAGE_TOOLS def + search_images branch in execute_tool
- intent.py: search_images routing rule (explicit visual language only)
- app.py: register images_bp
- docker-compose.yml: image_cache named volume mounted at /data/images
- ToolCallCard.vue: "image_search" label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
config.py:
- Default OLLAMA_INTENT_MODEL: qwen2.5:1.5b → qwen2.5:7b
- Startup will auto-pull and warm the new model on next container restart
intent.py:
- Replaced phrase-matching examples in search_web and research_topic rules
with semantic descriptions. The 7B model doesn't need example phrases to
understand intent — it can reason from the tool's purpose. Removes implied
usage patterns that caused misclassifications on conversational phrasing
(e.g. "I've been thinking about buying shirts, can you research this?").
- research_topic rule now explicitly covers any subject regardless of phrasing,
including shopping decisions, comparisons, how-things-work questions, etc.
- search_web rule clarified as "short summary, no note" vs research_topic's
"comprehensive written reference"
The 1.5B model required prescriptive phrase examples to route correctly; the
7B model has sufficient language understanding to classify from semantic intent.
Expected improvement: ~1-2s intent calls (vs 0.4-9s for the 1.5B model which
sometimes timed out or misclassified longer/conversational messages).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Default OLLAMA_INTENT_MODEL to qwen2.5:1.5b in code instead of empty
- Add GET /api/settings/models endpoint returning installed models and defaults
- Validate intent_model against installed models on save (same as default_model)
- Replace intent model text input with a dropdown of installed models
- Add chat model dropdown to Assistant settings section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the hardcoded num_ctx=32768 KV cache allocation with a
configurable env var defaulting to 8192. This significantly reduces
VRAM pressure when multiple services share the GPU.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend security & correctness:
- Add rate_limit.py: sliding-window rate limiter (asyncio) applied to login,
register, forgot/reset password endpoints (10/60s or 5/300s per IP)
- app.py: add security headers in after_request (X-Frame-Options, CSP,
X-Content-Type-Options, Referrer-Policy) using setdefault to preserve SSE headers
- auth.py: refactor duplicate login_required/admin_required into shared _check_auth()
- config.py: add TRUST_PROXY_HEADERS for proxy-aware client IP resolution
- routes/auth.py: rate limiting, _client_ip() helper, cleaned-up reset_password route
- routes/chat.py, notes.py, tasks.py: int() DoS fix on last_event_id; limit capped
at 500; date.fromisoformat() wrapped in try/except → 400 on invalid dates
- services/auth.py: fix Setting.user_id update bug (filter on NULL not user.id);
reset_password_with_token returns int|None (user_id) instead of bool
- services/backup.py: add _security_notice to full backup JSON export
- services/assist.py: system prompt explicitly preserves markdown list structure
and nested indented sub-items
Infrastructure:
- docker-compose.yml: add healthcheck on app service (/api/health, 10s interval)
- .dockerignore: prevent secrets/node_modules/__pycache__/.env.* leaking into build
Frontend bug fixes:
- TaskCard.vue, TaskViewerView.vue: fix isOverdue() timezone bug (ISO string compare)
- useAssist.ts: accept() now resets state to idle when document changed since proposal
- stores/chat.ts: fix memory leak in _pollUntilLoaded() (try/catch around fetchStatus)
- TiptapEditor.vue: selection offset uses closest-match strategy (not first-match)
- utils/markdown.ts: explicit DOMPurify config with FORCE_BODY; remove as const
(DOMPurify expects mutable string[])
New features:
- Auto-save (5-minute interval) in NoteEditorView and TaskEditorView — only when
editing an existing dirty record; silent on error, shows "Auto-saved" toast
- sectionParser.ts: top-level bullet/numbered list items are now individual sections
in the AI Assist panel (previously treated as one undifferentiated block)
- editor-shared.css: extracted ~500 lines of CSS duplicated between both editors;
includes .inline-assist-btn at global scope (required for teleported elements)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New NoteEmbedding model + migration 0014 stores float embeddings (JSONB)
- services/embeddings.py: get_embedding, upsert_note_embedding,
semantic_search_notes (cosine similarity), backfill_note_embeddings
- build_context() now tries semantic search first, falls back to keyword search;
accepts cached_note_ids to reuse last-turn notes and stabilise the system
prompt prefix for Ollama's KV cache
- generation_buffer.py: per-conversation note ID cache (get/set/clear)
- generation_task.py: passes cached IDs into build_context, updates cache
after each turn, and invalidates it after create_note/update_note/create_task
- app.py: pulls nomic-embed-text at startup and launches a background backfill
to embed all existing notes (30 s delay so Ollama has time to load the model)
- routes/notes.py + services/tools.py: fire-and-forget embedding update on
every note create or update via the API or LLM tool calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mistral didn't reliably use Ollama's structured tool calling API — it wrote
tool calls as JSON text instead of invoking them. This adds an intent routing
layer that classifies user intent via a fast non-streaming LLM call before
streaming, executing detected tools directly and bypassing native tool calling.
- Change default OLLAMA_MODEL from mistral to qwen3
- Add intent.py: classify_intent() with JSON parsing and fallback regex
- Integrate intent routing into generation_task.py round 0
- Add all-day event support (iCalendar DATE values) to CalDAV service
- Add recurring event support (RRULE) to CalDAV service and tool definition
- Improve create_event tool description for descriptive titles
- Enhance system prompt with structured tool usage guidance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CalDAV integration: per-user calendar config, create/list/search events
via caldav library, LLM tools for calendar operations from chat
- LLM-suggested tags: new tag_suggestions service prompts LLM with existing
tags and note content to suggest 3-5 relevant tags; exposed via API
endpoints (suggest-tags, append-tag); integrated into editor views
(suggest button + clickable pills) and chat tool calls (pills in
ToolCallCard with one-click apply)
- Settings/model UI refinements, generation task improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users who forget their password can now request a reset link via email.
Tokens are SHA-256 hashed before storage, expire after 1 hour, and
previous unused tokens are invalidated on new requests. The forgot-password
endpoint always returns success to prevent email enumeration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 5.4 — Application Logging:
- AppLog model + migration 0010 for unified audit/usage/error logging
- Usage logging middleware in app.py (after_request for /api/* requests)
- Error logging in 500 handler with traceback capture
- Audit logging for auth events (register, login, login_failed, logout,
password_change) and admin actions (backup, restore, user_delete,
registration_toggle, smtp_config, smtp_test)
- Admin log viewer (LogsView.vue) with stats, category/search/date
filters, paginated table with expandable detail rows
- Admin logs API endpoints in admin.py (GET /logs, GET /logs/stats)
- Configurable retention via LOG_RETENTION_DAYS with hourly cleanup
Phase 5.5 — SMTP Email Notifications:
- aiosmtplib dependency for async email sending
- Email service (services/email.py) with STARTTLS/implicit TLS support
- Notification service (services/notifications.py) for security alerts
and task due date reminders with per-user preferences
- Admin SMTP config endpoints (GET/PUT /api/admin/smtp, POST test)
- SMTP config in Config class with env var + Docker secret support
- Settings UI: notification preferences for all users, SMTP config
section for admin with test email
Other changes:
- stream_chat() now accepts optional options dict (for num_predict)
- Increase assist MAX_BODY_CHARS from 3000 to 8000
- get_user_by_username() added to auth service
- apiStreamPost buffer processing refactored for robustness
- AppHeader: admin Logs nav link
- Router: /admin/logs route
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Registration auto-closes after first user; admin can toggle from /admin/users
- Admin user management view with user list and delete
- Password confirmation on registration form
- Password change in Settings (PUT /api/auth/password)
- Session cookie hardening: HttpOnly, SameSite=Lax, optional Secure flag
- Startup warning when SECRET_KEY is default
- Production deployment docs: reverse proxy, rate limiting, CSP headers
- Fix assist prompt to preserve markdown headings in target sections
- Simplify prod compose networking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4: Full chat system with SSE streaming, note-aware context, and
conversation persistence.
Backend:
- Migration 0005: conversations + messages tables with FKs and indexes
- Conversation/Message SQLAlchemy models with relationships
- LLM service: ensure_model (auto-pull on startup), stream_chat (NDJSON),
generate_completion, fetch_url_content (HTML stripping), build_context
(keyword extraction, related note search, URL content injection)
- Chat service: conversation CRUD, save_response_as_note,
summarize_conversation_as_note
- Chat routes blueprint: 9 endpoints including SSE streaming for messages,
save/summarize as note, Ollama model listing
- Auto-pull llama3.1 model on app startup (non-blocking)
Frontend:
- apiStreamPost: SSE client using fetch + ReadableStream
- Chat Pinia store with streaming state management
- ChatView: dedicated /chat page with conversation sidebar + message thread
- ChatPanel: slide-out panel with contextNoteId from current route
- ChatMessage: markdown-rendered message bubble with "Save as Note" action
- Updated AppHeader with Chat nav link + panel toggle button
- Updated App.vue to mount ChatPanel with route-derived context
- Added /chat and /chat/:id routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>