unsloth/studio/frontend/src/features/chat/utils/rag-index-queue.ts
Roland Tannous 08cee7cc0a Studio: bounded parallel RAG indexing + folder upload
Uploading many docs (or a folder) previously spawned an ingestion
subprocess per file all at once, thrashing the GPU/CPU. Add a
configurable concurrency limit and a folder picker.

- ragIndexConcurrency setting (default 1) in the chat runtime store,
  persisted like the other RAG scalar settings; exposed as a 'Parallel
  indexing' slider (1-8) at the bottom of the sidebar Retrieval section.
- New rag-index-queue.ts semaphore: each document upload acquires a slot
  before it starts and releases it once its ingestion job finishes
  (complete / error / already-indexed), so bulk uploads drain at the
  configured rate. Wired into both composer upload paths
  (use-thread-doc-uploads + shared-composer).
- Folder upload: a second 'Attach a folder' button on the RAG attach
  control uses a webkitdirectory input; every compatible file is routed
  through the same queue. Multi-file select already worked (the input has
  'multiple' and loops addDoc).
- Content-hash dedup (shipped earlier) means re-scanning a folder skips
  already-indexed files.

Not build/UI verified here (no bun); needs bun typecheck + a browser
check of bulk/folder upload draining at the set concurrency.
2026-05-28 19:52:49 +04:00

41 lines
1.3 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { useChatRuntimeStore } from "../stores/chat-runtime-store";
/** Bounds how many documents index in parallel. Each RAG upload acquires a
* slot before it starts and releases it once its ingestion job finishes
* (complete/error/already-indexed), so bulk/folder uploads drain at the
* user-configured `ragIndexConcurrency` rate instead of spawning a
* subprocess per file all at once. Module-scoped singleton — shared across
* both composer surfaces. */
let active = 0;
const waiters: Array<() => void> = [];
function limit(): number {
const n = useChatRuntimeStore.getState().ragIndexConcurrency;
return Math.max(1, Number.isFinite(n) ? Math.round(n) : 1);
}
function admitWaiters(): void {
while (waiters.length > 0 && active < limit()) {
active += 1;
const next = waiters.shift();
next?.();
}
}
/** Resolves once a slot is free (immediately if under the limit). */
export function acquireIndexSlot(): Promise<void> {
return new Promise<void>((resolve) => {
waiters.push(resolve);
admitWaiters();
});
}
/** Release a previously-acquired slot and admit the next waiter. */
export function releaseIndexSlot(): void {
active = Math.max(0, active - 1);
admitWaiters();
}