Backend efficiency & consistency pass
- Rewrite get_all_tags() with SQL unnest instead of loading all notes - Consolidate convert_note_to_task/convert_task_to_note to single-session ops - Add search_notes_for_context() with single OR-keyword query for build_context() - Drop selectinload from list_conversations(), use correlated subquery for message_count - Add set_settings_batch() for single-transaction multi-setting upserts - Extract get_installed_models() shared helper into services/llm.py - Delete services/tasks.py pass-through wrapper; routes/tasks.py imports from services.notes - Add B-tree indexes on notes.title and conversations.updated_at (migration 0007) - Add logging to services/notes.py, services/chat.py, services/settings.py - Safe Conversation.to_dict() when messages relationship is not loaded Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator
|
||||
import httpx
|
||||
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.notes import get_note, list_notes
|
||||
from fabledassistant.services.notes import get_note, search_notes_for_context
|
||||
from fabledassistant.services.settings import get_setting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -23,18 +23,32 @@ STOP_WORDS = frozenset({
|
||||
})
|
||||
|
||||
|
||||
async def ensure_model(model: str) -> None:
|
||||
"""Check if model exists in Ollama, pull if missing."""
|
||||
async def get_installed_models() -> set[str]:
|
||||
"""Return set of installed Ollama model names (with and without :latest)."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(f"{Config.OLLAMA_URL}/api/tags")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
names = {m["name"] for m in data.get("models", [])}
|
||||
# Check both with and without :latest tag
|
||||
if model in names or f"{model}:latest" in names:
|
||||
logger.info("Model '%s' already available", model)
|
||||
return
|
||||
names: set[str] = set()
|
||||
for m in data.get("models", []):
|
||||
name = m["name"]
|
||||
names.add(name)
|
||||
if name.endswith(":latest"):
|
||||
names.add(name.removesuffix(":latest"))
|
||||
return names
|
||||
except Exception:
|
||||
logger.warning("Failed to fetch installed Ollama models")
|
||||
return set()
|
||||
|
||||
|
||||
async def ensure_model(model: str) -> None:
|
||||
"""Check if model exists in Ollama, pull if missing."""
|
||||
try:
|
||||
installed = await get_installed_models()
|
||||
if model in installed or f"{model}:latest" in installed:
|
||||
logger.info("Model '%s' already available", model)
|
||||
return
|
||||
except Exception:
|
||||
logger.warning("Failed to check Ollama models, attempting pull anyway")
|
||||
|
||||
@@ -172,39 +186,29 @@ async def build_context(
|
||||
f"--- End Note ---"
|
||||
)
|
||||
|
||||
# Search notes by keywords from user message — search per keyword
|
||||
# individually and deduplicate to get OR-style matching (any keyword hit
|
||||
# is relevant context). Collect up to 3 unique notes.
|
||||
# Search notes by keywords from user message — single OR query
|
||||
keywords = _extract_keywords(user_message)
|
||||
if keywords:
|
||||
seen_ids: set[int] = set()
|
||||
snippets: list[str] = []
|
||||
for kw in keywords[:5]:
|
||||
if len(snippets) >= 3:
|
||||
break
|
||||
try:
|
||||
notes, _ = await list_notes(q=kw, limit=3)
|
||||
for n in notes:
|
||||
if n.id in seen_ids:
|
||||
continue
|
||||
if current_note_id and n.id == current_note_id:
|
||||
continue
|
||||
if n.id in exclude_set:
|
||||
continue
|
||||
seen_ids.add(n.id)
|
||||
body_preview = n.body[:2000] if n.body else ""
|
||||
snippets.append(f"- {n.title}: {body_preview}")
|
||||
context_meta["auto_notes"].append({"id": n.id, "title": n.title})
|
||||
if len(snippets) >= 3:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if snippets:
|
||||
system_parts.append(
|
||||
"\n\n--- Related Notes ---\n"
|
||||
+ "\n".join(snippets)
|
||||
+ "\n--- End Related Notes ---"
|
||||
search_exclude = set(exclude_set)
|
||||
if current_note_id:
|
||||
search_exclude.add(current_note_id)
|
||||
try:
|
||||
notes = await search_notes_for_context(
|
||||
keywords, exclude_ids=search_exclude or None, limit=3
|
||||
)
|
||||
snippets: list[str] = []
|
||||
for n in notes:
|
||||
body_preview = n.body[:2000] if n.body else ""
|
||||
snippets.append(f"- {n.title}: {body_preview}")
|
||||
context_meta["auto_notes"].append({"id": n.id, "title": n.title})
|
||||
if snippets:
|
||||
system_parts.append(
|
||||
"\n\n--- Related Notes ---\n"
|
||||
+ "\n".join(snippets)
|
||||
+ "\n--- End Related Notes ---"
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to search notes for context", exc_info=True)
|
||||
|
||||
# Fetch URL content from user message
|
||||
urls = _find_urls(user_message)
|
||||
|
||||
Reference in New Issue
Block a user