# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Unified SQLite store: relational chunks + FTS5 lexical + sqlite-vec dense. Module-level functions each take a ``conn`` the caller opens and closes. Inserts are incremental: ``add_chunks`` appends one document's rows without rebuilding the scope. Scope ("kb_" / "thread_") is a column on every table and the vec0 partition key. """ from __future__ import annotations import json import re import sqlite3 import struct import uuid from datetime import datetime, timezone from storage import rag_db def kb_scope(kb_id: str) -> str: return f"kb_{kb_id}" def thread_scope(thread_id: str) -> str: return f"thread_{thread_id}" def project_scope(project_id: str) -> str: return f"project_{project_id}" def _scopes(scope) -> list[str]: """Search helpers accept one scope or several (e.g. project + thread).""" return [scope] if isinstance(scope, str) else list(scope) def _f32(vector) -> bytes: """Pack a vector into float32 bytes for vec0.""" return struct.pack(f"{len(vector)}f", *(float(x) for x in vector)) def _now() -> str: return datetime.now(timezone.utc).isoformat() _TOKEN = re.compile(r"\w+", re.UNICODE) def _match_query(query: str) -> str: """User text -> safe FTS5 OR-of-quoted-terms query; quoting defuses FTS5 operators. "" (no tokens) means no lexical results.""" toks = _TOKEN.findall(query.lower()) return " OR ".join(f'"{t}"' for t in toks) def create_kb( conn: sqlite3.Connection, *, name: str, description: str | None = None, embedding_model: str | None = None, kb_id: str | None = None, ) -> str: kb_id = kb_id or str(uuid.uuid4()) conn.execute( "INSERT INTO knowledge_bases(id, name, description, embedding_model, created_at) " "VALUES(?,?,?,?,?)", (kb_id, name, description, embedding_model, _now()), ) conn.commit() return kb_id def list_kbs(conn: sqlite3.Connection) -> list[dict]: rows = conn.execute("SELECT * FROM knowledge_bases ORDER BY created_at").fetchall() return [dict(r) for r in rows] def get_kb(conn: sqlite3.Connection, kb_id: str) -> dict | None: row = conn.execute("SELECT * FROM knowledge_bases WHERE id=?", (kb_id,)).fetchone() return dict(row) if row else None def delete_kb(conn: sqlite3.Connection, kb_id: str) -> None: """Delete a knowledge base and every document (+ chunks) under it.""" scope = kb_scope(kb_id) doc_ids = [ r["id"] for r in conn.execute("SELECT id FROM documents WHERE scope=?", (scope,)).fetchall() ] for doc_id in doc_ids: delete_document(conn, doc_id) conn.execute("DELETE FROM knowledge_bases WHERE id=?", (kb_id,)) conn.commit() def create_document( conn: sqlite3.Connection, *, scope: str, filename: str, sha256: str, kb_id: str | None = None, thread_id: str | None = None, project_id: str | None = None, status: str = "pending", stored_path: str | None = None, document_id: str | None = None, embedding_model: str | None = None, ) -> str: document_id = document_id or str(uuid.uuid4()) conn.execute( "INSERT INTO documents(id, scope, kb_id, thread_id, project_id, filename, sha256, " "status, stored_path, created_at, embedding_model) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ( document_id, scope, kb_id, thread_id, project_id, filename, sha256, status, stored_path, _now(), embedding_model, ), ) conn.commit() return document_id def set_document_status( conn: sqlite3.Connection, document_id: str, status: str, *, num_chunks: int | None = None, error: str | None = None, ) -> None: conn.execute( "UPDATE documents SET status=?, num_chunks=COALESCE(?, num_chunks), error=? WHERE id=?", (status, num_chunks, error, document_id), ) conn.commit() def list_documents(conn: sqlite3.Connection, scope: str) -> list[dict]: rows = conn.execute( "SELECT id, scope, kb_id, thread_id, project_id, filename, sha256, status, error, " "num_chunks, created_at " "FROM documents WHERE scope=? ORDER BY created_at DESC", (scope,), ).fetchall() return [dict(r) for r in rows] def get_document(conn: sqlite3.Connection, document_id: str) -> dict | None: row = conn.execute("SELECT * FROM documents WHERE id=?", (document_id,)).fetchone() return dict(row) if row else None def document_by_hash(conn: sqlite3.Connection, scope: str, sha256: str) -> str | None: row = conn.execute( "SELECT id FROM documents WHERE scope=? AND sha256=? AND status!='failed' " "ORDER BY created_at DESC LIMIT 1", (scope, sha256), ).fetchone() return row["id"] if row else None def failed_documents_by_hash(conn: sqlite3.Connection, scope: str, sha256: str) -> list[dict]: rows = conn.execute( "SELECT id, stored_path FROM documents WHERE scope=? AND sha256=? AND status='failed'", (scope, sha256), ).fetchall() return [dict(r) for r in rows] def add_chunks( conn: sqlite3.Connection, scope: str, document_id: str, chunks, vectors, regions = None, ) -> None: """Incrementally index one document's chunks into chunks + FTS5 + vec0. ``vectors`` parallels ``chunks``; optional ``regions`` (also parallel) holds per-chunk PDF highlight rects, stored as JSON.""" if len(vectors): rag_db.ensure_vec(conn, len(vectors[0])) for i, (chunk, vector) in enumerate(zip(chunks, vectors)): chunk_id = f"{document_id}:{chunk.chunk_index}" chunk_regions = regions[i] if regions and i < len(regions) else None regions_json = json.dumps(chunk_regions) if chunk_regions else None conn.execute( "INSERT OR REPLACE INTO chunks(" "id, document_id, scope, chunk_index, text, page_number, " "source_page_index, token_count, kind, pdf_regions_json) " "VALUES(?,?,?,?,?,?,?,?,?,?)", ( chunk_id, document_id, scope, chunk.chunk_index, chunk.text, chunk.page_number, chunk.source_page_index, chunk.token_count, getattr(chunk, "kind", "text"), regions_json, ), ) conn.execute( "INSERT INTO chunks_fts(text, chunk_id, scope) VALUES(?,?,?)", (chunk.text, chunk_id, scope), ) conn.execute( "INSERT INTO chunks_vec(scope, chunk_id, embedding) VALUES(?,?,?)", (scope, chunk_id, _f32(vector)), ) conn.commit() def delete_document(conn: sqlite3.Connection, document_id: str) -> None: """Remove a document and all its chunks (+ fts + vec rows).""" ids = [ r["id"] for r in conn.execute( "SELECT id FROM chunks WHERE document_id=?", (document_id,) ).fetchall() ] has_vec = rag_db.vec_table_exists(conn) for chunk_id in ids: conn.execute("DELETE FROM chunks_fts WHERE chunk_id=?", (chunk_id,)) if has_vec: conn.execute("DELETE FROM chunks_vec WHERE chunk_id=?", (chunk_id,)) conn.execute("DELETE FROM chunks WHERE document_id=?", (document_id,)) conn.execute("DELETE FROM documents WHERE id=?", (document_id,)) conn.commit() def search_lexical(conn: sqlite3.Connection, scope, query: str, k: int): """BM25 lexical search over one scope or several. Returns [(chunk_id, score)], higher = better.""" mq = _match_query(query) if not mq: return [] scopes = _scopes(scope) if not scopes: return [] placeholders = ",".join("?" * len(scopes)) rows = conn.execute( f"SELECT chunk_id, bm25(chunks_fts) AS s FROM chunks_fts " f"WHERE chunks_fts MATCH ? AND scope IN ({placeholders}) ORDER BY s LIMIT ?", (mq, *scopes, k), ).fetchall() # bm25() is negative (more negative = better); flip to higher-is-better. return [(r["chunk_id"], -r["s"]) for r in rows] def search_dense( conn: sqlite3.Connection, scope, vector, k: int, *, embedding_model: str | None = None, ): """Cosine KNN over vec0 for one scope or several. Returns [(chunk_id, 1 - distance)]. vec0 KNN constrains its partition key by equality, so multi-scope runs one query per scope and merges by score. ``embedding_model`` drops hits from documents indexed under a different (same-width) model, whose vectors live in another space; NULL-model legacy documents are assumed current, matching the ingestion dedupe rule.""" if not rag_db.vec_table_exists(conn): return [] dim = rag_db.vec_table_dim(conn) if dim is not None and dim != len(vector): # Embedding model switched widths and nothing re-indexed yet; the stale # table cannot answer new-model queries (vec0 errors on the MATCH). return [] # Over-fetch when filtering so stale-model hits don't starve the top-k. fetch = k * 3 if embedding_model else k out: list[tuple[str, float]] = [] for s in _scopes(scope): rows = conn.execute( "SELECT chunk_id, distance FROM chunks_vec " "WHERE scope=? AND embedding MATCH ? ORDER BY distance LIMIT ?", (s, _f32(vector), fetch), ).fetchall() out.extend((r["chunk_id"], 1.0 - r["distance"]) for r in rows) if embedding_model and out: ids = [cid for cid, _ in out] placeholders = ",".join("?" * len(ids)) valid = { r["id"] for r in conn.execute( f"SELECT c.id FROM chunks c JOIN documents d ON d.id=c.document_id " f"WHERE c.id IN ({placeholders}) " f"AND (d.embedding_model IS NULL OR d.embedding_model=?)", (*ids, embedding_model), ).fetchall() } out = [t for t in out if t[0] in valid] out.sort(key = lambda t: t[1], reverse = True) return out[:k] def chunks_by_id(conn: sqlite3.Connection, ids) -> dict: """Hydrate chunk rows (joined with document filename), keyed by id.""" if not ids: return {} placeholders = ",".join("?" * len(ids)) rows = conn.execute( f"SELECT c.id, c.text, c.document_id, c.chunk_index, c.page_number, " f"c.source_page_index, d.filename " f"FROM chunks c JOIN documents d ON d.id=c.document_id " f"WHERE c.id IN ({placeholders})", list(ids), ).fetchall() return {r["id"]: r for r in rows} def all_chunks_for_scope(conn: sqlite3.Connection, scope) -> list[dict]: """Every completed-document chunk for a scope, ordered document-then-index and joined with the document filename. Backs whole-document context injection, so it does no retrieval or embedding.""" scopes = _scopes(scope) if not scopes: return [] placeholders = ",".join("?" * len(scopes)) rows = conn.execute( f"SELECT c.id, c.text, c.document_id, c.chunk_index, c.page_number, " f"c.token_count, d.filename, d.created_at " f"FROM chunks c JOIN documents d ON d.id=c.document_id " f"WHERE c.scope IN ({placeholders}) AND d.status='completed' " f"ORDER BY d.created_at, c.document_id, c.chunk_index", list(scopes), ).fetchall() return [dict(r) for r in rows] def scope_token_estimate(conn: sqlite3.Connection, scope) -> int: """Upper-bound token total for a scope's completed chunks without hydrating text. Mirrors ``all_chunks_for_scope`` + the ``tool._row_token_count`` fallback (stored count, else length/4), so the whole-doc budget can be checked before loading text.""" scopes = _scopes(scope) if not scopes: return 0 placeholders = ",".join("?" * len(scopes)) row = conn.execute( f"SELECT COALESCE(SUM(CASE WHEN c.token_count > 0 THEN c.token_count " f"ELSE MAX(1, length(COALESCE(c.text, '')) / 4) END), 0) AS total " f"FROM chunks c JOIN documents d ON d.id=c.document_id " f"WHERE c.scope IN ({placeholders}) AND d.status='completed'", list(scopes), ).fetchone() return int(row["total"] or 0)