Remove CalDAV todo tools; overhaul quick-capture
- Remove all 6 CalDAV todo tools (create/list/update/complete/delete/search_todos) from tools.py definitions, imports, execute_tool branches, intent routing rules, generation_task labels/actions, and llm.py system prompt hints. CalDAV event tools remain. Todo functions still exist in caldav.py but are no longer exposed. - Quick-capture now uses a dedicated classify_capture_intent() with a focused _CAPTURE_SYSTEM_PROMPT that always routes to a tool (never null). Tool set expanded: create_note/task/event + update_note + research_topic. - research_topic in quick-capture calls run_research_pipeline() directly (no SSE buffer). run_research_pipeline() now accepts buf=None; all buf.append_event calls are guarded so status events are skipped when no buffer is provided. - Fallback note now always sets body=text (was empty for texts ≤80 chars). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,17 +11,16 @@ from quart import Blueprint, jsonify, request
|
||||
|
||||
from fabledassistant.auth import get_current_user_id, login_required
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.intent import classify_intent
|
||||
from fabledassistant.services.intent import classify_capture_intent
|
||||
from fabledassistant.services.tools import execute_tool, get_tools_for_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
quick_capture_bp = Blueprint("quick_capture", __name__, url_prefix="/api/quick-capture")
|
||||
|
||||
# Only creation tools are offered to the intent classifier here — no delete,
|
||||
# update, search, or research. This keeps the fallback safe (worst case we
|
||||
# create a plain note) and avoids accidental destructive actions.
|
||||
_CAPTURE_TOOL_NAMES = {"create_note", "create_task", "create_event", "create_todo"}
|
||||
# Tools offered to the quick-capture classifier. Excludes destructive ops
|
||||
# (delete_*) and read-only queries — worst-case fallback is a plain note.
|
||||
_CAPTURE_TOOL_NAMES = {"create_note", "create_task", "create_event", "update_note", "research_topic"}
|
||||
|
||||
|
||||
@quick_capture_bp.route("", methods=["POST"])
|
||||
@@ -42,21 +41,43 @@ async def quick_capture_route():
|
||||
t for t in all_tools if t.get("function", {}).get("name") in _CAPTURE_TOOL_NAMES
|
||||
]
|
||||
|
||||
intent = await classify_intent(text, capture_tools, intent_model)
|
||||
intent = await classify_capture_intent(text, capture_tools, intent_model)
|
||||
|
||||
if intent.should_execute:
|
||||
# research_topic bypasses execute_tool — run the pipeline directly
|
||||
if intent.tool_name == "research_topic" and Config.searxng_enabled():
|
||||
from fabledassistant.services.research import run_research_pipeline
|
||||
|
||||
topic = intent.arguments.get("topic", text)
|
||||
model = Config.OLLAMA_MODEL
|
||||
try:
|
||||
note = await run_research_pipeline(topic, uid, model, intent_model)
|
||||
logger.info(
|
||||
"Quick-capture uid=%d: research note id=%d '%s'",
|
||||
uid, note.id, note.title,
|
||||
)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"type": "note",
|
||||
"message": f"Research note created: {note.title}",
|
||||
"data": {"id": note.id, "title": note.title},
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.exception("Quick-capture research failed for topic: %s", topic)
|
||||
return jsonify({"error": f"Research failed: {exc}"}), 500
|
||||
|
||||
result = await execute_tool(uid, intent.tool_name, intent.arguments)
|
||||
if result.get("success"):
|
||||
item_type = result.get("type", "note")
|
||||
title = (result.get("data") or {}).get("title", "")
|
||||
logger.info(
|
||||
"Quick-capture uid=%d: created %s '%s' via intent '%s'",
|
||||
"Quick-capture uid=%d: %s '%s' via intent '%s'",
|
||||
uid, item_type, title, intent.tool_name,
|
||||
)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"type": item_type,
|
||||
"message": f"{item_type.capitalize()} created: {title}",
|
||||
"message": f"{item_type.capitalize()}: {title}",
|
||||
"data": result.get("data"),
|
||||
})
|
||||
logger.warning(
|
||||
@@ -66,13 +87,9 @@ async def quick_capture_route():
|
||||
# Fall through to plain-note fallback
|
||||
|
||||
# Fallback: create a plain note with the raw text as the body.
|
||||
# Use the first 80 chars as the title so short snippets look clean.
|
||||
if len(text) <= 80:
|
||||
fallback_title = text
|
||||
fallback_body = ""
|
||||
else:
|
||||
fallback_title = text[:77] + "..."
|
||||
fallback_body = text
|
||||
# Always preserve the full text as the body; derive a short title.
|
||||
fallback_title = text if len(text) <= 80 else text[:77] + "..."
|
||||
fallback_body = text
|
||||
|
||||
result = await execute_tool(
|
||||
uid, "create_note", {"title": fallback_title, "body": fallback_body}
|
||||
|
||||
@@ -54,12 +54,6 @@ _TOOL_LABELS: dict[str, str] = {
|
||||
"update_event": "Updating calendar event",
|
||||
"delete_event": "Removing calendar event",
|
||||
"list_calendars": "Listing calendars",
|
||||
"create_todo": "Creating todo",
|
||||
"list_todos": "Listing todos",
|
||||
"search_todos": "Searching todos",
|
||||
"update_todo": "Updating todo",
|
||||
"complete_todo": "Completing todo",
|
||||
"delete_todo": "Removing todo",
|
||||
"search_web": "Searching the web",
|
||||
"research_topic": "Researching topic",
|
||||
}
|
||||
@@ -68,7 +62,6 @@ _TOOL_LABELS: dict[str, str] = {
|
||||
_WRITE_TOOLS: frozenset[str] = frozenset({
|
||||
"create_task", "create_note", "update_note", "delete_note", "delete_task",
|
||||
"create_event", "update_event", "delete_event",
|
||||
"create_todo", "update_todo", "complete_todo", "delete_todo",
|
||||
})
|
||||
|
||||
# Action phrases used in the acknowledgment prompt — "You are about to: {action}"
|
||||
@@ -82,18 +75,12 @@ _TOOL_ACTIONS: dict[str, str] = {
|
||||
"list_notes": "list notes",
|
||||
"list_tasks": "look up tasks",
|
||||
"search_notes": "search through notes",
|
||||
"search_todos": "search calendar todos",
|
||||
"create_event": "schedule a calendar event",
|
||||
"list_events": "check the calendar",
|
||||
"search_events": "search calendar events",
|
||||
"update_event": "update a calendar event",
|
||||
"delete_event": "remove a calendar event",
|
||||
"list_calendars": "list available calendars",
|
||||
"create_todo": "create a calendar todo",
|
||||
"list_todos": "check calendar todos",
|
||||
"update_todo": "update a calendar todo",
|
||||
"complete_todo": "mark a todo complete",
|
||||
"delete_todo": "remove a calendar todo",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -84,15 +84,9 @@ Rules:
|
||||
- "what are my overdue tasks", "show overdue", "tasks due today", "high priority tasks", "in progress tasks", "what's due this week" → use list_tasks with appropriate status/priority/due_before/due_after filters. For overdue, set due_before to today's date.
|
||||
- If a note was created earlier in the conversation and the user provides more content for it, use update_note (not create_note).
|
||||
- Use create_note ONLY when the user explicitly wants a brand new note that doesn't already exist.
|
||||
- "update", "change", "rename", "set due date on" a CalDAV todo → use update_todo.
|
||||
- "update", "change", "move", "reschedule" an event → use update_event.
|
||||
- "delete", "cancel", "remove" an event → use delete_event.
|
||||
- "which calendars", "list calendars", "my calendars" → use list_calendars.
|
||||
- "create a calendar todo", "add a CalDAV todo" → use create_todo. Default to create_task for general tasks unless the user explicitly mentions CalDAV or calendar todo.
|
||||
- "show calendar todos", "list calendar todos" → use list_todos.
|
||||
- "find calendar todo", "search calendar todos", "find todo named X" → use search_todos with query=<keyword>.
|
||||
- "completed/finished calendar todo" → use complete_todo.
|
||||
- "delete/remove calendar todo" → use delete_todo.
|
||||
- "remind me X minutes/hours before" an event → convert to reminder_minutes parameter on create_event.
|
||||
- When the user asks about events in a time period (e.g. "events in September", "what's on next week", "my schedule for March"), use list_events with date_from/date_to covering that period. Do NOT use search_events for time-based queries — search_events is only for keyword matching against event titles.
|
||||
- "delete", "remove", "trash", "get rid of" a note (not a task) → use delete_note with query=<note name/keyword>. NEVER use this for tasks.
|
||||
@@ -279,3 +273,74 @@ def _try_json(text: str) -> dict | list | None:
|
||||
return json.loads(text)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
# ── Quick-capture classifier ──────────────────────────────────────────────────
|
||||
# A stripped-down prompt designed for the /api/quick-capture endpoint.
|
||||
# Unlike the general intent prompt, this ALWAYS routes to a create tool —
|
||||
# null is not a valid response.
|
||||
|
||||
_CAPTURE_SYSTEM_PROMPT = """\
|
||||
You are a quick-capture classifier. The user has sent a short snippet of text \
|
||||
from a mobile app or external client. Classify it as a note, task, or calendar \
|
||||
event, then extract the relevant fields.
|
||||
|
||||
Today's date is {today}.
|
||||
|
||||
Available tools:
|
||||
{tool_summary}
|
||||
|
||||
Rules:
|
||||
- You MUST choose one of the available tools. Never return null.
|
||||
- create_task: action items, todos, reminders, things to do ("buy milk", "call John", "fix the bug", "remind me to…")
|
||||
- create_event: appointments, meetings, scheduled occurrences with a date/time ("dentist Friday 2pm", "team meeting next Tuesday")
|
||||
- update_note: updating, editing, appending to an existing note or task ("add to my shopping list: eggs", "mark buy milk done", "append to my meeting notes", "update my project note")
|
||||
- research_topic: user wants a comprehensive research note from web sources ("research X", "look up X and make a note", "find everything about X", "compile a note on X")
|
||||
- create_note: everything else — ideas, observations, links, excerpts, longer text
|
||||
- For create_task / create_event: extract a concise title; put any extra detail in "body"
|
||||
- For create_note: use a short descriptive title (≤60 chars); put the FULL original text as "body"
|
||||
- For update_note: set "query" to the note or task title to find; set other fields as needed
|
||||
- For research_topic: set "topic" to the subject being researched
|
||||
- For dates use YYYY-MM-DD; for datetime use ISO 8601
|
||||
- confidence: "high" if the type is clear; "medium" if you're guessing
|
||||
|
||||
Respond with ONLY a JSON object:
|
||||
{{"tool": "tool_name", "arguments": {{...}}, "confidence": "high"|"medium"}}
|
||||
|
||||
Do NOT wrap in markdown code fences."""
|
||||
|
||||
|
||||
async def classify_capture_intent(
|
||||
text: str,
|
||||
tools: list[dict],
|
||||
model: str,
|
||||
) -> IntentResult:
|
||||
"""Classify quick-capture text and extract arguments.
|
||||
|
||||
Uses a simplified prompt that always routes to a create tool — never null.
|
||||
Returns IntentResult with tool_name set. Falls back to IntentResult() only
|
||||
on LLM/parse failure (caller should handle that case).
|
||||
"""
|
||||
if not tools:
|
||||
return IntentResult()
|
||||
|
||||
tool_summary = _build_tool_summary(tools)
|
||||
today = date_type.today().isoformat()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": _CAPTURE_SYSTEM_PROMPT.format(
|
||||
today=today, tool_summary=tool_summary
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
|
||||
try:
|
||||
raw = await generate_completion(messages, model, max_tokens=300, num_ctx=2048)
|
||||
except Exception:
|
||||
logger.warning("Quick-capture intent LLM call failed", exc_info=True)
|
||||
return IntentResult()
|
||||
|
||||
return _parse_intent(raw, tools)
|
||||
|
||||
@@ -389,13 +389,10 @@ async def build_context(
|
||||
if has_caldav:
|
||||
tool_lines[-1] = (
|
||||
"Available actions: create_task, create_note, update_note, delete_note, delete_task, get_note, list_notes, list_tasks, search_notes, "
|
||||
"create_event, list_events, search_events, update_event, delete_event, "
|
||||
"list_calendars, create_todo, list_todos, search_todos, update_todo, complete_todo, delete_todo."
|
||||
"create_event, list_events, search_events, update_event, delete_event, list_calendars."
|
||||
)
|
||||
tool_lines.append("For calendar events, use ISO 8601 datetime format (e.g. 2026-09-30T00:00:00).")
|
||||
tool_lines.append("CalDAV todos are separate from app tasks. Use create_task for app tasks, create_todo for CalDAV todos.")
|
||||
tool_lines.append("When the user says 'remind me' with a time before an event, use the reminder_minutes parameter.")
|
||||
tool_lines.append("Use search_todos to find a specific CalDAV todo by keyword when list_todos would return too many results.")
|
||||
tool_lines.append("For relative dates like 'Friday' or 'next week', resolve them to YYYY-MM-DD format.")
|
||||
tool_lines.append("When creating notes, use the `tags` parameter — do not embed #tag text in the note body.")
|
||||
tool_lines.append(
|
||||
|
||||
@@ -26,15 +26,16 @@ async def run_research_pipeline(
|
||||
user_id: int,
|
||||
model: str,
|
||||
intent_model: str,
|
||||
buf,
|
||||
buf=None,
|
||||
) -> Note:
|
||||
"""Full research pipeline: search → fetch → synthesize → create note.
|
||||
|
||||
Emits status events via buf.append_event throughout.
|
||||
Emits status events via buf.append_event throughout (when buf is provided).
|
||||
Returns the created Note.
|
||||
"""
|
||||
# Step 1: Generate sub-queries
|
||||
buf.append_event("status", {"status": "Generating search queries..."})
|
||||
if buf is not None:
|
||||
buf.append_event("status", {"status": "Generating search queries..."})
|
||||
queries = await _generate_sub_queries(topic, intent_model)
|
||||
logger.info("Research: generated %d sub-queries for topic '%s'", len(queries), topic)
|
||||
|
||||
@@ -42,7 +43,8 @@ async def run_research_pipeline(
|
||||
async def _search_with_stagger(i: int, query: str) -> tuple[str, list[dict]]:
|
||||
if i > 0:
|
||||
await asyncio.sleep(0.2 * i)
|
||||
buf.append_event("status", {"status": f"Searching: {query}..."})
|
||||
if buf is not None:
|
||||
buf.append_event("status", {"status": f"Searching: {query}..."})
|
||||
results = await _search_searxng(query)
|
||||
logger.info("Research: query '%s' → %d results", query, len(results))
|
||||
return query, results
|
||||
@@ -64,7 +66,8 @@ async def run_research_pipeline(
|
||||
# Fetch all unique URLs in parallel
|
||||
async def _fetch_source(url: str, result: dict, query: str) -> dict:
|
||||
title = result.get("title", url)
|
||||
buf.append_event("status", {"status": f"Reading: {title[:60]}..."})
|
||||
if buf is not None:
|
||||
buf.append_event("status", {"status": f"Reading: {title[:60]}..."})
|
||||
content = await fetch_url_content(url)
|
||||
return {
|
||||
"url": url,
|
||||
@@ -98,11 +101,13 @@ async def run_research_pipeline(
|
||||
)
|
||||
|
||||
# Step 4: Synthesize (streams tokens into chat as the note is being written)
|
||||
buf.append_event("status", {"status": f"Synthesizing report from {len(synthesis_sources)} sources..."})
|
||||
if buf is not None:
|
||||
buf.append_event("status", {"status": f"Synthesizing report from {len(synthesis_sources)} sources..."})
|
||||
title, body = await _synthesize_note(topic, synthesis_sources, model, buf)
|
||||
|
||||
# Step 5: Create note
|
||||
buf.append_event("status", {"status": "Saving note..."})
|
||||
if buf is not None:
|
||||
buf.append_event("status", {"status": "Saving note..."})
|
||||
note = await create_note(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
|
||||
@@ -5,19 +5,13 @@ import logging
|
||||
from datetime import date, datetime
|
||||
|
||||
from fabledassistant.services.caldav import (
|
||||
complete_todo,
|
||||
create_event,
|
||||
create_todo,
|
||||
delete_event,
|
||||
delete_todo,
|
||||
is_caldav_configured,
|
||||
list_calendars,
|
||||
list_events,
|
||||
list_todos,
|
||||
search_events,
|
||||
search_todos,
|
||||
update_event,
|
||||
update_todo,
|
||||
)
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.notes import create_note, delete_note, get_note_by_title, list_notes, update_note
|
||||
@@ -489,178 +483,6 @@ _CALDAV_TOOLS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_todo",
|
||||
"description": "Create a CalDAV todo item on the calendar server. Use this when the user explicitly asks to create a calendar todo or CalDAV todo (NOT for regular app tasks).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "The todo summary/title",
|
||||
},
|
||||
"due": {
|
||||
"type": "string",
|
||||
"description": "Optional due date/datetime in ISO 8601 format",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description",
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Optional priority (1=highest, 9=lowest)",
|
||||
},
|
||||
"reminder_minutes": {
|
||||
"type": "integer",
|
||||
"description": "Optional reminder N minutes before due date",
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional IANA timezone (e.g. 'America/New_York')",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name",
|
||||
},
|
||||
},
|
||||
"required": ["summary"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_todos",
|
||||
"description": "List CalDAV todos from the calendar server. Use this when the user asks to see their calendar todos.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_completed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to include completed todos (default false)",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_todo",
|
||||
"description": "Update a CalDAV todo's summary, due date, description, or priority.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search term to find the todo to update (matches against summary)",
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "New summary/title for the todo",
|
||||
},
|
||||
"due": {
|
||||
"type": "string",
|
||||
"description": "New due date or datetime in ISO 8601 format",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "New description",
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "New priority (1=highest, 9=lowest)",
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional IANA timezone for due datetime",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name to search in",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "complete_todo",
|
||||
"description": "Mark a CalDAV todo as completed. Use this when the user says they finished or completed a calendar todo.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search term to find the todo to complete (matches against summary)",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_todo",
|
||||
"description": "Delete a CalDAV todo. Use this when the user asks to remove or delete a calendar todo.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search term to find the todo to delete (matches against summary)",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_todos",
|
||||
"description": (
|
||||
"Search CalDAV todos by keyword. Use this when the user asks to find a specific calendar todo "
|
||||
"by name or content, rather than listing all todos."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Keyword to search for in todo summary or description",
|
||||
},
|
||||
"include_completed": {
|
||||
"type": "boolean",
|
||||
"description": "Include completed todos in search (default false)",
|
||||
},
|
||||
"calendar_name": {
|
||||
"type": "string",
|
||||
"description": "Optional calendar name to restrict search",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1040,95 +862,6 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "create_todo":
|
||||
result = await create_todo(
|
||||
user_id=user_id,
|
||||
summary=arguments.get("summary", "Untitled Todo"),
|
||||
due=arguments.get("due"),
|
||||
description=arguments.get("description"),
|
||||
priority=arguments.get("priority"),
|
||||
reminder_minutes=arguments.get("reminder_minutes"),
|
||||
timezone=arguments.get("timezone"),
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todo",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
elif tool_name == "list_todos":
|
||||
todos = await list_todos(
|
||||
user_id=user_id,
|
||||
include_completed=arguments.get("include_completed", False),
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todos",
|
||||
"data": {
|
||||
"count": len(todos),
|
||||
"todos": todos,
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "update_todo":
|
||||
result = await update_todo(
|
||||
user_id=user_id,
|
||||
query=arguments["query"],
|
||||
summary=arguments.get("summary"),
|
||||
due=arguments.get("due"),
|
||||
description=arguments.get("description"),
|
||||
priority=arguments.get("priority"),
|
||||
timezone=arguments.get("timezone"),
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todo_updated",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
elif tool_name == "complete_todo":
|
||||
result = await complete_todo(
|
||||
user_id=user_id,
|
||||
query=arguments["query"],
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todo_completed",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
elif tool_name == "delete_todo":
|
||||
result = await delete_todo(
|
||||
user_id=user_id,
|
||||
query=arguments["query"],
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todo_deleted",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
elif tool_name == "search_todos":
|
||||
results = await search_todos(
|
||||
user_id=user_id,
|
||||
query=arguments.get("query", ""),
|
||||
include_completed=bool(arguments.get("include_completed", False)),
|
||||
calendar_name=arguments.get("calendar_name"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "todos",
|
||||
"data": {
|
||||
"count": len(results),
|
||||
"todos": results,
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "get_note":
|
||||
query = arguments.get("query", "")
|
||||
note = await get_note_by_title(user_id, query)
|
||||
|
||||
+6
-6
@@ -12,7 +12,7 @@
|
||||
> Include file-level details in the commit body when the change is non-trivial.
|
||||
|
||||
## Last Updated
|
||||
2026-03-01 — Image search with local cache (Phase 23); suggested notes improvements (limit 8, threshold 0.45, relevance scores); suggested notes UX; quick-capture endpoint
|
||||
2026-03-01 — Quick-capture overhaul (dedicated classifier, research + update_note support); CalDAV todo tools removed; run_research_pipeline buf optional
|
||||
|
||||
## Project Overview
|
||||
Fabled Assistant is a self-hosted note-taking and task-tracking application with
|
||||
@@ -278,7 +278,7 @@ fabledassistant/
|
||||
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
|
||||
│ │ ├── images.py # /api/images/<id> GET — serve locally-cached images (no auth required; IDs are opaque)
|
||||
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks + tag suggestions (all @login_required)
|
||||
│ │ ├── quick_capture.py # /api/quick-capture POST — mobile/external single-shot item creation (session auth)
|
||||
│ │ ├── quick_capture.py # /api/quick-capture POST — mobile/external single-shot item creation (session auth); uses classify_capture_intent(); supports create_note/task/event, update_note, research_topic; fallback always preserves full text as note body
|
||||
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
|
||||
│ │ └── settings.py # /api/settings GET/PUT, GET /models — per-user settings + installed model list (@login_required)
|
||||
│ ├── services/
|
||||
@@ -290,12 +290,12 @@ fabledassistant/
|
||||
│ │ ├── 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-first pipeline + tool loop) + run_assist_generation (lightweight, no DB); _INTENT_TRIGGER_WORDS + _should_skip_intent() for skipping intent on conversational messages
|
||||
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call (num_ctx=4096); IntentResult has ack field (one-sentence acknowledgment streamed as TTFT)
|
||||
│ │ ├── intent.py # Intent routing: classify_intent() makes fast non-streaming LLM call (num_ctx=4096); IntentResult has ack field; classify_capture_intent() uses dedicated _CAPTURE_SYSTEM_PROMPT (always routes to a tool, never null)
|
||||
│ │ ├── images.py # Image cache service: fetch_and_store_image (SHA-256 dedup, content-type validation, 5MB cap), get_image_record, get_image_path
|
||||
│ │ ├── 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, search_web, research_topic, search_images when SearXNG enabled) + execute_tool dispatcher
|
||||
│ │ ├── research.py # SearXNG research pipeline: parallel query/fetch, streaming synthesis; run_research_pipeline() → Note; _search_searxng_images() for image category search; constants SEARXNG_QUERIES=5, PAGES_PER_QUERY=3, MAX_SYNTHESIS_SOURCES=12, CHARS_PER_SOURCE=2000
|
||||
│ │ ├── 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, CalDAV event suite (no todos), search_web, research_topic, search_images when SearXNG enabled) + execute_tool dispatcher
|
||||
│ │ ├── research.py # SearXNG research pipeline: parallel query/fetch, streaming synthesis; run_research_pipeline(buf=None) → Note (buf optional — no status events when omitted); _search_searxng_images() for image category search; constants SEARXNG_QUERIES=5, PAGES_PER_QUERY=3, MAX_SYNTHESIS_SOURCES=12, CHARS_PER_SOURCE=2000
|
||||
│ │ ├── tag_suggestions.py # LLM-powered tag suggestions: suggest_tags() builds prompt with existing tags, calls generate_completion, parses JSON response
|
||||
│ │ ├── caldav.py # CalDAV integration: full event lifecycle (create/list/search/update/delete), todos (create/list/search/update/complete/delete), list_calendars, timezone (ZoneInfo), reminders (VALARM), attendees, multi-calendar search
|
||||
│ │ ├── caldav.py # CalDAV integration: full event lifecycle (create/list/search/update/delete), list_calendars, timezone (ZoneInfo), reminders (VALARM), attendees, multi-calendar search. Todo functions exist in code but are NOT exposed as LLM tools.
|
||||
│ │ ├── 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
|
||||
|
||||
Reference in New Issue
Block a user