mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-05-02 09:03:12 +00:00
Add LMStudio embedding endpoint support (#1141)
* Add LMStudio embedding endpoint support * update alive path check for HEAD remove commented JSX * update comment
This commit is contained in:
parent
c009904664
commit
c65f890afc
10 changed files with 321 additions and 44 deletions
|
@ -85,7 +85,7 @@ Some cool features of AnythingLLM
|
|||
- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)
|
||||
- [LocalAi (all)](https://localai.io/)
|
||||
- [Ollama (all)](https://ollama.ai/)
|
||||
<!-- - [LM Studio (all)](https://lmstudio.ai) -->
|
||||
- [LM Studio (all)](https://lmstudio.ai)
|
||||
|
||||
**Supported Transcription models:**
|
||||
|
||||
|
|
|
@ -85,10 +85,15 @@ GID='1000'
|
|||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=1000 # The max chunk size in chars a string to embed can be
|
||||
|
||||
# EMBEDDING_ENGINE='ollama'
|
||||
# EMBEDDING_BASE_PATH='http://127.0.0.1:11434'
|
||||
# EMBEDDING_BASE_PATH='http://host.docker.internal:11434'
|
||||
# EMBEDDING_MODEL_PREF='nomic-embed-text:latest'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
|
||||
# EMBEDDING_ENGINE='lmstudio'
|
||||
# EMBEDDING_BASE_PATH='https://host.docker.internal:1234/v1'
|
||||
# EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
|
||||
###########################################
|
||||
######## Vector Database Selection ########
|
||||
###########################################
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import System from "@/models/system";
|
||||
|
||||
export default function LMStudioEmbeddingOptions({ settings }) {
|
||||
const [basePathValue, setBasePathValue] = useState(
|
||||
settings?.EmbeddingBasePath
|
||||
);
|
||||
const [basePath, setBasePath] = useState(settings?.EmbeddingBasePath);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div className="w-full flex items-center gap-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
LMStudio Base URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="EmbeddingBasePath"
|
||||
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="http://localhost:1234/v1"
|
||||
defaultValue={settings?.EmbeddingBasePath}
|
||||
onChange={(e) => setBasePathValue(e.target.value)}
|
||||
onBlur={() => setBasePath(basePathValue)}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<LMStudioModelSelection settings={settings} basePath={basePath} />
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Max embedding chunk length
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="EmbeddingModelMaxChunkLength"
|
||||
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="8192"
|
||||
min={1}
|
||||
onScroll={(e) => e.target.blur()}
|
||||
defaultValue={settings?.EmbeddingModelMaxChunkLength}
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LMStudioModelSelection({ settings, basePath = null }) {
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function findCustomModels() {
|
||||
if (!basePath || !basePath.includes("/v1")) {
|
||||
setCustomModels([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const { models } = await System.customModels("lmstudio", null, basePath);
|
||||
setCustomModels(models || []);
|
||||
setLoading(false);
|
||||
}
|
||||
findCustomModels();
|
||||
}, [basePath]);
|
||||
|
||||
if (loading || customModels.length == 0) {
|
||||
return (
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Chat Model Selection
|
||||
</label>
|
||||
<select
|
||||
name="EmbeddingModelPref"
|
||||
disabled={true}
|
||||
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||
>
|
||||
<option disabled={true} selected={true}>
|
||||
{basePath?.includes("/v1")
|
||||
? "-- loading available models --"
|
||||
: "-- waiting for URL --"}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Chat Model Selection
|
||||
</label>
|
||||
<select
|
||||
name="EmbeddingModelPref"
|
||||
required={true}
|
||||
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||
>
|
||||
{customModels.length > 0 && (
|
||||
<optgroup label="Your loaded models">
|
||||
{customModels.map((model) => {
|
||||
return (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={settings.EmbeddingModelPref === model.id}
|
||||
>
|
||||
{model.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -8,6 +8,7 @@ import OpenAiLogo from "@/media/llmprovider/openai.png";
|
|||
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
||||
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
||||
import OllamaLogo from "@/media/llmprovider/ollama.png";
|
||||
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
|
||||
import PreLoader from "@/components/Preloader";
|
||||
import ChangeWarningModal from "@/components/ChangeWarning";
|
||||
import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
|
||||
|
@ -15,6 +16,7 @@ import AzureAiOptions from "@/components/EmbeddingSelection/AzureAiOptions";
|
|||
import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
|
||||
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
|
||||
import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
|
||||
import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions";
|
||||
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
@ -58,6 +60,14 @@ const EMBEDDERS = [
|
|||
options: (settings) => <OllamaEmbeddingOptions settings={settings} />,
|
||||
description: "Run embedding models locally on your own machine.",
|
||||
},
|
||||
{
|
||||
name: "LM Studio",
|
||||
value: "lmstudio",
|
||||
logo: LMStudioLogo,
|
||||
options: (settings) => <LMStudioEmbeddingOptions settings={settings} />,
|
||||
description:
|
||||
"Discover, download, and run thousands of cutting edge LLMs in a few clicks.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function GeneralEmbeddingPreference() {
|
||||
|
|
|
@ -237,6 +237,13 @@ export const EMBEDDING_ENGINE_PRIVACY = {
|
|||
],
|
||||
logo: OllamaLogo,
|
||||
},
|
||||
lmstudio: {
|
||||
name: "LMStudio",
|
||||
description: [
|
||||
"Your document text is embedded privately on the server running LMStudio",
|
||||
],
|
||||
logo: LMStudioLogo,
|
||||
},
|
||||
};
|
||||
|
||||
export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
|
||||
|
|
|
@ -5,11 +5,13 @@ import OpenAiLogo from "@/media/llmprovider/openai.png";
|
|||
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
||||
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
||||
import OllamaLogo from "@/media/llmprovider/ollama.png";
|
||||
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
|
||||
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
|
||||
import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
|
||||
import AzureAiOptions from "@/components/EmbeddingSelection/AzureAiOptions";
|
||||
import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
|
||||
import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
|
||||
import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions";
|
||||
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
|
||||
import System from "@/models/system";
|
||||
import paths from "@/utils/paths";
|
||||
|
@ -19,6 +21,52 @@ import { useNavigate } from "react-router-dom";
|
|||
const TITLE = "Embedding Preference";
|
||||
const DESCRIPTION =
|
||||
"AnythingLLM can work with many embedding models. This will be the model which turns documents into vectors.";
|
||||
const EMBEDDERS = [
|
||||
{
|
||||
name: "AnythingLLM Embedder",
|
||||
value: "native",
|
||||
logo: AnythingLLMIcon,
|
||||
options: (settings) => <NativeEmbeddingOptions settings={settings} />,
|
||||
description:
|
||||
"Use the built-in embedding engine for AnythingLLM. Zero setup!",
|
||||
},
|
||||
{
|
||||
name: "OpenAI",
|
||||
value: "openai",
|
||||
logo: OpenAiLogo,
|
||||
options: (settings) => <OpenAiOptions settings={settings} />,
|
||||
description: "The standard option for most non-commercial use.",
|
||||
},
|
||||
{
|
||||
name: "Azure OpenAI",
|
||||
value: "azure",
|
||||
logo: AzureOpenAiLogo,
|
||||
options: (settings) => <AzureAiOptions settings={settings} />,
|
||||
description: "The enterprise option of OpenAI hosted on Azure services.",
|
||||
},
|
||||
{
|
||||
name: "Local AI",
|
||||
value: "localai",
|
||||
logo: LocalAiLogo,
|
||||
options: (settings) => <LocalAiOptions settings={settings} />,
|
||||
description: "Run embedding models locally on your own machine.",
|
||||
},
|
||||
{
|
||||
name: "Ollama",
|
||||
value: "ollama",
|
||||
logo: OllamaLogo,
|
||||
options: (settings) => <OllamaEmbeddingOptions settings={settings} />,
|
||||
description: "Run embedding models locally on your own machine.",
|
||||
},
|
||||
{
|
||||
name: "LM Studio",
|
||||
value: "lmstudio",
|
||||
logo: LMStudioLogo,
|
||||
options: (settings) => <LMStudioEmbeddingOptions settings={settings} />,
|
||||
description:
|
||||
"Discover, download, and run thousands of cutting edge LLMs in a few clicks.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function EmbeddingPreference({
|
||||
setHeader,
|
||||
|
@ -42,45 +90,6 @@ export default function EmbeddingPreference({
|
|||
fetchKeys();
|
||||
}, []);
|
||||
|
||||
const EMBEDDERS = [
|
||||
{
|
||||
name: "AnythingLLM Embedder",
|
||||
value: "native",
|
||||
logo: AnythingLLMIcon,
|
||||
options: <NativeEmbeddingOptions settings={settings} />,
|
||||
description:
|
||||
"Use the built-in embedding engine for AnythingLLM. Zero setup!",
|
||||
},
|
||||
{
|
||||
name: "OpenAI",
|
||||
value: "openai",
|
||||
logo: OpenAiLogo,
|
||||
options: <OpenAiOptions settings={settings} />,
|
||||
description: "The standard option for most non-commercial use.",
|
||||
},
|
||||
{
|
||||
name: "Azure OpenAI",
|
||||
value: "azure",
|
||||
logo: AzureOpenAiLogo,
|
||||
options: <AzureAiOptions settings={settings} />,
|
||||
description: "The enterprise option of OpenAI hosted on Azure services.",
|
||||
},
|
||||
{
|
||||
name: "Local AI",
|
||||
value: "localai",
|
||||
logo: LocalAiLogo,
|
||||
options: <LocalAiOptions settings={settings} />,
|
||||
description: "Run embedding models locally on your own machine.",
|
||||
},
|
||||
{
|
||||
name: "Ollama",
|
||||
value: "ollama",
|
||||
logo: OllamaLogo,
|
||||
options: <OllamaEmbeddingOptions settings={settings} />,
|
||||
description: "Run embedding models locally on your own machine.",
|
||||
},
|
||||
];
|
||||
|
||||
function handleForward() {
|
||||
if (hiddenSubmitButtonRef.current) {
|
||||
hiddenSubmitButtonRef.current.click();
|
||||
|
@ -161,8 +170,9 @@ export default function EmbeddingPreference({
|
|||
</div>
|
||||
<div className="mt-4 flex flex-col gap-y-1">
|
||||
{selectedEmbedder &&
|
||||
EMBEDDERS.find((embedder) => embedder.value === selectedEmbedder)
|
||||
?.options}
|
||||
EMBEDDERS.find(
|
||||
(embedder) => embedder.value === selectedEmbedder
|
||||
)?.options(settings)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
@ -86,6 +86,11 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
|
|||
# EMBEDDING_MODEL_PREF='nomic-embed-text:latest'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
|
||||
# EMBEDDING_ENGINE='lmstudio'
|
||||
# EMBEDDING_BASE_PATH='https://localhost:1234/v1'
|
||||
# EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf'
|
||||
# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192
|
||||
|
||||
###########################################
|
||||
######## Vector Database Selection ########
|
||||
###########################################
|
||||
|
|
110
server/utils/EmbeddingEngines/lmstudio/index.js
Normal file
110
server/utils/EmbeddingEngines/lmstudio/index.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
const { maximumChunkLength } = require("../../helpers");
|
||||
|
||||
class LMStudioEmbedder {
|
||||
constructor() {
|
||||
if (!process.env.EMBEDDING_BASE_PATH)
|
||||
throw new Error("No embedding base path was set.");
|
||||
if (!process.env.EMBEDDING_MODEL_PREF)
|
||||
throw new Error("No embedding model was set.");
|
||||
this.basePath = `${process.env.EMBEDDING_BASE_PATH}/embeddings`;
|
||||
this.model = process.env.EMBEDDING_MODEL_PREF;
|
||||
|
||||
// Limit of how many strings we can process in a single pass to stay with resource or network limits
|
||||
// Limit of how many strings we can process in a single pass to stay with resource or network limits
|
||||
this.maxConcurrentChunks = 1;
|
||||
this.embeddingMaxChunkLength = maximumChunkLength();
|
||||
}
|
||||
|
||||
log(text, ...args) {
|
||||
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
|
||||
}
|
||||
|
||||
async #isAlive() {
|
||||
return await fetch(`${this.basePath}/models`, {
|
||||
method: "HEAD",
|
||||
})
|
||||
.then((res) => res.ok)
|
||||
.catch((e) => {
|
||||
this.log(e.message);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async embedTextInput(textInput) {
|
||||
const result = await this.embedChunks(textInput);
|
||||
return result?.[0] || [];
|
||||
}
|
||||
|
||||
async embedChunks(textChunks = []) {
|
||||
if (!(await this.#isAlive()))
|
||||
throw new Error(
|
||||
`LMStudio service could not be reached. Is LMStudio running?`
|
||||
);
|
||||
|
||||
this.log(
|
||||
`Embedding ${textChunks.length} chunks of text with ${this.model}.`
|
||||
);
|
||||
|
||||
// LMStudio will drop all queued requests now? So if there are many going on
|
||||
// we need to do them sequentially or else only the first resolves and the others
|
||||
// get dropped or go unanswered >:(
|
||||
let results = [];
|
||||
let hasError = false;
|
||||
for (const chunk of textChunks) {
|
||||
if (hasError) break; // If an error occurred don't continue and exit early.
|
||||
results.push(
|
||||
await fetch(this.basePath, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
input: chunk,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const embedding = json.data[0].embedding;
|
||||
if (!Array.isArray(embedding) || !embedding.length)
|
||||
throw {
|
||||
type: "EMPTY_ARR",
|
||||
message: "The embedding was empty from LMStudio",
|
||||
};
|
||||
return { data: embedding, error: null };
|
||||
})
|
||||
.catch((error) => {
|
||||
hasError = true;
|
||||
return { data: [], error };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Accumulate errors from embedding.
|
||||
// If any are present throw an abort error.
|
||||
const errors = results
|
||||
.filter((res) => !!res.error)
|
||||
.map((res) => res.error)
|
||||
.flat();
|
||||
|
||||
if (errors.length > 0) {
|
||||
let uniqueErrors = new Set();
|
||||
console.log(errors);
|
||||
errors.map((error) =>
|
||||
uniqueErrors.add(`[${error.type}]: ${error.message}`)
|
||||
);
|
||||
|
||||
if (errors.length > 0)
|
||||
throw new Error(
|
||||
`LMStudio Failed to embed: ${Array.from(uniqueErrors).join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = results.map((res) => res?.data || []);
|
||||
return data.length > 0 ? data : null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LMStudioEmbedder,
|
||||
};
|
|
@ -102,6 +102,9 @@ function getEmbeddingEngineSelection() {
|
|||
case "native":
|
||||
const { NativeEmbedder } = require("../EmbeddingEngines/native");
|
||||
return new NativeEmbedder();
|
||||
case "lmstudio":
|
||||
const { LMStudioEmbedder } = require("../EmbeddingEngines/lmstudio");
|
||||
return new LMStudioEmbedder();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -408,7 +408,14 @@ function validAnthropicModel(input = "") {
|
|||
}
|
||||
|
||||
function supportedEmbeddingModel(input = "") {
|
||||
const supported = ["openai", "azure", "localai", "native", "ollama"];
|
||||
const supported = [
|
||||
"openai",
|
||||
"azure",
|
||||
"localai",
|
||||
"native",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
];
|
||||
return supported.includes(input)
|
||||
? null
|
||||
: `Invalid Embedding model type. Must be one of ${supported.join(", ")}.`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue