From 5bf4b4db58040e367c3ce5a76d4f482aa7dd1a11 Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Sun, 19 May 2024 11:20:23 -0700
Subject: [PATCH] [FEAT] Add support for Voyage AI embedder (#1401)

* add support for voyageai embedder

* remove unneeded import

* linting

* Add ENV examples
Update how chunks are processed for Voyage
use correct langchain import
Add data handling

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
---
 docker/.env.example                           |   4 ++
 .../VoyageAiOptions/index.jsx                 |  50 ++++++++++++++++++
 .../src/media/embeddingprovider/voyageai.png  | Bin 0 -> 20060 bytes
 .../EmbeddingPreference/index.jsx             |  10 ++++
 .../Steps/DataHandling/index.jsx              |   9 ++++
 server/.env.example                           |   4 ++
 server/endpoints/api/workspace/index.js       |   9 ++--
 server/models/systemSettings.js               |   3 ++
 server/swagger/openapi.json                   |  22 ++++----
 .../utils/EmbeddingEngines/voyageAi/index.js  |  45 ++++++++++++++++
 server/utils/helpers/index.js                 |   3 ++
 server/utils/helpers/updateENV.js             |   7 +++
 12 files changed, 150 insertions(+), 16 deletions(-)
 create mode 100644 frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx
 create mode 100644 frontend/src/media/embeddingprovider/voyageai.png
 create mode 100644 server/utils/EmbeddingEngines/voyageAi/index.js

diff --git a/docker/.env.example b/docker/.env.example
index 7fedf944c..23789af45 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -124,6 +124,10 @@ GID='1000'
 # COHERE_API_KEY=
 # EMBEDDING_MODEL_PREF='embed-english-v3.0'
 
+# EMBEDDING_ENGINE='voyageai'
+# VOYAGEAI_API_KEY=
+# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
+
 ###########################################
 ######## Vector Database Selection ########
 ###########################################
diff --git a/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx
new file mode 100644
index 000000000..33ce693db
--- /dev/null
+++ b/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx
@@ -0,0 +1,50 @@
+export default function VoyageAiOptions({ settings }) {
+  return (
+    <div className="w-full flex flex-col gap-y-4">
+      <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">
+            API Key
+          </label>
+          <input
+            type="password"
+            name="VoyageAiApiKey"
+            className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
+            placeholder="Voyage AI API Key"
+            defaultValue={settings?.VoyageAiApiKey ? "*".repeat(20) : ""}
+            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">
+            Model Preference
+          </label>
+          <select
+            name="EmbeddingModelPref"
+            required={true}
+            defaultValue={settings?.EmbeddingModelPref}
+            className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+          >
+            <optgroup label="Available embedding models">
+              {[
+                "voyage-large-2-instruct",
+                "voyage-law-2",
+                "voyage-code-2",
+                "voyage-large-2",
+                "voyage-2",
+              ].map((model) => {
+                return (
+                  <option key={model} value={model}>
+                    {model}
+                  </option>
+                );
+              })}
+            </optgroup>
+          </select>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/frontend/src/media/embeddingprovider/voyageai.png b/frontend/src/media/embeddingprovider/voyageai.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fd57eaac410eebba278abadf2a2ed882de7959c
GIT binary patch
literal 20060
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
G^gjUK3_@f8

literal 0
HcmV?d00001

diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
index 8f234b5ac..5a0f51c1d 100644
--- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
@@ -10,6 +10,8 @@ import LocalAiLogo from "@/media/llmprovider/localai.png";
 import OllamaLogo from "@/media/llmprovider/ollama.png";
 import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
 import CohereLogo from "@/media/llmprovider/cohere.png";
+import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
+
 import PreLoader from "@/components/Preloader";
 import ChangeWarningModal from "@/components/ChangeWarning";
 import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
@@ -19,6 +21,7 @@ import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbedd
 import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
 import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions";
 import CohereEmbeddingOptions from "@/components/EmbeddingSelection/CohereOptions";
+import VoyageAiOptions from "@/components/EmbeddingSelection/VoyageAiOptions";
 
 import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
 import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
@@ -78,6 +81,13 @@ const EMBEDDERS = [
     options: (settings) => <CohereEmbeddingOptions settings={settings} />,
     description: "Run powerful embedding models from Cohere.",
   },
+  {
+    name: "Voyage AI",
+    value: "voyageai",
+    logo: VoyageAiLogo,
+    options: (settings) => <VoyageAiOptions settings={settings} />,
+    description: "Run powerful embedding models from Voyage AI.",
+  },
 ];
 
 export default function GeneralEmbeddingPreference() {
diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
index b6ae8cb20..35358636d 100644
--- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
@@ -28,6 +28,8 @@ import LanceDbLogo from "@/media/vectordbs/lancedb.png";
 import WeaviateLogo from "@/media/vectordbs/weaviate.png";
 import QDrantLogo from "@/media/vectordbs/qdrant.png";
 import MilvusLogo from "@/media/vectordbs/milvus.png";
+import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
+
 import React, { useState, useEffect } from "react";
 import paths from "@/utils/paths";
 import { useNavigate } from "react-router-dom";
@@ -292,6 +294,13 @@ export const EMBEDDING_ENGINE_PRIVACY = {
     ],
     logo: CohereLogo,
   },
+  voyageai: {
+    name: "Voyage AI",
+    description: [
+      "Data sent to Voyage AI's servers is shared according to the terms of service of voyageai.com.",
+    ],
+    logo: VoyageAiLogo,
+  },
 };
 
 export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
diff --git a/server/.env.example b/server/.env.example
index 4be9ab75e..e38250beb 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -121,6 +121,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
 # COHERE_API_KEY=
 # EMBEDDING_MODEL_PREF='embed-english-v3.0'
 
+# EMBEDDING_ENGINE='voyageai'
+# VOYAGEAI_API_KEY=
+# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
+
 ###########################################
 ######## Vector Database Selection ########
 ###########################################
diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js
index 7cd2dd470..cbbf1f236 100644
--- a/server/endpoints/api/workspace/index.js
+++ b/server/endpoints/api/workspace/index.js
@@ -498,15 +498,18 @@ function apiWorkspaceEndpoints(app) {
         const { slug = null } = request.params;
         const { docPath, pinStatus = false } = reqBody(request);
         const workspace = await Workspace.get({ slug });
-  
+
         const document = await Document.get({
           workspaceId: workspace.id,
           docpath: docPath,
         });
         if (!document) return response.sendStatus(404).end();
-  
+
         await Document.update(document.id, { pinned: pinStatus });
-        return response.status(200).json({ message: 'Pin status updated successfully' }).end();
+        return response
+          .status(200)
+          .json({ message: "Pin status updated successfully" })
+          .end();
       } catch (error) {
         console.error("Error processing the pin status update:", error);
         return response.status(500).end();
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index c8e239f15..a5bb6a23c 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -426,6 +426,9 @@ const SystemSettings = {
       // Cohere API Keys
       CohereApiKey: !!process.env.COHERE_API_KEY,
       CohereModelPref: process.env.COHERE_MODEL_PREF,
+
+      // VoyageAi API Keys
+      VoyageAiApiKey: !!process.env.VOYAGEAI_API_KEY,
     };
   },
 
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index b98891c9d..8616943c2 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -1999,7 +1999,8 @@
           }
         }
       }
