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
+8
View File
@@ -2,6 +2,8 @@ import asyncio
import logging
from datetime import date
from fabledassistant.services.embeddings import upsert_note_embedding
from quart import Blueprint, Response, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id
@@ -84,6 +86,9 @@ async def create_note_route():
priority=priority,
due_date=due_date,
)
text = f"{note.title}\n{note.body}".strip() if note.body else (note.title or "")
if text:
asyncio.create_task(upsert_note_embedding(note.id, uid, text))
return jsonify(note.to_dict()), 201
@@ -182,6 +187,9 @@ async def update_note_route(note_id: int):
note = await update_note(uid, note_id, **fields)
if note is None:
return jsonify({"error": "Note not found"}), 404
text = f"{note.title}\n{note.body}".strip() if note.body else (note.title or "")
if text:
asyncio.create_task(upsert_note_embedding(note.id, uid, text))
return jsonify(note.to_dict())