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 <rambat1010@gmail.com>

* 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 <seanhatfield5@gmail.com>

* 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 <rambat1010@gmail.com>
Co-authored-by: Sean Hatfield <seanhatfield5@gmail.com>
---
 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 032b58929..00301a368 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 4ab09a1e2..1bd2b7082 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 000000000..1f00c070d
--- /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 (
+    <div className="w-full flex flex-col">
+      {showAlert && (
+        <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-6 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
+          <div className="gap-x-2 flex items-center">
+            <Info size={12} className="hidden md:visible" />
+            <p className="text-sm md:text-base">
+              LMStudio as your LLM requires you to set an embedding service to
+              use.
+            </p>
+          </div>
+          <a
+            href={paths.general.embeddingPreference()}
+            className="text-sm md:text-base my-2 underline"
+          >
+            Manage embedding &rarr;
+          </a>
+        </div>
+      )}
+      <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="LMStudioBasePath"
+            className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
+            placeholder="http://localhost:1234/v1"
+            defaultValue={settings?.LMStudioBasePath}
+            required={true}
+            autoComplete="off"
+            spellCheck={false}
+          />
+        </div>
+        <div className="flex flex-col w-60">
+          <label className="text-white text-sm font-semibold block mb-4">
+            Token context window
+          </label>
+          <input
+            type="number"
+            name="LMStudioTokenLimit"
+            className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
+            placeholder="4096"
+            min={1}
+            onScroll={(e) => e.target.blur()}
+            defaultValue={settings?.LMStudioTokenLimit}
+            required={true}
+            autoComplete="off"
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
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 e933ab5ee..f2883d05a 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}
                 />
+                <LLMProviderOption
+                  name="LM Studio"
+                  value="lmstudio"
+                  link="lmstudio.ai"
+                  description="Discover, download, and run thousands of cutting edge LLMs in a few clicks."
+                  checked={llmChoice === "lmstudio"}
+                  image={LMStudioLogo}
+                  onClick={updateLLMChoice}
+                />
               </div>
               <div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
                 {llmChoice === "openai" && (
@@ -141,6 +152,9 @@ export default function GeneralLLMPreference() {
                 {llmChoice === "anthropic" && (
                   <AnthropicAiOptions settings={settings} showAlert={true} />
                 )}
+                {llmChoice === "lmstudio" && (
+                  <LMStudioOptions settings={settings} showAlert={true} />
+                )}
               </div>
             </div>
           </form>
diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx
index 3a19d3874..429a0a661 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}
             />
+            <LLMProviderOption
+              name="LM Studio"
+              value="lmstudio"
+              link="lmstudio.ai"
+              description="Discover, download, and run thousands of cutting edge LLMs in a few clicks."
+              checked={llmChoice === "lmstudio"}
+              image={LMStudioLogo}
+              onClick={updateLLMChoice}
+            />
           </div>
           <div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
             {llmChoice === "openai" && <OpenAiOptions settings={settings} />}
@@ -101,6 +114,9 @@ function LLMSelection({ nextStep, prevStep, currentStep }) {
             {llmChoice === "anthropic" && (
               <AnthropicAiOptions settings={settings} />
             )}
+            {llmChoice === "lmstudio" && (
+              <LMStudioOptions settings={settings} />
+            )}
           </div>
         </div>
         <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
diff --git a/server/.env.example b/server/.env.example
index d7a9cbe76..327aa6eee 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 d15f73060..b28c5e865 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 000000000..bb025b3b1
--- /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 9df2e8f12..cf48937a3 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 976849d92..e97f97917 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 = "") {