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
+48 -13
View File
@@ -2,7 +2,7 @@
import asyncio
import logging
from datetime import datetime, timedelta
from datetime import date as date_type, datetime, timedelta
import caldav
import icalendar
@@ -76,30 +76,61 @@ async def create_event(
duration: int | None = None,
description: str | None = None,
location: str | None = None,
all_day: bool = False,
recurrence: str | None = None,
) -> dict:
"""Create a calendar event. start/end are ISO datetime strings."""
"""Create a calendar event.
start/end are ISO date (YYYY-MM-DD) or datetime strings.
If all_day is True, DTSTART/DTEND use DATE values.
recurrence is an iCalendar RRULE string (e.g. "FREQ=YEARLY").
"""
config = await get_caldav_config(user_id)
if not (config.get("caldav_url") and config.get("caldav_username") and config.get("caldav_password")):
raise ValueError("CalDAV is not configured. Go to Settings → Calendar to set it up.")
dt_start = datetime.fromisoformat(start)
if end:
dt_end = datetime.fromisoformat(end)
else:
dt_end = dt_start + timedelta(minutes=duration or 60)
cal = icalendar.Calendar()
cal.add("prodid", "-//FabledAssistant//EN")
cal.add("version", "2.0")
event = icalendar.Event()
event.add("summary", title)
event.add("dtstart", dt_start)
event.add("dtend", dt_end)
if all_day:
# All-day events use DATE values (no time component)
d_start = datetime.fromisoformat(start).date() if "T" in start else date_type.fromisoformat(start)
if end:
d_end = datetime.fromisoformat(end).date() if "T" in end else date_type.fromisoformat(end)
else:
d_end = d_start + timedelta(days=1)
event.add("dtstart", d_start)
event.add("dtend", d_end)
result_start = d_start.isoformat()
result_end = d_end.isoformat()
else:
dt_start = datetime.fromisoformat(start)
if end:
dt_end = datetime.fromisoformat(end)
else:
dt_end = dt_start + timedelta(minutes=duration or 60)
event.add("dtstart", dt_start)
event.add("dtend", dt_end)
result_start = dt_start.isoformat()
result_end = dt_end.isoformat()
if description:
event.add("description", description)
if location:
event.add("location", location)
if recurrence:
# Parse RRULE string like "FREQ=YEARLY" into a vRecur dict
rrule_parts = {}
for part in recurrence.split(";"):
if "=" in part:
key, value = part.split("=", 1)
rrule_parts[key.strip().lower()] = value.strip()
event.add("rrule", rrule_parts)
cal.add_component(event)
ical_str = cal.to_ical().decode("utf-8")
@@ -111,11 +142,15 @@ async def create_event(
await asyncio.to_thread(_save)
return {
result = {
"title": title,
"start": dt_start.isoformat(),
"end": dt_end.isoformat(),
"start": result_start,
"end": result_end,
"all_day": all_day,
}
if recurrence:
result["recurrence"] = recurrence
return result
async def list_events(user_id: int, date_from: str, date_to: str) -> list[dict]: