diff --git a/README.md b/README.md index 032b5892..00301a36 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,10 @@ Some cool features of AnythingLLM ### Supported LLMs and Vector Databases **Supported LLMs:** -- OpenAI -- Azure OpenAI -- Anthropic ClaudeV2 +- [OpenAI](https://openai.com) +- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) +- [Anthropic ClaudeV2](https://www.anthropic.com/) +- [LM Studio (all models)](https://lmstudio.ai) **Supported Vector Databases:** - [LanceDB](https://github.com/lancedb/lancedb) (default) @@ -73,7 +74,7 @@ This monorepo consists of three main sections: ### Requirements - `yarn` and `node` on your machine - `python` 3.9+ for running scripts in `collector/`. -- access to an LLM like `GPT-3.5`, `GPT-4`, etc. +- access to an LLM service like `GPT-3.5`, `GPT-4`, `Mistral`, `LLama`, etc. - (optional) a vector database like Pinecone, qDrant, Weaviate, or Chroma*. *AnythingLLM by default uses a built-in vector db called LanceDB. diff --git a/docker/.env.example b/docker/.env.example index 4ab09a1e..1bd2b708 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -19,6 +19,10 @@ CACHE_VECTORS="true" # ANTHROPIC_API_KEY=sk-ant-xxxx # ANTHROPIC_MODEL_PREF='claude-2' +# LLM_PROVIDER='lmstudio' +# LMSTUDIO_BASE_PATH='http://your-server:1234/v1' +# LMSTUDIO_MODEL_TOKEN_LIMIT=4096 + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx new file mode 100644 index 00000000..1f00c070 --- /dev/null +++ b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx @@ -0,0 +1,59 @@ +import { Info } from "@phosphor-icons/react"; +import paths from "../../../utils/paths"; + +export default function LMStudioOptions({ settings, showAlert = false }) { + return ( +
+ {showAlert && ( +
+
+ +

+ LMStudio as your LLM requires you to set an embedding service to + use. +

+
+ + Manage embedding → + +
+ )} +
+
+ + +
+
+ + e.target.blur()} + defaultValue={settings?.LMStudioTokenLimit} + required={true} + autoComplete="off" + /> +
+
+
+ ); +} diff --git a/frontend/src/media/llmprovider/lmstudio.png b/frontend/src/media/llmprovider/lmstudio.png new file mode 100644 index 00000000..a5dc75af Binary files /dev/null and b/frontend/src/media/llmprovider/lmstudio.png differ diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index e933ab5e..f2883d05 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -8,11 +8,13 @@ import showToast from "../../../utils/toast"; import OpenAiLogo from "../../../media/llmprovider/openai.png"; import AzureOpenAiLogo from "../../../media/llmprovider/azure.png"; import AnthropicLogo from "../../../media/llmprovider/anthropic.png"; +import LMStudioLogo from "../../../media/llmprovider/LMStudio.png"; import PreLoader from "../../../components/Preloader"; import LLMProviderOption from "../../../components/LLMSelection/LLMProviderOption"; import OpenAiOptions from "../../../components/LLMSelection/OpenAiOptions"; import AzureAiOptions from "../../../components/LLMSelection/AzureAiOptions"; import AnthropicAiOptions from "../../../components/LLMSelection/AnthropicAiOptions"; +import LMStudioOptions from "../../../components/LLMSelection/LMStudioOptions"; export default function GeneralLLMPreference() { const [saving, setSaving] = useState(false); @@ -130,6 +132,15 @@ export default function GeneralLLMPreference() { image={AnthropicLogo} onClick={updateLLMChoice} /> +
{llmChoice === "openai" && ( @@ -141,6 +152,9 @@ export default function GeneralLLMPreference() { {llmChoice === "anthropic" && ( )} + {llmChoice === "lmstudio" && ( + + )}
diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx index 3a19d387..429a0a66 100644 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx @@ -2,12 +2,14 @@ import React, { memo, useEffect, useState } from "react"; import OpenAiLogo from "../../../../../media/llmprovider/openai.png"; import AzureOpenAiLogo from "../../../../../media/llmprovider/azure.png"; import AnthropicLogo from "../../../../../media/llmprovider/anthropic.png"; +import LMStudioLogo from "../../../../../media/llmprovider/lmstudio.png"; import System from "../../../../../models/system"; import PreLoader from "../../../../../components/Preloader"; import LLMProviderOption from "../../../../../components/LLMSelection/LLMProviderOption"; import OpenAiOptions from "../../../../../components/LLMSelection/OpenAiOptions"; import AzureAiOptions from "../../../../../components/LLMSelection/AzureAiOptions"; import AnthropicAiOptions from "../../../../../components/LLMSelection/AnthropicAiOptions"; +import LMStudioOptions from "../../../../../components/LLMSelection/LMStudioOptions"; function LLMSelection({ nextStep, prevStep, currentStep }) { const [llmChoice, setLLMChoice] = useState("openai"); @@ -46,6 +48,8 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { switch (data.LLMProvider) { case "anthropic": return nextStep("embedding_preferences"); + case "lmstudio": + return nextStep("embedding_preferences"); default: return nextStep("vector_database"); } @@ -94,6 +98,15 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { image={AnthropicLogo} onClick={updateLLMChoice} /> +
{llmChoice === "openai" && } @@ -101,6 +114,9 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { {llmChoice === "anthropic" && ( )} + {llmChoice === "lmstudio" && ( + + )}
diff --git a/server/.env.example b/server/.env.example index d7a9cbe7..327aa6ee 100644 --- a/server/.env.example +++ b/server/.env.example @@ -19,6 +19,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # ANTHROPIC_API_KEY=sk-ant-xxxx # ANTHROPIC_MODEL_PREF='claude-2' +# LLM_PROVIDER='lmstudio' +# LMSTUDIO_BASE_PATH='http://your-server:1234/v1' +# LMSTUDIO_MODEL_TOKEN_LIMIT=4096 + ########################################### ######## Embedding API SElECTION ########## ########################################### @@ -58,4 +62,4 @@ VECTOR_DB="lancedb" # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. # STORAGE_DIR= # absolute filesystem path with no trailing slash -# NO_DEBUG="true" \ No newline at end of file +# NO_DEBUG="true" diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index d15f7306..b28c5e86 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -81,6 +81,19 @@ const SystemSettings = { AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, } : {}), + + ...(llmProvider === "lmstudio" + ? { + LMStudioBasePath: process.env.LMSTUDIO_BASE_PATH, + LMStudioTokenLimit: process.env.LMSTUDIO_MODEL_TOKEN_LIMIT, + + // For embedding credentials when lmstudio is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), }; }, diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js new file mode 100644 index 00000000..bb025b3b --- /dev/null +++ b/server/utils/AiProviders/lmStudio/index.js @@ -0,0 +1,139 @@ +const { chatPrompt } = require("../../chats"); + +// hybrid of openAi LLM chat completion for LMStudio +class LMStudioLLM { + constructor(embedder = null) { + if (!process.env.LMSTUDIO_BASE_PATH) + throw new Error("No LMStudio API Base Path was set."); + + const { Configuration, OpenAIApi } = require("openai"); + const config = new Configuration({ + basePath: process.env.LMSTUDIO_BASE_PATH?.replace(/\/+$/, ""), // here is the URL to your LMStudio instance + }); + this.lmstudio = new OpenAIApi(config); + // When using LMStudios inference server - the model param is not required so + // we can stub it here. + this.model = "model-placeholder"; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + throw new Error( + "INVALID LM STUDIO SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use LMStudio as your LLM." + ); + this.embedder = embedder; + } + + // Ensure the user set a value for the token limit + // and if undefined - assume 4096 window. + promptWindowLimit() { + const limit = process.env.LMSTUDIO_MODEL_TOKEN_LIMIT || 4096; + if (!limit || isNaN(Number(limit))) + throw new Error("No LMStudio token context limit was set."); + return Number(limit); + } + + async isValidChatCompletionModel(_ = "") { + // LMStudio may be anything. The user must do it correctly. + // See comment about this.model declaration in constructor + return true; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt} +Context: + ${contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("")}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_input = "") { + // Not implemented so must be stubbed + return { safe: true, reasons: [] }; + } + + async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!this.model) + throw new Error( + `LMStudio chat: ${model} is not valid or defined for chat completion!` + ); + + const textResponse = await this.lmstudio + .createChatCompletion({ + model: this.model, + temperature: Number(workspace?.openAiTemp ?? 0.7), + n: 1, + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }) + .then((json) => { + const res = json.data; + if (!res.hasOwnProperty("choices")) + throw new Error("LMStudio chat: No results!"); + if (res.choices.length === 0) + throw new Error("LMStudio chat: No results length!"); + return res.choices[0].message.content; + }) + .catch((error) => { + throw new Error( + `LMStudio::createChatCompletion failed with: ${error.message}` + ); + }); + + return textResponse; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!this.model) + throw new Error( + `LMStudio chat: ${this.model} is not valid or defined model for chat completion!` + ); + + const { data } = await this.lmstudio.createChatCompletion({ + model: this.model, + messages, + temperature, + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations + async embedTextInput(textInput) { + return await this.embedder.embedTextInput(textInput); + } + async embedChunks(textChunks = []) { + return await this.embedder.embedChunks(textChunks); + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } +} + +module.exports = { + LMStudioLLM, +}; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 9df2e8f1..cf48937a 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -23,6 +23,7 @@ function getVectorDbClass() { function getLLMProvider() { const vectorSelection = process.env.LLM_PROVIDER || "openai"; + let embedder = null; switch (vectorSelection) { case "openai": const { OpenAiLLM } = require("../AiProviders/openAi"); @@ -32,8 +33,12 @@ function getLLMProvider() { return new AzureOpenAiLLM(); case "anthropic": const { AnthropicLLM } = require("../AiProviders/anthropic"); - const embedder = getEmbeddingEngineSelection(); + embedder = getEmbeddingEngineSelection(); return new AnthropicLLM(embedder); + case "lmstudio": + const { LMStudioLLM } = require("../AiProviders/lmStudio"); + embedder = getEmbeddingEngineSelection(); + return new LMStudioLLM(embedder); default: throw new Error("ENV: No LLM_PROVIDER value found in environment!"); } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 976849d9..e97f9791 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -44,6 +44,16 @@ const KEY_MAPPING = { checks: [isNotEmpty, validAnthropicModel], }, + // LMStudio Settings + LMStudioBasePath: { + envKey: "LMSTUDIO_BASE_PATH", + checks: [isNotEmpty, validLMStudioBasePath], + }, + LMStudioTokenLimit: { + envKey: "LMSTUDIO_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, + EmbeddingEngine: { envKey: "EMBEDDING_ENGINE", checks: [supportedEmbeddingModel], @@ -117,6 +127,11 @@ function isNotEmpty(input = "") { return !input || input.length === 0 ? "Value cannot be empty" : null; } +function nonZero(input = "") { + if (isNaN(Number(input))) return "Value must be a number"; + return Number(input) <= 0 ? "Value must be greater than zero" : null; +} + function isValidURL(input = "") { try { new URL(input); @@ -136,8 +151,20 @@ function validAnthropicApiKey(input = "") { : "Anthropic Key must start with sk-ant-"; } +function validLMStudioBasePath(input = "") { + try { + new URL(input); + if (!input.includes("v1")) return "URL must include /v1"; + if (input.split("").slice(-1)?.[0] === "/") + return "URL cannot end with a slash"; + return null; + } catch { + return "Not a valid URL"; + } +} + function supportedLLM(input = "") { - return ["openai", "azure", "anthropic"].includes(input); + return ["openai", "azure", "anthropic", "lmstudio"].includes(input); } function validAnthropicModel(input = "") {