Commit graph

1,504 commits

Author SHA1 Message Date
Roland Tannous
bab409ef84 Studio: fix KB files view widening dialog (WebKit overflow-x), use grid doc rows 2026-05-29 20:37:40 +04:00
Roland Tannous
8cc0b80704 Studio: clip KB detail slot so files view can't widen the dialog 2026-05-29 20:13:51 +04:00
Roland Tannous
978028bf82 Studio: fix KB files view overflow by using native scroll for the document list 2026-05-29 20:03:49 +04:00
Roland Tannous
1a7c1cccba Studio: stop KB preview from overflowing the settings dialog; inset counts, add header spacing 2026-05-29 19:55:53 +04:00
Roland Tannous
aa68568ff5 Studio: remove KB embedding override; prevent KB tab horizontal overflow 2026-05-29 19:39:37 +04:00
Roland Tannous
187a5853ba Studio: KB panel — embedder on its own line, uniform full-width doc pills 2026-05-29 19:20:46 +04:00
Roland Tannous
80e7ad4daf Studio: redesign KB management — per-base upload/files panels, doc pills, shared upload toast 2026-05-29 19:02:14 +04:00
Roland Tannous
6388f3f349 Studio: full-width KB list when no base is selected 2026-05-29 18:33:09 +04:00
Roland Tannous
71b5214ca7 Studio: restructure Knowledge bases tab layout; hint under header, compact when empty 2026-05-29 18:24:16 +04:00
Roland Tannous
b17b3ad727 Studio: fix overlapping sections in Knowledge bases settings tab 2026-05-29 16:25:18 +04:00
Roland Tannous
1844ec0776 Studio: clarify caption toggle text searchability 2026-05-29 16:11:17 +04:00
Roland Tannous
fc7fa0b5a7 Studio: tweak caption toggle helper text 2026-05-29 15:58:41 +04:00
Roland Tannous
1a31aa5b27 Studio: keep local RAG answers grounded; cap tools and steer off web when RAG is on 2026-05-29 15:42:32 +04:00
Roland Tannous
7335dc07a9 Studio: make figure captioning optional with a Retrieval toggle 2026-05-29 15:05:29 +04:00
Roland Tannous
c149cf58d1 Studio: place Cancel left of dismiss X, vertically centered 2026-05-29 14:11:29 +04:00
Roland Tannous
1112fc891b Studio: center Cancel in indexing toast, start file counter at 1 2026-05-29 13:18:11 +04:00
Roland Tannous
4635b0bc36 Studio: add dismiss X alongside Cancel on the indexing toast 2026-05-29 13:04:00 +04:00
Roland Tannous
2b0c6cf220 Studio: fix scopeKey type in RAG upload cancel wiring 2026-05-29 12:42:21 +04:00
Roland Tannous
68646a7abf Studio: cancel RAG indexing from the toast and reset the batch 2026-05-29 11:02:54 +04:00
Roland Tannous
05944a1e9b Studio: suppress RAG sources for uncited external prefetch 2026-05-28 21:06:55 +04:00
Roland Tannous
c34dd5f19a Studio: point OpenEnv dep at huggingface/ org (repo moved)
meta-pytorch/OpenEnv was transferred to huggingface/OpenEnv. The old
URL still works via GitHub's redirect from a clean clone, but uv's git
cache can fail to follow it on some machines — surfacing as a
'could not read Username for github.com' credential prompt mid-install.
Point the requirement at the canonical huggingface/OpenEnv.git (same
HEAD), which also sidesteps any stale cache entry keyed on the old URL.
2026-05-28 20:34:57 +04:00
Roland Tannous
cbe4b0866a Studio: show total chunks in the aggregate indexing toast
The completion toast reported only document count. Capture each job's
num_chunks from its complete event into the index-progress store and sum
across the batch, so the toast reads 'N documents and M chunks indexed'.
Already-indexed (deduped) files contribute 0 new chunks.
2026-05-28 20:24:15 +04:00
Roland Tannous
8c6acdc6a3 Studio: aggregate RAG indexing into one toast for multi-doc uploads
Uploading several documents (or a folder) produced a separate toast per
file, which piled up. Replace the per-job toast stack with a single
aggregate toast driven by a new index-progress-store that's populated at
addDoc time — so it counts queued files (held by the concurrency
semaphore) in the denominator, which the per-job rag-store map can't see.

