@@ -180,6 +142,7 @@ export default function GithubOptions() {
+
diff --git a/frontend/src/media/embeddingprovider/voyageai.png b/frontend/src/media/embeddingprovider/voyageai.png
new file mode 100644
index 00000000..4fd57eaa
Binary files /dev/null and b/frontend/src/media/embeddingprovider/voyageai.png differ
diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
index 8f234b5a..5a0f51c1 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) => ,
description: "Run powerful embedding models from Cohere.",
},
+ {
+ name: "Voyage AI",
+ value: "voyageai",
+ logo: VoyageAiLogo,
+ options: (settings) => ,
+ description: "Run powerful embedding models from Voyage AI.",
+ },
];
export default function GeneralEmbeddingPreference() {
diff --git a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx
index 5fbd196c..07907af7 100644
--- a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx
@@ -12,6 +12,23 @@ import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import CTAButton from "@/components/lib/CTAButton";
+const PROVIDERS = [
+ {
+ name: "OpenAI",
+ value: "openai",
+ logo: OpenAiLogo,
+ options: (settings) => ,
+ description: "Leverage the OpenAI Whisper-large model using your API key.",
+ },
+ {
+ name: "AnythingLLM Built-In",
+ value: "local",
+ logo: AnythingLLMIcon,
+ options: (settings) => ,
+ description: "Run a built-in whisper model on this instance privately.",
+ },
+];
+
export default function TranscriptionModelPreference() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
@@ -68,24 +85,6 @@ export default function TranscriptionModelPreference() {
fetchKeys();
}, []);
- const PROVIDERS = [
- {
- name: "OpenAI",
- value: "openai",
- logo: OpenAiLogo,
- options: ,
- description:
- "Leverage the OpenAI Whisper-large model using your API key.",
- },
- {
- name: "AnythingLLM Built-In",
- value: "local",
- logo: AnythingLLMIcon,
- options: ,
- description: "Run a built-in whisper model on this instance privately.",
- },
- ];
-
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
@@ -228,7 +227,7 @@ export default function TranscriptionModelPreference() {
{selectedProvider &&
PROVIDERS.find(
(provider) => provider.value === selectedProvider
- )?.options}
+ )?.options(settings)}
diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
index b6ae8cb2..35358636 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/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx
index 5cbcdc3b..08565f58 100644
--- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx
@@ -20,19 +20,23 @@ export default function ChatTemperatureSettings({
LLM Temperature
- This setting controls how "random" or dynamic your chat
- responses will be.
+ This setting controls how "creative" your LLM responses will
+ be.
- The higher the number (1.0 maximum) the more random and incoherent.
+ The higher the number the more creative. For some models this can lead
+ to incoherent responses when set too high.
- Recommended: {defaults.temp}
+
+
+ Most LLMs have various acceptable ranges of valid values. Consult
+ your LLM provider for that information.
+
e.target.blur()}
defaultValue={workspace?.openAiTemp ?? defaults.temp}
diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
index 5e4053f0..101a3a9b 100644
--- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
@@ -2,7 +2,6 @@ import Workspace from "@/models/workspace";
import { castToType } from "@/utils/types";
import showToast from "@/utils/toast";
import { useEffect, useRef, useState } from "react";
-import VectorCount from "./VectorCount";
import WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
@@ -51,7 +50,6 @@ export default function GeneralInfo({ slug }) {
onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6"
>
-
Number of vectors
-
- Total number of vectors in your vector database.
-
{totalVectors}
diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/index.jsx
index 0a9a0e87..97d63291 100644
--- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/index.jsx
@@ -6,6 +6,7 @@ import VectorDBIdentifier from "./VectorDBIdentifier";
import MaxContextSnippets from "./MaxContextSnippets";
import DocumentSimilarityThreshold from "./DocumentSimilarityThreshold";
import ResetDatabase from "./ResetDatabase";
+import VectorCount from "./VectorCount";
export default function VectorDatabase({ workspace }) {
const [hasChanges, setHasChanges] = useState(false);
@@ -38,7 +39,10 @@ export default function VectorDatabase({ workspace }) {
onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6"
>
-
+
+
+
+
{
+ /*
+ #swagger.tags = ['Workspaces']
+ #swagger.description = 'Add or remove pin from a document in a workspace by its unique slug.'
+ #swagger.path = '/workspace/{slug}/update-pin'
+ #swagger.parameters['slug'] = {
+ in: 'path',
+ description: 'Unique slug of workspace to find',
+ required: true,
+ type: 'string'
+ }
+ #swagger.requestBody = {
+ description: 'JSON object with the document path and pin status to update.',
+ required: true,
+ type: 'object',
+ content: {
+ "application/json": {
+ example: {
+ docPath: "custom-documents/my-pdf.pdf-hash.json",
+ pinStatus: true
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'OK',
+ content: {
+ "application/json": {
+ schema: {
+ type: 'object',
+ example: {
+ message: 'Pin status updated successfully'
+ }
+ }
+ }
+ }
+ }
+ #swagger.responses[404] = {
+ description: 'Document not found'
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error'
+ }
+ */
+ try {
+ 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();
+ } catch (error) {
+ console.error("Error processing the pin status update:", error);
+ return response.status(500).end();
+ }
+ }
+ );
+
app.post(
"/v1/workspace/:slug/chat",
[validApiKey],
diff --git a/server/index.js b/server/index.js
index 7874045b..59d8fec6 100644
--- a/server/index.js
+++ b/server/index.js
@@ -36,7 +36,12 @@ app.use(
})
);
-require("express-ws")(app);
+if (!!process.env.ENABLE_HTTPS) {
+ bootSSL(app, process.env.SERVER_PORT || 3001);
+} else {
+ require("express-ws")(app); // load WebSockets in non-SSL mode.
+}
+
app.use("/api", apiRouter);
systemEndpoints(apiRouter);
extensionEndpoints(apiRouter);
@@ -109,8 +114,6 @@ app.all("*", function (_, response) {
response.sendStatus(404);
});
-if (!!process.env.ENABLE_HTTPS) {
- bootSSL(app, process.env.SERVER_PORT || 3001);
-} else {
- bootHTTP(app, process.env.SERVER_PORT || 3001);
-}
+// In non-https mode we need to boot at the end since the server has not yet
+// started and is `.listen`ing.
+if (!process.env.ENABLE_HTTPS) bootHTTP(app, process.env.SERVER_PORT || 3001);
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index 68d1d0dd..a5bb6a23 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -150,6 +150,8 @@ const SystemSettings = {
// - then it can be shared.
// --------------------------------------------------------
WhisperProvider: process.env.WHISPER_PROVIDER || "local",
+ WhisperModelPref:
+ process.env.WHISPER_MODEL_PREF || "Xenova/whisper-small",
// --------------------------------------------------------
// TTS/STT Selection Settings & Configs
@@ -424,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 e0ee35a5..8616943c 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -2000,6 +2000,69 @@
}
}
},
+ "/workspace/{slug}/update-pin": {
+ "post": {
+ "tags": [
+ "Workspaces"
+ ],
+ "description": "Add or remove pin from a document in a workspace by its unique slug.",
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "Unique slug of workspace to find"
+ },
+ {
+ "name": "Authorization",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "example": {
+ "message": "Pin status updated successfully"
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden"
+ },
+ "404": {
+ "description": "Document not found"
+ },
+ "500": {
+ "description": "Internal Server Error"
+ }
+ },
+ "requestBody": {
+ "description": "JSON object with the document path and pin status to update.",
+ "required": true,
+ "type": "object",
+ "content": {
+ "application/json": {
+ "example": {
+ "docPath": "custom-documents/my-pdf.pdf-hash.json",
+ "pinStatus": true
+ }
+ }
+ }
+ }
+ }
+ },
"/v1/workspace/{slug}/chat": {
"post": {
"tags": [
diff --git a/server/utils/AiProviders/genericOpenAi/index.js b/server/utils/AiProviders/genericOpenAi/index.js
index dc0264e4..46b8aefb 100644
--- a/server/utils/AiProviders/genericOpenAi/index.js
+++ b/server/utils/AiProviders/genericOpenAi/index.js
@@ -2,6 +2,7 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
+const { toValidNumber } = require("../../http");
class GenericOpenAiLLM {
constructor(embedder = null, modelPreference = null) {
@@ -18,7 +19,9 @@ class GenericOpenAiLLM {
});
this.model =
modelPreference ?? process.env.GENERIC_OPEN_AI_MODEL_PREF ?? null;
- this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS ?? 1024;
+ this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS
+ ? toValidNumber(process.env.GENERIC_OPEN_AI_MAX_TOKENS, 1024)
+ : 1024;
if (!this.model)
throw new Error("GenericOpenAI must have a valid model set.");
this.limits = {
diff --git a/server/utils/EmbeddingEngines/voyageAi/index.js b/server/utils/EmbeddingEngines/voyageAi/index.js
new file mode 100644
index 00000000..b25d3208
--- /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/agents/aibitat/plugins/summarize.js b/server/utils/agents/aibitat/plugins/summarize.js
index 526de116..de1657c9 100644
--- a/server/utils/agents/aibitat/plugins/summarize.js
+++ b/server/utils/agents/aibitat/plugins/summarize.js
@@ -1,6 +1,5 @@
const { Document } = require("../../../../models/documents");
const { safeJsonParse } = require("../../../http");
-const { validate } = require("uuid");
const { summarizeContent } = require("../utils/summarize");
const Provider = require("../providers/ai-provider");
diff --git a/server/utils/agents/aibitat/providers/genericOpenAi.js b/server/utils/agents/aibitat/providers/genericOpenAi.js
index a1b2db3e..9a753ca2 100644
--- a/server/utils/agents/aibitat/providers/genericOpenAi.js
+++ b/server/utils/agents/aibitat/providers/genericOpenAi.js
@@ -2,6 +2,7 @@ const OpenAI = require("openai");
const Provider = require("./ai-provider.js");
const InheritMultiple = require("./helpers/classes.js");
const UnTooled = require("./helpers/untooled.js");
+const { toValidNumber } = require("../../../http/index.js");
/**
* The agent provider for the Generic OpenAI provider.
@@ -24,7 +25,9 @@ class GenericOpenAiProvider extends InheritMultiple([Provider, UnTooled]) {
this._client = client;
this.model = model;
this.verbose = true;
- this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS ?? 1024;
+ this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS
+ ? toValidNumber(process.env.GENERIC_OPEN_AI_MAX_TOKENS, 1024)
+ : 1024;
}
get client() {
diff --git a/server/utils/boot/index.js b/server/utils/boot/index.js
index ea95e1f5..2022f66e 100644
--- a/server/utils/boot/index.js
+++ b/server/utils/boot/index.js
@@ -12,16 +12,18 @@ function bootSSL(app, port = 3001) {
const privateKey = fs.readFileSync(process.env.HTTPS_KEY_PATH);
const certificate = fs.readFileSync(process.env.HTTPS_CERT_PATH);
const credentials = { key: privateKey, cert: certificate };
+ const server = https.createServer(credentials, app);
- https
- .createServer(credentials, app)
+ server
.listen(port, async () => {
await setupTelemetry();
new CommunicationKey(true);
console.log(`Primary server in HTTPS mode listening on port ${port}`);
})
.on("error", catchSigTerms);
- return app;
+
+ require("express-ws")(app, server); // Apply same certificate + server for WSS connections
+ return { app, server };
} catch (e) {
console.error(
`\x1b[31m[SSL BOOT FAILED]\x1b[0m ${e.message} - falling back to HTTP boot.`,
@@ -46,7 +48,8 @@ function bootHTTP(app, port = 3001) {
console.log(`Primary server in HTTP mode listening on port ${port}`);
})
.on("error", catchSigTerms);
- return app;
+
+ return { app, server: null };
}
function catchSigTerms() {
diff --git a/server/utils/chats/embed.js b/server/utils/chats/embed.js
index 98b096fb..8488aedd 100644
--- a/server/utils/chats/embed.js
+++ b/server/utils/chats/embed.js
@@ -29,6 +29,7 @@ async function streamChatWithForEmbed(
const uuid = uuidv4();
const LLMConnector = getLLMProvider({
+ provider: embed?.workspace?.chatProvider,
model: chatModel ?? embed.workspace?.chatModel,
});
const VectorDb = getVectorDbClass();
diff --git a/server/utils/collectorApi/index.js b/server/utils/collectorApi/index.js
index 1a1431ac..6971f640 100644
--- a/server/utils/collectorApi/index.js
+++ b/server/utils/collectorApi/index.js
@@ -17,6 +17,7 @@ class CollectorApi {
#attachOptions() {
return {
whisperProvider: process.env.WHISPER_PROVIDER || "local",
+ WhisperModelPref: process.env.WHISPER_MODEL_PREF,
openAiKey: process.env.OPEN_AI_KEY || null,
};
}
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index d9a1ba09..e60202a6 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 8630d85a..40154163 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -350,12 +350,23 @@ const KEY_MAPPING = {
checks: [isNotEmpty],
},
+ // VoyageAi Options
+ VoyageAiApiKey: {
+ envKey: "VOYAGEAI_API_KEY",
+ checks: [isNotEmpty],
+ },
+
// Whisper (transcription) providers
WhisperProvider: {
envKey: "WHISPER_PROVIDER",
checks: [isNotEmpty, supportedTranscriptionProvider],
postUpdate: [],
},
+ WhisperModelPref: {
+ envKey: "WHISPER_MODEL_PREF",
+ checks: [validLocalWhisper],
+ postUpdate: [],
+ },
// System Settings
AuthToken: {
@@ -468,6 +479,16 @@ function supportedTTSProvider(input = "") {
return validSelection ? null : `${input} is not a valid TTS provider.`;
}
+function validLocalWhisper(input = "") {
+ const validSelection = [
+ "Xenova/whisper-small",
+ "Xenova/whisper-large",
+ ].includes(input);
+ return validSelection
+ ? null
+ : `${input} is not a valid Whisper model selection.`;
+}
+
function supportedLLM(input = "") {
const validSelection = [
"openai",
@@ -530,6 +551,7 @@ function supportedEmbeddingModel(input = "") {
"ollama",
"lmstudio",
"cohere",
+ "voyageai",
];
return supported.includes(input)
? null
diff --git a/server/utils/http/index.js b/server/utils/http/index.js
index 6400c36b..e812b8ab 100644
--- a/server/utils/http/index.js
+++ b/server/utils/http/index.js
@@ -91,6 +91,11 @@ function isValidUrl(urlString = "") {
return false;
}
+function toValidNumber(number = null, fallback = null) {
+ if (isNaN(Number(number))) return fallback;
+ return Number(number);
+}
+
module.exports = {
reqBody,
multiUserMode,
@@ -101,4 +106,5 @@ module.exports = {
parseAuthHeader,
safeJsonParse,
isValidUrl,
+ toValidNumber,
};