Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
Timothy Carambat
2026-02-10 09:12:27 -08:00
8 changed files with 254 additions and 85 deletions

View File

@@ -1,3 +1,6 @@
import { Info } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
const DEFAULT_MODELS = [
{
id: "gemini-embedding-001",
@@ -7,47 +10,93 @@ const DEFAULT_MODELS = [
export default function GeminiOptions({ settings }) {
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-[36px] mt-1.5">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
API Key
<div className="w-full flex flex-col gap-y-6">
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-[36px] mt-1.5">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
API Key
</label>
<input
type="password"
name="GeminiEmbeddingApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Gemini API Key"
defaultValue={
settings?.GeminiEmbeddingApiKey ? "*".repeat(20) : ""
}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Model Preference
</label>
<select
name="EmbeddingModelPref"
required={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<optgroup label="Available embedding models">
{DEFAULT_MODELS.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={settings?.EmbeddingModelPref === model.id}
>
{model.name}
</option>
);
})}
</optgroup>
</select>
</div>
</div>
</div>
<div className="flex flex-col w-60">
<div
data-tooltip-id="embedding-output-dimensions-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Output dimensions
</label>
<input
type="password"
name="GeminiEmbeddingApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Gemini API Key"
defaultValue={settings?.GeminiEmbeddingApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Model Preference
</label>
<select
name="EmbeddingModelPref"
required={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
<Tooltip
id="embedding-output-dimensions-tooltip"
place="top"
delayShow={300}
className="tooltip !text-xs !opacity-100"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
<optgroup label="Available embedding models">
{DEFAULT_MODELS.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={settings?.EmbeddingModelPref === model.id}
>
{model.name}
</option>
);
})}
</optgroup>
</select>
The number of dimensions the resulting output embeddings should have
if it supports multiple dimensions output.
<br />
<br /> Leave blank to use the default dimensions for the selected
model.
</Tooltip>
</div>
<input
type="number"
name="EmbeddingOutputDimensions"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Assume default dimensions"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.EmbeddingOutputDimensions}
required={false}
autoComplete="off"
/>
</div>
</div>
);

View File

