From f3a6147ffd5cd773fb6af97cc417b965b1678f01 Mon Sep 17 00:00:00 2001
From: Timothy Carambat <rambat1010@gmail.com>
Date: Tue, 8 Aug 2023 18:02:30 -0700
Subject: [PATCH] Add support for Weaviate VectorDB (#181)

---
 .vscode/settings.json                         |   3 +-
 docker/.env.example                           |   5 +
 .../Modals/Settings/ExportImport/index.jsx    |   2 +-
 .../Modals/Settings/LLMSelection/index.jsx    |   4 +-
 .../Modals/Settings/MultiUserMode/index.jsx   |   2 +-
 .../Settings/PasswordProtection/index.jsx     |   2 +-
 .../Modals/Settings/VectorDbs/index.jsx       |  49 +-
 .../src/components/Modals/Settings/index.jsx  |   2 +-
 frontend/src/media/vectordbs/weaviate.png     | Bin 0 -> 32173 bytes
 server/.env.example                           |   6 +
 server/endpoints/system.js                    |   6 +
 server/package.json                           |   4 +-
 server/utils/helpers/camelcase.js             | 143 +++++
 server/utils/helpers/index.js                 |   3 +
 server/utils/helpers/updateENV.js             |  11 +-
 .../weaviate/WEAVIATE_SETUP.md                |  17 +
 .../utils/vectorDbProviders/weaviate/index.js | 503 ++++++++++++++++++
 server/yarn.lock                              |  43 ++
 18 files changed, 794 insertions(+), 11 deletions(-)
 create mode 100644 frontend/src/media/vectordbs/weaviate.png
 create mode 100644 server/utils/helpers/camelcase.js
 create mode 100644 server/utils/vectorDbProviders/weaviate/WEAVIATE_SETUP.md
 create mode 100644 server/utils/vectorDbProviders/weaviate/index.js

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 450dd7797..c8c7ea995 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,6 @@
 {
   "cSpell.words": [
-    "openai"
+    "openai",
+    "Weaviate"
   ]
 }
\ No newline at end of file
diff --git a/docker/.env.example b/docker/.env.example
index 6b9791eb5..77550b6f0 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -32,6 +32,11 @@ PINECONE_INDEX=
 # Enable all below if you are using vector database: LanceDB.
 # VECTOR_DB="lancedb"
 
+# Enable all below if you are using vector database: Weaviate.
+# VECTOR_DB="weaviate"
+# WEAVIATE_ENDPOINT="http://localhost:8080"
+# WEAVIATE_API_KEY=
+
 # CLOUD DEPLOYMENT VARIRABLES ONLY
 # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
 # NO_DEBUG="true"
diff --git a/frontend/src/components/Modals/Settings/ExportImport/index.jsx b/frontend/src/components/Modals/Settings/ExportImport/index.jsx
index 4099e8c06..e2245d538 100644
--- a/frontend/src/components/Modals/Settings/ExportImport/index.jsx
+++ b/frontend/src/components/Modals/Settings/ExportImport/index.jsx
@@ -7,7 +7,7 @@ import paths from "../../../../utils/paths";
 const noop = () => false;
 export default function ExportOrImportData({ hideModal = noop }) {
   return (
-    <div className="relative w-full max-w-2xl max-h-full">
+    <div className="relative w-full w-full max-h-full">
       <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
         <div className="flex flex-col items-start justify-between px-6 py-4">
           <p className="text-gray-800 dark:text-stone-200 text-base ">
diff --git a/frontend/src/components/Modals/Settings/LLMSelection/index.jsx b/frontend/src/components/Modals/Settings/LLMSelection/index.jsx
index 94b75ace8..2cf014352 100644
--- a/frontend/src/components/Modals/Settings/LLMSelection/index.jsx
+++ b/frontend/src/components/Modals/Settings/LLMSelection/index.jsx
@@ -37,7 +37,7 @@ export default function LLMSelection({
     setHasChanges(!!error ? true : false);
   };
   return (
-    <div className="relative w-full max-w-2xl max-h-full">
+    <div className="relative w-full w-full max-h-full">
       <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
         <div className="flex items-start justify-between px-6 py-4">
           <p className="text-gray-800 dark:text-stone-200 text-base ">
@@ -59,7 +59,7 @@ export default function LLMSelection({
               <p className="block text-sm font-medium text-gray-800 dark:text-slate-200">
                 LLM providers
               </p>
-              <div className="w-full flex overflow-x-scroll gap-x-4 no-scroll">
+              <div className="w-full flex overflow-x-scroll gap-x-4">
                 <input hidden={true} name="LLMProvider" value={llmChoice} />
                 <LLMProviderOption
                   name="OpenAI"
diff --git a/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx b/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx
index 6a8b96e9e..4f93d9759 100644
--- a/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx
+++ b/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx
@@ -39,7 +39,7 @@ export default function MultiUserMode({ hideModal = noop }) {
   };
 
   return (
-    <div className="relative w-full max-w-2xl max-h-full">
+    <div className="relative w-full w-full max-h-full">
       <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
         <div className="flex items-start justify-between px-6 py-4">
           <p className="text-gray-800 dark:text-stone-200 text-base ">
diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx
index 387c44bc6..5e6269121 100644
--- a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx
+++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx
@@ -41,7 +41,7 @@ export default function PasswordProtection({
   };
 
   return (
-    <div className="relative w-full max-w-2xl max-h-full">
+    <div className="relative w-full w-full max-h-full">
       <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
         <div className="flex items-start justify-between px-6 py-4">
           <p className="text-gray-800 dark:text-stone-200 text-base ">
diff --git a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
index c4ad0aec1..b1a5a97b5 100644
--- a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
+++ b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
@@ -3,6 +3,7 @@ import System from "../../../../models/system";
 import ChromaLogo from "../../../../media/vectordbs/chroma.png";
 import PineconeLogo from "../../../../media/vectordbs/pinecone.png";
 import LanceDbLogo from "../../../../media/vectordbs/lancedb.png";
+import WeaviateLogo from "../../../../media/vectordbs/weaviate.png";
 
 const noop = () => false;
 export default function VectorDBSelection({
@@ -37,7 +38,7 @@ export default function VectorDBSelection({
     setHasChanges(!!error ? true : false);
   };
   return (
-    <div className="relative w-full max-w-2xl max-h-full">
+    <div className="relative w-full w-full max-h-full">
       <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
         <div className="flex items-start justify-between px-6 py-4">
           <p className="text-gray-800 dark:text-stone-200 text-base ">
@@ -59,7 +60,7 @@ export default function VectorDBSelection({
               <p className="block text-sm font-medium text-gray-800 dark:text-slate-200">
                 Vector database providers
               </p>
-              <div className="w-full flex overflow-x-scroll gap-x-4 no-scroll">
+              <div className="w-full flex overflow-x-scroll gap-x-4">
                 <input hidden={true} name="VectorDB" value={vectorDB} />
                 <VectorDBOption
                   name="Chroma"
@@ -79,6 +80,15 @@ export default function VectorDBSelection({
                   image={PineconeLogo}
                   onClick={updateVectorChoice}
                 />
+                <VectorDBOption
+                  name="Weaviate"
+                  value="weaviate"
+                  link="weaviate.io"
+                  description="Open source local and cloud hosted multi-modal vector database."
+                  checked={vectorDB === "weaviate"}
+                  image={WeaviateLogo}
+                  onClick={updateVectorChoice}
+                />
                 <VectorDBOption
                   name="LanceDB"
                   value="lancedb"
@@ -171,6 +181,41 @@ export default function VectorDBSelection({
                   </p>
                 </div>
               )}
+              {vectorDB === "weaviate" && (
+                <>
+                  <div>
+                    <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200">
+                      Weaviate Endpoint
+                    </label>
+                    <input
+                      type="url"
+                      name="WeaviateEndpoint"
+                      disabled={!canDebug}
+                      className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200"
+                      placeholder="http://localhost:8080"
+                      defaultValue={settings?.WeaviateEndpoint}
+                      required={true}
+                      autoComplete="off"
+                      spellCheck={false}
+                    />
+                  </div>
+                  <div>
+                    <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200">
+                      Api Key
+                    </label>
+                    <input
+                      type="password"
+                      name="WeaviateApiKey"
+                      disabled={!canDebug}
+                      className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200"
+                      placeholder="sk-123Abcweaviate"
+                      defaultValue={settings?.WeaviateApiKey}
+                      autoComplete="off"
+                      spellCheck={false}
+                    />
+                  </div>
+                </>
+              )}
             </div>
           </div>
           <div className="w-full p-4">
diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx
index bdf8e6e56..f644c5e19 100644
--- a/frontend/src/components/Modals/Settings/index.jsx
+++ b/frontend/src/components/Modals/Settings/index.jsx
@@ -46,7 +46,7 @@ export default function SystemSettingsModal({ hideModal = noop }) {
         className="flex fixed top-0 left-0 right-0 w-full h-full"
         onClick={hideModal}
       />
-      <div className="relative w-full max-w-2xl max-h-full">
+      <div className="relative w-full w-full md:w-1/2 max-h-full">
         <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
           <div className="flex flex-col gap-y-1 border-b dark:border-gray-600 px-4 pt-4 ">
             <div className="flex items-start justify-between rounded-t ">
diff --git a/frontend/src/media/vectordbs/weaviate.png b/frontend/src/media/vectordbs/weaviate.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7980bf6a6ba14a9b8e1bbc02188d006f7290eea
GIT binary patch
literal 32173
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

literal 0
HcmV?d00001

diff --git a/server/.env.example b/server/.env.example
index e06c0f7e7..606dd8988 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -31,6 +31,12 @@ PINECONE_INDEX=
 # Enable all below if you are using vector database: LanceDB.
 # VECTOR_DB="lancedb"
 
+# Enable all below if you are using vector database: Weaviate.
+# VECTOR_DB="weaviate"
+# WEAVIATE_ENDPOINT="http://localhost:8080"
+# WEAVIATE_API_KEY=
+
+
 # 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
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 98354f4a9..01a367c7e 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -60,6 +60,12 @@ function systemEndpoints(app) {
               ChromaEndpoint: process.env.CHROMA_ENDPOINT,
             }
           : {}),
+        ...(vectorDB === "weaviate"
+          ? {
+              WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT,
+              WeaviateApiKey: process.env.WEAVIATE_API_KEY,
+            }
+          : {}),
         LLMProvider: llmProvider,
         ...(llmProvider === "openai"
           ? {
diff --git a/server/package.json b/server/package.json
index 3d8ec2b80..e35eb69da 100644
--- a/server/package.json
+++ b/server/package.json
@@ -26,6 +26,7 @@
     "dotenv": "^16.0.3",
     "express": "^4.18.2",
     "extract-zip": "^2.0.1",
+    "graphql": "^16.7.1",
     "jsonwebtoken": "^8.5.1",
     "langchain": "^0.0.90",
     "moment": "^2.29.4",
@@ -38,7 +39,8 @@
     "sqlite3": "^5.1.6",
     "uuid": "^9.0.0",
     "uuid-apikey": "^1.5.3",
-    "vectordb": "0.1.12"
+    "vectordb": "0.1.12",
+    "weaviate-ts-client": "^1.4.0"
   },
   "devDependencies": {
     "nodemon": "^2.0.22",
diff --git a/server/utils/helpers/camelcase.js b/server/utils/helpers/camelcase.js
new file mode 100644
index 000000000..4a8e1b28d
--- /dev/null
+++ b/server/utils/helpers/camelcase.js
@@ -0,0 +1,143 @@
+const UPPERCASE = /[\p{Lu}]/u;
+const LOWERCASE = /[\p{Ll}]/u;
+const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
+const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
+const SEPARATORS = /[_.\- ]+/;
+
+const LEADING_SEPARATORS = new RegExp("^" + SEPARATORS.source);
+const SEPARATORS_AND_IDENTIFIER = new RegExp(
+  SEPARATORS.source + IDENTIFIER.source,
+  "gu"
+);
+const NUMBERS_AND_IDENTIFIER = new RegExp("\\d+" + IDENTIFIER.source, "gu");
+
+const preserveCamelCase = (
+  string,
+  toLowerCase,
+  toUpperCase,
+  preserveConsecutiveUppercase
+) => {
+  let isLastCharLower = false;
+  let isLastCharUpper = false;
+  let isLastLastCharUpper = false;
+  let isLastLastCharPreserved = false;
+
+  for (let index = 0; index < string.length; index++) {
+    const character = string[index];
+    isLastLastCharPreserved = index > 2 ? string[index - 3] === "-" : true;
+
+    if (isLastCharLower && UPPERCASE.test(character)) {
+      string = string.slice(0, index) + "-" + string.slice(index);
+      isLastCharLower = false;
+      isLastLastCharUpper = isLastCharUpper;
+      isLastCharUpper = true;
+      index++;
+    } else if (
+      isLastCharUpper &&
+      isLastLastCharUpper &&
+      LOWERCASE.test(character) &&
+      (!isLastLastCharPreserved || preserveConsecutiveUppercase)
+    ) {
+      string = string.slice(0, index - 1) + "-" + string.slice(index - 1);
+      isLastLastCharUpper = isLastCharUpper;
+      isLastCharUpper = false;
+      isLastCharLower = true;
+    } else {
+      isLastCharLower =
+        toLowerCase(character) === character &&
+        toUpperCase(character) !== character;
+      isLastLastCharUpper = isLastCharUpper;
+      isLastCharUpper =
+        toUpperCase(character) === character &&
+        toLowerCase(character) !== character;
+    }
+  }
+
+  return string;
+};
+
+const preserveConsecutiveUppercase = (input, toLowerCase) => {
+  LEADING_CAPITAL.lastIndex = 0;
+
+  return input.replace(LEADING_CAPITAL, (m1) => toLowerCase(m1));
+};
+
+const postProcess = (input, toUpperCase) => {
+  SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
+  NUMBERS_AND_IDENTIFIER.lastIndex = 0;
+
+  return input
+    .replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) =>
+      toUpperCase(identifier)
+    )
+    .replace(NUMBERS_AND_IDENTIFIER, (m) => toUpperCase(m));
+};
+
+function camelCase(input, options) {
+  if (!(typeof input === "string" || Array.isArray(input))) {
+    throw new TypeError("Expected the input to be `string | string[]`");
+  }
+
+  options = {
+    pascalCase: true,
+    preserveConsecutiveUppercase: false,
+    ...options,
+  };
+
+  if (Array.isArray(input)) {
+    input = input
+      .map((x) => x.trim())
+      .filter((x) => x.length)
+      .join("-");
+  } else {
+    input = input.trim();
+  }
+
+  if (input.length === 0) {
+    return "";
+  }
+
+  const toLowerCase =
+    options.locale === false
+      ? (string) => string.toLowerCase()
+      : (string) => string.toLocaleLowerCase(options.locale);
+
+  const toUpperCase =
+    options.locale === false
+      ? (string) => string.toUpperCase()
+      : (string) => string.toLocaleUpperCase(options.locale);
+
+  if (input.length === 1) {
+    if (SEPARATORS.test(input)) {
+      return "";
+    }
+
+    return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
+  }
+
+  const hasUpperCase = input !== toLowerCase(input);
+
+  if (hasUpperCase) {
+    input = preserveCamelCase(
+      input,
+      toLowerCase,
+      toUpperCase,
+      options.preserveConsecutiveUppercase
+    );
+  }
+
+  input = input.replace(LEADING_SEPARATORS, "");
+  input = options.preserveConsecutiveUppercase
+    ? preserveConsecutiveUppercase(input, toLowerCase)
+    : toLowerCase(input);
+
+  if (options.pascalCase) {
+    input = toUpperCase(input.charAt(0)) + input.slice(1);
+  }
+
+  return postProcess(input, toUpperCase);
+}
+
+module.exports = {
+  camelCase,
+};
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index 5be565074..b7fb5ae00 100644
--- a/server/utils/helpers/index.js
+++ b/server/utils/helpers/index.js
@@ -10,6 +10,9 @@ function getVectorDbClass() {
     case "lancedb":
       const { LanceDb } = require("../vectorDbProviders/lance");
       return LanceDb;
+    case "weaviate":
+      const { Weaviate } = require("../vectorDbProviders/weaviate");
+      return Weaviate;
     default:
       throw new Error("ENV: No VECTOR_DB value found in environment!");
   }
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 64c919884..9f00ec423 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -39,6 +39,15 @@ const KEY_MAPPING = {
     envKey: "CHROMA_ENDPOINT",
     checks: [isValidURL, validChromaURL],
   },
+  WeaviateEndpoint: {
+    envKey: "WEAVIATE_ENDPOINT",
+    checks: [isValidURL],
+  },
+  WeaviateApiKey: {
+    envKey: "WEAVIATE_API_KEY",
+    checks: [],
+  },
+
   PineConeEnvironment: {
     envKey: "PINECONE_ENVIRONMENT",
     checks: [],
@@ -103,7 +112,7 @@ function validOpenAIModel(input = "") {
 }
 
 function supportedVectorDB(input = "") {
-  const supported = ["chroma", "pinecone", "lancedb"];
+  const supported = ["chroma", "pinecone", "lancedb", "weaviate"];
   return supported.includes(input)
     ? null
     : `Invalid VectorDB type. Must be one of ${supported.join(", ")}.`;
diff --git a/server/utils/vectorDbProviders/weaviate/WEAVIATE_SETUP.md b/server/utils/vectorDbProviders/weaviate/WEAVIATE_SETUP.md
new file mode 100644
index 000000000..fc0acaecb
--- /dev/null
+++ b/server/utils/vectorDbProviders/weaviate/WEAVIATE_SETUP.md
@@ -0,0 +1,17 @@
+# How to setup a local (or cloud) Weaviate Vector Database
+
+[Get a Weaviate Cloud instance](https://weaviate.io/developers/weaviate/quickstart#create-an-instance).
+[Set up Weaviate locally on Docker](https://weaviate.io/developers/weaviate/installation/docker-compose).
+
+Fill out the variables in the "Vector Database" tab of settings. Select Weaviate as your provider and fill out the appropriate fields
+with the information from either of the above steps.
+
+### How to get started _Development mode only_
+
+After setting up either the Weaviate cloud or local dockerized instance you just need to set these variable in `.env.development` or defined them at runtime via the UI.
+
+```
+VECTOR_DB="weaviate"
+WEAVIATE_ENDPOINT='http://localhost:8080'
+WEAVIATE_API_KEY= # Optional
+```
diff --git a/server/utils/vectorDbProviders/weaviate/index.js b/server/utils/vectorDbProviders/weaviate/index.js
new file mode 100644
index 000000000..884c08e01
--- /dev/null
+++ b/server/utils/vectorDbProviders/weaviate/index.js
@@ -0,0 +1,503 @@
+const { default: weaviate } = require("weaviate-ts-client");
+const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
+const { storeVectorResult, cachedVectorInformation } = require("../../files");
+const { v4: uuidv4 } = require("uuid");
+const { toChunks, getLLMProvider } = require("../../helpers");
+const { chatPrompt } = require("../../chats");
+const { camelCase } = require("../../helpers/camelcase");
+
+const Weaviate = {
+  name: "Weaviate",
+  connect: async function () {
+    if (process.env.VECTOR_DB !== "weaviate")
+      throw new Error("Weaviate::Invalid ENV settings");
+
+    const weaviateUrl = new URL(process.env.WEAVIATE_ENDPOINT);
+    const options = {
+      scheme: weaviateUrl.protocol?.replace(":", "") || "http",
+      host: weaviateUrl?.host,
+      ...(process.env?.WEAVIATE_API_KEY?.length > 0
+        ? { apiKey: new weaviate.ApiKey(process.env?.WEAVIATE_API_KEY) }
+        : {}),
+    };
+    const client = weaviate.client(options);
+    const isAlive = await await client.misc.liveChecker().do();
+    if (!isAlive)
+      throw new Error(
+        "Weaviate::Invalid Alive signal received - is the service online?"
+      );
+    return { client };
+  },
+  heartbeat: async function () {
+    await this.connect();
+    return { heartbeat: Number(new Date()) };
+  },
+  totalIndicies: async function () {
+    const { client } = await this.connect();
+    const collectionNames = await this.allNamespaces(client);
+    var totalVectors = 0;
+    for (const name of collectionNames) {
+      totalVectors += await this.namespaceCountWithClient(client, name);
+    }
+    return totalVectors;
+  },
+  namespaceCountWithClient: async function (client, namespace) {
+    try {
+      const response = await client.graphql
+        .aggregate()
+        .withClassName(camelCase(namespace))
+        .withFields("meta { count }")
+        .do();
+      return (
+        response?.data?.Aggregate?.[camelCase(namespace)]?.[0]?.meta?.count || 0
+      );
+    } catch (e) {
+      console.error(`Weaviate:namespaceCountWithClient`, e.message);
+      return 0;
+    }
+  },
+  namespaceCount: async function (namespace = null) {
+    try {
+      const { client } = await this.connect();
+      const response = await client.graphql
+        .aggregate()
+        .withClassName(camelCase(namespace))
+        .withFields("meta { count }")
+        .do();
+
+      return (
+        response?.data?.Aggregate?.[camelCase(namespace)]?.[0]?.meta?.count || 0
+      );
+    } catch (e) {
+      console.error(`Weaviate:namespaceCountWithClient`, e.message);
+      return 0;
+    }
+  },
+  similarityResponse: async function (client, namespace, queryVector) {
+    const result = {
+      contextTexts: [],
+      sourceDocuments: [],
+    };
+
+    const weaviateClass = await this.namespace(client, namespace);
+    const fields = weaviateClass.properties.map((prop) => prop.name).join(" ");
+    const queryResponse = await client.graphql
+      .get()
+      .withClassName(camelCase(namespace))
+      .withFields(`${fields} _additional { id }`)
+      .withNearVector({ vector: queryVector })
+      .withLimit(4)
+      .do();
+
+    const responses = queryResponse?.data?.Get?.[camelCase(namespace)];
+    responses.forEach((response) => {
+      // In Weaviate we have to pluck id from _additional and spread it into the rest
+      // of the properties.
+      const {
+        _additional: { id },
+        ...rest
+      } = response;
+      result.contextTexts.push(rest.text);
+      result.sourceDocuments.push({ ...rest, id });
+    });
+
+    return result;
+  },
+  allNamespaces: async function (client) {
+    try {
+      const { classes = [] } = await client.schema.getter().do();
+      return classes.map((classObj) => classObj.class);
+    } catch (e) {
+      console.error("Weaviate::AllNamespace", e);
+      return [];
+    }
+  },
+  namespace: async function (client, namespace = null) {
+    if (!namespace) throw new Error("No namespace value provided.");
+    if (!(await this.namespaceExists(client, namespace))) return null;
+
+    const weaviateClass = await client.schema
+      .classGetter()
+      .withClassName(camelCase(namespace))
+      .do();
+
+    return {
+      ...weaviateClass,
+      vectorCount: await this.namespaceCount(namespace),
+    };
+  },
+  addVectors: async function (client, vectors = []) {
+    const response = { success: true, errors: new Set([]) };
+    const results = await client.batch
+      .objectsBatcher()
+      .withObjects(...vectors)
+      .do();
+
+    results.forEach((res) => {
+      const { status, errors = [] } = res.result;
+      if (status === "SUCCESS" || errors.length === 0) return;
+      response.success = false;
+      response.errors.add(errors.error?.[0]?.message || null);
+    });
+
+    response.errors = [...response.errors];
+    return response;
+  },
+  hasNamespace: async function (namespace = null) {
+    if (!namespace) return false;
+    const { client } = await this.connect();
+    const weaviateClasses = await this.allNamespaces(client);
+    return weaviateClasses.includes(camelCase(namespace));
+  },
+  namespaceExists: async function (client, namespace = null) {
+    if (!namespace) throw new Error("No namespace value provided.");
+    const weaviateClasses = await this.allNamespaces(client);
+    return weaviateClasses.includes(camelCase(namespace));
+  },
+  deleteVectorsInNamespace: async function (client, namespace = null) {
+    await client.schema.classDeleter().withClassName(camelCase(namespace)).do();
+    return true;
+  },
+  addDocumentToNamespace: async function (
+    namespace,
+    documentData = {},
+    fullFilePath = null
+  ) {
+    const { DocumentVectors } = require("../../../models/vectors");
+    try {
+      const {
+        pageContent,
+        docId,
+        id: _id, // Weaviate will abort if `id` is present in properties
+        ...metadata
+      } = documentData;
+      if (!pageContent || pageContent.length == 0) return false;
+
+      console.log("Adding new vectorized document into namespace", namespace);
+      const cacheResult = await cachedVectorInformation(fullFilePath);
+      if (cacheResult.exists) {
+        const { client } = await this.connect();
+        const weaviateClassExits = await this.hasNamespace(namespace);
+        if (!weaviateClassExits) {
+          await client.schema
+            .classCreator()
+            .withClass({
+              class: camelCase(namespace),
+              description: `Class created by AnythingLLM named ${camelCase(
+                namespace
+              )}`,
+              vectorizer: "none",
+            })
+            .do();
+        }
+
+        const { chunks } = cacheResult;
+        const documentVectors = [];
+        const vectors = [];
+
+        for (const chunk of chunks) {
+          // Before sending to Weaviate and saving the records to our db
+          // we need to assign the id of each chunk that is stored in the cached file.
+          chunk.forEach((chunk) => {
+            const id = uuidv4();
+            const flattenedMetadata = this.flattenObjectForWeaviate(
+              chunk.properties
+            );
+            documentVectors.push({ docId, vectorId: id });
+            const vectorRecord = {
+              id,
+              class: camelCase(namespace),
+              vector: chunk.vector || chunk.values || [],
+              properties: { ...flattenedMetadata },
+            };
+            vectors.push(vectorRecord);
+          });
+
+          const { success: additionResult, errors = [] } =
+            await this.addVectors(client, vectors);
+          if (!additionResult) {
+            console.error("Weaviate::addVectors failed to insert", errors);
+            throw new Error("Error embedding into Weaviate");
+          }
+        }
+
+        await DocumentVectors.bulkInsert(documentVectors);
+        return true;
+      }
+
+      // If we are here then we are going to embed and store a novel document.
+      // We have to do this manually as opposed to using LangChains `Chroma.fromDocuments`
+      // because we then cannot atomically control our namespace to granularly find/remove documents
+      // from vectordb.
+      const textSplitter = new RecursiveCharacterTextSplitter({
+        chunkSize: 1000,
+        chunkOverlap: 20,
+      });
+      const textChunks = await textSplitter.splitText(pageContent);
+
+      console.log("Chunks created from document:", textChunks.length);
+      const LLMConnector = getLLMProvider();
+      const documentVectors = [];
+      const vectors = [];
+      const vectorValues = await LLMConnector.embedChunks(textChunks);
+      const submission = {
+        ids: [],
+        vectors: [],
+        properties: [],
+      };
+
+      if (!!vectorValues && vectorValues.length > 0) {
+        for (const [i, vector] of vectorValues.entries()) {
+          const flattenedMetadata = this.flattenObjectForWeaviate(metadata);
+          const vectorRecord = {
+            class: camelCase(namespace),
+            id: uuidv4(),
+            vector: vector,
+            // [DO NOT REMOVE]
+            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.
+            // https://github.com/hwchase17/langchainjs/blob/5485c4af50c063e257ad54f4393fa79e0aff6462/langchain/src/vectorstores/weaviate.ts#L133
+            properties: { ...flattenedMetadata, text: textChunks[i] },
+          };
+
+          submission.ids.push(vectorRecord.id);
+          submission.vectors.push(vectorRecord.values);
+          submission.properties.push(metadata);
+
+          vectors.push(vectorRecord);
+          documentVectors.push({ docId, vectorId: vectorRecord.id });
+        }
+      } else {
+        console.error(
+          "Could not use OpenAI to embed document chunks! This document will not be recorded."
+        );
+      }
+
+      const { client } = await this.connect();
+      const weaviateClassExits = await this.hasNamespace(namespace);
+      if (!weaviateClassExits) {
+        await client.schema
+          .classCreator()
+          .withClass({
+            class: camelCase(namespace),
+            description: `Class created by AnythingLLM named ${camelCase(
+              namespace
+            )}`,
+            vectorizer: "none",
+          })
+          .do();
+      }
+
+      if (vectors.length > 0) {
+        const chunks = [];
+        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);
+
+        console.log("Inserting vectorized chunks into Weaviate collection.");
+        const { success: additionResult, errors = [] } = await this.addVectors(
+          client,
+          vectors
+        );
+        if (!additionResult) {
+          console.error("Weaviate::addVectors failed to insert", errors);
+          throw new Error("Error embedding into Weaviate");
+        }
+        await storeVectorResult(chunks, fullFilePath);
+      }
+
+      await DocumentVectors.bulkInsert(documentVectors);
+      return true;
+    } catch (e) {
+      console.error(e);
+      console.error("addDocumentToNamespace", e.message);
+      return false;
+    }
+  },
+  deleteDocumentFromNamespace: async function (namespace, docId) {
+    const { DocumentVectors } = require("../../../models/vectors");
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) return;
+
+    const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
+    if (knownDocuments.length === 0) return;
+
+    for (const doc of knownDocuments) {
+      await client.data
+        .deleter()
+        .withClassName(camelCase(namespace))
+        .withId(doc.vectorId)
+        .do();
+    }
+
+    const indexes = knownDocuments.map((doc) => doc.id);
+    await DocumentVectors.deleteIds(indexes);
+    return true;
+  },
+  query: async function (reqBody = {}) {
+    const { namespace = null, input, workspace = {} } = reqBody;
+    if (!namespace || !input) throw new Error("Invalid request body");
+
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) {
+      return {
+        response: null,
+        sources: [],
+        message: "Invalid query - no documents found for workspace!",
+      };
+    }
+
+    const LLMConnector = getLLMProvider();
+    const queryVector = await LLMConnector.embedTextInput(input);
+    const { contextTexts, sourceDocuments } = await this.similarityResponse(
+      client,
+      namespace,
+      queryVector
+    );
+
+    const prompt = {
+      role: "system",
+      content: `${chatPrompt(workspace)}
+    Context:
+    ${contextTexts
+          .map((text, i) => {
+            return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
+          })
+          .join("")}`,
+    };
+    const memory = [prompt, { role: "user", content: input }];
+    const responseText = await LLMConnector.getChatCompletion(memory, {
+      temperature: workspace?.openAiTemp ?? 0.7,
+    });
+
+    return {
+      response: responseText,
+      sources: this.curateSources(sourceDocuments),
+      message: false,
+    };
+  },
+  // This implementation of chat uses the chat history and modifies the system prompt at execution
+  // this is improved over the regular langchain implementation so that chats do not directly modify embeddings
+  // because then multi-user support will have all conversations mutating the base vector collection to which then
+  // the only solution is replicating entire vector databases per user - which will very quickly consume space on VectorDbs
+  chat: async function (reqBody = {}) {
+    const {
+      namespace = null,
+      input,
+      workspace = {},
+      chatHistory = [],
+    } = reqBody;
+    if (!namespace || !input) throw new Error("Invalid request body");
+
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) {
+      return {
+        response: null,
+        sources: [],
+        message: "Invalid query - no documents found for workspace!",
+      };
+    }
+
+    const LLMConnector = getLLMProvider();
+    const queryVector = await LLMConnector.embedTextInput(input);
+    const { contextTexts, sourceDocuments } = await this.similarityResponse(
+      client,
+      namespace,
+      queryVector
+    );
+    const prompt = {
+      role: "system",
+      content: `${chatPrompt(workspace)}
+    Context:
+    ${contextTexts
+          .map((text, i) => {
+            return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
+          })
+          .join("")}`,
+    };
+    const memory = [prompt, ...chatHistory, { role: "user", content: input }];
+    const responseText = await LLMConnector.getChatCompletion(memory, {
+      temperature: workspace?.openAiTemp ?? 0.7,
+    });
+
+    return {
+      response: responseText,
+      sources: this.curateSources(sourceDocuments),
+      message: false,
+    };
+  },
+  "namespace-stats": async function (reqBody = {}) {
+    const { namespace = null } = reqBody;
+    if (!namespace) throw new Error("namespace required");
+    const { client } = await this.connect();
+    const stats = await this.namespace(client, namespace);
+    return stats
+      ? stats
+      : { message: "No stats were able to be fetched from DB for namespace" };
+  },
+  "delete-namespace": async function (reqBody = {}) {
+    const { namespace = null } = reqBody;
+    const { client } = await this.connect();
+    const details = await this.namespace(client, namespace);
+    await this.deleteVectorsInNamespace(client, namespace);
+    return {
+      message: `Namespace ${camelCase(namespace)} was deleted along with ${details?.vectorCount
+        } vectors.`,
+    };
+  },
+  reset: async function () {
+    const { client } = await this.connect();
+    const weaviateClasses = await this.allNamespaces(client);
+    for (const weaviateClass of weaviateClasses) {
+      await client.schema.classDeleter().withClassName(weaviateClass).do();
+    }
+    return { reset: true };
+  },
+  curateSources: function (sources = []) {
+    const documents = [];
+    for (const source of sources) {
+      if (Object.keys(source).length > 0) {
+        documents.push(source);
+      }
+    }
+
+    return documents;
+  },
+  flattenObjectForWeaviate: function (obj = {}) {
+    // Note this function is not generic, it is designed specifically for Weaviate
+    // https://weaviate.io/developers/weaviate/config-refs/datatypes#introduction
+    // Credit to LangchainJS
+    // https://github.com/hwchase17/langchainjs/blob/5485c4af50c063e257ad54f4393fa79e0aff6462/langchain/src/vectorstores/weaviate.ts#L11C1-L50C3
+    const flattenedObject = {};
+
+    for (const key in obj) {
+      if (!Object.hasOwn(obj, key)) {
+        continue;
+      }
+      const value = obj[key];
+      if (typeof obj[key] === "object" && !Array.isArray(value)) {
+        const recursiveResult = this.flattenObjectForWeaviate(value);
+
+        for (const deepKey in recursiveResult) {
+          if (Object.hasOwn(obj, key)) {
+            flattenedObject[`${key}_${deepKey}`] = recursiveResult[deepKey];
+          }
+        }
+      } else if (Array.isArray(value)) {
+        if (
+          value.length > 0 &&
+          typeof value[0] !== "object" &&
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          value.every((el) => typeof el === typeof value[0])
+        ) {
+          // Weaviate only supports arrays of primitive types,
+          // where all elements are of the same type
+          flattenedObject[key] = value;
+        }
+      } else {
+        flattenedObject[key] = value;
+      }
+    }
+
+    return flattenedObject;
+  },
+};
+
+module.exports.Weaviate = Weaviate;
diff --git a/server/yarn.lock b/server/yarn.lock
index cd1514e7a..3b1caaa86 100644
--- a/server/yarn.lock
+++ b/server/yarn.lock
@@ -130,6 +130,11 @@
   dependencies:
     googleapis-common "^6.0.3"
 
+"@graphql-typed-document-node/core@^3.1.1":
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
+  integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
+
 "@mapbox/node-pre-gyp@^1.0.0", "@mapbox/node-pre-gyp@^1.0.10":
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
@@ -916,6 +921,11 @@ extend@^3.0.2:
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
   integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
 
+extract-files@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
+  integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
+
 extract-zip@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
@@ -981,6 +991,15 @@ follow-redirects@^1.14.8:
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
   integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
 
+form-data@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
+  integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 form-data@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@@ -1149,6 +1168,21 @@ graceful-fs@^4.2.0, graceful-fs@^4.2.6:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
   integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
 
+graphql-request@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.2.0.tgz#a05fb54a517d91bb2d7aefa17ade4523dc5ebdca"
+  integrity sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==
+  dependencies:
+    "@graphql-typed-document-node/core" "^3.1.1"
+    cross-fetch "^3.1.5"
+    extract-files "^9.0.0"
+    form-data "^3.0.0"
+
+graphql@^16.7.1:
+  version "16.7.1"
+  resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
+  integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
+
 gtoken@^6.1.0:
   version "6.1.2"
   resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc"
@@ -2507,6 +2541,15 @@ vectordb@0.1.12:
     "@apache-arrow/ts" "^12.0.0"
     apache-arrow "^12.0.0"
 
+weaviate-ts-client@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/weaviate-ts-client/-/weaviate-ts-client-1.4.0.tgz#e1adb670f2c1930a82601efb915b0131f6988b7e"
+  integrity sha512-G2V/IWMHXDjoJeATUYKkZXzAs7iRj4GE8B3AX59XDqMRW12X7VUkRgo4xWcHH1bjpLIHUYTzD5qZXcB8P9Hdmw==
+  dependencies:
+    graphql-request "^5.1.0"
+    isomorphic-fetch "^3.0.0"
+    uuid "^9.0.0"
+
 webidl-conversions@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
-- 
GitLab