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:
2026-02-11 08:27:11 -05:00
parent fb18d2c41d
commit db01106714
12 changed files with 225 additions and 174 deletions
+28 -4
View File
@@ -1,3 +1,4 @@
import logging
from datetime import datetime, timezone
from sqlalchemy import func, select
@@ -8,6 +9,8 @@ from fabledassistant.models.conversation import Conversation, Message
from fabledassistant.services.llm import generate_completion
from fabledassistant.services.notes import create_note
logger = logging.getLogger(__name__)
async def create_conversation(title: str = "", model: str = "") -> Conversation:
async with async_session() as session:
@@ -35,20 +38,40 @@ async def get_conversation(conversation_id: int) -> Conversation | None:
async def list_conversations(
limit: int = 50, offset: int = 0
) -> tuple[list[Conversation], int]:
) -> tuple[list[dict], int]:
async with async_session() as session:
total = await session.scalar(
select(func.count(Conversation.id))
) or 0
# Subquery for message count — avoids loading all messages
msg_count = (
select(func.count(Message.id))
.where(Message.conversation_id == Conversation.id)
.correlate(Conversation)
.scalar_subquery()
)
result = await session.execute(
select(Conversation)
.options(selectinload(Conversation.messages))
select(Conversation, msg_count.label("message_count"))
.order_by(Conversation.updated_at.desc())
.limit(limit)
.offset(offset)
)
conversations = list(result.scalars().all())
conversations = []
for row in result.all():
conv = row[0]
d = {
"id": conv.id,
"title": conv.title,
"model": conv.model,
"message_count": row[1],
"created_at": conv.created_at.isoformat(),
"updated_at": conv.updated_at.isoformat(),
}
conversations.append(d)
return conversations, total
@@ -149,6 +172,7 @@ async def summarize_conversation_as_note(
{"role": "user", "content": "\n\n".join(conv_text)},
]
logger.info("Summarizing conversation %d with model %s", conversation_id, model)
summary = await generate_completion(prompt_messages, model)
title = conv.title or "Conversation Summary"