-    },"/v1/workspace/{slug}/update-pin": {
+    },
+    "/workspace/{slug}/update-pin": {
       "post": {
         "tags": [
           "Workspaces"
@@ -2037,6 +2038,9 @@
               }
             }
           },
+          "403": {
+            "description": "Forbidden"
+          },
           "404": {
             "description": "Document not found"
           },
@@ -2047,20 +2051,12 @@
         "requestBody": {
           "description": "JSON object with the document path and pin status to update.",
           "required": true,
+          "type": "object",
           "content": {
             "application/json": {
-              "schema": {
-                "type": "object",
-                "properties": {
-                  "docPath": {
-                    "type": "string",
-                    "example": "custom-documents/my-pdf.pdf-hash.json"
-                  },
-                  "pinStatus": {
-                    "type": "boolean",
-                    "example": true
-                  }
-                }
+              "example": {
+                "docPath": "custom-documents/my-pdf.pdf-hash.json",
+                "pinStatus": true
               }
             }
           }
diff --git a/server/utils/EmbeddingEngines/voyageAi/index.js b/server/utils/EmbeddingEngines/voyageAi/index.js
new file mode 100644
index 000000000..b25d3208d
--- /dev/null
+++ b/server/utils/EmbeddingEngines/voyageAi/index.js
@@ -0,0 +1,45 @@
+class VoyageAiEmbedder {
+  constructor() {
+    if (!process.env.VOYAGEAI_API_KEY)
+      throw new Error("No Voyage AI API key was set.");
+
+    const {
+      VoyageEmbeddings,
+    } = require("@langchain/community/embeddings/voyage");
+    const voyage = new VoyageEmbeddings({
+      apiKey: process.env.VOYAGEAI_API_KEY,
+    });
+
+    this.voyage = voyage;
+    this.model = process.env.EMBEDDING_MODEL_PREF || "voyage-large-2-instruct";
+
+    // Limit of how many strings we can process in a single pass to stay with resource or network limits
+    this.batchSize = 128; // Voyage AI's limit per request is 128 https://docs.voyageai.com/docs/rate-limits#use-larger-batches
+    this.embeddingMaxChunkLength = 4000; // https://docs.voyageai.com/docs/embeddings - assume a token is roughly 4 letters with some padding
+  }
+
+  async embedTextInput(textInput) {
+    const result = await this.voyage.embedDocuments(
+      Array.isArray(textInput) ? textInput : [textInput],
+      { modelName: this.model }
+    );
+    return result || [];
+  }
+
+  async embedChunks(textChunks = []) {
+    try {
+      const embeddings = await this.voyage.embedDocuments(textChunks, {
+        modelName: this.model,
+        batchSize: this.batchSize,
+      });
+      return embeddings;
+    } catch (error) {
+      console.error("Voyage AI Failed to embed:", error);
+      throw error;
+    }
+  }
+}
+
+module.exports = {
+  VoyageAiEmbedder,
+};
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index d9a1ba090..e60202a60 100644
--- a/server/utils/helpers/index.js
+++ b/server/utils/helpers/index.js
@@ -125,6 +125,9 @@ function getEmbeddingEngineSelection() {
     case "cohere":
       const { CohereEmbedder } = require("../EmbeddingEngines/cohere");
       return new CohereEmbedder();
+    case "voyageai":
+      const { VoyageAiEmbedder } = require("../EmbeddingEngines/voyageAi");
+      return new VoyageAiEmbedder();
     default:
       return new NativeEmbedder();
   }
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 48c98e957..401541634 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -350,6 +350,12 @@ const KEY_MAPPING = {
     checks: [isNotEmpty],
   },
 
+  // VoyageAi Options
+  VoyageAiApiKey: {
+    envKey: "VOYAGEAI_API_KEY",
+    checks: [isNotEmpty],
+  },
+
   // Whisper (transcription) providers
   WhisperProvider: {
     envKey: "WHISPER_PROVIDER",
@@ -545,6 +551,7 @@ function supportedEmbeddingModel(input = "") {
     "ollama",
     "lmstudio",
     "cohere",
+    "voyageai",
   ];
   return supported.includes(input)
     ? null
-- 
GitLab