- index-progress-store: one entry per file in the batch
  (queued/indexing/ready/error + 0..1 progress).
- Both addDoc paths register each file on entry and update it through
  the lifecycle (setIndexing after acquiring a slot; setProgress on job
  progress events; setReady/setError on terminal).
- ingestion-toast-stack: renders ONE toast — 'Indexing document(s) · X/Y
  · Z%' with a progress bar while in flight (overall = completed files +
  in-flight fractions, over total), 'RAG index ready · N documents
  indexed' (+ failures) when the last finishes, then auto-dismisses.
  Single-file uploads still read naturally ('Indexing document' / '1
  document indexed').

IngestionProgress / rag-store jobs are untouched (still used by the
KB detail panel). Not build/UI verified here (no bun).
2026-05-28 20:22:09 +04:00
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
pre-commit-ci[bot]
dafc7092ad [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-28 13:18:30 +00:00
Roland Tannous
b836c3c76b Studio: RAG for external model providers via prefetch
External providers (OpenAI/Anthropic/Gemini) can't run the local
search_knowledge_base tool loop, so give them RAG by prefetching:
studio retrieves before calling the provider, injects the chunks into
the user prompt, and surfaces it as a synthetic tool call. Local models
are untouched (they keep tool-based RAG + decomposition).

Backend:
- New POST /api/rag/prefetch: momentarily loads the pre-cached helper
  (gemma-4-E2B-it-GGUF) via LlamaCppBackend(kill_orphans=False) to
  decompose the question into up to 3 queries, retrieves+merges+dedups
  per query, unloads the helper. Raw single-query fallback if the helper
  can't load. New core/rag/query_decompose.py owns the helper lifecycle.
- Factored the retrieval body of /search into _execute_search, reused by
  both endpoints.

Frontend:
- prefetchRag() client.
- chat-adapter external branch: gated on isExternalRequest + ragToolEnabled
  + scope!=off + ragScopeHasDocs (no docs -> no prefetch, prior behavior
  preserved). Formats hits as <chunk id=N> (parseChunks shape), injects
  into the last user message (send-only; not shown in the user bubble),
  seeds a synthetic search_knowledge_base tool-call part so the existing
  chunk-card UI + [N] citations + source badges all work unchanged.
- Extends PR #5674's disabled-tool guard: when RAG is off, reinforce
  'no document search (RAG) capabilities'; when prefetch ran, point the
  model at the injected excerpts instead.
- RAG pill enabled for external providers regardless of supports_tools.

Not build/UI verified here (no bun/GPU/keys); needs bun typecheck+test
and a browser round-trip with real provider keys.
2026-05-28 17:18:08 +04:00
Roland Tannous
cf7dec1f13 Studio: grey out RAG pill when the model can't call tools
RAG retrieval runs entirely through the local search_knowledge_base
tool. If the loaded model doesn't support tool calling (e.g. a
safetensors model whose template advertises tools in an unparsable
emission format, so supports_tools is suppressed), enabling RAG does
nothing — the model never calls the tool. The pill stayed lit and
clickable, which was misleading.

Gate the RAG pill on supportsTools (in addition to modelLoaded), the
same condition web/code use when there's no provider builtin. Applied
to both composer surfaces (shared-composer and the in-thread
RagToggle), with a 'RAG needs a model that supports tool calling'
tooltip on the disabled state.
2026-05-28 16:02:50 +04:00
Roland Tannous
f30c0a48dd Studio: don't duplicate composer chip when re-uploading an indexed doc
The backend dedups re-uploads and the sidepanel shows the doc once, but
the composer's pending-doc chips are created per addDoc call, so each
re-upload of an already-indexed file appended another 'Ready' chip for
the same document. In the already_indexed branch, if a chip with the
returned documentId already exists, drop the chip we just added instead
of marking it ready — so the composer shows each document only once.
2026-05-28 15:33:17 +04:00
Roland Tannous
266342a64e Studio: skip re-indexing an already-indexed document (content-hash dedup)
Re-uploading the same file into the same scope (KB or thread) used to
parse, chunk, caption and embed it all over again, creating a duplicate
set of chunks. Dedup by content hash instead:

  - schema: add rag_documents.content_hash (sha256 of the bytes) via the
    standard PRAGMA/ALTER migration, plus (scope, content_hash) indexes.
  - upload: _save_upload now streams the bytes through sha256 and returns
    the digest alongside path/name/size.
  - _start_ingestion: before inserting, look for a COMPLETED row in the
    same scope with the same hash. If found, delete the redundant upload
    from disk and return the existing document_id with already_indexed=
    true and an empty job_id — no ingestion job is started. Only
    'completed' rows dedup, so a failed/in-flight prior attempt can still
    retry. Scope-local: the same file in two KBs is indexed in each.
  - frontend: UploadResponse.already_indexed flows through the rag-store
    (skips job subscription) into both upload paths, which mark the chip
    ready immediately and toast '<file> is already indexed'.

Pre-existing rows have NULL content_hash and won't dedup until
re-uploaded once under the new path. Not build/UI-verified here (no bun
in this env); needs typecheck + browser check.
2026-05-28 15:19:06 +04:00
Roland Tannous
d55e5d1474 Revert "Studio: inline DOCX preview via docx-preview + DOMPurify"
This reverts commit ba78141ac5.
2026-05-28 15:08:19 +04:00
Roland Tannous
32e57fa1c5 Revert "Studio: render embedded images in DOCX preview"
This reverts commit f4b34f71c5.
2026-05-28 15:08:19 +04:00
Roland Tannous
f4b34f71c5 Studio: render embedded images in DOCX preview
docx-preview defaults to blob: URLs for embedded images, which DOMPurify
strips from img src (blob: isn't in its default allowed-URI list), so
figures vanished after sanitize. Switch docx-preview to useBase64URL so
images inline as data: URIs, and add ADD_DATA_URI_TAGS: ['img'] to the
DOMPurify config so those data: URIs survive sanitization. Script /
handler / javascript: stripping is unchanged.
2026-05-28 14:59:52 +04:00
Roland Tannous
ba78141ac5 Studio: inline DOCX preview via docx-preview + DOMPurify
Previously a DOCX citation only showed the extracted snippet + a
Download button (Risk #3: never render a user-supplied .docx inline).
Add a faithful inline render that keeps that guarantee:

  - New PreviewDocxView renders the .docx with docx-preview into an
    off-screen element, then injects DOMPurify-sanitized HTML into the
    live DOM (keeping <style> for docx-preview's scoped layout CSS).
    Script tags, event handlers and javascript: URLs are stripped, so
    a malicious .docx can't execute in the app origin.
  - preview-store now fetches the raw bytes for docx and exposes them
    via previewBlob, but deliberately keeps previewBlobUrl = null — no
    object URL is created, so the 'open raw original inline' path stays
    disabled (Risk #3) and Download remains the only raw-file path.
  - preview-panel routes docx -> PreviewDocxView when a blob is present,
    falling back to the text-view snippet otherwise. isInlineBlobAllowed
    still returns false for docx, so html/unknown behaviour is unchanged.
  - Deps: docx-preview + dompurify added to package.json.
  - Tests updated: docx now asserts bytes-fetched-without-object-URL.

Not build/UI-verified in this environment (deps not installed here);
needs bun install + browser check.
2026-05-28 14:53:06 +04:00
Roland Tannous
3a0c774795 Studio: humanize RAG ingest stage labels and completion toast
- Add human-readable stage labels for caption_images ('Captioning
  images') and extract_images ('Extracting images') so the raw
  underscore stage names no longer leak into the progress toast.
- On completion, the toast title is now 'RAG index ready' (was
  'Indexed') and the body reads '1 document and N chunk(s) indexed'
  (was 'Indexed N chunks'), with chunk pluralization.
2026-05-28 14:06:27 +04:00
Roland Tannous
3173689b59
Merge branch 'main' into feature/rag 2026-05-28 13:46:11 +04:00
Roland Tannous
95622dc405 Studio: don't leak exception details in RAG warmup/precache responses
CodeQL flagged information exposure through an exception in the /warmup
and /reranker/precache endpoints: both returned str(exc) in the JSON
body, exposing internal paths and stack details to the client. Keep
the full exception in the server-side warning log and return a generic
error message ('Failed to load embedder' / 'Failed to download
reranker') to the caller instead. The frontend only surfaces the
message in a toast, so a generic string is sufficient.
2026-05-28 13:42:46 +04:00
Roland Tannous
290201f62e Studio: trim captioner logs to invoked+complete, render subprocess logs as JSON
Two changes to the RAG captioning log output:

  - Drop the noisy per-image and path-selection info lines
    (using-chat-VLM, loading-helper, per-image done). Only the
    'caption_images: invoked' and 'caption_images: complete' lines
    remain; warnings for genuine failures (helper load, per-image
    request, helper unload) are kept.
  - Configure structlog at the top of the ingestion subprocess worker
    with the same env the parent uses. The worker runs in a spawned
    process where structlog was never set up, so its logs fell back to
    structlog's dev ConsoleRenderer ([info] ...) instead of the JSON
    renderer the rest of the app uses. Now captioner/parser logs from
    the subprocess match the parent's JSON format.
2026-05-28 13:39:44 +04:00
alkinun
185ff00c62
Fix non-streaming GGUF chat completion usage (#5781)
* Fix GGUF non-stream chat completion usage

* Handle nullable GGUF completion usage

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
2026-05-28 13:28:52 +04:00
pre-commit-ci[bot]
d6a7c9f8c7 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-28 07:50:36 +00:00
Etherll
f25ea25570 Merge branch 'feature/rag' of https://github.com/unslothai/unsloth into feature/rag 2026-05-28 10:49:59 +03:00
Roland Tannous
7e3e69db3f Studio: raise ingestion toast stack above the settings sheet
The toast stack and the chat settings sheet were both z-50, so an open
side panel (rendered later in the DOM) covered the indexing toast. Bump
the stack to z-[9999] — comfortably above the sheet's z-50 — so the
ingestion toast stays visible like the Sonner reranker toast does.
2026-05-28 11:41:39 +04:00
Etherll
c0f8d486a4 Studio: fix RAG PDF main page rendering as a thin white strip
The thumbnail-rail refactor hoisted <Document> to wrap both the rail and
the main page so the PDF loads once. That moved the width-measuring scroll
container INSIDE <Document>, which only renders its children after the PDF
finishes loading. The old `useEffect(..., [])` ran on component mount —
when the container was still absent — so the ResizeObserver never attached,
`width` stayed null, and the main <Page> collapsed to width 0.

Replace the mount-effect measurement with a callback ref: the
ResizeObserver now attaches the instant the container node mounts,
regardless of when that happens relative to PDF load. Disconnects cleanly
on unmount / re-attach.

Verified: tsc clean, vite build succeeds, preview-pdf-smoke (incl. the
resize/debounce case) passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:36:20 +03:00
Matt Van Horn
15d70a1d7b
fix: honor --ctx-size and other forwarded args from unsloth studio run in Studio's context-fit logic (#5815)
* fix: honor --ctx-size and other forwarded args from `unsloth studio run` in Studio's context-fit logic

* refactor: extract resolve_requested_ctx as single source of truth

The test helper was reimplementing the two-line
'ctx_override = parse_ctx_override(...); requested_ctx = ctx_override
if ctx_override is not None else n_ctx' pattern locally, so the test
asserted against its own reimplementation rather than production logic.
Extract the conditional into resolve_requested_ctx and have both the
production caller and the test use it.

* fix(studio): honor pass-through cache type flags in KV VRAM estimate

Studio's KV cache VRAM estimate computed from the first-class
cache_type_kv even when the user passed -ctk/--cache-type-k/-ctv/
--cache-type-v via extras. Those flags reached llama-server fine
(last-wins on the CLI) but the pre-launch estimate kept using the
default f16 bytes-per-element, so GPU placement decisions could be
off when the user lowered cache precision via pass-through.

Adds parse_cache_override + resolve_cache_type_kv in llama_server_args.py
(mirroring parse_ctx_override / resolve_requested_ctx), wires both into
load_model alongside the existing ctx resolution, and adds focused
unit tests for the parser + resolver.

Follow-up to @rolandtannous review on #5815.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
2026-05-28 11:34:35 +04:00
Roland Tannous
3117cb5f99 Revert "Studio: restore draft thread (and its RAG docs) across page reloads"
This reverts commit d652f03b6e.
2026-05-28 11:32:22 +04:00
Roland Tannous
d652f03b6e Studio: restore draft thread (and its RAG docs) across page reloads
assistant-ui mints a fresh __LOCALID_* draft id on every page load, so
RAG docs uploaded under the previous draft id were orphaned after a
refresh — the doc panel queries useThreadDocuments(activeThreadId) and
the new id had nothing.

Persist activeThreadId in localStorage and, on the first settled render
after load, have ActiveThreadSync ask aui to switchToThread(persisted)
when it differs from the freshly-minted draft. Because the draft was
already persisted to the backend by initialize()/ensureThreadRecord
when its first doc was uploaded, the adapter's fetch() resolves it and
aui adopts it as mainThreadId. That keeps aui's mainThreadId and our
activeThreadId unified, so the earlier divergence (uploads under the
persisted id vs chat-completion reading aui's fresh id) can't recur —
unlike the reverted localStorage-only attempt, the chat-adapter's
unstable_threadId now equals the persisted id after the switch.

A one-shot ref ensures we only re-adopt on initial load; user-driven
new-chat / thread switches still flow through normally. If the
persisted draft was never initialized (no doc/message, not in the
backend), switchToThread rejects and we fall back to the fresh draft.
2026-05-28 10:26:12 +04:00
Etherll
79fc69741c Studio: post-merge build fixes — drop dead score handling, dedupe activeThreadId, add knowledgeBases i18n key
Follow-ups after merging origin/main into feature/rag:

* chat-adapter.ts: drop `score` field from DocumentSourcePart and the
  `chunk.score` copy — the remote "hide RAG retrieval scores from chunks,
  citations, and side panel" commit removed `score` from ParsedChunk.
* chat-settings-sheet.tsx: remove the duplicate `const activeThreadId =`
  introduced by the merge (kept the HEAD-side declaration at line 497).
* chat-settings-sheet.tsx: drop the "Min relevance" Slider that referenced
  `ragMinScore` / `setRagMinScore` — same intent as the hide-scores commit
  (these are still on the runtime store but the side-panel UI is gone).
* i18n locales (en, zh-CN): add `settings.tabs.knowledgeBases` translation
  key so the new TabDef entry passes the TranslationKey union check.

Verified: `tsc --noEmit` clean, `vite build` succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 05:20:22 +03:00
Etherll
9eb0778628 Merge remote-tracking branch 'origin/main' into feature/rag
# Conflicts:
#	studio/backend/core/inference/llama_cpp.py
#	studio/backend/routes/__init__.py
#	studio/backend/routes/inference.py
#	studio/frontend/package.json
#	studio/frontend/src/components/assistant-ui/sources.tsx
#	studio/frontend/src/components/assistant-ui/thread.tsx
#	studio/frontend/src/features/chat/api/chat-adapter.ts
#	studio/frontend/src/features/chat/chat-settings-sheet.tsx
#	studio/frontend/src/features/chat/shared-composer.tsx
#	studio/frontend/src/features/chat/stores/chat-runtime-store.ts
#	studio/frontend/src/features/settings/settings-dialog.tsx
2026-05-28 00:38:58 +03:00
Etherll
27b0a50a84 Studio: WIP — RAG preview UI, locator/auth refactor, tests, fixtures (pre-merge snapshot)
Snapshot taken before fast-forwarding feature/rag to origin and merging main.
Bundles in-flight work so the merge has a clean tree:

Frontend
- PDF preview panel (preview-panel, preview-pdf-view, preview-text-view,
  preview-unavailable) with lazy-rendered page thumbnail rail
- Resizable preview slot via useResizablePanelWidth hook (drag handle,
  localStorage persistence, viewport clamping)
- Neutral scrollbar + Source Excerpt card restyle (no brand-coloured rail)
- Preview-store + chat-adapter / rag-api / kb-detail wiring
- Frontend test harness (vitest.config, setupTests, biome update) and the
  paired __tests__ suites for preview, sources, document-row, chat-adapter,
  rag-api, knowledge-bases-tab, search-knowledge-base-tool-ui

Backend
- RAG locator + authorization modules with chunking / retrieval / tool /
  vector_store / studio_db updates
- Paired test_rag_* suites (authorization, locators, locator_backfill,
  locator_migration, preview_routes, preview_target_locators, source_identity)

Other
- tests/fixtures/rag-preview for preview route fixtures (sample.pdf,
  sample.txt, make_fixture_pdf.py)
- .gitignore + package(-lock).json adjustments for the new test runner

Will be squashed/reworked via interactive rebase after main is merged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:13:43 +03:00
Roland Tannous
dd3ee02648 Studio: revert activeThreadId persistence (caused fresh-chat doc loss)
The two previous commits (41e43b6c7 and 3d3a00c2b) persisted
activeThreadId in localStorage so RAG docs would survive a page
reload. That broke fresh chats: stale localStorage values from a
prior session pinned activeThreadId to an old draft id, but the
chat-completion path reads aui's current mainThreadId via
unstable_threadId. The two diverged — uploads went under the stale
persisted id, the chat-completion turn looked up docs under the new
aui id, and nothing matched.

Revert the persistence + ActiveThreadSync guard. We're back to the
pre-fix behaviour where uploads-in-the-same-session work, and a
proper fix for the reload case (promote drafts to real chat_threads
rows on first doc upload so the id never changes) will land next.
2026-05-27 22:31:05 +04:00
Roland Tannous
3d3a00c2be Studio: stop ActiveThreadSync clearing persisted draft on reload
ActiveThreadSync was reacting to aui's mainThreadId === null on mount
(aui hasn't booted yet) by calling setActiveThreadId(null), which
wiped the just-restored persisted draft id from localStorage and
emptied the doc panel for the user's thread. The previous fix only
covered the 'aui minted a different LOCALID' branch; it missed the
'mainThreadId is null while aui boots' branch.

Treat a null mainThreadId as a no-op for the sync. Explicit clears
(new chat, sidebar delete) keep going through setActiveThreadId(null)
directly, so this guard doesn't trap stale state — it just gives the
persisted draft id a chance to survive until aui finishes booting.
2026-05-27 21:46:52 +04:00