Studio: app-level RAG defaults for new KBs
Power users can now set their preferred chunking strategy / mode /
embedder once in Settings → Knowledge Bases and have new KBs use those
values by default, instead of toggling on every create.
Backend
- routes/rag.py:
- GET /api/rag/defaults returns the stored RagDefaults (or sensible
fallbacks when nothing is set: standard / text / null embedder).
- PUT /api/rag/defaults is PATCH-style — only fields present in the
body overwrite. The (multimodal, late) constraint is enforced
here too, so users can't poison the defaults with a combination
the create path would reject.
- Persistence reuses the existing chat_settings store via
upsert_chat_settings_merge; the values live under a single
rag.defaults key as a nested JSON dict.
Frontend
- rag-api.ts: getRagDefaults / setRagDefaults wrappers + RagDefaults
+ UpdateRagDefaultsRequest types.
- rag-store.ts: defaults state, loadDefaults / updateDefaults
actions. loadDefaults swallows errors so a missing endpoint just
leaves defaults null.
- rag-defaults-section.tsx (new): self-contained mode + strategy +
embedding-model controls, persists on change. Used in the Settings
KB tab below the ThreadIndexList section.
- knowledge-bases-tab.tsx: mounts RagDefaultsSection below thread
indexes with a separator.
- kb-create-dialog.tsx: loads defaults on open and prefills the form
with them (falls back to hard-coded standard / text when defaults
haven't loaded yet). reset() returns to the latest defaults rather
than the hard-coded ones.
This commit is contained in:
parent
2b85a165e6
commit
ca83bea538
6 changed files with 314 additions and 8 deletions
|
|
@ -43,7 +43,11 @@ async def _sse_auth(
|
|||
from core.rag import embeddings, ingestion, reranker, retrieval, vector_store
|
||||
from core.rag.vector_store import kb_scope, thread_scope
|
||||
from loggers import get_logger
|
||||
from storage.studio_db import get_connection
|
||||
from storage.studio_db import (
|
||||
get_connection,
|
||||
list_chat_settings,
|
||||
upsert_chat_settings_merge,
|
||||
)
|
||||
from utils.paths.storage_roots import ensure_dir, rag_uploads_root
|
||||
from utils.rag.config import (
|
||||
RAG_MAX_UPLOAD_MB,
|
||||
|
|
@ -407,6 +411,75 @@ def list_knowledge_bases(
|
|||
return KBListResponse(knowledge_bases = [_row_to_kb(r) for r in rows])
|
||||
|
||||
|
||||
class RagDefaults(BaseModel):
|
||||
chunking_strategy: ChunkingStrategy = "standard"
|
||||
mode: KBMode = "text"
|
||||
embedding_model: str | None = None
|
||||
|
||||
|
||||
class UpdateRagDefaultsRequest(BaseModel):
|
||||
"""Patch shape — only fields present overwrite stored values."""
|
||||
chunking_strategy: ChunkingStrategy | None = None
|
||||
mode: KBMode | None = None
|
||||
embedding_model: str | None = None
|
||||
|
||||
|
||||
_DEFAULTS_KEY = "rag.defaults"
|
||||
|
||||
|
||||
def _load_rag_defaults() -> RagDefaults:
|
||||
settings = list_chat_settings()
|
||||
raw = settings.get(_DEFAULTS_KEY) or {}
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
return RagDefaults(
|
||||
chunking_strategy = raw.get("chunking_strategy") or "standard",
|
||||
mode = raw.get("mode") or "text",
|
||||
embedding_model = raw.get("embedding_model"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/defaults", response_model = RagDefaults)
|
||||
def get_rag_defaults(
|
||||
current_subject: str = Depends(get_current_subject),
|
||||
) -> RagDefaults:
|
||||
return _load_rag_defaults()
|
||||
|
||||
|
||||
@router.put("/defaults", response_model = RagDefaults)
|
||||
def set_rag_defaults(
|
||||
payload: UpdateRagDefaultsRequest,
|
||||
current_subject: str = Depends(get_current_subject),
|
||||
) -> RagDefaults:
|
||||
current = _load_rag_defaults()
|
||||
new_strategy = payload.chunking_strategy or current.chunking_strategy
|
||||
new_mode = payload.mode or current.mode
|
||||
# PATCH-style — passing an empty string clears the override; a
|
||||
# null/missing field keeps the current value.
|
||||
if payload.embedding_model is None:
|
||||
new_embedder = current.embedding_model
|
||||
elif payload.embedding_model.strip() == "":
|
||||
new_embedder = None
|
||||
else:
|
||||
new_embedder = payload.embedding_model.strip()
|
||||
_validate_mode_combo(new_mode, new_strategy)
|
||||
|
||||
upsert_chat_settings_merge(
|
||||
{
|
||||
_DEFAULTS_KEY: {
|
||||
"chunking_strategy": new_strategy,
|
||||
"mode": new_mode,
|
||||
"embedding_model": new_embedder,
|
||||
}
|
||||
}
|
||||
)
|
||||
return RagDefaults(
|
||||
chunking_strategy = new_strategy,
|
||||
mode = new_mode,
|
||||
embedding_model = new_embedder,
|
||||
)
|
||||
|
||||
|
||||
class ReingestKBRequest(BaseModel):
|
||||
"""All fields optional — omitting one keeps the KB's current value."""
|
||||
chunking_strategy: ChunkingStrategy | None = None
|
||||
|
|
|
|||
|
|
@ -246,6 +246,34 @@ export async function reingestThreadDocuments(
|
|||
return parseJsonOrThrow<ReingestResponse>(response);
|
||||
}
|
||||
|
||||
export interface RagDefaults {
|
||||
chunking_strategy: ChunkingStrategy;
|
||||
mode: KBMode;
|
||||
embedding_model: string | null;
|
||||
}
|
||||
|
||||
export async function getRagDefaults(): Promise<RagDefaults> {
|
||||
const response = await authFetch("/api/rag/defaults");
|
||||
return parseJsonOrThrow<RagDefaults>(response);
|
||||
}
|
||||
|
||||
export interface UpdateRagDefaultsRequest {
|
||||
chunking_strategy?: ChunkingStrategy;
|
||||
mode?: KBMode;
|
||||
embedding_model?: string | null;
|
||||
}
|
||||
|
||||
export async function setRagDefaults(
|
||||
payload: UpdateRagDefaultsRequest,
|
||||
): Promise<RagDefaults> {
|
||||
const response = await authFetch("/api/rag/defaults", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return parseJsonOrThrow<RagDefaults>(response);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Search
|
||||
// ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -19,13 +19,14 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type {
|
||||
ChunkingStrategy,
|
||||
KBMode,
|
||||
KnowledgeBase,
|
||||
} from "../api/rag-api";
|
||||
import { useKnowledgeBases } from "../hooks/use-knowledge-bases";
|
||||
import { useRagStore } from "../stores/rag-store";
|
||||
|
||||
export function KBCreateDialog({
|
||||
open,
|
||||
|
|
@ -37,21 +38,48 @@ export function KBCreateDialog({
|
|||
onCreated?: (kb: KnowledgeBase) => void;
|
||||
}) {
|
||||
const { createKB } = useKnowledgeBases();
|
||||
const defaults = useRagStore((s) => s.defaults);
|
||||
const loadDefaults = useRagStore((s) => s.loadDefaults);
|
||||
|
||||
// Cache the initial values so reset() puts us back to the latest
|
||||
// saved defaults rather than the hard-coded ones.
|
||||
const initialStrategy: ChunkingStrategy =
|
||||
defaults?.chunking_strategy ?? "standard";
|
||||
const initialMode: KBMode = defaults?.mode ?? "text";
|
||||
const initialEmbedder = defaults?.embedding_model ?? "";
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [embeddingModel, setEmbeddingModel] = useState("");
|
||||
const [embeddingModel, setEmbeddingModel] = useState(initialEmbedder);
|
||||
const [chunkingStrategy, setChunkingStrategy] =
|
||||
useState<ChunkingStrategy>("standard");
|
||||
const [mode, setMode] = useState<KBMode>("text");
|
||||
useState<ChunkingStrategy>(initialStrategy);
|
||||
const [mode, setMode] = useState<KBMode>(initialMode);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch defaults on first open and re-sync the form when they arrive.
|
||||
useEffect(() => {
|
||||
if (open && !defaults) {
|
||||
void loadDefaults();
|
||||
}
|
||||
}, [open, defaults, loadDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && defaults) {
|
||||
setChunkingStrategy(defaults.chunking_strategy);
|
||||
setMode(defaults.mode);
|
||||
setEmbeddingModel(defaults.embedding_model ?? "");
|
||||
}
|
||||
// Intentional: only when `open` flips, not on every defaults change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const reset = () => {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setEmbeddingModel("");
|
||||
setChunkingStrategy("standard");
|
||||
setMode("text");
|
||||
setEmbeddingModel(defaults?.embedding_model ?? "");
|
||||
setChunkingStrategy(defaults?.chunking_strategy ?? "standard");
|
||||
setMode(defaults?.mode ?? "text");
|
||||
setError(null);
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
// 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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ChunkingStrategy, KBMode } from "../api/rag-api";
|
||||
import { useRagStore } from "../stores/rag-store";
|
||||
|
||||
/**
|
||||
* Defaults for newly-created KBs, surfaced in the Settings → Knowledge
|
||||
* Bases tab. Values pre-fill the KB create dialog so power users can
|
||||
* pick their preferred strategy/mode once instead of toggling per KB.
|
||||
*
|
||||
* Backed by chat_settings via PUT /api/rag/defaults. The selects
|
||||
* enforce the same (multimodal, late) constraint as the create
|
||||
* dialog.
|
||||
*/
|
||||
export function RagDefaultsSection() {
|
||||
const defaults = useRagStore((s) => s.defaults);
|
||||
const loadDefaults = useRagStore((s) => s.loadDefaults);
|
||||
const updateDefaults = useRagStore((s) => s.updateDefaults);
|
||||
|
||||
const [chunkingStrategy, setChunkingStrategy] =
|
||||
useState<ChunkingStrategy>("standard");
|
||||
const [mode, setMode] = useState<KBMode>("text");
|
||||
const [embeddingModel, setEmbeddingModel] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load defaults on mount; reflect them in local state.
|
||||
useEffect(() => {
|
||||
void loadDefaults();
|
||||
}, [loadDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaults) {
|
||||
setChunkingStrategy(defaults.chunking_strategy);
|
||||
setMode(defaults.mode);
|
||||
setEmbeddingModel(defaults.embedding_model ?? "");
|
||||
}
|
||||
}, [defaults]);
|
||||
|
||||
const lateDisabled = mode === "multimodal";
|
||||
const multimodalDisabled = chunkingStrategy === "late";
|
||||
|
||||
const persist = (patch: {
|
||||
chunking_strategy?: ChunkingStrategy;
|
||||
mode?: KBMode;
|
||||
embedding_model?: string | null;
|
||||
}) => {
|
||||
setError(null);
|
||||
void updateDefaults(patch).catch((err) => {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Defaults for new knowledge bases</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Pre-fills the KB create dialog. Existing KBs keep their own
|
||||
settings — use the Reconfigure button to change those.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="defaults-mode">Mode</Label>
|
||||
<Select
|
||||
value={mode}
|
||||
onValueChange={(v) => {
|
||||
const next = v as KBMode;
|
||||
setMode(next);
|
||||
persist({ mode: next });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="defaults-mode">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">Text only</SelectItem>
|
||||
<SelectItem
|
||||
value="multimodal"
|
||||
disabled={multimodalDisabled}
|
||||
title={
|
||||
multimodalDisabled
|
||||
? "Multimodal cannot be combined with late chunking"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Multimodal
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="defaults-strategy">Chunking strategy</Label>
|
||||
<Select
|
||||
value={chunkingStrategy}
|
||||
onValueChange={(v) => {
|
||||
const next = v as ChunkingStrategy;
|
||||
setChunkingStrategy(next);
|
||||
persist({ chunking_strategy: next });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="defaults-strategy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standard">Standard</SelectItem>
|
||||
<SelectItem
|
||||
value="late"
|
||||
disabled={lateDisabled}
|
||||
title={
|
||||
lateDisabled
|
||||
? "Late chunking cannot be combined with multimodal mode"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Late chunking
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="defaults-model">
|
||||
Default embedding model override (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="defaults-model"
|
||||
value={embeddingModel}
|
||||
onChange={(e) => setEmbeddingModel(e.target.value)}
|
||||
onBlur={() => persist({ embedding_model: embeddingModel })}
|
||||
placeholder="Leave blank to use the matrix default"
|
||||
/>
|
||||
</div>
|
||||
{error ? <div className="text-xs text-destructive">{error}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,18 +8,22 @@ import {
|
|||
type CreateKnowledgeBaseRequest,
|
||||
deleteDocument as apiDeleteDocument,
|
||||
deleteKnowledgeBase as apiDeleteKB,
|
||||
getRagDefaults as apiGetRagDefaults,
|
||||
type JobEvent,
|
||||
type KnowledgeBase,
|
||||
listKBDocuments,
|
||||
listKnowledgeBases,
|
||||
listThreadDocuments,
|
||||
listThreadIndexes,
|
||||
type RagDefaults,
|
||||
type RagDocument,
|
||||
type ReingestKBOptions,
|
||||
reingestKnowledgeBase as apiReingestKB,
|
||||
reingestThreadDocuments as apiReingestThread,
|
||||
setRagDefaults as apiSetRagDefaults,
|
||||
subscribeToJobEvents,
|
||||
type ThreadIndexSummary,
|
||||
type UpdateRagDefaultsRequest,
|
||||
uploadKBDocument,
|
||||
uploadThreadDocument,
|
||||
} from "../api/rag-api";
|
||||
|
|
@ -57,6 +61,10 @@ interface RagStoreState {
|
|||
reingestKB: (kbId: string, opts?: ReingestKBOptions) => Promise<string[]>;
|
||||
reingestThread: (threadId: string) => Promise<string[]>;
|
||||
|
||||
defaults: RagDefaults | null;
|
||||
loadDefaults: () => Promise<void>;
|
||||
updateDefaults: (patch: UpdateRagDefaultsRequest) => Promise<void>;
|
||||
|
||||
subscribeJob: (jobId: string, onComplete?: () => void) => void;
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +90,8 @@ export const useRagStore = create<RagStoreState>((set, get) => ({
|
|||
threadIndexes: [],
|
||||
threadIndexesLoading: false,
|
||||
|
||||
defaults: null,
|
||||
|
||||
async loadKnowledgeBases() {
|
||||
set({ kbsLoading: true, kbsError: null });
|
||||
try {
|
||||
|
|
@ -243,6 +253,22 @@ export const useRagStore = create<RagStoreState>((set, get) => ({
|
|||
return response.job_ids;
|
||||
},
|
||||
|
||||
async loadDefaults() {
|
||||
try {
|
||||
const defaults = await apiGetRagDefaults();
|
||||
set({ defaults });
|
||||
} catch {
|
||||
// Defaults endpoint is best-effort; a 401 / network blip just
|
||||
// leaves defaults null and dialogs fall back to hard-coded
|
||||
// ('standard', 'text', no embedder override).
|
||||
}
|
||||
},
|
||||
|
||||
async updateDefaults(patch) {
|
||||
const defaults = await apiSetRagDefaults(patch);
|
||||
set({ defaults });
|
||||
},
|
||||
|
||||
async reingestThread(threadId) {
|
||||
const response = await apiReingestThread(threadId);
|
||||
void get().loadThreadDocuments(threadId);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import type { KnowledgeBase } from "@/features/rag/api/rag-api";
|
||||
import { KBDetailPanel } from "@/features/rag/components/kb-detail-panel";
|
||||
import { KBList } from "@/features/rag/components/kb-list";
|
||||
import { RagDefaultsSection } from "@/features/rag/components/rag-defaults-section";
|
||||
import { ThreadIndexList } from "@/features/rag/components/thread-index-list";
|
||||
import { useState } from "react";
|
||||
|
||||
|
|
@ -38,6 +39,8 @@ export function KnowledgeBasesTab() {
|
|||
</div>
|
||||
<Separator />
|
||||
<ThreadIndexList />
|
||||
<Separator />
|
||||
<RagDefaultsSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue