Switch default model to qwen3 and add intent routing for reliable tool calling

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>
This commit is contained in:
2026-02-16 16:24:01 -05:00
parent d7bc3f3222
commit 75560dee4e
7 changed files with 272 additions and 31 deletions
+13 -7
View File
@@ -12,7 +12,7 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-02-15 — Phase 8: CalDAV calendar integration, LLM-suggested tags, settings/model UI refinements
2026-02-16 — Phase 9: Switch to qwen3, intent routing for reliable tool calling, all-day/recurring events
## Project Overview
Fabled Assistant is a self-hosted note-taking and task-tracking application with
@@ -263,10 +263,11 @@ fabledassistant/
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming (stream_chat + stream_chat_with_tools), ChatChunk dataclass, URL fetching
│ │ ├── 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, tool loop) + run_assist_generation (lightweight, no DB)
│ │ ├── tools.py # LLM tool definitions (create_task, create_note, search_notes, CalDAV events) + execute_tool dispatcher
│ │ ├── generation_task.py # Background asyncio tasks: run_generation (chat, DB flush, titles, intent routing + tool loop) + run_assist_generation (lightweight, no DB)
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call to detect tool intent before streaming
│ │ ├── tools.py # LLM tool definitions (create_task, create_note, search_notes, CalDAV events with all-day/recurrence) + execute_tool dispatcher
│ │ ├── tag_suggestions.py # LLM-powered tag suggestions: suggest_tags() builds prompt with existing tags, calls generate_completion, parses JSON response
│ │ ├── caldav.py # CalDAV integration: create/list/search calendar events via caldav library (per-user config from settings)
│ │ ├── caldav.py # CalDAV integration: create/list/search calendar events via caldav library, all-day + recurring event support (per-user config from settings)
│ │ ├── settings.py # Settings CRUD with user_id isolation: get_setting, set_setting, set_settings_batch, get_all_settings
│ │ ├── logging.py # App logging: log_audit, log_usage, log_error, get_logs, get_log_stats, delete_old_logs, start_log_retention_loop
│ │ ├── email.py # SMTP email service: get_smtp_config, is_smtp_configured, send_email, send_test_email
@@ -523,7 +524,7 @@ When adding a new migration, follow these conventions:
- All task sections hidden when empty; marking done removes from all lists
### LLM Chat
- Ollama integration via async HTTP (httpx), auto-pull default model on startup
- Ollama integration via async HTTP (httpx), default model qwen3 (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
@@ -546,9 +547,14 @@ When adding a new migration, follow these conventions:
components (linked titles for created items, search result lists). SSE emits `tool_call` events
for real-time rendering during streaming. System prompt includes today's date for relative date
resolution. Graceful degradation: models without tool support respond normally.
- **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.
- **CalDAV calendar integration:** Per-user CalDAV settings (URL, username, password, calendar name).
LLM tools: `create_event`, `list_events`, `search_events`. Runs synchronous caldav library calls
in asyncio executor. Settings UI for CalDAV configuration.
LLM tools: `create_event` (with `all_day` and `recurrence` support), `list_events`, `search_events`.
All-day events use iCalendar DATE values; recurrence uses RRULE (e.g. `FREQ=YEARLY`).
Runs synchronous caldav library calls in asyncio executor. Settings UI for CalDAV configuration.
- **LLM-suggested tags:** Backend service (`tag_suggestions.py`) prompts LLM with existing user tags
and note content, returns 3-5 relevant tag suggestions. Tags already in body are filtered out.
Exposed via `POST /api/notes/suggest-tags` and `POST /api/notes/:id/append-tag`. Integrated in: