From f499f1ba59f2e9f8be5e44c89a951e859382e005 Mon Sep 17 00:00:00 2001 From: Francisco Bischoff <984592+franzbischoff@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:33:21 +0000 Subject: [PATCH] Using OpenAI API locally (#335) * Using OpenAI API locally * Infinite prompt input and compression implementation (#332) * WIP on continuous prompt window summary * wip * Move chat out of VDB simplify chat interface normalize LLM model interface have compression abstraction Cleanup compressor TODO: Anthropic stuff * Implement compression for Anythropic Fix lancedb sources * cleanup vectorDBs and check that lance, chroma, and pinecone are returning valid metadata sources * Resolve Weaviate citation sources not working with schema * comment cleanup * disable import on hosted instances (#339) * disable import on hosted instances * Update UI on disabled import/export --------- Co-authored-by: timothycarambat * Add support for gpt-4-turbo 128K model (#340) resolves #336 Add support for gpt-4-turbo 128K model * 315 show citations based on relevancy score (#316) * settings for similarity score threshold and prisma schema updated * prisma schema migration for adding similarityScore setting * WIP * Min score default change * added similarityThreshold checking for all vectordb providers * linting --------- Co-authored-by: shatfield4 * rename localai to lmstudio * forgot files that were renamed * normalize model interface * add model and context window limits * update LMStudio tagline * Fully working LMStudio integration --------- Co-authored-by: Francisco Bischoff <984592+franzbischoff@users.noreply.github.com> Co-authored-by: Timothy Carambat Co-authored-by: Sean Hatfield --- README.md | 9 +- docker/.env.example | 4 + .../LLMSelection/LMStudioOptions/index.jsx | 59 ++++++++ frontend/src/media/llmprovider/lmstudio.png | Bin 0 -> 23553 bytes .../GeneralSettings/LLMPreference/index.jsx | 14 ++ .../Steps/LLMSelection/index.jsx | 16 ++ server/.env.example | 6 +- server/models/systemSettings.js | 13 ++ server/utils/AiProviders/lmStudio/index.js | 139 ++++++++++++++++++ server/utils/helpers/index.js | 7 +- server/utils/helpers/updateENV.js | 29 +++- 11 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/LLMSelection/LMStudioOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/lmstudio.png create mode 100644 server/utils/AiProviders/lmStudio/index.js 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 0000000000000000000000000000000000000000..a5dc75afb71539541e5fc6a9a54a088df72ab763 GIT binary patch literal 23553 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 SZvsuw5V*0GQN5vO!v6q3CTw#6 literal 0 HcmV?d00001 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 = "") {