diff --git a/collector/utils/WhisperProviders/localWhisper.js b/collector/utils/WhisperProviders/localWhisper.js index 46dbe226..af13c8a9 100644 --- a/collector/utils/WhisperProviders/localWhisper.js +++ b/collector/utils/WhisperProviders/localWhisper.js @@ -1,19 +1,23 @@ const fs = require("fs"); const path = require("path"); const { v4 } = require("uuid"); +const defaultWhisper = "Xenova/whisper-small"; // Model Card: https://huggingface.co/Xenova/whisper-small +const fileSize = { + "Xenova/whisper-small": "250mb", + "Xenova/whisper-large": "1.56GB", +}; class LocalWhisper { - constructor() { - // Model Card: https://huggingface.co/Xenova/whisper-small - this.model = "Xenova/whisper-small"; + constructor({ options }) { + this.model = options?.WhisperModelPref ?? defaultWhisper; + this.fileSize = fileSize[this.model]; this.cacheDir = path.resolve( process.env.STORAGE_DIR ? path.resolve(process.env.STORAGE_DIR, `models`) : path.resolve(__dirname, `../../../server/storage/models`) ); - this.modelPath = path.resolve(this.cacheDir, "Xenova", "whisper-small"); - + this.modelPath = path.resolve(this.cacheDir, ...this.model.split("/")); // Make directory when it does not exist in existing installations if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir, { recursive: true }); @@ -104,7 +108,7 @@ class LocalWhisper { async client() { if (!fs.existsSync(this.modelPath)) { this.#log( - `The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~250MB)` + `The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~${this.fileSize})` ); } diff --git a/docker/.env.example b/docker/.env.example index 7fedf944..23789af4 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/.gitignore b/frontend/.gitignore index 78720603..77e294d0 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -9,10 +9,8 @@ lerna-debug.log* node_modules dist -lib dist-ssr *.local -!frontend/components/lib # Editor directories and files .vscode/* diff --git a/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx new file mode 100644 index 00000000..33ce693d --- /dev/null +++ b/frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx @@ -0,0 +1,50 @@ +export default function VoyageAiOptions({ settings }) { + return ( +
+
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/Footer/index.jsx b/frontend/src/components/Footer/index.jsx index 10cd80cd..6e80f0df 100644 --- a/frontend/src/components/Footer/index.jsx +++ b/frontend/src/components/Footer/index.jsx @@ -14,6 +14,8 @@ import { import React, { useEffect, useState } from "react"; import SettingsButton from "../SettingsButton"; import { isMobile } from "react-device-detect"; +import { Tooltip } from "react-tooltip"; +import { v4 } from "uuid"; export const MAX_ICONS = 3; export const ICON_COMPONENTS = { @@ -47,36 +49,48 @@ export default function Footer() { return (
- - - - - - - - - + + + + + + + + + + + + + + + {!isMobile && }
@@ -105,3 +119,17 @@ export default function Footer() { ); } + +export function ToolTipWrapper({ id = v4(), children }) { + return ( +
+ {children} + +
+ ); +} diff --git a/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx index ac143e94..d1088063 100644 --- a/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx @@ -1,80 +1,84 @@ export default function GenericOpenAiOptions({ settings }) { return ( -
-
- - +
+
+
+ + +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- - e.target.blur()} - defaultValue={settings?.GenericOpenAiTokenLimit} - required={true} - autoComplete="off" - /> -
-
- - +
+
+ + e.target.blur()} + defaultValue={settings?.GenericOpenAiTokenLimit} + required={true} + autoComplete="off" + /> +
+
+ + +
); diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx index de6ed77e..00b1cc46 100644 --- a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx @@ -3,7 +3,7 @@ import System from "@/models/system"; import showToast from "@/utils/toast"; import pluralize from "pluralize"; import { TagsInput } from "react-tag-input-component"; -import { Warning } from "@phosphor-icons/react"; +import { Info, Warning } from "@phosphor-icons/react"; import { Tooltip } from "react-tooltip"; const DEFAULT_BRANCHES = ["main", "master"]; @@ -92,45 +92,7 @@ export default function GithubOptions() {

Github Access Token

{" "}

optional - {!accessToken && ( - - )} - -

- Without a{" "} - e.stopPropagation()} - > - Personal Access Token - - , the GitHub API may limit the number of files that - can be collected due to rate limits. You can{" "} - e.stopPropagation()} - > - create a temporary Access Token - {" "} - to avoid this issue. -

- +

@@ -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, };