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:
@@ -15,7 +15,13 @@ from sqlalchemy import update
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.models import async_session
|
||||
from fabledassistant.models.conversation import Message
|
||||
from fabledassistant.services.generation_buffer import GenerationBuffer, GenerationState
|
||||
from fabledassistant.services.generation_buffer import (
|
||||
GenerationBuffer,
|
||||
GenerationState,
|
||||
clear_conv_note_cache,
|
||||
get_conv_note_cache,
|
||||
set_conv_note_cache,
|
||||
)
|
||||
from fabledassistant.services.llm import ChatChunk, build_context, generate_completion, stream_chat, stream_chat_with_tools, summarize_history_for_context
|
||||
from fabledassistant.services.chat import update_conversation_title
|
||||
from fabledassistant.services.intent import IntentResult, classify_intent
|
||||
@@ -196,6 +202,10 @@ async def run_generation(
|
||||
history_to_use, history_summary = await summarize_history_for_context(history, intent_model)
|
||||
|
||||
# Phase 3: Build context and classify intent in parallel — the two slow legs.
|
||||
# Pass cached note IDs so build_context can reuse them, keeping the system
|
||||
# prompt prefix stable and helping Ollama's KV cache stay warm.
|
||||
cached_note_ids = get_conv_note_cache(conv_id) or None
|
||||
|
||||
pre_intent: IntentResult = IntentResult()
|
||||
intent_timing_ms: int | None = None
|
||||
if tools:
|
||||
@@ -209,6 +219,7 @@ async def run_generation(
|
||||
user_id, history_to_use, context_note_id, user_content,
|
||||
exclude_note_ids=exclude_note_ids,
|
||||
history_summary=history_summary,
|
||||
cached_note_ids=cached_note_ids,
|
||||
))
|
||||
intent_task = asyncio.create_task(
|
||||
classify_intent(user_content, tools, intent_model, history=intent_history)
|
||||
@@ -220,8 +231,14 @@ async def run_generation(
|
||||
user_id, history_to_use, context_note_id, user_content,
|
||||
exclude_note_ids=exclude_note_ids,
|
||||
history_summary=history_summary,
|
||||
cached_note_ids=cached_note_ids,
|
||||
)
|
||||
|
||||
# Update the note cache with whatever notes ended up in context.
|
||||
new_note_ids = context_meta.get("auto_note_ids") or []
|
||||
if new_note_ids:
|
||||
set_conv_note_cache(conv_id, new_note_ids)
|
||||
|
||||
# Emit context event
|
||||
buf.append_event("context", {"context": context_meta})
|
||||
|
||||
@@ -332,6 +349,11 @@ async def run_generation(
|
||||
if timing["ttft_ms"] is None:
|
||||
timing["ttft_ms"] = int((time.monotonic() - t_start) * 1000)
|
||||
|
||||
# Invalidate the note context cache after any successful note write
|
||||
# so the next turn can pick up newly created/modified notes.
|
||||
if result.get("success") and tool_name in {"create_task", "create_note", "update_note"}:
|
||||
clear_conv_note_cache(conv_id)
|
||||
|
||||
tool_record = {
|
||||
"function": tool_name,
|
||||
"arguments": intent.arguments,
|
||||
@@ -400,6 +422,9 @@ async def run_generation(
|
||||
timing["tools"].append({"name": tool_name, "ms": int((time.monotonic() - t_tool) * 1000)})
|
||||
logger.info("Tool %s result: success=%s", tool_name, result.get("success"))
|
||||
|
||||
if result.get("success") and tool_name in {"create_task", "create_note", "update_note"}:
|
||||
clear_conv_note_cache(conv_id)
|
||||
|
||||
tool_record = {
|
||||
"function": tool_name,
|
||||
"arguments": arguments,
|
||||
|
||||
Reference in New Issue
Block a user