Add semantic note search (nomic-embed-text) and per-conversation note cache

- 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>
This commit is contained in:
2026-02-18 21:44:58 -05:00
parent de5921904d
commit d6f4a6dbb6
11 changed files with 349 additions and 22 deletions
+13
View File
@@ -1,5 +1,6 @@
"""Tool definitions and executor for LLM tool calling."""
import asyncio
import logging
from datetime import date, datetime
@@ -22,6 +23,15 @@ from fabledassistant.services.tag_suggestions import suggest_tags
logger = logging.getLogger(__name__)
def _schedule_embedding(note_id: int, user_id: int, title: str, body: str) -> None:
"""Fire-and-forget: update the embedding for a note after it's created/modified."""
from fabledassistant.services.embeddings import upsert_note_embedding
text = f"{title}\n{body}".strip() if body else (title or "")
if text:
asyncio.create_task(upsert_note_embedding(note_id, user_id, text))
# Core tools — always available
_CORE_TOOLS = [
{
@@ -546,6 +556,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
due_date=_parse_due_date(arguments.get("due_date")),
)
suggested = await suggest_tags(user_id, task_title, task_body)
_schedule_embedding(note.id, user_id, task_title, task_body)
return {
"success": True,
"type": "task",
@@ -568,6 +579,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
body=note_body,
)
suggested = await suggest_tags(user_id, note_title, note_body)
_schedule_embedding(note.id, user_id, note_title, note_body)
return {
"success": True,
"type": "note",
@@ -616,6 +628,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
return {"success": False, "error": "Failed to update note."}
suggested = await suggest_tags(user_id, updated.title, updated.body or "")
_schedule_embedding(updated.id, user_id, updated.title, updated.body or "")
return {
"success": True,
"type": "note_updated",