@@ -30,20 +30,70 @@ export default function LocalAiOptions({ settings }) {
apiKey={apiKey}
basePath={basePath.value}
/>
<div className="flex flex-col w-60">
<div className="flex flex-col gap-y-1 mb-2">
<div className="flex gap-x-1 items-center">
<label className="text-white text-sm font-semibold block">
Local AI API Key
</label>
<Info
size={16}
data-tooltip-id="localai-api-key-tooltip"
className="text-theme-text-secondary cursor-pointer"
/>
<Tooltip
id="localai-api-key-tooltip"
place="top"
delayShow={300}
className="tooltip !text-xs !opacity-100"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
The API key for the LocalAI server (if applicable).
</Tooltip>
</div>
</div>
<input
type="password"
name="LocalAiApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="sk-mysecretkey"
defaultValue={settings?.LocalAiApiKey ? "*".repeat(20) : ""}
autoComplete="off"
spellCheck={false}
onChange={(e) => setApiKeyValue(e.target.value)}
onBlur={() => setApiKey(apiKeyValue)}
/>
</div>
</div>
<div className="w-full flex items-center gap-[36px] mt-1.5">
<div className="flex flex-col w-60">
<div
data-tooltip-place="top"
data-tooltip-id="max-embedding-chunk-length-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Max embedding chunk length
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<label className="text-white text-sm font-semibold block">
Max embedding chunk length
</label>
<Tooltip id="max-embedding-chunk-length-tooltip">
<Tooltip
id="max-embedding-chunk-length-tooltip"
place="top"
delayShow={300}
className="tooltip !text-xs !opacity-100"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
Maximum length of text chunks, in characters, for embedding.
</Tooltip>
</div>
@@ -59,23 +109,47 @@ export default function LocalAiOptions({ settings }) {
autoComplete="off"
/>
</div>
<div className="flex flex-col w-60">
<div className="flex flex-col gap-y-1 mb-2">
<label className="text-white text-sm font-semibold flex items-center gap-x-2">
Local AI API Key{" "}
<p className="!text-xs !italic !font-thin">optional</p>
<div
data-tooltip-id="embedding-output-dimensions-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Output dimensions
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<Tooltip
id="embedding-output-dimensions-tooltip"
place="top"
delayShow={300}
className="tooltip !text-xs !opacity-100"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
The number of dimensions the resulting output embeddings should
have if it supports multiple dimensions output.
<br />
<br /> Leave blank to use the default dimensions for the selected
model.
</Tooltip>
</div>
<input
type="password"
name="LocalAiApiKey"
type="number"
name="EmbeddingOutputDimensions"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="sk-mysecretkey"
defaultValue={settings?.LocalAiApiKey ? "*".repeat(20) : ""}
placeholder="Assume default dimensions"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.EmbeddingOutputDimensions}
required={false}
autoComplete="off"
spellCheck={false}
onChange={(e) => setApiKeyValue(e.target.value)}
onBlur={() => setApiKey(apiKeyValue)}
/>
</div>
</div>

View File

@@ -75,7 +75,7 @@ export function ToggleSidebarButton({ showSidebar, setShowSidebar }) {
<>
<button
type="button"
className={`hidden md:block border-none bg-transparent outline-none ring-0 transition-left duration-500 ${showSidebar ? "left-[247px]" : "absolute top-[20px] left-[30px] z-10"}`}
className={`hidden md:block border-none bg-transparent outline-none ring-0 absolute transition-all duration-500 z-10 ${showSidebar ? "top-[18px] left-[248px]" : "top-[20px] left-[30px]"}`}
onClick={() => setShowSidebar((prev) => !prev)}
data-tooltip-id="sidebar-toggle"
data-tooltip-content={

View File

@@ -34,39 +34,41 @@ export default function Sidebar() {
width: showSidebar ? "292px" : "0px",
paddingLeft: showSidebar ? "0px" : "16px",
}}
className="transition-all duration-500"
className="relative transition-all duration-500"
>
<div className="flex shrink-0 w-full justify-center my-[18px]">
<div className="flex justify-between w-[250px] min-w-[250px]">
<Link to={paths.home()} aria-label="Home">
<img
src={logo}
alt="Logo"
className={`rounded max-h-[24px] object-contain transition-opacity duration-500 ${showSidebar ? "opacity-100" : "opacity-0"}`}
/>
</Link>
{canToggleSidebar && (
<ToggleSidebarButton
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
/>
)}
{canToggleSidebar && (
<ToggleSidebarButton
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
/>
)}
<div className="overflow-hidden h-full">
<div className="flex shrink-0 w-full justify-center my-[18px]">
<div className="flex w-[250px] min-w-[250px]">
<Link to={paths.home()} aria-label="Home">
<img
src={logo}
alt="Logo"
className={`rounded max-h-[24px] object-contain transition-opacity duration-500 ${showSidebar ? "opacity-100" : "opacity-0"}`}
/>
</Link>
</div>
</div>
</div>
<div
ref={sidebarRef}
className="relative m-[16px] rounded-[16px] bg-theme-bg-sidebar border-[2px] border-theme-sidebar-border light:border-none min-w-[250px] p-[10px] h-[calc(100%-76px)]"
>
<div className="flex flex-col h-full overflow-hidden">
<div className="flex-grow flex flex-col min-w-[235px] min-h-0">
<div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
<div className="flex flex-col gap-y-[14px]">
<SearchBox user={user} showNewWsModal={showNewWsModal} />
<ActiveWorkspaces />
<div
ref={sidebarRef}
className="relative m-[16px] rounded-[16px] bg-theme-bg-sidebar border-[2px] border-theme-sidebar-border light:border-none min-w-[250px] p-[10px] h-[calc(100%-76px)]"
>
<div className="flex flex-col h-full overflow-hidden">
<div className="flex-grow flex flex-col min-w-[235px] min-h-0">
<div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
<div className="flex flex-col gap-y-[14px]">
<SearchBox user={user} showNewWsModal={showNewWsModal} />
<ActiveWorkspaces />
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 pb-3 rounded-b-[16px] bg-theme-bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md z-10">
<Footer />
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 pb-3 rounded-b-[16px] bg-theme-bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md z-10">
<Footer />
</div>
</div>
</div>

View File

@@ -233,6 +233,8 @@ const SystemSettings = {
embeddingEngine === "native"
? NativeEmbedder._getEmbeddingModel()
: process.env.EMBEDDING_MODEL_PREF,
EmbeddingOutputDimensions:
process.env.EMBEDDING_OUTPUT_DIMENSIONS || null,
EmbeddingModelMaxChunkLength:
process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH,
OllamaEmbeddingBatchSize: process.env.OLLAMA_EMBEDDING_BATCH_SIZE || 1,

View File

@@ -23,7 +23,10 @@ class GeminiEmbedder {
// https://ai.google.dev/gemini-api/docs/models/gemini#text-embedding-and-embedding
this.embeddingMaxChunkLength = MODEL_MAP[this.model] || 2_048;
this.log(
`Initialized with ${this.model} - Max Size: ${this.embeddingMaxChunkLength}`
`Initialized with ${this.model} - Max Size: ${this.embeddingMaxChunkLength}` +
(this.outputDimensions
? ` - Output Dimensions: ${this.outputDimensions}`
: " Assuming default output dimensions")
);
}
@@ -31,6 +34,16 @@ class GeminiEmbedder {
console.log(`\x1b[36m[${this.className}]\x1b[0m ${text}`, ...args);
}
get outputDimensions() {
if (
process.env.EMBEDDING_OUTPUT_DIMENSIONS &&
!isNaN(process.env.EMBEDDING_OUTPUT_DIMENSIONS) &&
process.env.EMBEDDING_OUTPUT_DIMENSIONS > 0
)
return parseInt(process.env.EMBEDDING_OUTPUT_DIMENSIONS);
return null;
}
/**
* Embeds a single text input
* @param {string|string[]} textInput - The text to embed
@@ -62,6 +75,7 @@ class GeminiEmbedder {
.create({
model: this.model,
input: chunk,
dimensions: this.outputDimensions,
})
.then((result) => {
resolve({ data: result?.data, error: null });

View File

@@ -7,7 +7,9 @@ class LocalAiEmbedder {
if (!process.env.EMBEDDING_MODEL_PREF)
throw new Error("No embedding model was set.");
this.className = "LocalAiEmbedder";
const { OpenAI: OpenAIApi } = require("openai");
this.model = process.env.EMBEDDING_MODEL_PREF;
this.openai = new OpenAIApi({
baseURL: process.env.EMBEDDING_BASE_PATH,
apiKey: process.env.LOCAL_AI_API_KEY ?? null,
@@ -16,6 +18,27 @@ class LocalAiEmbedder {
// Limit of how many strings we can process in a single pass to stay with resource or network limits
this.maxConcurrentChunks = 50;
this.embeddingMaxChunkLength = maximumChunkLength();
this.log(
`Initialized with ${this.model} - Max Size: ${this.embeddingMaxChunkLength}` +
(this.outputDimensions
? ` - Output Dimensions: ${this.outputDimensions}`
: " Assuming default output dimensions")
);
}
log(text, ...args) {
console.log(`\x1b[36m[${this.className}]\x1b[0m ${text}`, ...args);
}
get outputDimensions() {
if (
process.env.EMBEDDING_OUTPUT_DIMENSIONS &&
!isNaN(process.env.EMBEDDING_OUTPUT_DIMENSIONS) &&
process.env.EMBEDDING_OUTPUT_DIMENSIONS > 0
)
return parseInt(process.env.EMBEDDING_OUTPUT_DIMENSIONS);
return null;
}
async embedTextInput(textInput) {
@@ -32,8 +55,9 @@ class LocalAiEmbedder {
new Promise((resolve) => {
this.openai.embeddings
.create({
model: process.env.EMBEDDING_MODEL_PREF,
model: this.model,
input: chunk,
dimensions: this.outputDimensions,
})
.then((result) => {
resolve({ data: result?.data, error: null });

View File

@@ -307,6 +307,10 @@ const KEY_MAPPING = {
envKey: "EMBEDDING_MODEL_MAX_CHUNK_LENGTH",
checks: [nonZero],
},
EmbeddingOutputDimensions: {
envKey: "EMBEDDING_OUTPUT_DIMENSIONS",
checks: [],
},
OllamaEmbeddingBatchSize: {
envKey: "OLLAMA_EMBEDDING_BATCH_SIZE",
checks: [nonZero],