unsloth/studio/backend/core/rag/scope.py
Daniel Han ab0828b976 Studio: fix RAG correctness bugs
Backend:
- Deterministic SQLite connection cleanup. The RAG code used bare
  `with get_connection() as conn:`, which commits but never closes, leaning
  on GC to release handles (the rest of studio_db closes explicitly). Add a
  closing_connection() context manager that commits/rolls back like sqlite3's
  own manager and always closes, and route all 30 RAG call sites through it.
- filter_by_min_score no longer drops BM25-only and figure-ref hits. min_score
  is a cosine floor, so it now gates only hits that carry a dense_score;
  lexical and figure-ref hits (dense_score is None) pass through instead of
  being silently discarded when the floor is raised.
- Fix two tests that could not pass against the production code: the RRF
  fusion test asserted the wrong winner (c edges out b: 0.032266 vs 0.032258),
  and two tool-handler scope tests stubbed retrieve_hybrid without accepting
  the embedder_model kwarg the handler now passes (TypeError was swallowed,
  leaving captured["scope"] unset).

Frontend:
- Removing an in-flight upload chip now routes through the teardown thunk
  already registered for the aggregate-progress toast (abort, unsubscribe,
  release the index slot, delete the backend doc with the correct kb/thread
  scope key it closed over) and clears the toast entry. Deleting directly
  leaked the concurrency slot and hardcoded the thread scope, mis-targeting
  KB-scoped docs. Applied in both the composer hook and the compare-view
  composer; drop the now-vestigial chip-scope-key tracking and unused
  activeThreadId selectors. Add index-progress-store.remove(id).
2026-05-31 09:56:23 +00:00

49 lines
1.8 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Scope identifiers (kb_<id> / thread_<id>) + per-scope embedder resolver."""
from __future__ import annotations
from storage.studio_db import closing_connection, list_chat_settings
from utils.rag.config import resolve_embedder
RAG_DEFAULTS_KEY = "rag.defaults"
def thread_settings_key(thread_id: str) -> str:
return f"thread:{thread_id}:rag"
def resolve_scope_embedder(scope: str) -> str | None:
"""KB → kb.embedding_model; thread → per-thread/defaults/matrix. None = use default."""
if scope.startswith("kb_"):
kb_id = scope[len("kb_") :]
with closing_connection() as conn:
row = conn.execute(
"SELECT embedding_model FROM rag_knowledge_bases WHERE id = ?",
(kb_id,),
).fetchone()
return row["embedding_model"] if row else None
if scope.startswith("thread_"):
thread_id = scope[len("thread_") :]
all_settings = list_chat_settings()
defaults = all_settings.get(RAG_DEFAULTS_KEY) or {}
if not isinstance(defaults, dict):
defaults = {}
per_thread = all_settings.get(thread_settings_key(thread_id)) or {}
if not isinstance(per_thread, dict):
per_thread = {}
explicit = per_thread.get("embedding_model") or defaults.get("embedding_model")
if explicit:
return explicit
mode = per_thread.get("mode") or defaults.get("mode") or "text"
chunking_strategy = (
per_thread.get("chunking_strategy")
or defaults.get("chunking_strategy")
or "standard"
)
return resolve_embedder(mode, chunking_strategy)
return None