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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
F0sufJY;ynr

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 = "") {
-- 
GitLab