From 6faa0efaa8f8674c8b1221d84224729853218039 Mon Sep 17 00:00:00 2001 From: Shuyoou Date: Sat, 13 Jan 2024 05:23:57 +0800 Subject: [PATCH 001/298] Issue #543 support milvus vector db (#579) * issue #543 support milvus vector db * migrate Milvus to use MilvusClient instead of ORM normalize env setup for docs/implementation feat: embedder model dimension added * update comments --------- Co-authored-by: timothycarambat --- README.md | 1 + docker/.env.example | 6 + .../MilvusDBOptions/index.jsx | 52 +++ frontend/src/media/vectordbs/milvus.png | Bin 0 -> 4250 bytes .../GeneralSettings/VectorDatabase/index.jsx | 9 + .../Steps/DataHandling/index.jsx | 8 + .../Steps/VectorDatabaseConnection/index.jsx | 9 + server/.env.example | 5 + server/models/systemSettings.js | 7 + server/package.json | 3 +- .../EmbeddingEngines/azureOpenAi/index.js | 1 + .../utils/EmbeddingEngines/localAi/index.js | 4 + server/utils/EmbeddingEngines/native/index.js | 1 + server/utils/EmbeddingEngines/openAi/index.js | 1 + server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 23 +- .../vectorDbProviders/milvus/MILVUS_SETUP.md | 40 ++ .../utils/vectorDbProviders/milvus/index.js | 360 ++++++++++++++++++ .../utils/vectorDbProviders/qdrant/index.js | 8 +- server/yarn.lock | 223 ++++++++++- 20 files changed, 759 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/VectorDBSelection/MilvusDBOptions/index.jsx create mode 100644 frontend/src/media/vectordbs/milvus.png create mode 100644 server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md create mode 100644 server/utils/vectorDbProviders/milvus/index.js diff --git a/README.md b/README.md index 5af9278b48..4249c42bcf 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Some cool features of AnythingLLM - [Chroma](https://trychroma.com) - [Weaviate](https://weaviate.io) - [QDrant](https://qdrant.tech) +- [Milvus](https://milvus.io) ### Technical Overview diff --git a/docker/.env.example b/docker/.env.example index 2f8b2ff35c..5bd909af66 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -89,6 +89,12 @@ GID='1000' # QDRANT_ENDPOINT="http://localhost:6333" # QDRANT_API_KEY= +# Enable all below if you are using vector database: Milvus. +# VECTOR_DB="milvus" +# MILVUS_ADDRESS="http://localhost:19530" +# MILVUS_USERNAME= +# MILVUS_PASSWORD= + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/frontend/src/components/VectorDBSelection/MilvusDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/MilvusDBOptions/index.jsx new file mode 100644 index 0000000000..07a0ef2f57 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/MilvusDBOptions/index.jsx @@ -0,0 +1,52 @@ +export default function MilvusDBOptions({ settings }) { + return ( +
+
+
+ + +
+ +
+ + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/media/vectordbs/milvus.png b/frontend/src/media/vectordbs/milvus.png new file mode 100644 index 0000000000000000000000000000000000000000..e1bcbbd105480ebe9bb6d111823f9e5add9a466e GIT binary patch literal 4250 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 9ef9cff2dd..f49054b900 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -8,6 +8,7 @@ import PineconeLogo from "@/media/vectordbs/pinecone.png"; 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 PreLoader from "@/components/Preloader"; import ChangeWarningModal from "@/components/ChangeWarning"; import { MagnifyingGlass } from "@phosphor-icons/react"; @@ -17,6 +18,7 @@ import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions" import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; +import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -79,6 +81,13 @@ export default function GeneralVectorDatabase() { description: "Open source local and cloud hosted multi-modal vector database.", }, + { + name: "Milvus", + value: "milvus", + logo: MilvusLogo, + options: , + description: "Open-source, highly scalable, and blazing fast.", + }, ]; const updateVectorChoice = (selection) => { diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index d9fea4c621..281f1e8cdd 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -14,6 +14,7 @@ import PineconeLogo from "@/media/vectordbs/pinecone.png"; 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 React, { useState, useEffect } from "react"; import paths from "@/utils/paths"; import { useNavigate } from "react-router-dom"; @@ -123,6 +124,13 @@ const VECTOR_DB_PRIVACY = { ], logo: WeaviateLogo, }, + milvus: { + name: "Milvus", + description: [ + "Your vectors and document text are stored on your Milvus instance (cloud or self-hosted)", + ], + logo: MilvusLogo, + }, lancedb: { name: "LanceDB", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx index f451fc3e24..37e0e5b736 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx @@ -5,6 +5,7 @@ import PineconeLogo from "@/media/vectordbs/pinecone.png"; 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 System from "@/models/system"; import paths from "@/utils/paths"; import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions"; @@ -12,6 +13,7 @@ import ChromaDBOptions from "@/components/VectorDBSelection/ChromaDBOptions"; import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions"; +import MilvusOptions from "@/components/VectorDBSelection/MilvusDBOptions"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; @@ -81,6 +83,13 @@ export default function VectorDatabaseConnection({ description: "Open source local and cloud hosted multi-modal vector database.", }, + { + name: "Milvus", + value: "milvus", + logo: MilvusLogo, + options: , + description: "Open-source, highly scalable, and blazing fast.", + }, ]; function handleForward() { diff --git a/server/.env.example b/server/.env.example index e41ab63d07..d060e0ab50 100644 --- a/server/.env.example +++ b/server/.env.example @@ -86,6 +86,11 @@ VECTOR_DB="lancedb" # QDRANT_ENDPOINT="http://localhost:6333" # QDRANT_API_KEY= +# Enable all below if you are using vector database: Milvus. +# VECTOR_DB="milvus" +# MILVUS_ADDRESS="http://localhost:19530" +# MILVUS_USERNAME= +# MILVUS_PASSWORD= # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 29c2238ff1..cd008d420f 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -56,6 +56,13 @@ const SystemSettings = { QdrantApiKey: process.env.QDRANT_API_KEY, } : {}), + ...(vectorDB === "milvus" + ? { + MilvusAddress: process.env.MILVUS_ADDRESS, + MilvusUsername: process.env.MILVUS_USERNAME, + MilvusPassword: !!process.env.MILVUS_PASSWORD, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { diff --git a/server/package.json b/server/package.json index 0e2d909c8c..69cb790c38 100644 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "@prisma/client": "5.3.0", "@qdrant/js-client-rest": "^1.4.0", "@xenova/transformers": "^2.10.0", + "@zilliz/milvus2-sdk-node": "^2.3.5", "archiver": "^5.3.1", "bcrypt": "^5.1.0", "body-parser": "^1.20.2", @@ -77,4 +78,4 @@ "nodemon": "^2.0.22", "prettier": "^3.0.3" } -} \ No newline at end of file +} diff --git a/server/utils/EmbeddingEngines/azureOpenAi/index.js b/server/utils/EmbeddingEngines/azureOpenAi/index.js index e80b4b734b..8cde1fc7ca 100644 --- a/server/utils/EmbeddingEngines/azureOpenAi/index.js +++ b/server/utils/EmbeddingEngines/azureOpenAi/index.js @@ -13,6 +13,7 @@ class AzureOpenAiEmbedder { new AzureKeyCredential(process.env.AZURE_OPENAI_KEY) ); this.openai = openai; + this.dimensions = 1536; // Limit of how many strings we can process in a single pass to stay with resource or network limits // https://learn.microsoft.com/en-us/azure/ai-services/openai/faq#i-am-trying-to-use-embeddings-and-received-the-error--invalidrequesterror--too-many-inputs--the-max-number-of-inputs-is-1---how-do-i-fix-this-:~:text=consisting%20of%20up%20to%2016%20inputs%20per%20API%20request diff --git a/server/utils/EmbeddingEngines/localAi/index.js b/server/utils/EmbeddingEngines/localAi/index.js index 1480755d76..6f9d721b9b 100644 --- a/server/utils/EmbeddingEngines/localAi/index.js +++ b/server/utils/EmbeddingEngines/localAi/index.js @@ -16,6 +16,10 @@ class LocalAiEmbedder { : {}), }); this.openai = new OpenAIApi(config); + // We don't know this for user's set model so for vectorDB integrations that requires dimensionality + // in schema, we will throw an error. + // Applies to QDrant and Milvus. + this.dimensions = null; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 50; diff --git a/server/utils/EmbeddingEngines/native/index.js b/server/utils/EmbeddingEngines/native/index.js index 69e13a9e3a..d2acde32ae 100644 --- a/server/utils/EmbeddingEngines/native/index.js +++ b/server/utils/EmbeddingEngines/native/index.js @@ -12,6 +12,7 @@ class NativeEmbedder { : path.resolve(__dirname, `../../../storage/models`) ); this.modelPath = path.resolve(this.cacheDir, "Xenova", "all-MiniLM-L6-v2"); + this.dimensions = 384; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 50; diff --git a/server/utils/EmbeddingEngines/openAi/index.js b/server/utils/EmbeddingEngines/openAi/index.js index 105be9d73a..31f556e899 100644 --- a/server/utils/EmbeddingEngines/openAi/index.js +++ b/server/utils/EmbeddingEngines/openAi/index.js @@ -9,6 +9,7 @@ class OpenAiEmbedder { }); const openai = new OpenAIApi(config); this.openai = openai; + this.dimensions = 1536; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 500; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index ac70293621..1685acc1a0 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -16,6 +16,9 @@ function getVectorDbClass() { case "qdrant": const { QDrant } = require("../vectorDbProviders/qdrant"); return QDrant; + case "milvus": + const { Milvus } = require("../vectorDbProviders/milvus"); + return Milvus; 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 e6e97df5f2..c699cf2df3 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -170,6 +170,20 @@ const KEY_MAPPING = { checks: [], }, + // Milvus Options + MilvusAddress: { + envKey: "MILVUS_ADDRESS", + checks: [isValidURL, validDockerizedUrl], + }, + MilvusUsername: { + envKey: "MILVUS_USERNAME", + checks: [isNotEmpty], + }, + MilvusPassword: { + envKey: "MILVUS_PASSWORD", + checks: [isNotEmpty], + }, + // Together Ai Options TogetherAiApiKey: { envKey: "TOGETHER_AI_API_KEY", @@ -279,7 +293,14 @@ function supportedEmbeddingModel(input = "") { } function supportedVectorDB(input = "") { - const supported = ["chroma", "pinecone", "lancedb", "weaviate", "qdrant"]; + const supported = [ + "chroma", + "pinecone", + "lancedb", + "weaviate", + "qdrant", + "milvus", + ]; return supported.includes(input) ? null : `Invalid VectorDB type. Must be one of ${supported.join(", ")}.`; diff --git a/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md b/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md new file mode 100644 index 0000000000..6bd9b81502 --- /dev/null +++ b/server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md @@ -0,0 +1,40 @@ +# How to setup a local (or remote) Milvus Vector Database + +[Official Milvus Docs](https://milvus.io/docs/example_code.md) for reference. + +### How to get started + +**Requirements** + +Choose one of the following + +- Cloud + + - [Cloud account](https://cloud.zilliz.com/) + +- Local + - Docker + - `git` available in your CLI/terminal + +**Instructions** + +- Cloud + + - Create a Cluster on your cloud account + - Get connect Public Endpoint and Token + - Set .env.development variable in server + +- Local + - Download yaml file `wget https://github.com/milvus-io/milvus/releases/download/v2.3.4/milvus-standalone-docker-compose.yml -O docker-compose.yml` + - Start Milvus `sudo docker compose up -d` + - Check the containers are up and running `sudo docker compose ps` + - Get port number and set .env.development variable in server + +eg: `server/.env.development` + +``` +VECTOR_DB="milvus" +MILVUS_ADDRESS="http://localhost:19530" +MILVUS_USERNAME=minioadmin # Whatever your username and password are +MILVUS_PASSWORD=minioadmin +``` diff --git a/server/utils/vectorDbProviders/milvus/index.js b/server/utils/vectorDbProviders/milvus/index.js new file mode 100644 index 0000000000..a9104784bb --- /dev/null +++ b/server/utils/vectorDbProviders/milvus/index.js @@ -0,0 +1,360 @@ +const { + DataType, + MetricType, + IndexType, + MilvusClient, +} = require("@zilliz/milvus2-sdk-node"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { v4: uuidv4 } = require("uuid"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +const Milvus = { + name: "Milvus", + connect: async function () { + if (process.env.VECTOR_DB !== "milvus") + throw new Error("Milvus::Invalid ENV settings"); + + const client = new MilvusClient({ + address: process.env.MILVUS_ADDRESS, + username: process.env.MILVUS_USERNAME, + password: process.env.MILVUS_PASSWORD, + }); + + const { isHealthy } = await client.checkHealth(); + if (!isHealthy) + throw new Error( + "MilvusDB::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalVectors: async function () { + const { client } = await this.connect(); + const { collection_names } = await client.listCollections(); + const total = collection_names.reduce(async (acc, collection_name) => { + const statistics = await client.getCollectionStatistics({ + collection_name, + }); + return Number(acc) + Number(statistics?.data?.row_count ?? 0); + }, 0); + return total; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const statistics = await client.getCollectionStatistics({ + collection_name: _namespace, + }); + return Number(statistics?.data?.row_count ?? 0); + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client + .getCollectionStatistics({ collection_name: namespace }) + .catch(() => null); + return collection; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { value } = await client + .hasCollection({ collection_name: namespace }) + .catch((e) => { + console.error("MilvusDB::namespaceExists", e.message); + return { value: false }; + }); + return value; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection({ collection_name: namespace }); + return true; + }, + getOrCreateCollection: async function (client, namespace) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + const embedder = getEmbeddingEngineSelection(); + if (!embedder.dimensions) + throw new Error( + `Your embedder selection has unknown dimensions output. It should be defined when using ${this.name}. Open an issue on Github for support.` + ); + + await client.createCollection({ + collection_name: namespace, + fields: [ + { + name: "id", + description: "id", + data_type: DataType.VarChar, + max_length: 255, + is_primary_key: true, + }, + { + name: "vector", + description: "vector", + data_type: DataType.FloatVector, + dim: embedder.dimensions, + }, + { + name: "metadata", + decription: "metadata", + data_type: DataType.JSON, + }, + ], + }); + await client.createIndex({ + collection_name: namespace, + field_name: "vector", + index_type: IndexType.AUTOINDEX, + metric_type: MetricType.COSINE, + }); + await client.loadCollectionSync({ + collection_name: namespace, + }); + } + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + const { pageContent, docId, ...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(); + await this.getOrCreateCollection(client, namespace); + + const { chunks } = cacheResult; + const documentVectors = []; + + for (const chunk of chunks) { + // Before sending to Pinecone and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const id = uuidv4(); + documentVectors.push({ docId, vectorId: id }); + return { id, vector: chunk.values, metadata: chunk.metadata }; + }); + const insertResult = await client.insert({ + collection_name: namespace, + data: newChunks, + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Milvus! Reason:${insertResult?.status.reason}` + ); + } + } + await DocumentVectors.bulkInsert(documentVectors); + await client.flushSync({ collection_names: [namespace] }); + return true; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + 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); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + const vectorRecord = { + id: uuidv4(), + values: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + + if (vectors.length > 0) { + const chunks = []; + const { client } = await this.connect(); + await this.getOrCreateCollection(client, namespace); + + console.log("Inserting vectorized chunks into Milvus."); + for (const chunk of toChunks(vectors, 100)) { + chunks.push(chunk); + const insertResult = await client.insert({ + collection_name: namespace, + data: chunk.map((item) => ({ + id: item.id, + vector: item.values, + metadata: chunk.metadata, + })), + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Milvus! Reason:${insertResult?.status.reason}` + ); + } + } + await storeVectorResult(chunks, fullFilePath); + await client.flushSync({ collection_names: [namespace] }); + } + + 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 }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + const queryIn = vectorIds.map((v) => `'${v}'`).join(","); + await client.deleteEntities({ + collection_name: namespace, + expr: `id in [${queryIn}]`, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + + // Even after flushing Milvus can take some time to re-calc the count + // so all we can hope to do is flushSync so that the count can be correct + // on a later call. + await client.flushSync({ collection_names: [namespace] }); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + const response = await client.search({ + collection_name: namespace, + vectors: queryVector, + }); + response.results.forEach((match) => { + if (match.score < similarityThreshold) return; + result.contextTexts.push(match.metadata.text); + result.sourceDocuments.push(match); + result.scores.push(match.score); + }); + return result; + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + 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(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const statistics = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + const vectorCount = Number(statistics?.data?.row_count ?? 0); + return { + message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, +}; + +module.exports.Milvus = Milvus; diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js index 49b25a3d65..ddc3408da8 100644 --- a/server/utils/vectorDbProviders/qdrant/index.js +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -112,9 +112,15 @@ const QDrant = { if (await this.namespaceExists(client, namespace)) { return await client.getCollection(namespace); } + + const embedder = getEmbeddingEngineSelection(); + if (!embedder.dimensions) + throw new Error( + `Your embedder selection has unknown dimensions output. It should be defined when using ${this.name}. Open an issue on Github for support.` + ); await client.createCollection(namespace, { vectors: { - size: 1536, //TODO: Fixed to OpenAI models - when other embeddings exist make variable. + size: embedder.dimensions, distance: "Cosine", }, }); diff --git a/server/yarn.lock b/server/yarn.lock index 6215bf01fd..175a67947d 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -160,6 +160,20 @@ "@azure/logger" "^1.0.3" tslib "^2.4.0" +"@colors/colors@1.6.0", "@colors/colors@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -214,6 +228,35 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@grpc/grpc-js@1.8.17": + version "1.8.17" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.17.tgz#a3a2f826fc033eae7d2f5ee41e0ab39cee948838" + integrity sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@0.7.7": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.7.tgz#d33677a77eea8407f7c66e2abd97589b60eb4b21" + integrity sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^7.0.0" + yargs "^17.7.2" + +"@grpc/proto-loader@^0.7.0": + version "0.7.10" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" + integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -755,6 +798,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== +"@types/node@>=12.12.47": + version "20.10.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.8.tgz#f1e223cbde9e25696661d167a5b93a9b2a5d57c7" + integrity sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA== + dependencies: + undici-types "~5.26.4" + "@types/node@>=13.7.0": version "20.10.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.3.tgz#4900adcc7fc189d5af5bb41da8f543cea6962030" @@ -779,6 +829,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/triple-beam@^1.3.2": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== + "@types/uuid@^9.0.1": version "9.0.7" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" @@ -806,6 +861,18 @@ optionalDependencies: onnxruntime-node "1.14.0" +"@zilliz/milvus2-sdk-node@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.3.5.tgz#6540bc03ebb99ab35f63e4eca7a1fd3ede2cf38c" + integrity sha512-bWbQnhvu+7jZXoqI+qySycwph3vloy0LDV54TBY4wRmu6HhMlqIqyIiI8sQNeSJFs8M1jHg1PlmhE/dvckA1bA== + dependencies: + "@grpc/grpc-js" "1.8.17" + "@grpc/proto-loader" "0.7.7" + dayjs "^1.11.7" + lru-cache "^9.1.2" + protobufjs "7.2.4" + winston "^3.9.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1487,7 +1554,7 @@ cmake-js@^7.2.1: which "^2.0.2" yargs "^17.6.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1511,7 +1578,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -1524,6 +1591,14 @@ color-support@^1.1.2, color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + color@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" @@ -1537,6 +1612,14 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1680,6 +1763,11 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +dayjs@^1.11.7: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1835,6 +1923,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encode32@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/encode32/-/encode32-1.1.0.tgz#0c54b45fb314ad5502e3c230cb95acdc5e5cd1dd" @@ -2254,6 +2347,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2339,6 +2437,11 @@ flow-remove-types@^2.217.1: pirates "^3.0.2" vlq "^0.2.1" +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@^1.14.8, follow-redirects@^1.14.9: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -3344,6 +3447,11 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + ky@^0.33.1: version "0.33.3" resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" @@ -3500,11 +3608,28 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" +logform@^2.3.2, logform@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.0.tgz#8c82a983f05d6eaeb2d75e3decae7a768b2bf9b5" + integrity sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -3524,6 +3649,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835" + integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== + make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4042,6 +4172,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -4334,6 +4471,24 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@7.2.4: + version "7.2.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@^6.8.8: version "6.11.4" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" @@ -4353,6 +4508,24 @@ protobufjs@^6.8.8: "@types/node" ">=13.7.0" long "^4.0.0" +protobufjs@^7.0.0, protobufjs@^7.2.4: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -4605,6 +4778,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-stable-stringify@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -4835,6 +5013,11 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -5078,6 +5261,11 @@ tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -5107,6 +5295,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + tslib@^2.2.0, tslib@^2.4.0: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" @@ -5448,6 +5641,32 @@ wide-align@^1.1.2, wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +winston-transport@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.6.0.tgz#f1c1a665ad1b366df72199e27892721832a19e1b" + integrity sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.9.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91" + integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + wordwrapjs@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" From 315b92e1647aea790dfaa7389ffa40c43e9407e8 Mon Sep 17 00:00:00 2001 From: Ahren Stevens-Taylor Date: Fri, 12 Jan 2024 21:31:59 +0000 Subject: [PATCH 002/298] 572 add docker tags (#581) * [FEAT]: Docker Tags specific to a build version #572 * fix: dockerhub repo name * feat: add Docker build caches * fix: docker username Fix the DockerHub repository owner name --- .github/workflows/build-and-push-image.yaml | 35 ++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 17ca5a9755..03318320d2 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -36,6 +36,19 @@ jobs: shell: bash run: echo "repo=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT id: lowercase_repo + + - name: Check if DockerHub build needed + shell: bash + run: | + # Check if the secret for USERNAME is set (don't even check for the password) + if [[ -z "${{ secrets.DOCKER_USERNAME }}" ]]; then + echo "DockerHub build not needed" + echo "enabled=false" >> $GITHUB_OUTPUT + else + echo "DockerHub build needed" + echo "enabled=true" >> $GITHUB_OUTPUT + fi + id: dockerhub - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -45,6 +58,8 @@ jobs: - name: Log in to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + # Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR + if: steps.dockerhub.outputs.enabled == 'true' with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -61,9 +76,16 @@ jobs: uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: images: | - mintplexlabs/anythingllm + ${{ steps.dockerhub.outputs.enabled == 'true' && 'mintplexlabs/anythingllm' || '' }} ghcr.io/${{ github.repository }} - + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + + - name: Build and push multi-platform Docker image uses: docker/build-push-action@v5 with: @@ -71,8 +93,7 @@ jobs: file: ./docker/Dockerfile push: true platforms: linux/amd64,linux/arm64 - tags: | - ${{ steps.meta.outputs.tags }} - ${{ github.ref_name == 'master' && 'mintplexlabs/anythingllm:latest' || '' }} - ${{ github.ref_name == 'master' && format('ghcr.io/{0}:{1}', steps.lowercase_repo.outputs.repo, 'latest') || '' }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From e5826d8c24c833a8ac560ed160a52b9de314f587 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 12 Jan 2024 13:33:11 -0800 Subject: [PATCH 003/298] remove unneeded build step --- .github/workflows/build-and-push-image.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 03318320d2..b6f2b3be51 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -32,11 +32,6 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 - - name: Parse repository name to lowercase - shell: bash - run: echo "repo=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT - id: lowercase_repo - - name: Check if DockerHub build needed shell: bash run: | From b4ab0a5e8c889e7dd5e3b83b3614d1bb916ad6b4 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 12 Jan 2024 14:22:19 -0800 Subject: [PATCH 004/298] nopub sha --- .github/workflows/build-and-push-image.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index b6f2b3be51..12b274b753 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -75,7 +75,6 @@ jobs: ghcr.io/${{ github.repository }} tags: | type=raw,value=latest,enable={{is_default_branch}} - type=sha type=ref,event=branch type=ref,event=tag type=ref,event=pr From 4f6d93159f686500c5e4c5ab60256496de1a18e6 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Sat, 13 Jan 2024 00:32:43 -0800 Subject: [PATCH 005/298] improve native embedder handling of large files (#584) * improve native embedder handling of large files * perf changes * ignore storage tmp --- .../cloudformation/aws_https_instructions.md | 8 ++- server/.gitignore | 1 + server/package.json | 4 +- server/utils/EmbeddingEngines/native/index.js | 65 +++++++++++++++++-- server/yarn.lock | 14 ++-- 5 files changed, 78 insertions(+), 14 deletions(-) diff --git a/cloud-deployments/aws/cloudformation/aws_https_instructions.md b/cloud-deployments/aws/cloudformation/aws_https_instructions.md index 5eb3cc7532..39591820bc 100644 --- a/cloud-deployments/aws/cloudformation/aws_https_instructions.md +++ b/cloud-deployments/aws/cloudformation/aws_https_instructions.md @@ -64,8 +64,14 @@ server { listen 80; server_name [insert FQDN here]; location / { + # Prevent timeouts on long-running requests. + proxy_connect_timeout 605; + proxy_send_timeout 605; + proxy_read_timeout 605; + send_timeout 605; + keepalive_timeout 605; proxy_pass http://0.0.0.0:3001; - } + } } 3. Enter ':wq' to save the changes to the anything config file diff --git a/server/.gitignore b/server/.gitignore index be4af591de..0913f96636 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -3,6 +3,7 @@ storage/assets/* !storage/assets/anything-llm.png storage/documents/* +storage/tmp/* storage/vector-cache/*.json storage/exports storage/imports diff --git a/server/package.json b/server/package.json index 69cb790c38..9761125a4a 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,7 @@ "@pinecone-database/pinecone": "^0.1.6", "@prisma/client": "5.3.0", "@qdrant/js-client-rest": "^1.4.0", - "@xenova/transformers": "^2.10.0", + "@xenova/transformers": "^2.14.0", "@zilliz/milvus2-sdk-node": "^2.3.5", "archiver": "^5.3.1", "bcrypt": "^5.1.0", @@ -78,4 +78,4 @@ "nodemon": "^2.0.22", "prettier": "^3.0.3" } -} +} \ No newline at end of file diff --git a/server/utils/EmbeddingEngines/native/index.js b/server/utils/EmbeddingEngines/native/index.js index d2acde32ae..789e51fe9e 100644 --- a/server/utils/EmbeddingEngines/native/index.js +++ b/server/utils/EmbeddingEngines/native/index.js @@ -1,6 +1,7 @@ const path = require("path"); const fs = require("fs"); const { toChunks } = require("../../helpers"); +const { v4 } = require("uuid"); class NativeEmbedder { constructor() { @@ -15,13 +16,30 @@ class NativeEmbedder { this.dimensions = 384; // Limit of how many strings we can process in a single pass to stay with resource or network limits - this.maxConcurrentChunks = 50; + this.maxConcurrentChunks = 25; this.embeddingMaxChunkLength = 1_000; // Make directory when it does not exist in existing installations if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir); } + #tempfilePath() { + const filename = `${v4()}.tmp`; + const tmpPath = process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, "tmp") + : path.resolve(__dirname, `../../../storage/tmp`); + if (!fs.existsSync(tmpPath)) fs.mkdirSync(tmpPath, { recursive: true }); + return path.resolve(tmpPath, filename); + } + + async #writeToTempfile(filePath, data) { + try { + await fs.promises.appendFile(filePath, data, { encoding: "utf8" }); + } catch (e) { + console.error(`Error writing to tempfile: ${e}`); + } + } + async embedderClient() { if (!fs.existsSync(this.modelPath)) { console.log( @@ -62,18 +80,51 @@ class NativeEmbedder { return result?.[0] || []; } + // If you are thinking you want to edit this function - you probably don't. + // This process was benchmarked heavily on a t3.small (2GB RAM 1vCPU) + // and without careful memory management for the V8 garbage collector + // this function will likely result in an OOM on any resource-constrained deployment. + // To help manage very large documents we run a concurrent write-log each iteration + // to keep the embedding result out of memory. The `maxConcurrentChunk` is set to 25, + // as 50 seems to overflow no matter what. Given the above, memory use hovers around ~30% + // during a very large document (>100K words) but can spike up to 70% before gc. + // This seems repeatable for all document sizes. + // While this does take a while, it is zero set up and is 100% free and on-instance. async embedChunks(textChunks = []) { - const Embedder = await this.embedderClient(); - const embeddingResults = []; - for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { - const output = await Embedder(chunk, { + const tmpFilePath = this.#tempfilePath(); + const chunks = toChunks(textChunks, this.maxConcurrentChunks); + const chunkLen = chunks.length; + + for (let [idx, chunk] of chunks.entries()) { + if (idx === 0) await this.#writeToTempfile(tmpFilePath, "["); + let data; + let pipeline = await this.embedderClient(); + let output = await pipeline(chunk, { pooling: "mean", normalize: true, }); - if (output.length === 0) continue; - embeddingResults.push(output.tolist()); + + if (output.length === 0) { + pipeline = null; + output = null; + data = null; + continue; + } + + data = JSON.stringify(output.tolist()); + await this.#writeToTempfile(tmpFilePath, data); + console.log(`\x1b[34m[Embedded Chunk ${idx + 1} of ${chunkLen}]\x1b[0m`); + if (chunkLen - 1 !== idx) await this.#writeToTempfile(tmpFilePath, ","); + if (chunkLen - 1 === idx) await this.#writeToTempfile(tmpFilePath, "]"); + pipeline = null; + output = null; + data = null; } + const embeddingResults = JSON.parse( + fs.readFileSync(tmpFilePath, { encoding: "utf-8" }) + ); + fs.rmSync(tmpFilePath, { force: true }); return embeddingResults.length > 0 ? embeddingResults.flat() : null; } } diff --git a/server/yarn.lock b/server/yarn.lock index 175a67947d..cc129dfe9d 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -269,6 +269,11 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@huggingface/jinja@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.1.2.tgz#073fa0a68ef481a1806b0186bbafd8013e586fbe" + integrity sha512-x5mpbfJt1nKmVep5WNP5VjNsjWApWNj8pPYI+uYMkBWH9bWUJmQmHt2lbf0VCoQd54Oq3XuFEh/UyoVh7rPxmg== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -851,11 +856,12 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@xenova/transformers@^2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.10.0.tgz#ae97d724a3addf78de7314336a9f7b28ed96a140" - integrity sha512-Al9WKiOsimAC3mU9Ef434GkHF0izmeAM7mMMx5npdWsWLAYL8fmJXCrULj6uCfjomMQ7jyN9rDtKpp570hffiw== +"@xenova/transformers@^2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.14.0.tgz#6fe128957e64377ca4fca910e77f6092f3f3512a" + integrity sha512-rQ3O7SW5EM64b6XFZGx3XQ2cfiroefxUwU9ShfSpEZyhd082GvwNJJKndxgaukse1hZP1JUDoT0DfjDiq4IZiw== dependencies: + "@huggingface/jinja" "^0.1.0" onnxruntime-web "1.14.0" sharp "^0.32.0" optionalDependencies: From e150e99e453a310e49bc1340ed450376af8dbb64 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Sun, 14 Jan 2024 11:26:08 -0800 Subject: [PATCH 006/298] bump onboard ai link --- .github/ISSUE_TEMPLATE/01_bug.yml | 2 +- .github/ISSUE_TEMPLATE/02_feature.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml index d1ca5aba9a..02ff8a442e 100644 --- a/.github/ISSUE_TEMPLATE/01_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -8,7 +8,7 @@ body: value: | Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue. - Want help contributing a PR? Use our repo chatbot by OnboardAI! https://app.getonboardai.com/chat/github/mintplex-labs/anything-llm + Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm - type: dropdown id: runtime diff --git a/.github/ISSUE_TEMPLATE/02_feature.yml b/.github/ISSUE_TEMPLATE/02_feature.yml index 0fd29cbfb2..ab2be3abd5 100644 --- a/.github/ISSUE_TEMPLATE/02_feature.yml +++ b/.github/ISSUE_TEMPLATE/02_feature.yml @@ -9,7 +9,7 @@ body: Share a new idea for a feature or improvement. Be sure to search existing issues first to avoid duplicates. - Want help contributing a PR? Use our repo chatbot by OnboardAI! https://app.getonboardai.com/chat/github/mintplex-labs/anything-llm + Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm - type: textarea From 026849df0224b6a8754f4103530bc015874def62 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Sun, 14 Jan 2024 16:36:17 -0800 Subject: [PATCH 007/298] normalize paths for submit URLs of `remove-documents (#598) normalize paths for submit URLs --- server/endpoints/system.js | 6 ++-- server/utils/files/index.js | 56 +++++++++++++---------------- server/utils/files/purgeDocument.js | 7 ++-- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 345bd230a7..6d985065c2 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -1,7 +1,7 @@ process.env.NODE_ENV === "development" ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) : require("dotenv").config(); -const { viewLocalFiles } = require("../utils/files"); +const { viewLocalFiles, normalizePath } = require("../utils/files"); const { exportData, unpackAndOverwriteImport } = require("../utils/files/data"); const { checkProcessorAlive, @@ -401,9 +401,7 @@ function systemEndpoints(app) { app.get("/system/data-exports/:filename", (request, response) => { const exportLocation = __dirname + "/../storage/exports/"; - const sanitized = path - .normalize(request.params.filename) - .replace(/^(\.\.(\/|\\|$))+/, ""); + const sanitized = normalizePath(request.params.filename); const finalDestination = path.join(exportLocation, sanitized); if (!fs.existsSync(finalDestination)) { diff --git a/server/utils/files/index.js b/server/utils/files/index.js index b6c7a30701..2ff1d60cc1 100644 --- a/server/utils/files/index.js +++ b/server/utils/files/index.js @@ -2,32 +2,6 @@ const fs = require("fs"); const path = require("path"); const { v5: uuidv5 } = require("uuid"); -async function collectDocumentData(folderName = null) { - if (!folderName) throw new Error("No docPath provided in request"); - const folder = - process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents/${folderName}`) - : path.resolve(process.env.STORAGE_DIR, `documents/${folderName}`); - - const dirExists = fs.existsSync(folder); - if (!dirExists) - throw new Error( - `No documents folder for ${folderName} - did you run collector/main.py for this element?` - ); - - const files = fs.readdirSync(folder); - const fileData = []; - files.forEach((file) => { - if (path.extname(file) === ".json") { - const filePath = path.join(folder, file); - const data = fs.readFileSync(filePath, "utf8"); - console.log(`Parsing document: ${file}`); - fileData.push(JSON.parse(data)); - } - }); - return fileData; -} - // Should take in a folder that is a subfolder of documents // eg: youtube-subject/video-123.json async function fileData(filePath = null) { @@ -35,8 +9,15 @@ async function fileData(filePath = null) { const fullPath = process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents/${filePath}`) - : path.resolve(process.env.STORAGE_DIR, `documents/${filePath}`); + ? path.resolve( + __dirname, + `../../storage/documents/${normalizePath(filePath)}` + ) + : path.resolve( + process.env.STORAGE_DIR, + `documents/${normalizePath(filePath)}` + ); + const fileExists = fs.existsSync(fullPath); if (!fileExists) return null; @@ -142,11 +123,18 @@ async function storeVectorResult(vectorData = [], filename = null) { async function purgeSourceDocument(filename = null) { if (!filename) return; console.log(`Purging source document of ${filename}.`); - const filePath = process.env.NODE_ENV === "development" - ? path.resolve(__dirname, `../../storage/documents`, filename) - : path.resolve(process.env.STORAGE_DIR, `documents`, filename); + ? path.resolve( + __dirname, + `../../storage/documents`, + normalizePath(filename) + ) + : path.resolve( + process.env.STORAGE_DIR, + `documents`, + normalizePath(filename) + ); if (!fs.existsSync(filePath)) return; fs.rmSync(filePath); @@ -169,12 +157,16 @@ async function purgeVectorCache(filename = null) { return; } +function normalizePath(filepath = "") { + return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, ""); +} + module.exports = { cachedVectorInformation, - collectDocumentData, viewLocalFiles, purgeSourceDocument, purgeVectorCache, storeVectorResult, fileData, + normalizePath, }; diff --git a/server/utils/files/purgeDocument.js b/server/utils/files/purgeDocument.js index 27fe147101..46e9d37dad 100644 --- a/server/utils/files/purgeDocument.js +++ b/server/utils/files/purgeDocument.js @@ -1,7 +1,6 @@ const fs = require("fs"); const path = require("path"); - -const { purgeVectorCache, purgeSourceDocument } = require("."); +const { purgeVectorCache, purgeSourceDocument, normalizePath } = require("."); const { Document } = require("../../models/documents"); const { Workspace } = require("../../models/workspace"); @@ -22,10 +21,10 @@ async function purgeFolder(folderName) { ? path.resolve(__dirname, `../../storage/documents`) : path.resolve(process.env.STORAGE_DIR, `documents`); - const folderPath = path.resolve(documentsFolder, folderName); + const folderPath = path.resolve(documentsFolder, normalizePath(folderName)); const filenames = fs .readdirSync(folderPath) - .map((file) => path.join(folderName, file)); + .map((file) => path.join(folderPath, file)); const workspaces = await Workspace.where(); const purgePromises = []; From e1dcd5ded010b03abd6aa32d1bf0668a48e38e17 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Sun, 14 Jan 2024 16:53:44 -0800 Subject: [PATCH 008/298] Normalize pfp path to prevent traversal --- server/endpoints/system.js | 5 +++-- server/utils/files/pfp.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 6d985065c2..39b77a6a10 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -502,7 +502,8 @@ function systemEndpoints(app) { } const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = userRecord.pfpFilename; + const oldPfpFilename = normalizePath(userRecord.pfpFilename); + console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( @@ -536,7 +537,7 @@ function systemEndpoints(app) { try { const user = await userFromSession(request, response); const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = userRecord.pfpFilename; + const oldPfpFilename = normalizePath(userRecord.pfpFilename); console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( diff --git a/server/utils/files/pfp.js b/server/utils/files/pfp.js index 943aa595f0..dd6ba0fe2d 100644 --- a/server/utils/files/pfp.js +++ b/server/utils/files/pfp.js @@ -2,6 +2,7 @@ const path = require("path"); const fs = require("fs"); const { getType } = require("mime"); const { User } = require("../../models/user"); +const { normalizePath } = require("."); function fetchPfp(pfpPath) { if (!fs.existsSync(pfpPath)) { @@ -32,8 +33,7 @@ async function determinePfpFilepath(id) { const basePath = process.env.STORAGE_DIR ? path.join(process.env.STORAGE_DIR, "assets/pfp") : path.join(__dirname, "../../storage/assets/pfp"); - const pfpFilepath = path.join(basePath, pfpFilename); - + const pfpFilepath = path.join(basePath, normalizePath(pfpFilename)); if (!fs.existsSync(pfpFilepath)) return null; return pfpFilepath; } From 7aaa4b38e7112a6cd879c1238310c56b1844c6d8 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Sun, 14 Jan 2024 17:10:49 -0800 Subject: [PATCH 009/298] add flex role to export endpoint --- server/endpoints/system.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 39b77a6a10..15db895ad9 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -389,15 +389,19 @@ function systemEndpoints(app) { } }); - app.get("/system/data-export", [validatedRequest], async (_, response) => { - try { - const { filename, error } = await exportData(); - response.status(200).json({ filename, error }); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); + app.get( + "/system/data-export", + [validatedRequest, flexUserRoleValid], + async (_, response) => { + try { + const { filename, error } = await exportData(); + response.status(200).json({ filename, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } } - }); + ); app.get("/system/data-exports/:filename", (request, response) => { const exportLocation = __dirname + "/../storage/exports/"; From bd158ce7b1c5e9d09f568a05c277899755fbab47 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 09:32:51 -0800 Subject: [PATCH 010/298] [Feat] Query mode to return no-result when no context found (#601) * Query mode to return no-result when no context found * update default error for sync chat * remove unnecessary type conversion --- .../WorkspaceChat/ChatContainer/index.jsx | 15 ---- frontend/src/models/workspace.js | 16 ---- server/endpoints/api/workspace/index.js | 11 +-- server/endpoints/chat.js | 80 ------------------- server/swagger/openapi.json | 2 +- server/utils/chats/index.js | 26 ++++++ server/utils/chats/stream.js | 28 +++++++ 7 files changed, 61 insertions(+), 117 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 6dd1cdf503..372c79a7c6 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -71,21 +71,6 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { return false; } - // TODO: Delete this snippet once we have streaming stable. - // const chatResult = await Workspace.sendChat( - // workspace, - // promptMessage.userMessage, - // window.localStorage.getItem(`workspace_chat_mode_${workspace.slug}`) ?? - // "chat", - // ) - // handleChat( - // chatResult, - // setLoadingResponse, - // setChatHistory, - // remHistory, - // _chatHistory - // ) - await Workspace.streamChat( workspace, promptMessage.userMessage, diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index aa0b9f7444..d015918d46 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -168,22 +168,6 @@ const Workspace = { const data = await response.json(); return { response, data }; }, - - // TODO: Deprecated and should be removed from frontend. - sendChat: async function ({ slug }, message, mode = "query") { - const chatResult = await fetch(`${API_BASE}/workspace/${slug}/chat`, { - method: "POST", - body: JSON.stringify({ message, mode }), - headers: baseHeaders(), - }) - .then((res) => res.json()) - .catch((e) => { - console.error(e); - return null; - }); - - return chatResult; - }, }; export default Workspace; diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index 032fe41c39..ffead3adb4 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -196,10 +196,11 @@ function apiWorkspaceEndpoints(app) { return; } - await WorkspaceChats.delete({ workspaceId: Number(workspace.id) }); - await DocumentVectors.deleteForWorkspace(Number(workspace.id)); - await Document.delete({ workspaceId: Number(workspace.id) }); - await Workspace.delete({ id: Number(workspace.id) }); + const workspaceId = Number(workspace.id); + await WorkspaceChats.delete({ workspaceId: workspaceId }); + await DocumentVectors.deleteForWorkspace(workspaceId); + await Document.delete({ workspaceId: workspaceId }); + await Workspace.delete({ id: workspaceId }); try { await VectorDb["delete-namespace"]({ namespace: slug }); } catch (e) { @@ -441,7 +442,7 @@ function apiWorkspaceEndpoints(app) { #swagger.tags = ['Workspaces'] #swagger.description = 'Execute a chat with a workspace' #swagger.requestBody = { - description: 'prompt to send to the workspace and the type of conversation (query or chat).', + description: 'Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.', required: true, type: 'object', content: { diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index d0a2923c57..79fc10132d 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -1,7 +1,6 @@ const { v4: uuidv4 } = require("uuid"); const { reqBody, userFromSession, multiUserMode } = require("../utils/http"); const { Workspace } = require("../models/workspace"); -const { chatWithWorkspace } = require("../utils/chats"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { WorkspaceChats } = require("../models/workspaceChats"); const { SystemSettings } = require("../models/systemSettings"); @@ -95,85 +94,6 @@ function chatEndpoints(app) { } } ); - - app.post( - "/workspace/:slug/chat", - [validatedRequest], - async (request, response) => { - try { - const user = await userFromSession(request, response); - const { slug } = request.params; - const { message, mode = "query" } = reqBody(request); - - const workspace = multiUserMode(response) - ? await Workspace.getWithUser(user, { slug }) - : await Workspace.get({ slug }); - - if (!workspace) { - response.sendStatus(400).end(); - return; - } - - if (multiUserMode(response) && user.role !== "admin") { - const limitMessagesSetting = await SystemSettings.get({ - label: "limit_user_messages", - }); - const limitMessages = limitMessagesSetting?.value === "true"; - - if (limitMessages) { - const messageLimitSetting = await SystemSettings.get({ - label: "message_limit", - }); - const systemLimit = Number(messageLimitSetting?.value); - - if (!!systemLimit) { - const currentChatCount = await WorkspaceChats.count({ - user_id: user.id, - createdAt: { - gte: new Date(new Date() - 24 * 60 * 60 * 1000), - }, - }); - - if (currentChatCount >= systemLimit) { - response.status(500).json({ - id: uuidv4(), - type: "abort", - textResponse: null, - sources: [], - close: true, - error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`, - }); - return; - } - } - } - } - - const result = await chatWithWorkspace(workspace, message, mode, user); - await Telemetry.sendTelemetry( - "sent_chat", - { - multiUserMode: multiUserMode(response), - LLMSelection: process.env.LLM_PROVIDER || "openai", - Embedder: process.env.EMBEDDING_ENGINE || "inherit", - VectorDbSelection: process.env.VECTOR_DB || "pinecone", - }, - user?.id - ); - response.status(200).json({ ...result }); - } catch (e) { - console.error(e); - response.status(500).json({ - id: uuidv4(), - type: "abort", - textResponse: null, - sources: [], - close: true, - error: e.message, - }); - } - } - ); } module.exports = { chatEndpoints }; diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 184723ed7e..7b675c44b9 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1598,7 +1598,7 @@ } }, "requestBody": { - "description": "prompt to send to the workspace and the type of conversation (query or chat).", + "description": "Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.", "required": true, "type": "object", "content": { diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 7e9be6e5bb..7fdb473446 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -91,6 +91,18 @@ async function chatWithWorkspace( const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); if (!hasVectorizedSpace || embeddingsCount === 0) { + if (chatMode === "query") { + return { + id: uuid, + type: "textResponse", + sources: [], + close: true, + error: null, + textResponse: + "There is no relevant information in this workspace to answer your query.", + }; + } + // If there are no embeddings - chat like a normal LLM chat interface. return await emptyEmbeddingChat({ uuid, @@ -131,6 +143,20 @@ async function chatWithWorkspace( }; } + // If in query mode and no sources are found, do not + // let the LLM try to hallucinate a response or use general knowledge + if (chatMode === "query" && sources.length === 0) { + return { + id: uuid, + type: "textResponse", + sources: [], + close: true, + error: null, + textResponse: + "There is no relevant information in this workspace to answer your query.", + }; + } + // Compress message to ensure prompt passes token limit with room for response // and build system messages based on inputs and history. const messages = await LLMConnector.compressMessages( diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 73437eec50..11d4effd7c 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -50,6 +50,19 @@ async function streamChatWithWorkspace( const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); if (!hasVectorizedSpace || embeddingsCount === 0) { + if (chatMode === "query") { + writeResponseChunk(response, { + id: uuid, + type: "textResponse", + textResponse: + "There is no relevant information in this workspace to answer your query.", + sources: [], + close: true, + error: null, + }); + return; + } + // If there are no embeddings - chat like a normal LLM chat interface. return await streamEmptyEmbeddingChat({ response, @@ -93,6 +106,21 @@ async function streamChatWithWorkspace( return; } + // If in query mode and no sources are found, do not + // let the LLM try to hallucinate a response or use general knowledge + if (chatMode === "query" && sources.length === 0) { + writeResponseChunk(response, { + id: uuid, + type: "textResponse", + textResponse: + "There is no relevant information in this workspace to answer your query.", + sources: [], + close: true, + error: null, + }); + return; + } + // Compress message to ensure prompt passes token limit with room for response // and build system messages based on inputs and history. const messages = await LLMConnector.compressMessages( From f5bb064dee7c9c7c28f4071051f7247dbc42a79f Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 10:37:46 -0800 Subject: [PATCH 011/298] Implement streaming for workspace chats via API (#604) --- server/endpoints/api/workspace/index.js | 148 +++++++++++++++++++++++- server/endpoints/chat.js | 15 +++ server/swagger/openapi.json | 99 ++++++++++++++++ server/utils/chats/stream.js | 2 + 4 files changed, 263 insertions(+), 1 deletion(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index ffead3adb4..365e8b014c 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -11,6 +11,11 @@ const { const { getVectorDbClass } = require("../../../utils/helpers"); const { multiUserMode, reqBody } = require("../../../utils/http"); const { validApiKey } = require("../../../utils/middleware/validApiKey"); +const { + streamChatWithWorkspace, + writeResponseChunk, + VALID_CHAT_MODE, +} = require("../../../utils/chats/stream"); function apiWorkspaceEndpoints(app) { if (!app) return; @@ -483,7 +488,28 @@ function apiWorkspaceEndpoints(app) { const workspace = await Workspace.get({ slug }); if (!workspace) { - response.sendStatus(400).end(); + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: `Workspace ${slug} is not a valid workspace.`, + }); + return; + } + + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "message parameter cannot be empty." + : `${mode} is not a valid mode.`, + }); return; } @@ -506,6 +532,126 @@ function apiWorkspaceEndpoints(app) { } } ); + + app.post( + "/v1/workspace/:slug/stream-chat", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Execute a streamable chat with a workspace' + #swagger.requestBody = { + description: 'Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + message: "What is AnythingLLM?", + mode: "query | chat" + } + } + } + } + #swagger.responses[200] = { + content: { + "text/event-stream": { + schema: { + type: 'array', + example: [ + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "First chunk", + sources: [], + close: false, + error: "null | text string of the failure mode." + }, + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "chunk two", + sources: [], + close: false, + error: "null | text string of the failure mode." + }, + { + id: 'uuid-123', + type: "abort | textResponseChunk", + textResponse: "final chunk of LLM output!", + sources: [{title: "anythingllm.txt", chunk: "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk."}], + close: true, + error: "null | text string of the failure mode." + } + ] + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug } = request.params; + const { message, mode = "query" } = reqBody(request); + const workspace = await Workspace.get({ slug }); + + if (!workspace) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: `Workspace ${slug} is not a valid workspace.`, + }); + return; + } + + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "Message is empty" + : `${mode} is not a valid mode.`, + }); + return; + } + + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Content-Type", "text/event-stream"); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Connection", "keep-alive"); + response.flushHeaders(); + + await streamChatWithWorkspace(response, workspace, message, mode); + await Telemetry.sendTelemetry("sent_chat", { + LLMSelection: process.env.LLM_PROVIDER || "openai", + Embedder: process.env.EMBEDDING_ENGINE || "inherit", + VectorDbSelection: process.env.VECTOR_DB || "pinecone", + }); + response.end(); + } catch (e) { + console.error(e); + writeResponseChunk(response, { + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: e.message, + }); + response.end(); + } + } + ); } module.exports = { apiWorkspaceEndpoints }; diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index 79fc10132d..adfec0ec3f 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -8,6 +8,7 @@ const { Telemetry } = require("../models/telemetry"); const { streamChatWithWorkspace, writeResponseChunk, + VALID_CHAT_MODE, } = require("../utils/chats/stream"); function chatEndpoints(app) { @@ -31,6 +32,20 @@ function chatEndpoints(app) { return; } + if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { + response.status(400).json({ + id: uuidv4(), + type: "abort", + textResponse: null, + sources: [], + close: true, + error: !message?.length + ? "Message is empty." + : `${mode} is not a valid mode.`, + }); + return; + } + response.setHeader("Cache-Control", "no-cache"); response.setHeader("Content-Type", "text/event-stream"); response.setHeader("Access-Control-Allow-Origin", "*"); diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 7b675c44b9..e7b07484a6 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1612,6 +1612,105 @@ } } }, + "/v1/workspace/{slug}/stream-chat": { + "post": { + "tags": [ + "Workspaces" + ], + "description": "Execute a streamable chat with a workspace", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/event-stream": { + "schema": { + "type": "array", + "example": [ + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "First chunk", + "sources": [], + "close": false, + "error": "null | text string of the failure mode." + }, + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "chunk two", + "sources": [], + "close": false, + "error": "null | text string of the failure mode." + }, + { + "id": "uuid-123", + "type": "abort | textResponseChunk", + "textResponse": "final chunk of LLM output!", + "sources": [ + { + "title": "anythingllm.txt", + "chunk": "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk." + } + ], + "close": true, + "error": "null | text string of the failure mode." + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + } + }, + "requestBody": { + "description": "Send a prompt to the workspace and the type of conversation (query or chat).
Query: Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.
Chat: Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "message": "What is AnythingLLM?", + "mode": "query | chat" + } + } + } + } + } + }, "/v1/system/env-dump": { "get": { "tags": [ diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 11d4effd7c..04bb72b903 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -8,6 +8,7 @@ const { chatPrompt, } = require("."); +const VALID_CHAT_MODE = ["chat", "query"]; function writeResponseChunk(response, data) { response.write(`data: ${JSON.stringify(data)}\n\n`); return; @@ -503,6 +504,7 @@ function handleStreamResponses(response, stream, responseProps) { } module.exports = { + VALID_CHAT_MODE, streamChatWithWorkspace, writeResponseChunk, }; From d0a3f1e3e16e0ff6d7450bec0e2d45a0748da95b Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 13:41:01 -0800 Subject: [PATCH 012/298] Fix present diminsions on vectorDBs to be inferred for providers who require it (#605) --- .../EmbeddingEngines/azureOpenAi/index.js | 1 - .../utils/EmbeddingEngines/localAi/index.js | 4 --- server/utils/EmbeddingEngines/native/index.js | 1 - server/utils/EmbeddingEngines/openAi/index.js | 1 - .../utils/vectorDbProviders/milvus/index.js | 20 ++++++----- .../utils/vectorDbProviders/qdrant/index.js | 34 +++++++++++++------ 6 files changed, 35 insertions(+), 26 deletions(-) diff --git a/server/utils/EmbeddingEngines/azureOpenAi/index.js b/server/utils/EmbeddingEngines/azureOpenAi/index.js index 8cde1fc7ca..e80b4b734b 100644 --- a/server/utils/EmbeddingEngines/azureOpenAi/index.js +++ b/server/utils/EmbeddingEngines/azureOpenAi/index.js @@ -13,7 +13,6 @@ class AzureOpenAiEmbedder { new AzureKeyCredential(process.env.AZURE_OPENAI_KEY) ); this.openai = openai; - this.dimensions = 1536; // Limit of how many strings we can process in a single pass to stay with resource or network limits // https://learn.microsoft.com/en-us/azure/ai-services/openai/faq#i-am-trying-to-use-embeddings-and-received-the-error--invalidrequesterror--too-many-inputs--the-max-number-of-inputs-is-1---how-do-i-fix-this-:~:text=consisting%20of%20up%20to%2016%20inputs%20per%20API%20request diff --git a/server/utils/EmbeddingEngines/localAi/index.js b/server/utils/EmbeddingEngines/localAi/index.js index 6f9d721b9b..1480755d76 100644 --- a/server/utils/EmbeddingEngines/localAi/index.js +++ b/server/utils/EmbeddingEngines/localAi/index.js @@ -16,10 +16,6 @@ class LocalAiEmbedder { : {}), }); this.openai = new OpenAIApi(config); - // We don't know this for user's set model so for vectorDB integrations that requires dimensionality - // in schema, we will throw an error. - // Applies to QDrant and Milvus. - this.dimensions = null; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 50; diff --git a/server/utils/EmbeddingEngines/native/index.js b/server/utils/EmbeddingEngines/native/index.js index 789e51fe9e..fc933e1b84 100644 --- a/server/utils/EmbeddingEngines/native/index.js +++ b/server/utils/EmbeddingEngines/native/index.js @@ -13,7 +13,6 @@ class NativeEmbedder { : path.resolve(__dirname, `../../../storage/models`) ); this.modelPath = path.resolve(this.cacheDir, "Xenova", "all-MiniLM-L6-v2"); - this.dimensions = 384; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 25; diff --git a/server/utils/EmbeddingEngines/openAi/index.js b/server/utils/EmbeddingEngines/openAi/index.js index 31f556e899..105be9d73a 100644 --- a/server/utils/EmbeddingEngines/openAi/index.js +++ b/server/utils/EmbeddingEngines/openAi/index.js @@ -9,7 +9,6 @@ class OpenAiEmbedder { }); const openai = new OpenAIApi(config); this.openai = openai; - this.dimensions = 1536; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 500; diff --git a/server/utils/vectorDbProviders/milvus/index.js b/server/utils/vectorDbProviders/milvus/index.js index a9104784bb..cc934a9a2b 100644 --- a/server/utils/vectorDbProviders/milvus/index.js +++ b/server/utils/vectorDbProviders/milvus/index.js @@ -81,13 +81,15 @@ const Milvus = { await client.dropCollection({ collection_name: namespace }); return true; }, - getOrCreateCollection: async function (client, namespace) { + // Milvus requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { const isExists = await this.namespaceExists(client, namespace); if (!isExists) { - const embedder = getEmbeddingEngineSelection(); - if (!embedder.dimensions) + if (!dimensions) throw new Error( - `Your embedder selection has unknown dimensions output. It should be defined when using ${this.name}. Open an issue on Github for support.` + `Milvus:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` ); await client.createCollection({ @@ -104,7 +106,7 @@ const Milvus = { name: "vector", description: "vector", data_type: DataType.FloatVector, - dim: embedder.dimensions, + dim: dimensions, }, { name: "metadata", @@ -131,6 +133,7 @@ const Milvus = { ) { const { DocumentVectors } = require("../../../models/vectors"); try { + let vectorDimension = null; const { pageContent, docId, ...metadata } = documentData; if (!pageContent || pageContent.length == 0) return false; @@ -138,11 +141,11 @@ const Milvus = { const cacheResult = await cachedVectorInformation(fullFilePath); if (cacheResult.exists) { const { client } = await this.connect(); - await this.getOrCreateCollection(client, namespace); - const { chunks } = cacheResult; const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + await this.getOrCreateCollection(client, namespace, vectorDimension); for (const chunk of chunks) { // Before sending to Pinecone and saving the records to our db // we need to assign the id of each chunk that is stored in the cached file. @@ -182,6 +185,7 @@ const Milvus = { if (!!vectorValues && vectorValues.length > 0) { for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; const vectorRecord = { id: uuidv4(), values: vector, @@ -202,7 +206,7 @@ const Milvus = { if (vectors.length > 0) { const chunks = []; const { client } = await this.connect(); - await this.getOrCreateCollection(client, namespace); + await this.getOrCreateCollection(client, namespace, vectorDimension); console.log("Inserting vectorized chunks into Milvus."); for (const chunk of toChunks(vectors, 100)) { diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js index ddc3408da8..2783cde932 100644 --- a/server/utils/vectorDbProviders/qdrant/index.js +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -108,19 +108,20 @@ const QDrant = { await client.deleteCollection(namespace); return true; }, - getOrCreateCollection: async function (client, namespace) { + // QDrant requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { if (await this.namespaceExists(client, namespace)) { return await client.getCollection(namespace); } - - const embedder = getEmbeddingEngineSelection(); - if (!embedder.dimensions) + if (!dimensions) throw new Error( - `Your embedder selection has unknown dimensions output. It should be defined when using ${this.name}. Open an issue on Github for support.` + `Qdrant:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` ); await client.createCollection(namespace, { vectors: { - size: embedder.dimensions, + size: dimensions, distance: "Cosine", }, }); @@ -133,6 +134,7 @@ const QDrant = { ) { const { DocumentVectors } = require("../../../models/vectors"); try { + let vectorDimension = null; const { pageContent, docId, ...metadata } = documentData; if (!pageContent || pageContent.length == 0) return false; @@ -140,15 +142,20 @@ const QDrant = { const cacheResult = await cachedVectorInformation(fullFilePath); if (cacheResult.exists) { const { client } = await this.connect(); - const collection = await this.getOrCreateCollection(client, namespace); + const { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].vector.length || null; + + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); if (!collection) throw new Error("Failed to create new QDrant collection!", { namespace, }); - const { chunks } = cacheResult; - const documentVectors = []; - for (const chunk of chunks) { const submission = { ids: [], @@ -210,6 +217,7 @@ const QDrant = { if (!!vectorValues && vectorValues.length > 0) { for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; const vectorRecord = { id: uuidv4(), vector: vector, @@ -233,7 +241,11 @@ const QDrant = { } const { client } = await this.connect(); - const collection = await this.getOrCreateCollection(client, namespace); + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); if (!collection) throw new Error("Failed to create new QDrant collection!", { namespace, From 8b11288764ac9ce407283717daa819cbab3caa57 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 16 Jan 2024 13:43:32 -0800 Subject: [PATCH 013/298] =?UTF-8?q?truncate=20title=20to=20shorter=20lengt?= =?UTF-8?q?h=20so=20x=20button=20is=20not=20hidden=20for=20longer=E2=80=A6?= =?UTF-8?q?=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * truncate title to shorter length so x button is not hidden for longer title names in the citation modal * absolutely position x button on citation modal --- .../ChatHistory/Citation/index.jsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index 1146639ae2..c4bda294cc 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -119,21 +119,19 @@ function CitationDetailModal({ source, onClose }) { className="bg-transparent outline-none fixed top-0 left-0 w-full h-full flex items-center justify-center z-10" >
-
-
-

- {truncate(title, 52)} -

- {references > 1 && ( -

- Referenced {references} times. -

- )} -
+
+

+ {truncate(title, 45)} +

+ {references > 1 && ( +

+ Referenced {references} times. +

+ )} @@ -159,6 +157,7 @@ function CitationDetailModal({ source, onClose }) { ); } + function truncateMiddle(title) { if (title.length <= 18) return title; From e973c1edbf42260f9643764e16fac864c70efbc4 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 14:15:46 -0800 Subject: [PATCH 014/298] Preload onboarding (#606) * no lazy onboarding * no lazy onboarding steps * Do not lazy load onboarding to prevent lazy-load white flash --- frontend/src/App.jsx | 3 +- .../src/pages/OnboardingFlow/Steps/index.jsx | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fa74d434e4..7c14be4d9d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,6 +8,8 @@ import PrivateRoute, { import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import Login from "@/pages/Login"; +import OnboardingFlow from "@/pages/OnboardingFlow"; + import { PfpProvider } from "./PfpContext"; import { LogoProvider } from "./LogoContext"; @@ -42,7 +44,6 @@ const DataConnectors = lazy( const DataConnectorSetup = lazy( () => import("@/pages/GeneralSettings/DataConnectors/Connectors") ); -const OnboardingFlow = lazy(() => import("@/pages/OnboardingFlow")); export default function App() { return ( diff --git a/frontend/src/pages/OnboardingFlow/Steps/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/index.jsx index 3f218d531b..957d94a4af 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/index.jsx @@ -1,16 +1,26 @@ import { ArrowLeft, ArrowRight } from "@phosphor-icons/react"; -import { lazy, useState } from "react"; +import { useState } from "react"; import { isMobile } from "react-device-detect"; +import Home from "./Home"; +import LLMPreference from "./LLMPreference"; +import EmbeddingPreference from "./EmbeddingPreference"; +import VectorDatabaseConnection from "./VectorDatabaseConnection"; +import CustomLogo from "./CustomLogo"; +import UserSetup from "./UserSetup"; +import DataHandling from "./DataHandling"; +import Survey from "./Survey"; +import CreateWorkspace from "./CreateWorkspace"; + const OnboardingSteps = { - home: lazy(() => import("./Home")), - "llm-preference": lazy(() => import("./LLMPreference")), - "embedding-preference": lazy(() => import("./EmbeddingPreference")), - "vector-database": lazy(() => import("./VectorDatabaseConnection")), - "custom-logo": lazy(() => import("./CustomLogo")), - "user-setup": lazy(() => import("./UserSetup")), - "data-handling": lazy(() => import("./DataHandling")), - survey: lazy(() => import("./Survey")), - "create-workspace": lazy(() => import("./CreateWorkspace")), + home: Home, + "llm-preference": LLMPreference, + "embedding-preference": EmbeddingPreference, + "vector-database": VectorDatabaseConnection, + "custom-logo": CustomLogo, + "user-setup": UserSetup, + "data-handling": DataHandling, + survey: Survey, + "create-workspace": CreateWorkspace, }; export default OnboardingSteps; From c61cbd1502e900ebb421bc532647d548f41162d5 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 14:58:49 -0800 Subject: [PATCH 015/298] Add support for fetching single document in documents folder (#607) --- .../ChatHistory/Citation/index.jsx | 1 - server/endpoints/api/document/index.js | 60 ++++++++++++++- server/swagger/openapi.json | 75 +++++++++++++++++++ server/utils/files/index.js | 38 ++++++++++ 4 files changed, 172 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index c4bda294cc..9af36fc5a5 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -157,7 +157,6 @@ function CitationDetailModal({ source, onClose }) { ); } - function truncateMiddle(title) { if (title.length <= 18) return title; diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js index a813e2df6d..f1282e7c24 100644 --- a/server/endpoints/api/document/index.js +++ b/server/endpoints/api/document/index.js @@ -6,7 +6,10 @@ const { acceptedFileTypes, processDocument, } = require("../../../utils/files/documentProcessor"); -const { viewLocalFiles } = require("../../../utils/files"); +const { + viewLocalFiles, + findDocumentInDocuments, +} = require("../../../utils/files"); const { handleUploads } = setupMulter(); function apiDocumentEndpoints(app) { @@ -133,6 +136,61 @@ function apiDocumentEndpoints(app) { } }); + app.get("/v1/document/:docName", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Get a single document by its unique AnythingLLM document name' + #swagger.parameters['docName'] = { + in: 'path', + description: 'Unique document name to find (name in /documents)', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "localFiles": { + "name": "documents", + "type": "folder", + items: [ + { + "name": "my-stored-document.txt-uuid1234.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + }, + ] + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { docName } = request.params; + const document = await findDocumentInDocuments(docName); + if (!document) { + response.sendStatus(404).end(); + return; + } + response.status(200).json({ document }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + app.get( "/v1/document/accepted-file-types", [validApiKey], diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index e7b07484a6..7d91579fd0 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -953,6 +953,81 @@ } } }, + "/v1/document/{docName}": { + "get": { + "tags": [ + "Documents" + ], + "description": "Get a single document by its unique AnythingLLM document name", + "parameters": [ + { + "name": "docName", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique document name to find (name in /documents)" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "localFiles": { + "name": "documents", + "type": "folder", + "items": [ + { + "name": "my-stored-document.txt-uuid1234.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + } + ] + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/v1/document/accepted-file-types": { "get": { "tags": [ diff --git a/server/utils/files/index.js b/server/utils/files/index.js index 2ff1d60cc1..e713a318ad 100644 --- a/server/utils/files/index.js +++ b/server/utils/files/index.js @@ -157,11 +157,49 @@ async function purgeVectorCache(filename = null) { return; } +// Search for a specific document by its unique name in the entire `documents` +// folder via iteration of all folders and checking if the expected file exists. +async function findDocumentInDocuments(documentName = null) { + if (!documentName) return null; + const documentsFolder = + process.env.NODE_ENV === "development" + ? path.resolve(__dirname, `../../storage/documents`) + : path.resolve(process.env.STORAGE_DIR, `documents`); + + for (const folder of fs.readdirSync(documentsFolder)) { + const isFolder = fs + .lstatSync(path.join(documentsFolder, folder)) + .isDirectory(); + if (!isFolder) continue; + + const targetFilename = normalizePath(documentName); + const targetFileLocation = path.join( + documentsFolder, + folder, + targetFilename + ); + if (!fs.existsSync(targetFileLocation)) continue; + + const fileData = fs.readFileSync(targetFileLocation, "utf8"); + const cachefilename = `${folder}/${targetFilename}`; + const { pageContent, ...metadata } = JSON.parse(fileData); + return { + name: targetFilename, + type: "file", + ...metadata, + cached: await cachedVectorInformation(cachefilename, true), + }; + } + + return null; +} + function normalizePath(filepath = "") { return path.normalize(filepath).replace(/^(\.\.(\/|\\|$))+/, ""); } module.exports = { + findDocumentInDocuments, cachedVectorInformation, viewLocalFiles, purgeSourceDocument, From b35feede879c543e2d6cb58c89f973b29073ecc0 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Jan 2024 16:04:22 -0800 Subject: [PATCH 016/298] 570 document api return object (#608) * Add support for fetching single document in documents folder * Add document object to upload + support link scraping via API * hotfixes for documentation * update api docs --- collector/index.js | 16 ++- collector/processLink/convert/generic.js | 13 +- .../processSingleFile/convert/asAudio.js | 14 +- collector/processSingleFile/convert/asDocx.js | 13 +- collector/processSingleFile/convert/asMbox.js | 15 ++- .../processSingleFile/convert/asOfficeMime.js | 13 +- collector/processSingleFile/convert/asPDF.js | 13 +- collector/processSingleFile/convert/asTxt.js | 13 +- collector/processSingleFile/index.js | 4 + collector/utils/files/index.js | 19 ++- server/endpoints/api/document/index.js | 121 +++++++++++++++++- server/endpoints/api/workspace/index.js | 4 +- server/swagger/openapi.json | 105 ++++++++++++++- server/utils/files/documentProcessor.js | 4 +- 14 files changed, 324 insertions(+), 43 deletions(-) diff --git a/collector/index.js b/collector/index.js index 5070ae72f8..062d78959a 100644 --- a/collector/index.js +++ b/collector/index.js @@ -29,14 +29,21 @@ app.post("/process", async function (request, response) { const targetFilename = path .normalize(filename) .replace(/^(\.\.(\/|\\|$))+/, ""); - const { success, reason } = await processSingleFile(targetFilename); - response.status(200).json({ filename: targetFilename, success, reason }); + const { + success, + reason, + documents = [], + } = await processSingleFile(targetFilename); + response + .status(200) + .json({ filename: targetFilename, success, reason, documents }); } catch (e) { console.error(e); response.status(200).json({ filename: filename, success: false, reason: "A processing error occurred.", + documents: [], }); } return; @@ -45,14 +52,15 @@ app.post("/process", async function (request, response) { app.post("/process-link", async function (request, response) { const { link } = reqBody(request); try { - const { success, reason } = await processLink(link); - response.status(200).json({ url: link, success, reason }); + const { success, reason, documents = [] } = await processLink(link); + response.status(200).json({ url: link, success, reason, documents }); } catch (e) { console.error(e); response.status(200).json({ url: link, success: false, reason: "A processing error occurred.", + documents: [], }); } return; diff --git a/collector/processLink/convert/generic.js b/collector/processLink/convert/generic.js index f42dcd171d..c6431d7339 100644 --- a/collector/processLink/convert/generic.js +++ b/collector/processLink/convert/generic.js @@ -12,7 +12,11 @@ async function scrapeGenericUrl(link) { if (!content.length) { console.error(`Resulting URL content was empty at ${link}.`); - return { success: false, reason: `No URL content found at ${link}.` }; + return { + success: false, + reason: `No URL content found at ${link}.`, + documents: [], + }; } const url = new URL(link); @@ -32,9 +36,12 @@ async function scrapeGenericUrl(link) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `url-${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `url-${slugify(filename)}-${data.id}` + ); console.log(`[SUCCESS]: URL ${link} converted & ready for embedding.\n`); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } async function getPageContent(link) { diff --git a/collector/processSingleFile/convert/asAudio.js b/collector/processSingleFile/convert/asAudio.js index a15207fba4..7688d7b850 100644 --- a/collector/processSingleFile/convert/asAudio.js +++ b/collector/processSingleFile/convert/asAudio.js @@ -31,6 +31,7 @@ async function asAudio({ fullFilePath = "", filename = "" }) { return { success: false, reason: `Failed to parse content from ${filename}.`, + documents: [], }; } @@ -43,7 +44,11 @@ async function asAudio({ fullFilePath = "", filename = "" }) { if (!content.length) { console.error(`Resulting text content was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No text content found in ${filename}.` }; + return { + success: false, + reason: `No text content found in ${filename}.`, + documents: [], + }; } const data = { @@ -60,12 +65,15 @@ async function asAudio({ fullFilePath = "", filename = "" }) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}` + ); trashFile(fullFilePath); console.log( `[SUCCESS]: ${filename} transcribed, converted & ready for embedding.\n` ); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } async function convertToWavAudioData(sourcePath) { diff --git a/collector/processSingleFile/convert/asDocx.js b/collector/processSingleFile/convert/asDocx.js index 7a64a042d1..b4fe7d2c95 100644 --- a/collector/processSingleFile/convert/asDocx.js +++ b/collector/processSingleFile/convert/asDocx.js @@ -24,7 +24,11 @@ async function asDocX({ fullFilePath = "", filename = "" }) { if (!pageContent.length) { console.error(`Resulting text content was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No text content found in ${filename}.` }; + return { + success: false, + reason: `No text content found in ${filename}.`, + documents: [], + }; } const content = pageContent.join(""); @@ -42,10 +46,13 @@ async function asDocX({ fullFilePath = "", filename = "" }) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}` + ); trashFile(fullFilePath); console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\n`); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } module.exports = asDocX; diff --git a/collector/processSingleFile/convert/asMbox.js b/collector/processSingleFile/convert/asMbox.js index 30883f21b1..f62f6b2bad 100644 --- a/collector/processSingleFile/convert/asMbox.js +++ b/collector/processSingleFile/convert/asMbox.js @@ -22,10 +22,15 @@ async function asMbox({ fullFilePath = "", filename = "" }) { if (!mails.length) { console.error(`Resulting mail items was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No mail items found in ${filename}.` }; + return { + success: false, + reason: `No mail items found in ${filename}.`, + documents: [], + }; } let item = 1; + const documents = []; for (const mail of mails) { if (!mail.hasOwnProperty("text")) continue; @@ -52,14 +57,18 @@ async function asMbox({ fullFilePath = "", filename = "" }) { }; item++; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}-msg-${item}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}-msg-${item}` + ); + documents.push(document); } trashFile(fullFilePath); console.log( `[SUCCESS]: ${filename} messages converted & ready for embedding.\n` ); - return { success: true, reason: null }; + return { success: true, reason: null, documents }; } module.exports = asMbox; diff --git a/collector/processSingleFile/convert/asOfficeMime.js b/collector/processSingleFile/convert/asOfficeMime.js index a6eb0351a7..45b316610e 100644 --- a/collector/processSingleFile/convert/asOfficeMime.js +++ b/collector/processSingleFile/convert/asOfficeMime.js @@ -20,7 +20,11 @@ async function asOfficeMime({ fullFilePath = "", filename = "" }) { if (!content.length) { console.error(`Resulting text content was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No text content found in ${filename}.` }; + return { + success: false, + reason: `No text content found in ${filename}.`, + documents: [], + }; } const data = { @@ -37,10 +41,13 @@ async function asOfficeMime({ fullFilePath = "", filename = "" }) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}` + ); trashFile(fullFilePath); console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\n`); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } module.exports = asOfficeMime; diff --git a/collector/processSingleFile/convert/asPDF.js b/collector/processSingleFile/convert/asPDF.js index f6d869d5c4..b89b974113 100644 --- a/collector/processSingleFile/convert/asPDF.js +++ b/collector/processSingleFile/convert/asPDF.js @@ -29,7 +29,11 @@ async function asPDF({ fullFilePath = "", filename = "" }) { if (!pageContent.length) { console.error(`Resulting text content was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No text content found in ${filename}.` }; + return { + success: false, + reason: `No text content found in ${filename}.`, + documents: [], + }; } const content = pageContent.join(""); @@ -47,10 +51,13 @@ async function asPDF({ fullFilePath = "", filename = "" }) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}` + ); trashFile(fullFilePath); console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\n`); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } module.exports = asPDF; diff --git a/collector/processSingleFile/convert/asTxt.js b/collector/processSingleFile/convert/asTxt.js index ad35e54762..cf7260d4b0 100644 --- a/collector/processSingleFile/convert/asTxt.js +++ b/collector/processSingleFile/convert/asTxt.js @@ -19,7 +19,11 @@ async function asTxt({ fullFilePath = "", filename = "" }) { if (!content?.length) { console.error(`Resulting text content was empty for ${filename}.`); trashFile(fullFilePath); - return { success: false, reason: `No text content found in ${filename}.` }; + return { + success: false, + reason: `No text content found in ${filename}.`, + documents: [], + }; } console.log(`-- Working ${filename} --`); @@ -37,10 +41,13 @@ async function asTxt({ fullFilePath = "", filename = "" }) { token_count_estimate: tokenizeString(content).length, }; - writeToServerDocuments(data, `${slugify(filename)}-${data.id}`); + const document = writeToServerDocuments( + data, + `${slugify(filename)}-${data.id}` + ); trashFile(fullFilePath); console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\n`); - return { success: true, reason: null }; + return { success: true, reason: null, documents: [document] }; } module.exports = asTxt; diff --git a/collector/processSingleFile/index.js b/collector/processSingleFile/index.js index 37c9fd5c54..9efd3a70f8 100644 --- a/collector/processSingleFile/index.js +++ b/collector/processSingleFile/index.js @@ -13,11 +13,13 @@ async function processSingleFile(targetFilename) { return { success: false, reason: "Filename is a reserved filename and cannot be processed.", + documents: [], }; if (!fs.existsSync(fullFilePath)) return { success: false, reason: "File does not exist in upload directory.", + documents: [], }; const fileExtension = path.extname(fullFilePath).toLowerCase(); @@ -25,6 +27,7 @@ async function processSingleFile(targetFilename) { return { success: false, reason: `No file extension found. This file cannot be processed.`, + documents: [], }; } @@ -33,6 +36,7 @@ async function processSingleFile(targetFilename) { return { success: false, reason: `File extension ${fileExtension} not supported for parsing.`, + documents: [], }; } diff --git a/collector/utils/files/index.js b/collector/utils/files/index.js index 915c4ac10a..caf33c888a 100644 --- a/collector/utils/files/index.js +++ b/collector/utils/files/index.js @@ -38,14 +38,19 @@ function writeToServerDocuments( ); if (!fs.existsSync(destination)) fs.mkdirSync(destination, { recursive: true }); - const destinationFilePath = path.resolve(destination, filename); + const destinationFilePath = path.resolve(destination, filename) + ".json"; - fs.writeFileSync( - destinationFilePath + ".json", - JSON.stringify(data, null, 4), - { encoding: "utf-8" } - ); - return; + fs.writeFileSync(destinationFilePath, JSON.stringify(data, null, 4), { + encoding: "utf-8", + }); + + return { + ...data, + // relative location string that can be passed into the /update-embeddings api + // that will work since we know the location exists and since we only allow + // 1-level deep folders this will always work. This still works for integrations like GitHub and YouTube. + location: destinationFilePath.split("/").slice(-2).join("/"), + }; } // When required we can wipe the entire collector hotdir and tmp storage in case diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js index f1282e7c24..817043526b 100644 --- a/server/endpoints/api/document/index.js +++ b/server/endpoints/api/document/index.js @@ -5,11 +5,13 @@ const { checkProcessorAlive, acceptedFileTypes, processDocument, + processLink, } = require("../../../utils/files/documentProcessor"); const { viewLocalFiles, findDocumentInDocuments, } = require("../../../utils/files"); +const { reqBody } = require("../../../utils/http"); const { handleUploads } = setupMulter(); function apiDocumentEndpoints(app) { @@ -23,7 +25,6 @@ function apiDocumentEndpoints(app) { /* #swagger.tags = ['Documents'] #swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.' - #swagger.requestBody = { description: 'File to be uploaded.', required: true, @@ -50,6 +51,21 @@ function apiDocumentEndpoints(app) { example: { success: true, error: null, + documents: [ + { + "location": "custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "name": "anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "url": "file:///Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt", + "title": "anythingllm.txt", + "docAuthor": "Unknown", + "description": "Unknown", + "docSource": "a text file uploaded by the user.", + "chunkSource": "anythingllm.txt", + "published": "1/16/2024, 3:07:00 PM", + "wordCount": 93, + "token_count_estimate": 115, + } + ] } } } @@ -75,16 +91,113 @@ function apiDocumentEndpoints(app) { .end(); } - const { success, reason } = await processDocument(originalname); + const { success, reason, documents } = + await processDocument(originalname); if (!success) { - response.status(500).json({ success: false, error: reason }).end(); + response + .status(500) + .json({ success: false, error: reason, documents }) + .end(); + return; } console.log( `Document ${originalname} uploaded processed and successfully. It is now available in documents.` ); await Telemetry.sendTelemetry("document_uploaded"); - response.status(200).json({ success: true, error: null }); + response.status(200).json({ success: true, error: null, documents }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/document/upload-link", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Upload a valid URL for AnythingLLM to scrape and prepare for embedding.' + #swagger.requestBody = { + description: 'Link of web address to be scraped.', + required: true, + type: 'file', + content: { + "application/json": { + schema: { + type: 'object', + example: { + "link": "https://useanything.com" + } + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + documents: [ + { + "id": "c530dbe6-bff1-4b9e-b87f-710d539d20bc", + "url": "file://useanything_com.html", + "title": "useanything_com.html", + "docAuthor": "no author found", + "description": "No description found.", + "docSource": "URL link uploaded by the user.", + "chunkSource": "https:useanything.com.html", + "published": "1/16/2024, 3:46:33 PM", + "wordCount": 252, + "pageContent": "AnythingLLM is the best....", + "token_count_estimate": 447, + "location": "custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json" + } + ] + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { link } = reqBody(request); + const processingOnline = await checkProcessorAlive(); + + if (!processingOnline) { + response + .status(500) + .json({ + success: false, + error: `Document processing API is not online. Link ${link} will not be processed automatically.`, + }) + .end(); + } + + const { success, reason, documents } = await processLink(link); + if (!success) { + response + .status(500) + .json({ success: false, error: reason, documents }) + .end(); + return; + } + + console.log( + `Link ${link} uploaded processed and successfully. It is now available in documents.` + ); + await Telemetry.sendTelemetry("document_uploaded"); + response.status(200).json({ success: true, error: null, documents }); } catch (e) { console.log(e.message, e); response.sendStatus(500).end(); diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index 365e8b014c..c1642ce4aa 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -381,8 +381,8 @@ function apiWorkspaceEndpoints(app) { content: { "application/json": { example: { - adds: [], - deletes: ["custom-documents/anythingllm-hash.json"] + adds: ["custom-documents/my-pdf.pdf-hash.json"], + deletes: ["custom-documents/anythingllm.txt-hash.json"] } } } diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 7d91579fd0..c7532059d1 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -845,7 +845,22 @@ "type": "object", "example": { "success": true, - "error": null + "error": null, + "documents": [ + { + "location": "custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "name": "anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json", + "url": "file://Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt", + "title": "anythingllm.txt", + "docAuthor": "Unknown", + "description": "Unknown", + "docSource": "a text file uploaded by the user.", + "chunkSource": "anythingllm.txt", + "published": "1/16/2024, 3:07:00 PM", + "wordCount": 93, + "token_count_estimate": 115 + } + ] } } } @@ -890,6 +905,88 @@ } } }, + "/v1/document/upload-link": { + "post": { + "tags": [ + "Documents" + ], + "description": "Upload a valid URL for AnythingLLM to scrape and prepare for embedding.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null, + "documents": [ + { + "id": "c530dbe6-bff1-4b9e-b87f-710d539d20bc", + "url": "file://useanything_com.html", + "title": "useanything_com.html", + "docAuthor": "no author found", + "description": "No description found.", + "docSource": "URL link uploaded by the user.", + "chunkSource": "https:useanything.com.html", + "published": "1/16/2024, 3:46:33 PM", + "wordCount": 252, + "pageContent": "AnythingLLM is the best....", + "token_count_estimate": 447, + "location": "custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json" + } + ] + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Link of web address to be scraped.", + "required": true, + "type": "file", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "link": "https://useanything.com" + } + } + } + } + } + } + }, "/v1/documents": { "get": { "tags": [ @@ -1593,9 +1690,11 @@ "content": { "application/json": { "example": { - "adds": [], + "adds": [ + "custom-documents/my-pdf.pdf-hash.json" + ], "deletes": [ - "custom-documents/anythingllm-hash.json" + "custom-documents/anythingllm.txt-hash.json" ] } } diff --git a/server/utils/files/documentProcessor.js b/server/utils/files/documentProcessor.js index 5239a87089..27d0f5f2bb 100644 --- a/server/utils/files/documentProcessor.js +++ b/server/utils/files/documentProcessor.js @@ -35,7 +35,7 @@ async function processDocument(filename = "") { .then((res) => res) .catch((e) => { console.log(e.message); - return { success: false, reason: e.message }; + return { success: false, reason: e.message, documents: [] }; }); } @@ -55,7 +55,7 @@ async function processLink(link = "") { .then((res) => res) .catch((e) => { console.log(e.message); - return { success: false, reason: e.message }; + return { success: false, reason: e.message, documents: [] }; }); } From bf503ee0e9c9e8fe0164090612a6315a4debeb29 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 16 Jan 2024 18:23:51 -0800 Subject: [PATCH 017/298] add check to skip empty messages (#602) * add check to skip empty messages * add comment explaining prisma + sqlite not supporting createMany() --- server/models/welcomeMessages.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/models/welcomeMessages.js b/server/models/welcomeMessages.js index 43e2d3f964..88393f36cd 100644 --- a/server/models/welcomeMessages.js +++ b/server/models/welcomeMessages.js @@ -31,7 +31,10 @@ const WelcomeMessages = { await prisma.welcome_messages.deleteMany({}); // Delete all existing messages // Create new messages + // We create each message individually because prisma + // with sqlite does not support createMany() for (const [index, message] of messages.entries()) { + if (!message.response) continue; await prisma.welcome_messages.create({ data: { user: message.user, From 90df37582bcccba53282420eb61c8038c4699609 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 17 Jan 2024 12:59:25 -0800 Subject: [PATCH 018/298] Per workspace model selection (#582) * WIP model selection per workspace (migrations and openai saves properly * revert OpenAiOption * add support for models per workspace for anthropic, localAi, ollama, openAi, and togetherAi * remove unneeded comments * update logic for when LLMProvider is reset, reset Ai provider files with master * remove frontend/api reset of workspace chat and move logic to updateENV add postUpdate callbacks to envs * set preferred model for chat on class instantiation * remove extra param * linting * remove unused var * refactor chat model selection on workspace * linting * add fallback for base path to localai models --------- Co-authored-by: timothycarambat --- .../Settings/ChatModelPreference/index.jsx | 120 ++++++++++++++++++ .../useGetProviderModels.js | 49 +++++++ .../Modals/MangeWorkspace/Settings/index.jsx | 8 +- .../Modals/MangeWorkspace/index.jsx | 1 + .../GeneralSettings/LLMPreference/index.jsx | 6 +- server/endpoints/api/system/index.js | 2 +- server/endpoints/system.js | 6 +- server/models/workspace.js | 15 +++ .../20240113013409_init/migration.sql | 2 + server/prisma/schema.prisma | 1 + server/utils/AiProviders/anthropic/index.js | 5 +- server/utils/AiProviders/azureOpenAi/index.js | 2 +- server/utils/AiProviders/gemini/index.js | 5 +- server/utils/AiProviders/lmStudio/index.js | 4 +- server/utils/AiProviders/localAi/index.js | 4 +- server/utils/AiProviders/native/index.js | 4 +- server/utils/AiProviders/ollama/index.js | 4 +- server/utils/AiProviders/openAi/index.js | 5 +- server/utils/AiProviders/togetherAi/index.js | 4 +- server/utils/chats/index.js | 2 +- server/utils/chats/stream.js | 2 +- server/utils/helpers/customModels.js | 13 +- server/utils/helpers/index.js | 20 +-- server/utils/helpers/updateENV.js | 32 +++-- 24 files changed, 263 insertions(+), 53 deletions(-) create mode 100644 frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/index.jsx create mode 100644 frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js create mode 100644 server/prisma/migrations/20240113013409_init/migration.sql diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/index.jsx new file mode 100644 index 0000000000..ea03c09a96 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/index.jsx @@ -0,0 +1,120 @@ +import useGetProviderModels, { + DISABLED_PROVIDERS, +} from "./useGetProviderModels"; + +export default function ChatModelSelection({ + settings, + workspace, + setHasChanges, +}) { + const { defaultModels, customModels, loading } = useGetProviderModels( + settings?.LLMProvider + ); + if (DISABLED_PROVIDERS.includes(settings?.LLMProvider)) return null; + + if (loading) { + return ( +
+
+ +

+ The specific chat model that will be used for this workspace. If + empty, will use the system LLM preference. +

+
+ +
+ ); + } + + return ( +
+
+ +

+ The specific chat model that will be used for this workspace. If + empty, will use the system LLM preference. +

+
+ + +
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js new file mode 100644 index 0000000000..eae1b4adcb --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js @@ -0,0 +1,49 @@ +import System from "@/models/system"; +import { useEffect, useState } from "react"; + +// Providers which cannot use this feature for workspace<>model selection +export const DISABLED_PROVIDERS = ["azure", "lmstudio"]; +const PROVIDER_DEFAULT_MODELS = { + openai: ["gpt-3.5-turbo", "gpt-4", "gpt-4-1106-preview", "gpt-4-32k"], + gemini: ["gemini-pro"], + anthropic: ["claude-2", "claude-instant-1"], + azure: [], + lmstudio: [], + localai: [], + ollama: [], + togetherai: [], + native: [], +}; + +// For togetherAi, which has a large model list - we subgroup the options +// by their creator organization (eg: Meta, Mistral, etc) +// which makes selection easier to read. +function groupModels(models) { + return models.reduce((acc, model) => { + acc[model.organization] = acc[model.organization] || []; + acc[model.organization].push(model); + return acc; + }, {}); +} + +export default function useGetProviderModels(provider = null) { + const [defaultModels, setDefaultModels] = useState([]); + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchProviderModels() { + if (!provider) return; + const { models = [] } = await System.customModels(provider); + if (PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider)) + setDefaultModels(PROVIDER_DEFAULT_MODELS[provider]); + provider === "togetherai" + ? setCustomModels(groupModels(models)) + : setCustomModels(models); + setLoading(false); + } + fetchProviderModels(); + }, [provider]); + + return { defaultModels, customModels, loading }; +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx index 2fce91e1f8..a3089d6884 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -6,6 +6,7 @@ import System from "../../../../models/system"; import PreLoader from "../../../Preloader"; import { useParams } from "react-router-dom"; import showToast from "../../../../utils/toast"; +import ChatModelPreference from "./ChatModelPreference"; // Ensure that a type is correct before sending the body // to the backend. @@ -26,7 +27,7 @@ function castToType(key, value) { return definitions[key].cast(value); } -export default function WorkspaceSettings({ active, workspace }) { +export default function WorkspaceSettings({ active, workspace, settings }) { const { slug } = useParams(); const formEl = useRef(null); const [saving, setSaving] = useState(false); @@ -99,6 +100,11 @@ export default function WorkspaceSettings({ active, workspace }) {
+
diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 287716222e..bd6ae511dc 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -30,19 +30,17 @@ export default function GeneralLLMPreference() { const [hasChanges, setHasChanges] = useState(false); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); const [filteredLLMs, setFilteredLLMs] = useState([]); const [selectedLLM, setSelectedLLM] = useState(null); - const isHosted = window.location.hostname.includes("useanything.com"); const handleSubmit = async (e) => { e.preventDefault(); const form = e.target; - const data = {}; + const data = { LLMProvider: selectedLLM }; const formData = new FormData(form); - data.LLMProvider = selectedLLM; + for (var [key, value] of formData.entries()) data[key] = value; const { error } = await System.updateSystem(data); setSaving(true); diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js index 3548c3068a..b18019b147 100644 --- a/server/endpoints/api/system/index.js +++ b/server/endpoints/api/system/index.js @@ -139,7 +139,7 @@ function apiSystemEndpoints(app) { */ try { const body = reqBody(request); - const { newValues, error } = updateENV(body); + const { newValues, error } = await updateENV(body); if (process.env.NODE_ENV === "production") await dumpENV(); response.status(200).json({ newValues, error }); } catch (e) { diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 15db895ad9..e699cf84ca 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -290,7 +290,7 @@ function systemEndpoints(app) { } const body = reqBody(request); - const { newValues, error } = updateENV(body); + const { newValues, error } = await updateENV(body); if (process.env.NODE_ENV === "production") await dumpENV(); response.status(200).json({ newValues, error }); } catch (e) { @@ -312,7 +312,7 @@ function systemEndpoints(app) { } const { usePassword, newPassword } = reqBody(request); - const { error } = updateENV( + const { error } = await updateENV( { AuthToken: usePassword ? newPassword : "", JWTSecret: usePassword ? v4() : "", @@ -355,7 +355,7 @@ function systemEndpoints(app) { message_limit: 25, }); - updateENV( + await updateENV( { AuthToken: "", JWTSecret: process.env.JWT_SECRET || v4(), diff --git a/server/models/workspace.js b/server/models/workspace.js index 9139c25e9d..6de8053e95 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -14,6 +14,7 @@ const Workspace = { "lastUpdatedAt", "openAiPrompt", "similarityThreshold", + "chatModel", ], new: async function (name = null, creatorId = null) { @@ -191,6 +192,20 @@ const Workspace = { return { success: false, error: error.message }; } }, + + resetWorkspaceChatModels: async () => { + try { + await prisma.workspaces.updateMany({ + data: { + chatModel: null, + }, + }); + return { success: true, error: null }; + } catch (error) { + console.error("Error resetting workspace chat models:", error.message); + return { success: false, error: error.message }; + } + }, }; module.exports = { Workspace }; diff --git a/server/prisma/migrations/20240113013409_init/migration.sql b/server/prisma/migrations/20240113013409_init/migration.sql new file mode 100644 index 0000000000..09b9448ec8 --- /dev/null +++ b/server/prisma/migrations/20240113013409_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "chatModel" TEXT; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index e9aa8a8a51..2f632a46ab 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -93,6 +93,7 @@ model workspaces { lastUpdatedAt DateTime @default(now()) openAiPrompt String? similarityThreshold Float? @default(0.25) + chatModel String? workspace_users workspace_users[] documents workspace_documents[] } diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 709333231c..17f2abc4ac 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -2,7 +2,7 @@ const { v4 } = require("uuid"); const { chatPrompt } = require("../../chats"); class AnthropicLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.ANTHROPIC_API_KEY) throw new Error("No Anthropic API key was set."); @@ -12,7 +12,8 @@ class AnthropicLLM { apiKey: process.env.ANTHROPIC_API_KEY, }); this.anthropic = anthropic; - this.model = process.env.ANTHROPIC_MODEL_PREF || "claude-2"; + this.model = + modelPreference || process.env.ANTHROPIC_MODEL_PREF || "claude-2"; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/azureOpenAi/index.js b/server/utils/AiProviders/azureOpenAi/index.js index 185dac0219..f59fc51fa1 100644 --- a/server/utils/AiProviders/azureOpenAi/index.js +++ b/server/utils/AiProviders/azureOpenAi/index.js @@ -2,7 +2,7 @@ const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi"); const { chatPrompt } = require("../../chats"); class AzureOpenAiLLM { - constructor(embedder = null) { + constructor(embedder = null, _modelPreference = null) { const { OpenAIClient, AzureKeyCredential } = require("@azure/openai"); if (!process.env.AZURE_OPENAI_ENDPOINT) throw new Error("No Azure API endpoint was set."); diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 03388e3e20..348c8f5ed4 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -1,14 +1,15 @@ const { chatPrompt } = require("../../chats"); class GeminiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.GEMINI_API_KEY) throw new Error("No Gemini API key was set."); // Docs: https://ai.google.dev/tutorials/node_quickstart const { GoogleGenerativeAI } = require("@google/generative-ai"); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); - this.model = process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro"; + this.model = + modelPreference || process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro"; this.gemini = genAI.getGenerativeModel({ model: this.model }); this.limits = { history: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js index 28c107df08..614808034c 100644 --- a/server/utils/AiProviders/lmStudio/index.js +++ b/server/utils/AiProviders/lmStudio/index.js @@ -2,7 +2,7 @@ const { chatPrompt } = require("../../chats"); // hybrid of openAi LLM chat completion for LMStudio class LMStudioLLM { - constructor(embedder = null) { + constructor(embedder = null, _modelPreference = null) { if (!process.env.LMSTUDIO_BASE_PATH) throw new Error("No LMStudio API Base Path was set."); @@ -12,7 +12,7 @@ class LMStudioLLM { }); this.lmstudio = new OpenAIApi(config); // When using LMStudios inference server - the model param is not required so - // we can stub it here. + // we can stub it here. LMStudio can only run one model at a time. this.model = "model-placeholder"; this.limits = { history: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/localAi/index.js b/server/utils/AiProviders/localAi/index.js index 84954c9942..6623ac88ee 100644 --- a/server/utils/AiProviders/localAi/index.js +++ b/server/utils/AiProviders/localAi/index.js @@ -1,7 +1,7 @@ const { chatPrompt } = require("../../chats"); class LocalAiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.LOCAL_AI_BASE_PATH) throw new Error("No LocalAI Base Path was set."); @@ -15,7 +15,7 @@ class LocalAiLLM { : {}), }); this.openai = new OpenAIApi(config); - this.model = process.env.LOCAL_AI_MODEL_PREF; + this.model = modelPreference || process.env.LOCAL_AI_MODEL_PREF; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index faac4fa030..66cc84d0ca 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -10,11 +10,11 @@ const ChatLlamaCpp = (...args) => ); class NativeLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.NATIVE_LLM_MODEL_PREF) throw new Error("No local Llama model was set."); - this.model = process.env.NATIVE_LLM_MODEL_PREF || null; + this.model = modelPreference || process.env.NATIVE_LLM_MODEL_PREF || null; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/ollama/index.js b/server/utils/AiProviders/ollama/index.js index 55205c23d9..fce96f3698 100644 --- a/server/utils/AiProviders/ollama/index.js +++ b/server/utils/AiProviders/ollama/index.js @@ -3,12 +3,12 @@ const { StringOutputParser } = require("langchain/schema/output_parser"); // Docs: https://github.com/jmorganca/ollama/blob/main/docs/api.md class OllamaAILLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { if (!process.env.OLLAMA_BASE_PATH) throw new Error("No Ollama Base Path was set."); this.basePath = process.env.OLLAMA_BASE_PATH; - this.model = process.env.OLLAMA_MODEL_PREF; + this.model = modelPreference || process.env.OLLAMA_MODEL_PREF; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index ccc7ba0e9b..038d201d1b 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -2,7 +2,7 @@ const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); const { chatPrompt } = require("../../chats"); class OpenAiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { const { Configuration, OpenAIApi } = require("openai"); if (!process.env.OPEN_AI_KEY) throw new Error("No OpenAI API key was set."); @@ -10,7 +10,8 @@ class OpenAiLLM { apiKey: process.env.OPEN_AI_KEY, }); this.openai = new OpenAIApi(config); - this.model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; + this.model = + modelPreference || process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/AiProviders/togetherAi/index.js b/server/utils/AiProviders/togetherAi/index.js index df64c413ea..44061dd0a4 100644 --- a/server/utils/AiProviders/togetherAi/index.js +++ b/server/utils/AiProviders/togetherAi/index.js @@ -6,7 +6,7 @@ function togetherAiModels() { } class TogetherAiLLM { - constructor(embedder = null) { + constructor(embedder = null, modelPreference = null) { const { Configuration, OpenAIApi } = require("openai"); if (!process.env.TOGETHER_AI_API_KEY) throw new Error("No TogetherAI API key was set."); @@ -16,7 +16,7 @@ class TogetherAiLLM { apiKey: process.env.TOGETHER_AI_API_KEY, }); this.openai = new OpenAIApi(config); - this.model = process.env.TOGETHER_AI_MODEL_PREF; + this.model = modelPreference || process.env.TOGETHER_AI_MODEL_PREF; this.limits = { history: this.promptWindowLimit() * 0.15, system: this.promptWindowLimit() * 0.15, diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 7fdb473446..d63de47d5e 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -71,7 +71,7 @@ async function chatWithWorkspace( return await VALID_COMMANDS[command](workspace, message, uuid, user); } - const LLMConnector = getLLMProvider(); + const LLMConnector = getLLMProvider(workspace?.chatModel); const VectorDb = getVectorDbClass(); const { safe, reasons = [] } = await LLMConnector.isSafe(message); if (!safe) { diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 04bb72b903..ceea8d7d2f 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -30,7 +30,7 @@ async function streamChatWithWorkspace( return; } - const LLMConnector = getLLMProvider(); + const LLMConnector = getLLMProvider(workspace?.chatModel); const VectorDb = getVectorDbClass(); const { safe, reasons = [] } = await LLMConnector.isSafe(message); if (!safe) { diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 54976895e1..87fe976ec7 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -17,7 +17,7 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { case "localai": return await localAIModels(basePath, apiKey); case "ollama": - return await ollamaAIModels(basePath, apiKey); + return await ollamaAIModels(basePath); case "togetherai": return await getTogetherAiModels(); case "native-llm": @@ -53,7 +53,7 @@ async function openAiModels(apiKey = null) { async function localAIModels(basePath = null, apiKey = null) { const { Configuration, OpenAIApi } = require("openai"); const config = new Configuration({ - basePath, + basePath: basePath || process.env.LOCAL_AI_BASE_PATH, apiKey: apiKey || process.env.LOCAL_AI_API_KEY, }); const openai = new OpenAIApi(config); @@ -70,13 +70,14 @@ async function localAIModels(basePath = null, apiKey = null) { return { models, error: null }; } -async function ollamaAIModels(basePath = null, _apiKey = null) { +async function ollamaAIModels(basePath = null) { let url; try { - new URL(basePath); - if (basePath.split("").slice(-1)?.[0] === "/") + let urlPath = basePath ?? process.env.OLLAMA_BASE_PATH; + new URL(urlPath); + if (urlPath.split("").slice(-1)?.[0] === "/") throw new Error("BasePath Cannot end in /!"); - url = basePath; + url = urlPath; } catch { return { models: [], error: "Not a valid URL." }; } diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 1685acc1a0..2b1f3dacf4 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -24,37 +24,37 @@ function getVectorDbClass() { } } -function getLLMProvider() { +function getLLMProvider(modelPreference = null) { const vectorSelection = process.env.LLM_PROVIDER || "openai"; const embedder = getEmbeddingEngineSelection(); switch (vectorSelection) { case "openai": const { OpenAiLLM } = require("../AiProviders/openAi"); - return new OpenAiLLM(embedder); + return new OpenAiLLM(embedder, modelPreference); case "azure": const { AzureOpenAiLLM } = require("../AiProviders/azureOpenAi"); - return new AzureOpenAiLLM(embedder); + return new AzureOpenAiLLM(embedder, modelPreference); case "anthropic": const { AnthropicLLM } = require("../AiProviders/anthropic"); - return new AnthropicLLM(embedder); + return new AnthropicLLM(embedder, modelPreference); case "gemini": const { GeminiLLM } = require("../AiProviders/gemini"); - return new GeminiLLM(embedder); + return new GeminiLLM(embedder, modelPreference); case "lmstudio": const { LMStudioLLM } = require("../AiProviders/lmStudio"); - return new LMStudioLLM(embedder); + return new LMStudioLLM(embedder, modelPreference); case "localai": const { LocalAiLLM } = require("../AiProviders/localAi"); - return new LocalAiLLM(embedder); + return new LocalAiLLM(embedder, modelPreference); case "ollama": const { OllamaAILLM } = require("../AiProviders/ollama"); - return new OllamaAILLM(embedder); + return new OllamaAILLM(embedder, modelPreference); case "togetherai": const { TogetherAiLLM } = require("../AiProviders/togetherAi"); - return new TogetherAiLLM(embedder); + return new TogetherAiLLM(embedder, modelPreference); case "native": const { NativeLLM } = require("../AiProviders/native"); - return new NativeLLM(embedder); + return new NativeLLM(embedder, modelPreference); default: throw new Error("ENV: No LLM_PROVIDER value found in environment!"); } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index c699cf2df3..5c43da5194 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -2,6 +2,7 @@ const KEY_MAPPING = { LLMProvider: { envKey: "LLM_PROVIDER", checks: [isNotEmpty, supportedLLM], + postUpdate: [wipeWorkspaceModelPreference], }, // OpenAI Settings OpenAiKey: { @@ -362,11 +363,20 @@ function validDockerizedUrl(input = "") { return null; } +// If the LLMProvider has changed we need to reset all workspace model preferences to +// null since the provider<>model name combination will be invalid for whatever the new +// provider is. +async function wipeWorkspaceModelPreference(key, prev, next) { + if (prev === next) return; + const { Workspace } = require("../../models/workspace"); + await Workspace.resetWorkspaceChatModels(); +} + // This will force update .env variables which for any which reason were not able to be parsed or // read from an ENV file as this seems to be a complicating step for many so allowing people to write // to the process will at least alleviate that issue. It does not perform comprehensive validity checks or sanity checks // and is simply for debugging when the .env not found issue many come across. -function updateENV(newENVs = {}, force = false) { +async function updateENV(newENVs = {}, force = false) { let error = ""; const validKeys = Object.keys(KEY_MAPPING); const ENV_KEYS = Object.keys(newENVs).filter( @@ -374,21 +384,25 @@ function updateENV(newENVs = {}, force = false) { ); const newValues = {}; - ENV_KEYS.forEach((key) => { - const { envKey, checks } = KEY_MAPPING[key]; - const value = newENVs[key]; + for (const key of ENV_KEYS) { + const { envKey, checks, postUpdate = [] } = KEY_MAPPING[key]; + const prevValue = process.env[envKey]; + const nextValue = newENVs[key]; const errors = checks - .map((validityCheck) => validityCheck(value, force)) + .map((validityCheck) => validityCheck(nextValue, force)) .filter((err) => typeof err === "string"); if (errors.length > 0) { error += errors.join("\n"); - return; + break; } - newValues[key] = value; - process.env[envKey] = value; - }); + newValues[key] = nextValue; + process.env[envKey] = nextValue; + + for (const postUpdateFunc of postUpdate) + await postUpdateFunc(key, prevValue, nextValue); + } return { newValues, error: error?.length > 0 ? error : false }; } From c2c8fe97562202ef028d9f57a220e84655502f19 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 17 Jan 2024 14:42:05 -0800 Subject: [PATCH 019/298] add support for mistral api (#610) * add support for mistral api * update docs to show support for Mistral * add default temp to all providers, suggest different results per provider --------- Co-authored-by: timothycarambat --- README.md | 1 + docker/.env.example | 4 + .../LLMSelection/MistralOptions/index.jsx | 103 ++++++++++ .../Modals/MangeWorkspace/Settings/index.jsx | 18 +- frontend/src/media/llmprovider/mistral.jpeg | Bin 0 -> 4542 bytes .../GeneralSettings/LLMPreference/index.jsx | 11 +- .../Steps/DataHandling/index.jsx | 8 + .../Steps/LLMPreference/index.jsx | 9 + server/.env.example | 4 + server/models/systemSettings.js | 12 ++ server/utils/AiProviders/anthropic/index.js | 1 + server/utils/AiProviders/azureOpenAi/index.js | 5 +- server/utils/AiProviders/gemini/index.js | 1 + server/utils/AiProviders/lmStudio/index.js | 5 +- server/utils/AiProviders/localAi/index.js | 5 +- server/utils/AiProviders/mistral/index.js | 184 ++++++++++++++++++ server/utils/AiProviders/native/index.js | 5 +- server/utils/AiProviders/ollama/index.js | 5 +- server/utils/AiProviders/openAi/index.js | 5 +- server/utils/AiProviders/togetherAi/index.js | 5 +- server/utils/chats/index.js | 2 +- server/utils/chats/stream.js | 4 +- server/utils/helpers/customModels.js | 23 +++ server/utils/helpers/index.js | 4 + server/utils/helpers/updateENV.js | 10 + 25 files changed, 412 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/LLMSelection/MistralOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/mistral.jpeg create mode 100644 server/utils/AiProviders/mistral/index.js diff --git a/README.md b/README.md index 4249c42bcf..6e3df0df4a 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Some cool features of AnythingLLM - [LM Studio (all models)](https://lmstudio.ai) - [LocalAi (all models)](https://localai.io/) - [Together AI (chat models)](https://www.together.ai/) +- [Mistral](https://mistral.ai/) **Supported Embedding models:** diff --git a/docker/.env.example b/docker/.env.example index 5bd909af66..8d33a809d4 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -44,6 +44,10 @@ GID='1000' # TOGETHER_AI_API_KEY='my-together-ai-key' # TOGETHER_AI_MODEL_PREF='mistralai/Mixtral-8x7B-Instruct-v0.1' +# LLM_PROVIDER='mistral' +# MISTRAL_API_KEY='example-mistral-ai-api-key' +# MISTRAL_MODEL_PREF='mistral-tiny' + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/frontend/src/components/LLMSelection/MistralOptions/index.jsx b/frontend/src/components/LLMSelection/MistralOptions/index.jsx new file mode 100644 index 0000000000..d5c6664159 --- /dev/null +++ b/frontend/src/components/LLMSelection/MistralOptions/index.jsx @@ -0,0 +1,103 @@ +import { useState, useEffect } from "react"; +import System from "@/models/system"; + +export default function MistralOptions({ settings }) { + const [inputValue, setInputValue] = useState(settings?.MistralApiKey); + const [mistralKey, setMistralKey] = useState(settings?.MistralApiKey); + + return ( +
+
+ + setInputValue(e.target.value)} + onBlur={() => setMistralKey(inputValue)} + /> +
+ +
+ ); +} + +function MistralModelSelection({ apiKey, settings }) { + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!apiKey) { + setCustomModels([]); + setLoading(false); + return; + } + setLoading(true); + const { models } = await System.customModels( + "mistral", + typeof apiKey === "boolean" ? null : apiKey + ); + setCustomModels(models || []); + setLoading(false); + } + findCustomModels(); + }, [apiKey]); + + if (loading || customModels.length == 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx index a3089d6884..da0e7b9f02 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -27,11 +27,21 @@ function castToType(key, value) { return definitions[key].cast(value); } +function recommendedSettings(provider = null) { + switch (provider) { + case "mistral": + return { temp: 0 }; + default: + return { temp: 0.7 }; + } +} + export default function WorkspaceSettings({ active, workspace, settings }) { const { slug } = useParams(); const formEl = useRef(null); const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); + const defaults = recommendedSettings(settings?.LLMProvider); const handleUpdate = async (e) => { setSaving(true); @@ -143,20 +153,20 @@ export default function WorkspaceSettings({ active, workspace, settings }) { This setting controls how "random" or dynamic your chat responses will be.
- The higher the number (2.0 maximum) the more random and + The higher the number (1.0 maximum) the more random and incoherent.
- Recommended: 0.7 + Recommended: {defaults.temp}

e.target.blur()} - defaultValue={workspace?.openAiTemp ?? 0.7} + defaultValue={workspace?.openAiTemp ?? defaults.temp} className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="0.7" required={true} diff --git a/frontend/src/media/llmprovider/mistral.jpeg b/frontend/src/media/llmprovider/mistral.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1019f495d4d690dd639aa9f4e5751c403b2eff27 GIT binary patch literal 4542 zcmb7Hc|4Te+rMWu%*f0z_F}9tmSJp(7$R%7QrXHH8A7DRz9ppW*|Vh3gzPDW?6Rd4 zDoF}ik~Q0VPtViueSV+k{pUS%eLi#D=eo{y&iy^#`#RUzeY-mjU=4KjbpZ$j03hHG z*c}D50T>j@5U{~PL7)%_I2^&k#Dqj)STGng3mVP32ajdlgWH2fV>z)nb`Ao8fMMg} z=H%eUa}YQfK_D=23>?9XKrnN#qFFiq-)*-Uz@dN+paTXG0-!hu3%UU_Zn74>JPE1cyOUklhgg0|UomVOa20s|Ejz{^vi5tB^UVY1V)2 zCr{;yx;W(HxAHu%8vx3Zv`o*HEtI(&Wv#j|UnyaJz^-{00OJJAcD60YeXH|ugf;l8 zZy7%ACvFZS7CdguMa#Ex9Vm%=$2San)9t@c*Y9mqMa+njkD8&BEBO@cXc+(qsJ`$& zUdVkh4n|?4eC%?%n|3ty{N+M%@8XwjeRLk-LsdZfr|UTzR-L?xrU0-p{B=6EY4j{_ z>cP^vjD(uifyQTv0m9Et3JVn%*xj6dSH1tk(@JxtSFPR^X$J9U8V5B2w(Lvw>uR=d z2yvaO{Au^jziMe#7+WY)zS!o~Jteo<7%w9l+p@W&XLJ_cW*kSiG=FdOlIsdL0Qp&F zTQTgTcT`(xOqEa6nwHW|DH!0d$MwFJGVSpXc0X|P`XR6R#w$+ zv>~HUp1mR9v{$lg!gI{sFU*|y=7t|U#uw<+NxH`Dleny+c!2nzJgz-;F7AGHQMYCM z+iv{VA&pmi5p{)YmKRjs#qK%Dz_tJAl`3$` zoN&pr{DRn08(LveaE9nl@;`U+UpgDe3@LE8U`Qksr1js_fx?jh1cqP|#By*dC@K*s zczGO|op{CzWC{u-5Cj3;1xyP|#YK`{ytX``ma&mgo6e`1!6M~8Jllf$iJd+5>1NN3 zCmo|bH$r4@$(~-5418BE`na)n;m*~H@ad<)W5sJO&r@f9eQXS!Z(}Q3*#&&4Y~uwl zpH`lCr7$&JFV-DN_CRQJNcHOV6Jplq-8`Po?o7rA{OGHENSn=6 zo$9TxeCSHy(z@}$ru(wX!;GZEYW3wjzJh0M-VH1Hz0AIF)}~cpgTx3fyz#7GpuF)- z(z_4O=Jj^YFEf3Ytu9>kFU#8NkaP)hpa)S2aj@@AA4I0>D}|)p<~oI=2#`5@5TX3C z{2~=e*c7q6!u)zK>H@=)_WWep>QTl54VKo$H&P#{KQ!z7#55}Nc{b>mKqa1!Z+N?t z)IFX%WmUdfe?&Y&fYxx6zWFVvM~`;?^qR=f5v`2NSmgwmI2%-CF0E?G;a(1|kUgwj z!E9>a$Zb?*9*>?$h-K}G$U<-g4vEFH69mo7y*cFMDGF2z`-HqEPGO%4O)b;k zGy;>624u%cP*pdqUWBjYvDo27ZpF(CjeCN%gSTp8BSY@mE=+3kE!<8!al0!tNgAVsg!;&X^W@9|=>3&WWK@&#_hkp!? zxhUIJ-Q|kr)UJ^db;HE(0_xTp)vX(tlNKYV*0)MLpJwSwNq*DK?2Bc2TvwNEEp^%! zb+Bc%UdkQgNb~cKn%_!p=TuSgmn|l(-qf*n8#)mpHgEhDo!D01E7s0&;GjtxJ!7@q z8vVf&(z06hGW&Dwkd$;u*uxTK&Baei?_bO)XGv4eUYc7$qf3iRK1Wz|?DbY_-zozE zr{Dkp0f93y!4Q8wX9x&54uNDB#Nr7QGw(bOqMW?Bg}qPHfPyAf`!Bem47j%^*Eohw z*ihFJxJOrWp9E?^=Zu1Aeh;;(<8IE~+CqM;+WtJf&gIgdqUf0%IcuKoA+v50C`KNV znJeJ!vv<8!!jt<}>|^DSQ{rju0h#`r$lEh_sUOgXa)P2jr!Zp~bvs1rz&77A{b|Q5 z@y_`x!FHL&Py7>{bB@P+73S9C)?@aNP|>ju6YqDYS+iNQK~ID}65yQq zk*8Oeoc3H1Y1nExV0zK1Su)dw@yxo`kZG*jojG^A`834;mYa3Tdbg}sqm`6#SN*lx zb6@gb{FD-iie>A0@`#1Lzmds>b}CwI)J#0u%$nKS(jwDin$7^Su+X34yQ+ zocW^vcaMCqM4T|n;QI;FG4UZ!_8Dyaim8n1H}ZybES=5sB%ZO>xU zi>G*#t22@xO3=oK?mc+%rSGup(??1%Ewu6FK?Oe}eXk12h093zZsCdUzzHdrQ7ds(~uMc7VVN2jz( zXYnP^>+P$=bCF_BEbQAH$NHsjzZzGGC)Su%7^=opJfZokl)C$LO4f9r(bm^~lj=us zlf6Ho60yD>p?Xy5@S`ef)Wn?B2Vwc|G0y-U`(0pzVU8!pRV~%stqc=YCs(>|i$vQr z58jkRC+BE^z(NC7h)b`!z=fcamz!S}EGH5#pbgYs$e3SdkL2z}g~W&9e;X9>?H@gIaL`I+pfN0GQ}1C2Xv!}meaA9c6i(w5GR!RpKDbToz@ za%71jU?j2aI|__dgc@8qb8p%kw1iX><&zjQBP6E7a__cs3H^c&MRb{vqSrevW+>VD z3>v)`g4go8ygdDy*l>P~uV%Xmvw5GEsa2|)<_)cb6ZPFBMXhafLu8xIe)!`7I%1R_ z=y^zJm@G36_x)G^0)-cuXF_Pxg zb?&aB<{eyv>Llq_ftt9$33U`~x`>(f+@_$)g80^_qt)f@f<1q<#$dvF7P5u!*Ih;P zjACPp_=7xzmr<%8#%Cthpii|4Gyi@J3v7Ah;%A{+oPDgf-mll2+;wR*)wX(;c-!jbGw*QAs z`j!!U#s2bbBJ(kJvQKJQ6*k%*yBva=5UGQ!ab0gCAKW_@o##Dq3Qe_Q>DQe<$cP3= zIbE^tv~qkCQ${9@S*C<_B6Tj7Cd`uznw-Lg>dQ5;x71krNt!v7 z)had<{xqygaP-?V_l%}{b^$KRmhLQYDEN9mec73{F#Bi=x zPIeR3j)-}Fa8{&jQODEPb8YCc1MtY^J5l>?jb!bOtHY1$4^C(q%5|KV`POWxwxt8@ z*gtL>l4+ed9Hh-yaE`W;6 z+DDu4kFMaT;r9oY0|FcR)RuC&V13C;WokdW5B=!dhRp{2>{D&o1=JR48$O8#t*(d#961 z<^3E;;h8Gx2^9ftEAP`)E5$9zS+lmO#1y9g%?c$k zyf~=2@4(n}^UDLCUyTi`nl`7qE=t97T(l6~=}Xs>v(^es>qU<_8=T40e<)_S9b2$B zQ}}tLbqK=p!U9=al7Lc_@Qc8#Wo`C&?$tr2uvq4nfB;DR1^|hG|Ea$J006!})huDa zJ2dZyrfJis-=8Ss6vu9L@n`IdR>-9?RZ=-`n=YYQ@g{e|Yas+^iq$x$0nD|hKIo)j z>dYSPL19%oX2Ek`ZfqL$hUVHA`7f%0^z5X9_DeHftqY$Y`O!k&x5yVALn_}eajO`Q z$rc(POpCqx@A!*R%}8Jw+YjD8z|H+DK|yc;k!)ta_ya3PO~@M%tndzP`rn&}1{S<|^z^$% zB%}CTqw&fP&-G@lb86L%<0AB(n{@=20Bp_Kk%VTilU>|q5Sf)ohEdL5L{(2&Xoa>% z6_c5AWr{5-J#5mB9@Wk2` z;A~6>C<^CxQj>07I6NpP9TCm_C7XRa(cs+u=*^7RO?T%4HI{obP?wa9j7(3AeAn_>w#$rT%uRbT$kkdRXe35 zbUj1`=Xg!6IB^OkB^2<-&7&kVny4W*X;+!jcYU)=crDn=F6nPI(EHqI2)3^fO)?`F znYh1qT7BA2Y31PLd~2g}Wg3=8i^vZ?CVujk2tKB(bMEScoZ-h(`S9rUts%Eg1x;^l zqnO8<+6f0Pn^;{JW&TRn^EhZXD_%YS=&Ic}-N`Ud>J?9$fHb}g-94%@;#h;_Cx)+^ zm&fWhWRw-+`2B2-3Z_h7N-K@*8VI8d*07y>t9~Cfg1UC~nvt=Kefh

GOhkykkrx z5<$gl4%{{1&%57c5Qv$XoY%TcILb6UdoL+VK0U}ckiTqHx51ZU zmwjXW&11_L&eEgCFq@TP2dU4yzzlh^iUi9p_NpSM8p1Mj^7zmjK?2}Op8*u3nr2;h Kf#<=y@BRlu0mfYb literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index bd6ae511dc..1efa818d3e 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -12,6 +12,7 @@ import OllamaLogo from "@/media/llmprovider/ollama.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import TogetherAILogo from "@/media/llmprovider/togetherai.png"; +import MistralLogo from "@/media/llmprovider/mistral.jpeg"; import PreLoader from "@/components/Preloader"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions"; @@ -21,9 +22,10 @@ import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions"; import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions"; import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions"; import OllamaLLMOptions from "@/components/LLMSelection/OllamaLLMOptions"; +import TogetherAiOptions from "@/components/LLMSelection/TogetherAiOptions"; +import MistralOptions from "@/components/LLMSelection/MistralOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import { MagnifyingGlass } from "@phosphor-icons/react"; -import TogetherAiOptions from "@/components/LLMSelection/TogetherAiOptions"; export default function GeneralLLMPreference() { const [saving, setSaving] = useState(false); @@ -134,6 +136,13 @@ export default function GeneralLLMPreference() { options: , description: "Run open source models from Together AI.", }, + { + name: "Mistral", + value: "mistral", + logo: MistralLogo, + options: , + description: "Run open source models from Mistral AI.", + }, { name: "Native", value: "native", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 281f1e8cdd..3b0046382a 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -9,6 +9,7 @@ import OllamaLogo from "@/media/llmprovider/ollama.png"; import TogetherAILogo from "@/media/llmprovider/togetherai.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; +import MistralLogo from "@/media/llmprovider/mistral.jpeg"; import ChromaLogo from "@/media/vectordbs/chroma.png"; import PineconeLogo from "@/media/vectordbs/pinecone.png"; import LanceDbLogo from "@/media/vectordbs/lancedb.png"; @@ -91,6 +92,13 @@ const LLM_SELECTION_PRIVACY = { ], logo: TogetherAILogo, }, + mistral: { + name: "Mistral", + description: [ + "Your prompts and document text used in response creation are visible to Mistral", + ], + logo: MistralLogo, + }, }; const VECTOR_DB_PRIVACY = { diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index dc060594ed..9e8ab84a90 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -9,6 +9,7 @@ import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import TogetherAILogo from "@/media/llmprovider/togetherai.png"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; +import MistralLogo from "@/media/llmprovider/mistral.jpeg"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions"; import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions"; @@ -17,6 +18,7 @@ import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions"; import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions"; import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions"; import OllamaLLMOptions from "@/components/LLMSelection/OllamaLLMOptions"; +import MistralOptions from "@/components/LLMSelection/MistralOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import System from "@/models/system"; import paths from "@/utils/paths"; @@ -109,6 +111,13 @@ export default function LLMPreference({ options: , description: "Run open source models from Together AI.", }, + { + name: "Mistral", + value: "mistral", + logo: MistralLogo, + options: , + description: "Run open source models from Mistral AI.", + }, { name: "Native", value: "native", diff --git a/server/.env.example b/server/.env.example index d060e0ab50..26c51927cf 100644 --- a/server/.env.example +++ b/server/.env.example @@ -41,6 +41,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # TOGETHER_AI_API_KEY='my-together-ai-key' # TOGETHER_AI_MODEL_PREF='mistralai/Mixtral-8x7B-Instruct-v0.1' +# LLM_PROVIDER='mistral' +# MISTRAL_API_KEY='example-mistral-ai-api-key' +# MISTRAL_MODEL_PREF='mistral-tiny' + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index cd008d420f..53d42f2e2e 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -159,6 +159,18 @@ const SystemSettings = { AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, } : {}), + ...(llmProvider === "mistral" + ? { + MistralApiKey: !!process.env.MISTRAL_API_KEY, + MistralModelPref: process.env.MISTRAL_MODEL_PREF, + + // For embedding credentials when mistral is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), ...(llmProvider === "native" ? { NativeLLMModelPref: process.env.NATIVE_LLM_MODEL_PREF, diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 17f2abc4ac..56d3a80f0a 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -26,6 +26,7 @@ class AnthropicLLM { ); this.embedder = embedder; this.answerKey = v4().split("-")[0]; + this.defaultTemp = 0.7; } streamingEnabled() { diff --git a/server/utils/AiProviders/azureOpenAi/index.js b/server/utils/AiProviders/azureOpenAi/index.js index f59fc51fa1..639ac102ed 100644 --- a/server/utils/AiProviders/azureOpenAi/index.js +++ b/server/utils/AiProviders/azureOpenAi/index.js @@ -25,6 +25,7 @@ class AzureOpenAiLLM { "No embedding provider defined for AzureOpenAiLLM - falling back to AzureOpenAiEmbedder for embedding!" ); this.embedder = !embedder ? new AzureOpenAiEmbedder() : embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -93,7 +94,7 @@ class AzureOpenAiLLM { ); const textResponse = await this.openai .getChatCompletions(this.model, messages, { - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, }) .then((res) => { @@ -130,7 +131,7 @@ class AzureOpenAiLLM { this.model, messages, { - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, } ); diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 348c8f5ed4..63549fb8dd 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -22,6 +22,7 @@ class GeminiLLM { "INVALID GEMINI LLM SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Gemini as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; // not used for Gemini } #appendContext(contextTexts = []) { diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js index 614808034c..08950a7b96 100644 --- a/server/utils/AiProviders/lmStudio/index.js +++ b/server/utils/AiProviders/lmStudio/index.js @@ -25,6 +25,7 @@ class LMStudioLLM { "INVALID LM STUDIO SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use LMStudio as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -85,7 +86,7 @@ class LMStudioLLM { const textResponse = await this.lmstudio .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -122,7 +123,7 @@ class LMStudioLLM { const streamRequest = await this.lmstudio.createChatCompletion( { model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, stream: true, messages: await this.compressMessages( diff --git a/server/utils/AiProviders/localAi/index.js b/server/utils/AiProviders/localAi/index.js index 6623ac88ee..6d265cf828 100644 --- a/server/utils/AiProviders/localAi/index.js +++ b/server/utils/AiProviders/localAi/index.js @@ -27,6 +27,7 @@ class LocalAiLLM { "INVALID LOCAL AI SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use LocalAI as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -85,7 +86,7 @@ class LocalAiLLM { const textResponse = await this.openai .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -123,7 +124,7 @@ class LocalAiLLM { { model: this.model, stream: true, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { diff --git a/server/utils/AiProviders/mistral/index.js b/server/utils/AiProviders/mistral/index.js new file mode 100644 index 0000000000..a25185c763 --- /dev/null +++ b/server/utils/AiProviders/mistral/index.js @@ -0,0 +1,184 @@ +const { chatPrompt } = require("../../chats"); + +class MistralLLM { + constructor(embedder = null, modelPreference = null) { + const { Configuration, OpenAIApi } = require("openai"); + if (!process.env.MISTRAL_API_KEY) + throw new Error("No Mistral API key was set."); + + const config = new Configuration({ + basePath: "https://api.mistral.ai/v1", + apiKey: process.env.MISTRAL_API_KEY, + }); + this.openai = new OpenAIApi(config); + this.model = + modelPreference || process.env.MISTRAL_MODEL_PREF || "mistral-tiny"; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + console.warn( + "No embedding provider defined for MistralLLM - falling back to OpenAiEmbedder for embedding!" + ); + this.embedder = embedder; + this.defaultTemp = 0.0; + } + + #appendContext(contextTexts = []) { + if (!contextTexts || !contextTexts.length) return ""; + return ( + "\nContext:\n" + + contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("") + ); + } + + streamingEnabled() { + return "streamChat" in this && "streamGetChatCompletion" in this; + } + + promptWindowLimit() { + return 32000; + } + + async isValidChatCompletionModel(modelName = "") { + return true; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_ = "") { + return { safe: true, reasons: [] }; + } + + async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const textResponse = await this.openai + .createChatCompletion({ + model: this.model, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }) + .then((json) => { + const res = json.data; + if (!res.hasOwnProperty("choices")) + throw new Error("Mistral chat: No results!"); + if (res.choices.length === 0) + throw new Error("Mistral chat: No results length!"); + return res.choices[0].message.content; + }) + .catch((error) => { + throw new Error( + `Mistral::createChatCompletion failed with: ${error.message}` + ); + }); + + return textResponse; + } + + async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }, + { responseType: "stream" } + ); + + return streamRequest; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const { data } = await this.openai.createChatCompletion({ + model: this.model, + messages, + temperature, + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Mistral chat: ${this.model} is not valid for chat completion!` + ); + + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + messages, + temperature, + }, + { responseType: "stream" } + ); + return streamRequest; + } + + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations + async embedTextInput(textInput) { + return await this.embedder.embedTextInput(textInput); + } + async embedChunks(textChunks = []) { + return await this.embedder.embedChunks(textChunks); + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } +} + +module.exports = { + MistralLLM, +}; diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index 66cc84d0ca..fff904c462 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -29,6 +29,7 @@ class NativeLLM { // Make directory when it does not exist in existing installations if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir); + this.defaultTemp = 0.7; } async #initializeLlamaModel(temperature = 0.7) { @@ -132,7 +133,7 @@ class NativeLLM { ); const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const response = await model.call(messages); return response.content; @@ -145,7 +146,7 @@ class NativeLLM { async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const messages = await this.compressMessages( { diff --git a/server/utils/AiProviders/ollama/index.js b/server/utils/AiProviders/ollama/index.js index fce96f3698..af7fe8210f 100644 --- a/server/utils/AiProviders/ollama/index.js +++ b/server/utils/AiProviders/ollama/index.js @@ -20,6 +20,7 @@ class OllamaAILLM { "INVALID OLLAMA SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Ollama as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #ollamaClient({ temperature = 0.07 }) { @@ -113,7 +114,7 @@ class OllamaAILLM { ); const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const textResponse = await model .pipe(new StringOutputParser()) @@ -136,7 +137,7 @@ class OllamaAILLM { ); const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), }); const stream = await model .pipe(new StringOutputParser()) diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index 038d201d1b..582bc054d2 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -23,6 +23,7 @@ class OpenAiLLM { "No embedding provider defined for OpenAiLLM - falling back to OpenAiEmbedder for embedding!" ); this.embedder = !embedder ? new OpenAiEmbedder() : embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -127,7 +128,7 @@ class OpenAiLLM { const textResponse = await this.openai .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -165,7 +166,7 @@ class OpenAiLLM { { model: this.model, stream: true, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { diff --git a/server/utils/AiProviders/togetherAi/index.js b/server/utils/AiProviders/togetherAi/index.js index 44061dd0a4..341661f8db 100644 --- a/server/utils/AiProviders/togetherAi/index.js +++ b/server/utils/AiProviders/togetherAi/index.js @@ -28,6 +28,7 @@ class TogetherAiLLM { "INVALID TOGETHER AI SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Together AI as your LLM." ); this.embedder = embedder; + this.defaultTemp = 0.7; } #appendContext(contextTexts = []) { @@ -89,7 +90,7 @@ class TogetherAiLLM { const textResponse = await this.openai .createChatCompletion({ model: this.model, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { @@ -127,7 +128,7 @@ class TogetherAiLLM { { model: this.model, stream: true, - temperature: Number(workspace?.openAiTemp ?? 0.7), + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), n: 1, messages: await this.compressMessages( { diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index d63de47d5e..764c7795a6 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -171,7 +171,7 @@ async function chatWithWorkspace( // Send the text completion. const textResponse = await LLMConnector.getChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); if (!textResponse) { diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index ceea8d7d2f..cff565ed6e 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -141,7 +141,7 @@ async function streamChatWithWorkspace( `\x1b[31m[STREAMING DISABLED]\x1b[0m Streaming is not available for ${LLMConnector.constructor.name}. Will use regular chat method.` ); completeText = await LLMConnector.getChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); writeResponseChunk(response, { uuid, @@ -153,7 +153,7 @@ async function streamChatWithWorkspace( }); } else { const stream = await LLMConnector.streamGetChatCompletion(messages, { - temperature: workspace?.openAiTemp ?? 0.7, + temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); completeText = await handleStreamResponses(response, stream, { uuid, diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 87fe976ec7..53c641e75e 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -5,6 +5,7 @@ const SUPPORT_CUSTOM_MODELS = [ "ollama", "native-llm", "togetherai", + "mistral", ]; async function getCustomModels(provider = "", apiKey = null, basePath = null) { @@ -20,6 +21,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { return await ollamaAIModels(basePath); case "togetherai": return await getTogetherAiModels(); + case "mistral": + return await getMistralModels(apiKey); case "native-llm": return nativeLLMModels(); default: @@ -117,6 +120,26 @@ async function getTogetherAiModels() { return { models, error: null }; } +async function getMistralModels(apiKey = null) { + const { Configuration, OpenAIApi } = require("openai"); + const config = new Configuration({ + apiKey: apiKey || process.env.MISTRAL_API_KEY, + basePath: "https://api.mistral.ai/v1", + }); + const openai = new OpenAIApi(config); + const models = await openai + .listModels() + .then((res) => res.data.data.filter((model) => !model.id.includes("embed"))) + .catch((e) => { + console.error(`Mistral:listModels`, e.message); + return []; + }); + + // Api Key was successful so lets save it for future uses + if (models.length > 0 && !!apiKey) process.env.MISTRAL_API_KEY = apiKey; + return { models, error: null }; +} + function nativeLLMModels() { const fs = require("fs"); const path = require("path"); diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 2b1f3dacf4..2eed9057ca 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -52,6 +52,9 @@ function getLLMProvider(modelPreference = null) { case "togetherai": const { TogetherAiLLM } = require("../AiProviders/togetherAi"); return new TogetherAiLLM(embedder, modelPreference); + case "mistral": + const { MistralLLM } = require("../AiProviders/mistral"); + return new MistralLLM(embedder, modelPreference); case "native": const { NativeLLM } = require("../AiProviders/native"); return new NativeLLM(embedder, modelPreference); @@ -76,6 +79,7 @@ function getEmbeddingEngineSelection() { return new LocalAiEmbedder(); case "native": const { NativeEmbedder } = require("../EmbeddingEngines/native"); + console.log("\x1b[34m[INFO]\x1b[0m Using Native Embedder"); return new NativeEmbedder(); default: return null; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 5c43da5194..54e6840291 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -95,6 +95,15 @@ const KEY_MAPPING = { checks: [nonZero], }, + MistralApiKey: { + envKey: "MISTRAL_API_KEY", + checks: [isNotEmpty], + }, + MistralModelPref: { + envKey: "MISTRAL_MODEL_PREF", + checks: [isNotEmpty], + }, + // Native LLM Settings NativeLLMModelPref: { envKey: "NATIVE_LLM_MODEL_PREF", @@ -268,6 +277,7 @@ function supportedLLM(input = "") { "ollama", "native", "togetherai", + "mistral", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; } From 56dc49966d663b6d17c71e7e531e808bac415ca2 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 17 Jan 2024 16:22:06 -0800 Subject: [PATCH 020/298] add copy feature to assistant chat message (#611) * add copy feature to assistant chat message * fix tooltip not hiding on mobile * fix: add tooltips chore: breakout actions to extendable component + memoize add CopyText to hook we can reuse fix: Copy on code snippets broken, moved to event listener fix: highlightjs patch for new API support feat: add copy response support --------- Co-authored-by: timothycarambat --- frontend/package.json | 1 + .../HistoricalMessage/Actions/index.jsx | 43 ++++++++++++++++ .../ChatHistory/HistoricalMessage/index.jsx | 11 ++++- .../ChatContainer/ChatHistory/index.jsx | 8 +-- .../src/components/WorkspaceChat/index.jsx | 36 ++++++++++++++ frontend/src/hooks/useCopyText.js | 15 ++++++ frontend/src/index.css | 4 ++ frontend/src/utils/chat/markdown.js | 49 +++++++++---------- frontend/yarn.lock | 33 +++++++++++++ 9 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx create mode 100644 frontend/src/hooks/useCopyText.js diff --git a/frontend/package.json b/frontend/package.json index 86e552ab78..17d9af9131 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "react-router-dom": "^6.3.0", "react-tag-input-component": "^2.0.2", "react-toastify": "^9.1.3", + "react-tooltip": "^5.25.2", "text-case": "^1.0.9", "truncate": "^3.0.0", "uuid": "^9.0.0" diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx new file mode 100644 index 0000000000..12fa7dc739 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -0,0 +1,43 @@ +import useCopyText from "@/hooks/useCopyText"; +import { Check, ClipboardText } from "@phosphor-icons/react"; +import { memo } from "react"; +import { Tooltip } from "react-tooltip"; + +const Actions = ({ message }) => { + return ( +

+ + {/* Other actions to go here later. */} +
+ ); +}; + +function CopyMessage({ message }) { + const { copied, copyText } = useCopyText(); + return ( + <> +
+ +
+ + + ); +} + +export default memo(Actions); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 4637b1cd78..c39220f37f 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -1,14 +1,15 @@ -import { memo, forwardRef } from "react"; +import React, { memo, forwardRef } from "react"; import { Warning } from "@phosphor-icons/react"; import Jazzicon from "../../../../UserIcon"; +import Actions from "./Actions"; import renderMarkdown from "@/utils/chat/markdown"; import { userFromStorage } from "@/utils/request"; import Citations from "../Citation"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; import { v4 } from "uuid"; import createDOMPurify from "dompurify"; -const DOMPurify = createDOMPurify(window); +const DOMPurify = createDOMPurify(window); const HistoricalMessage = forwardRef( ( { uuid = v4(), message, role, workspace, sources = [], error = false }, @@ -53,6 +54,12 @@ const HistoricalMessage = forwardRef( /> )}
+ {role === "assistant" && ( +
+
+ +
+ )} {role === "assistant" && }
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 4a7cd48274..358e520a18 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -17,9 +17,12 @@ export default function ChatHistory({ history = [], workspace }) { }, [history]); const handleScroll = () => { - const isBottom = - chatHistoryRef.current.scrollHeight - chatHistoryRef.current.scrollTop === + const diff = + chatHistoryRef.current.scrollHeight - + chatHistoryRef.current.scrollTop - chatHistoryRef.current.clientHeight; + // Fuzzy margin for what qualifies as "bottom". Stronger than straight comparison since that may change over time. + const isBottom = diff <= 10; setIsAtBottom(isBottom); }; @@ -112,7 +115,6 @@ export default function ChatHistory({ history = [], workspace }) { /> ); })} - {showing && ( )} diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx index 3e129c2a6a..30bd494f34 100644 --- a/frontend/src/components/WorkspaceChat/index.jsx +++ b/frontend/src/components/WorkspaceChat/index.jsx @@ -59,5 +59,41 @@ export default function WorkspaceChat({ loading, workspace }) { ); } + setEventDelegatorForCodeSnippets(); return ; } + +// Enables us to safely markdown and sanitize all responses without risk of injection +// but still be able to attach a handler to copy code snippets on all elements +// that are code snippets. +function copyCodeSnippet(uuid) { + const target = document.querySelector(`[data-code="${uuid}"]`); + if (!target) return false; + const markdown = + target.parentElement?.parentElement?.querySelector( + "pre:first-of-type" + )?.innerText; + if (!markdown) return false; + + window.navigator.clipboard.writeText(markdown); + target.classList.add("text-green-500"); + const originalText = target.innerHTML; + target.innerText = "Copied!"; + target.setAttribute("disabled", true); + + setTimeout(() => { + target.classList.remove("text-green-500"); + target.innerHTML = originalText; + target.removeAttribute("disabled"); + }, 2500); +} + +// Listens and hunts for all data-code-snippet clicks. +function setEventDelegatorForCodeSnippets() { + document?.addEventListener("click", function (e) { + const target = e.target.closest("[data-code-snippet]"); + const uuidCode = target?.dataset?.code; + if (!uuidCode) return false; + copyCodeSnippet(uuidCode); + }); +} diff --git a/frontend/src/hooks/useCopyText.js b/frontend/src/hooks/useCopyText.js new file mode 100644 index 0000000000..04519b2ef3 --- /dev/null +++ b/frontend/src/hooks/useCopyText.js @@ -0,0 +1,15 @@ +import { useState } from "react"; + +export default function useCopyText(delay = 2500) { + const [copied, setCopied] = useState(false); + const copyText = async (content) => { + if (!content) return; + navigator?.clipboard?.writeText(content); + setCopied(content); + setTimeout(() => { + setCopied(false); + }, delay); + }; + + return { copyText, copied }; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 1d1b2da853..e8d7e2d8ca 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -399,3 +399,7 @@ dialog::backdrop { .rti--container { @apply !bg-zinc-900 !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5; } + +.tooltip { + @apply !bg-black !text-white !py-2 !px-3 !rounded-md; +} diff --git a/frontend/src/utils/chat/markdown.js b/frontend/src/utils/chat/markdown.js index 53b6804fef..ff4af77bc4 100644 --- a/frontend/src/utils/chat/markdown.js +++ b/frontend/src/utils/chat/markdown.js @@ -7,47 +7,44 @@ import { v4 } from "uuid"; const markdown = markdownIt({ html: true, typographer: true, - highlight: function (str, lang) { + highlight: function (code, lang) { const uuid = v4(); if (lang && hljs.getLanguage(lang)) { try { return ( - `
` +
-          hljs.highlight(lang, str, true).value +
+          `
+
+
+ ${lang || ""} +
+ +
+
` +
+          hljs.highlight(code, { language: lang, ignoreIllegals: true }).value +
           "
" ); } catch (__) {} } return ( - `
` +
-      HTMLEncode(str) +
+      `
+
+
+ +
+
` +
+      HTMLEncode(code) +
       "
" ); }, }); -window.copySnippet = function (uuid = "") { - const target = document.getElementById(`code-${uuid}`); - const markdown = - target.parentElement?.parentElement?.querySelector( - "pre:first-of-type" - )?.innerText; - if (!markdown) return false; - - window.navigator.clipboard.writeText(markdown); - target.classList.add("text-green-500"); - const originalText = target.innerHTML; - target.innerText = "Copied!"; - target.setAttribute("disabled", true); - - setTimeout(() => { - target.classList.remove("text-green-500"); - target.innerHTML = originalText; - target.removeAttribute("disabled"); - }, 5000); -}; - export default function renderMarkdown(text = "") { return markdown.render(text); } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c9181f15f5..fa1e713315 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -365,6 +365,26 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== +"@floating-ui/core@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" + integrity sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q== + dependencies: + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/dom@^1.0.0": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.4.tgz#28df1e1cb373884224a463235c218dcbd81a16bb" + integrity sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ== + dependencies: + "@floating-ui/core" "^1.5.3" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/utils@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -846,6 +866,11 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +classnames@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2543,6 +2568,14 @@ react-toastify@^9.1.3: dependencies: clsx "^1.1.1" +react-tooltip@^5.25.2: + version "5.25.2" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.25.2.tgz#efb51845ec2e863045812ad1dc1927573922d629" + integrity sha512-MwZ3S9xcHpojZaKqjr5mTs0yp/YBPpKFcayY7MaaIIBr2QskkeeyelpY2YdGLxIMyEj4sxl0rGoK6dQIKvNLlw== + dependencies: + "@floating-ui/dom" "^1.0.0" + classnames "^2.3.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" From 3fe7a25759bb2960997de81f9fd433ddf72987b6 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 17 Jan 2024 16:25:30 -0800 Subject: [PATCH 021/298] add token context limit for native llm settings (#614) Co-authored-by: timothycarambat --- .../LLMSelection/NativeLLMOptions/index.jsx | 70 ++++++++++++------- server/models/systemSettings.js | 1 + server/utils/AiProviders/native/index.js | 2 - server/utils/helpers/updateENV.js | 5 ++ 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/LLMSelection/NativeLLMOptions/index.jsx b/frontend/src/components/LLMSelection/NativeLLMOptions/index.jsx index a41a81fe87..457c09322d 100644 --- a/frontend/src/components/LLMSelection/NativeLLMOptions/index.jsx +++ b/frontend/src/components/LLMSelection/NativeLLMOptions/index.jsx @@ -54,31 +54,49 @@ function NativeModelSelection({ settings }) { } return ( -
- - -
+ <> +
+ + +
+
+ + e.target.blur()} + defaultValue={settings?.NativeLLMTokenLimit} + required={true} + autoComplete="off" + /> +
+ ); } diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 53d42f2e2e..1c4069ac9f 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -174,6 +174,7 @@ const SystemSettings = { ...(llmProvider === "native" ? { NativeLLMModelPref: process.env.NATIVE_LLM_MODEL_PREF, + NativeLLMTokenLimit: process.env.NATIVE_LLM_MODEL_TOKEN_LIMIT, // For embedding credentials when ollama is selected. OpenAiKey: !!process.env.OPEN_AI_KEY, diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index fff904c462..de1a97f3d7 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -94,8 +94,6 @@ class NativeLLM { } // Ensure the user set a value for the token limit - // and if undefined - assume 4096 window. - // DEV: Currently this ENV is not configurable. promptWindowLimit() { const limit = process.env.NATIVE_LLM_MODEL_TOKEN_LIMIT || 4096; if (!limit || isNaN(Number(limit))) diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 54e6840291..f44b040b71 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -110,6 +110,11 @@ const KEY_MAPPING = { checks: [isDownloadedModel], }, + NativeLLMTokenLimit: { + envKey: "NATIVE_LLM_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, + EmbeddingEngine: { envKey: "EMBEDDING_ENGINE", checks: [supportedEmbeddingModel], From 0df86699e7b4b8a76c83da796c1295864da583e3 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 17 Jan 2024 18:00:54 -0800 Subject: [PATCH 022/298] feat: Add support for Zilliz Cloud by Milvus (#615) * feat: Add support for Zilliz Cloud by Milvus * update placeholder text update data handling stmt * update zilliz descriptor --- .vscode/settings.json | 5 +- README.md | 1 + docker/.env.example | 5 + .../ZillizCloudOptions/index.jsx | 38 ++ frontend/src/media/vectordbs/zilliz.png | Bin 0 -> 14336 bytes .../GeneralSettings/VectorDatabase/index.jsx | 11 +- .../Steps/DataHandling/index.jsx | 8 + .../Steps/VectorDatabaseConnection/index.jsx | 10 + server/.env.example | 5 + server/models/systemSettings.js | 6 + server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 11 + .../utils/vectorDbProviders/zilliz/index.js | 365 ++++++++++++++++++ 13 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx create mode 100644 frontend/src/media/vectordbs/zilliz.png create mode 100644 server/utils/vectorDbProviders/zilliz/index.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 82165a1789..ab66c194b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,13 @@ "cSpell.words": [ "Dockerized", "Langchain", + "Milvus", "Ollama", "openai", "Qdrant", - "Weaviate" + "vectordbs", + "Weaviate", + "Zilliz" ], "eslint.experimental.useFlatConfig": true } \ No newline at end of file diff --git a/README.md b/README.md index 6e3df0df4a..c3eb429c02 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Some cool features of AnythingLLM - [Weaviate](https://weaviate.io) - [QDrant](https://qdrant.tech) - [Milvus](https://milvus.io) +- [Zilliz](https://zilliz.com) ### Technical Overview diff --git a/docker/.env.example b/docker/.env.example index 8d33a809d4..f3eba2418d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -99,6 +99,11 @@ GID='1000' # MILVUS_USERNAME= # MILVUS_PASSWORD= +# Enable all below if you are using vector database: Zilliz Cloud. +# VECTOR_DB="zilliz" +# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" +# ZILLIZ_API_TOKEN=api-token-here + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx b/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx new file mode 100644 index 0000000000..5a26b437a0 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx @@ -0,0 +1,38 @@ +export default function ZillizCloudOptions({ settings }) { + return ( +
+
+
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/frontend/src/media/vectordbs/zilliz.png b/frontend/src/media/vectordbs/zilliz.png new file mode 100644 index 0000000000000000000000000000000000000000..e755b0f12085c61990ce8ae45d62fec9fcb3c987 GIT binary patch literal 14336 zcmdtJWmwzI)-Q|{cXudKqku7P49I20>hyf_pn#oJQ6xVt;WC4u4=XmLB? z-tz3dpZ7f<&N*Ms$#vz*WY(HBzh#q|`L9I1(on?1rp87>Lc&v4l6#GWgsk%T#zaT- z_;HYTZjgonI=hpdaUGYIT~}9yO@t8+g}`Fi1uSQFPjwh)4#tY+-z*c zUdt)`ofYv-lI@L$2SkjQ*W25h$6J8M#m$bFUsP0-mk+=T0B|EXxZQoh9+p1bV0UJO zq<DXlh|1Ef}B8)hH>ZR z=i&c1ls4949`+tip#K0a63G@AztiKKUZ;}+O5Q2mNLI7bz)C=;9hzbGu z{yG0&#Q!!#$Hmk7&7)dUfPZrSSKoiJ|Npr8-_7+8RsY?r|A!n6`Ipps^zvUh`{-W; z0pVYPfAcS*PfW(sLPxfJscZSZ-yU4W#(=@Xp3j5bOq*_Ep)~G~1#6su=tdn0RDb;7#Ptt9zlm*nS zt%y5UQ}dbf1&gTUWb9Qai!Ja>NC4FMN*#?Cg%RZzKzTpGt0i ziqh57(_;!F>`Kaa!gv}+sHK=s`Vtd^MTId8^5XyYrb;#4y8&}@%`p3*V3l_nCk9kO zqo@2{m&d9>7>AmeYU4yCkQeHoP$CKaU= zrdQc4nJs-&2egd&?;c0&f3G(*eDCzg_T@(tjbhCzQ$GU1=M3ekL!Tl-Eg6I;*cN%O z1C^Z`RndET;v&oxk94$Wg&cCE2tH_`A58&3m5A&SN~(eCl)d2x7LNR|zl;6{U~shp zLa_d$Y6hckm!hZS3SZjLM7@3zon`2P^;(V&(Nqx|4nar}dX!Y>rx9Y?DnIsAEBZy% z$1|@HLD~1}nDvkNZ0+z!H|Nw*azwo9VOW;pb8YNnlb=asXC4Zcgs*PEVZ)HhWgi}0mg8FtF@kqfiwo~3GKW--4GY26q*`*; zDMU6`#_%3p`2Ft)meRQ0-dO7ky^IGNJ=t2eqNN<__Cp`gd=<83C}0S)%9m{9mec<} zF#tX7b~%Is)<85-2^#2ZdYY(p-596!p8azy0EKoQ_4pR`Q!Bl&zfGdO;qASeuc+FC z;w67GlP!I_uD7_*G%3?__JR>WE_ay7acD)oLF{y6gZ_2$Lgf)E@HC7N<^27A@8-uD zA|eGUP#Q-WPIG-m>~!xpZ(VMIQOcGxLLFLM=|J()Mp#6BVQO6y~iX5$+4FshnCl;W6XQ&at4QDSgM4j;TVDv z3nh$lm>)xrxku~n^9t5#)7P)!M>;5+YmsEXyAq_y`sYQL$9k>?k4c!GBfwY>5CH#} z_m2B)rbH5~F@F*|Xjb{8N<>2@9i&77?^TEG6?RoIhV&DTwL@)-6!$_BI9xjQB_` z&yW7vemf$x{3^UsK7Rxm-3;H8x{Lp;dn>H@eQA%R5K|zIW@}Be$XRK&nWvS(^iFFGRSJQ$Hh25TeMO^5oyF9%di;Xh3 z0E%|@%(0`b>Ef=-bj#(SJY5=KqsSjoye2PIX)t4wmO`erpacFL@2T)AhAv2#Dl})w za%j4<@9oaDu%Cul@3EXb+Q>)W%Ue7ei(5{}HXYQLk~o-8{Mr}w z@+D%lieu-*amFz3$&Rf_f4R-lCfvDN8n+*cCscxSsu^h2X2zhyadz<*uR*@#*fhu^wh z7|xXA1>9UZ=dnLw8KSkj^!}`H_-)0hk`QC|G~*A;!D{B!rNpo}hnIMrOC*6*&1@_ajR5`5|gIKF4=5IMSDvO}2=|nL{^bHyRd7cC`QP zoInY8jPh4=RQ`((?7!d16nu7d-v-y|dW*P@WaHAz+6W6Px-P}(FV4GNI*u_0uv?&hI z0xfA=o)th~P&w&%&gK zjQRRBIpHggf5y$gfHL&ms8HmiZeYF|T;bc=X@ zfXEWphQyWJoB0mAW~RQ6agdc`*vFd9a_mRl(TFIcj@#YZmR?Y;DH{l4-f zp*=Mf?v5P=AmHGMh^cVpC8#8y-H=MhDayLaTxkP3r#18V)zJhU2HVvzsLXGdthP8F(U$zHzr{(x0=gVK*?BBsJl=DQMl^|j{ zL$y5Ez}550xApis)3q`v_hMgPj}1vpY}$FGLeH~R*I^V{&a`fb$4LFeicA(=2Z zIQJHh=YfpNFw_yN=`dPaV0b42#~lHz7(@dM2y1*_=_5!oJ6pzgA1;Uj5jC_HnoJp4 z5?JwG7#(w5#eGYktUyye(uNAJr+{V~!)hA~pUH?Q%0H91eVf5O%#9Fa_Y2gg*099G zn_=geY!*CfZ$k@iUa*}~t-Kd7sG15ewRHx%zq^>P8Fh4zbsdN_6AsJd;4tzWX1gVr zb-}L)Xbh9f+@AT0vz_3zHd;3{J2V7~QfxI{dhX{b*+&^PK0`(B+3+35V#mUz$}TxU zzkA1=%Jh)8KC$ky#cP=JIV|z3Wi6oo13S(;8QDB%i~ zu_q1dSml$4#Z9*(M=)XWChuUcvew=lJmKSa&G%>&YiP$69l1}YfeL5AG~-w$WsMPX zhDfAjottw;c9s^5JS2%y*YB$&dbzzkGtHzyEpC(j@J-w82DK$Z>ErM3D{{CvBOf#X zO$)qZz}p0aR~w06702V@2b$2f;XL1N$YQo*ij6oD5&lYic=Ao>?Lyz0t()kdH%TYY zh8$-*W;h_{eA|w!!{g^~EySd-;ah)FfD*I1CQt0nz2k+9M-1L9yES+*M{de@Y+kO7 z(sXK$h5kOZaJHXNN zR*^IQqH-y=m)+N6t`Va{c#v^xK0p4SH4+kOl5Hincny0V4d+f@ayIr=x?h?eqvYGW z1MO16D5ttr9IS5S4TIfQ7l^VBSFdUr2eiBmlmO6^fxK2| zZ8LO>D5;PlMT?xWfG2{=rMs@|s9LGi-OLX!0zX_>_(rUWD)NV4w$##QBRQ3QtJs7z z)5=gN8T>%!aw(=VfFcrvk>}CaTpOGlfclmpwNrtNaEKiz_G2wHOTS^9DcFI8E0yww z{(f6}Y)&A#Fzb6Q0KK`b?hC5-3BfK-k&sCsA2Q(Bx&f}y;K${(rf!i8uvJV(hnEgR zi&nnEHDI82QjgTqRdpFie#e}V7ydzEjD6FnzYeCo-V{P@u{lEA{V$iT);_? zbPmKTZTik)c=%D6Kk>Ex`pzWUV(sZkrw_E$8PYEeoqC8-1tcf)5GV)q>TtxT=NHGq-+yg>i%;Y)T1+qC#?N&$fN4bB-?Q6q+pe67- z-)N0F6iAjR!ijAhFgRxpQKSL|YZs7~w+yiw7c{*X1&oHK!e0%!F_w&sSGUbv6q~KF zG^^7->xJk^n=?c)yNpG;yM$wWQctp>Zayn?|KW@s)JomA!lE1z1yGS$YlDpN!zYP1 zTzEH92gkY1Pz3C{JK7@my|mN!)uy*v-QW4Om4Nq;MhOpZh*|)y6|vnC&*cpc zd_-sC_n^*2qsqco?eCk81jUyEhX=_*GiUgTnrR)X3Eh2o3-N_wbmCnLc2~I;R}KOa z(w%wukxk!P=8Cg&jP&o`PCWFe1u*rkC?>SycY}jViZhe|vdI9weykj5qcLoC_}QRS zK`cNH@#`D278x}z+h0E6g87FE^Ngvwd30yk;+kuJZA|0#a7HL_?-`|7&z67g%E9w9 z)*SaO`^;vL>$z2fU3S%Q!q)-J^5qtyWif08gM#!A{9mx;D z8=4V;p?Uf)?1SsAgLpJup!BZx{-%9feP1==+0{Gvct5P+h&A7X()y2pnJ*Z{F=fHwI8%Zg6-q_xlks?=p~2S> z&P=c(jre``?D$IgdM31dt-}xJa0A;Y(5&fzzR-p!i!bmQj<8{nx_{U+-f zY%38m`L)l5I_eFxRz!#MTvxg|6zqnwuU1otp6!o~ON-m85w?!xVrbls+!lmGE3xC; zGBr+Vohl|VuH)++~P5BYp?>RazbwNA%-Vh2pe{7uh4#Lx4 z#T54fDFtR8RQK<5azxlagrVbmZ?s7D&&zplq+OCAs}E(Qir0pT^G==HHO9(OJtZT9 zOnY10cx||p@;3i;)TMVGQ%$j!pD4hra}>+hu%l z!S5K;^byp`%!i;&_%9mxgleUu=Ivl5tE1IKNomz`iKC+&ecoU;($$XaHgEH80nU{# z6~%HBomqwjE!`4LmqY-Pplf48qdvh-5cbI1ai;hg#dO!J!)k(kW8EYH3Tp44-|w03 zH?~mfK_bSqL|kg!5{JKN1T_R z6u1BYbx$|9HyMK2jw3#qTKwFG90Psqv|e{meSGiweC{jS99}qQ?VX`n!NU*a6W9&% z0B@-2d9;epF>1yG@=Q4zsz!JXhD^oA$kKE*l?edllTUBf?X5@6;UdIFb!VRX$Pf2Z zw$(DW%5AGl2>;<^*Tm4|IjeL$`Tq=hl$)9e!cB(}jvF-5q3K!a( ze+nNiV5F}+$*OI=d&H#<$?&b;ru;{Og?UKnX$zz zZiXn`Y+32^aK2yZ;Ph5MJD}gw?4JzO!hB{y0b;jXGTw%z`d6Yk)_N2t+uf82POF$; zNyI`!#zYJn_-^Km>As~lZ(H3qr76;EL^qf~FaPn@NSXi7E}AUH%ZFsxb#8Z3>a6G0 zpdk0shbfKqW63|1P@i9UhvN|rH#IL2_Z`G!WN;V;UjX_@4#k7>;iY4y5qS!5JfZSi z4^ik_&(G?r22DeLrn@#Diw$kwuN+vi+205-YAJ-Y`dJ>C`1ql78PS?QjnljWRU@`e zpK`%jGD9Ob|_H*H>U-ZX*=pEljOG|v2l;aR^Nxe)}QiitI=ukpoc5yB}R zdG~WaZu>S~|N5x9WM;dFXMG$)lHRZ#e;N34DHrQPP9deHq@!*Wu}{VuIyufb+Yj73 zy)~<;Uic*!v43qNG-6EofF=XrYJbI(%=3#!zGWFOU`Ax_cz!`{4{LX z;>8uhZu~4`dvnDH@71}tUEs^asVN;W7FV?HTI_a5I+<1kP26u2lQw-UizviwYE5W zTX8w+H-PoK;J%9Svhwxeo@w8E$!?X0(w-n(L$Wf^80RQ(+1MCn5Gt+ z$U4u<@0`h}?#75Talgl*d!bf^*+*(5su&f^&KReL;lecf{#IgO_7VH?Z0eXaAJZSb+mfmvvg9cG-24p# zf9)Hjg0~%&M-v^p^`0%aK?k}lqHhE3i3Ce`tBx=UskEZunPLoq4^RB-I7Ybu&#Q4n ziz#0jp=unXY}29MiNA~rtm3aoWXJhHwhwJ{<#SO^YhExLHjMA~sAffv+wT}Cz_X_> zU|=6sMVub2Zg_m)zCP#N>=k^UhWBjLb)FnBvDP~>H4BDoDR#dW@ zAFNdTP;rvy_R_ixb6~e%hzgAyK9klK$G&jaT|(!di{K1daw@WuA|h?A#FM41%$7P3 zd1kQV4=@sQbH|tRi9ALoMZ{G|Y{f;Fw0T6oc_=CdGs0z1p?ePw|2n$9AWTk?B8KFm zr>M^v)=(5YgJC(JE%A9WIam-_)>EnA9n+P*ewdC-$-|baB1QgcG(IO{1-Fyie6B=l zx0rQ@uIBo}K{KTa{J5gT3SJSx&Uq1O)Q{U$f=dY6vH-c15n2!6gZ$eMFAhIe){op> z$qZhK6hQMy2t?{zP;3LRNyxjGaw!))`)US|78k#eXD?wXc!tm`K4n5&dw2WSlHLCy zRrAO2lVPP7xZP~4CP44-PU>xCLd=G9 zsJkQsU_Mv^`-t1|fqMX@2-f`>;6Sp2Q8}WPwR%pAZ_8?DlK#jCw{Kiilugy@3T)nR zsOEI#B(8Qz*~c4vwp2q=y7Zeh?wtnS!4gY|B4UkS&Bkc=qm^14VU&3ZOcvs%?slf= zYHW=wIj4VjZ*4JtZ&ALj3w+iK-}8jnkeF0KP}K~uD|jPwGQqOwdk%&atMr@Y+)M^P zaD^2Os_>8KaDJy#2H81P_x1YLs=%7$l713C-Z-IN3e>o9Plpcig3o$Y?ZWRz5YO5|M$BxMy&4>3{gYVl(Xg$nx89>ehOZ!6 z<~NjV+yl3C{N+PDW5$hSpRSM;*tjWFBIDy=TiS8ftLPb(D8=6+Du!G8f7SZGrBMc` zuouJVD9lx_p7pxYqV0!KLG0mANyd zOtO#_u{>LAMuw+hRQNX1Ak-M& zTq;MFNDq~g+EtEtnTOfF6Hls^Mtauo;f>~E{VKQ`jy%c=M2LgwIm5fEWN4zk#aggn zwNmc-f!~kGQK_NrC!LoU0u;&GS9{v9<73*uRb6uX)5U<}7wzY-s86}vR;VF?Y#Z|p z7_8;tCytgsdG-wyz41j9VECS`vKB)omvFA`&ST12=<%j_zl5=ec?-tX2x{1&*b?EQ(X_ZLSK)1 zD3V73LQ@#RO4i;*eu^~p3s9Rac~)qW-tp(f3gui3|Zj3>)kG6M=P&c5bnV~DZo=SShQz?mGHc59@o;7GN zhHHpEAx#~sIoCl_kIt<8PMLvaC7gJINcCQP_hH278Dzhsa67Y@p6*1`J!DE&oX=+4Nhay)*sqyqb(7R$Uzs;-41W$awBu;bwP9Mta z+BaP1$fRu}YRhMG8#{dMY<9Ep0FA|eW&Ch~lfDx~mY}0U+3t0jUSg?wdqiq(Sg2U_ zXU8H|_8i+a!sGVk#YrRgl5HYu(i32vsEH3(%a570+V*AC1g?-QrfL%krzp@S@C3&+ z%W$rsF`GQqYO>fMzno5nUmtvXjccuLV~^Lcu7>!eOE#y({n%Iv<;)USZm}8s6ePYB zR_qDAUWNCIvTpmmw||hiyhFL1e$hOOWBF8TvGN!xHV5;8-X(=`nd@}K400ns!4orV z$m*}8yHGe8pV9xjG*$cZ`~-gc+Ms@VOOj_Od9JIzEiBpCK-e7fMg9+)Q`*Z7l0N6i z4|#*s>D!*p*30Pu-}(>70i&y^@U7QVtFK2->TAk|$V1d#8R1GFVu+}n(`!3ieL{mz zL2mZ37`2KkJ>p!KU?Ct5!vbx@=0UAmE8r5Q)F2plI35BWEmF1){*cKvrO28&^5Gm} zB%ZP1o_+>ef;DQwh`A|yF;FOX2Wp#yl)C!aQqKRiM?Kkd60$>ZNf)uwqWK zpBZBVR;ux=HA1y`W#reR1Ro}^_pK;b>GyVzC@p0=W2#%04REgDF@1{zm28gJbU4;L z;TqhCm{+T^rx-B{XS*uYn4Kv{_8VSlopV&cnrP%V@xqm8oaq;`sysuI_`|;(7}C5S z-Bs`z@#G8x@&Md8qSjn-5&=d27W)w<&J!#n3HbSO1h8As{qPD{+y6X9qLfn&l9Zuo zxaz~y(j?Yqni*qIHl*o~o$e8Vf3{+(PfAWiiG`CZl-uoj(RFswxyOsY7Gp|P`qrgx z_ilmz`|+fgD}!6B804mx4@NPW%2Mdj%pGsQ6eB3QC=@|rlzhJZOD_e_u?w%&WY&&E zQ55pPG|rIBPKf=yV7a!x;Sl~+BsM2-qe;ZyTiH z$TTh;v6oaWx+c8j0nJ*0HflnX>FiCIze-p$kKAKl3wwK;Y5_^r6t23A`POZH4r;qE zY_mX#k&vvs2H;!oaEc)G_9ltzq$iZQ+3LkML0*P7r~IcoJ|HP6k$U4}3X0C0);m*9 z*X0__AUu<^Mr>-v`$UU`DkB4nw2C)VF; zC(E8jbw)YoIm;%`E#n%pq?)p(AZ0INYq)IlNHu5Z9Y&Ps`(5?OF}~uuG)tH#aC8k-Y(JOokHw@BZl2@an0z5gYbk0 zuO8?zr^Qn6-dRjnk%ins9$74KXp`tR%kFOZ|UKc?trkn8k&D9Ft)ASIcXB`AQAxT!bD^4*Rmm2UtbdU${ zJl_KNVv6#4T4z=+R0`q5u^n8zB#~ptjTs4Y$?xBv=jcfukoj{TBy)`kIK7IA-VLZ? ztE4z*b?@YzNKf4n7y0L92}Rtr;f$|hv3X$E2xZz0xyh#Osw7dy?xgAD#XTuaRgV`; zQrVk*XB=PuWxAWv9u0ClVqvM<-^?KxB3KoJ7aUw}Jkfi@z*O_HrOc6T3i>;A@x7US70TV0b7ax-v z%=pr{iSi{UT#_l&Vj7&$if8325r`7hh;PJ@m@^-0HT62zDyDM)th@i?psP8>^&{3Zq}KR% zL}vFiB|V7-=FOH2v8MaXgozZSh3qjg&~i@5-KM<>L*SLO&n^$JC`-4jag-C>yh5I< z9oYxTUZCTq2TVCcY6iM}Khdr?tNHa@T8K6@5)lh`_Zviwsxr~UpS`^)7YLSkHI=bU ze92o*jI9#}@+Izmq`8D>3U6@Uh_4+m5Hc*-v)U}=3!5zcysIU)QcLOdxxIdRYgyE8 zAO}t^F%R%w4*be*Ykyuu1<9^*?WIe9akprQCjen(@#4Qoyo=j$&}tlxo%w!X7Ypdr z&B;sccFWU|(Mtr~o@~ZdDh-EYr-hJ%5Wf^PlR8&Z9!_Wp%eW6sSYirJ2?I~Rd4B%iP`olB{v(prTIMoJxeQS5maO^hpDR zq9q^UCS@7t8rxo%3IMAQbrwk8cPwniiP&XxiWt~jAirrpo5^mMzn^1)cybdb3gL65 z`vz7s$vk~?c73eZJyHCs85r6bHJ}ZrdiG)(J)C1u5(M8;@dx?a{fK;9BuwF&m}rzL zkn##;q7~VMcFmMs$oPA;R zjl7~^-*5#u+}DUlhY#^QQRR@h0Y>^pC+ko1N)8>*I-|IdI6yQr`LhC1Y!vTHZ}BDP zXT#>@qPH|~rgp^PKIK{nhS3T;Lxeh#pfP_EXt2KJ5K9TDxlWux?Y@+X0p!fgew zs;8n{5IAn4(&%F$4O}>CAFs+*@ zL+%!%#OP=;eYFK*Hm-0JH>>m z-k05upkIZ0sR5wk?%ywMRe?>PE}A`G=lc0ks7<@&waDj_cET(r0N<&Ob)v1|F&YNr z{u_MpnSVazy1aAarWjI1)S7aE1%|kWsx?t5S4kZW7bW{Gg9Dow?wdukGvpf(C3--A z%xVCOGV7(GAG`Xsq^oAN+S?-nb7}z?Q#+!bwX%UWK`o_JiM@w!X(sHrUgH6r)3Xte z-C*vgw?&7jmc{23T?8d^Y=m06$#TBY`gp%07S-Qh*V%v5m0J zq3mNh|L(!5n!JsV;vGm7c*_Jt0Hg(@*)wKWvbrZV!ZCNq)_N8k8b8n&j;-R5{4ilh zRC&o)8mxTESPT#%(MW4(&WXcy4Qk#PZx6XMBj1i0VA6_)LK5W{)w6A4NcOsV6@hl~ z8ixkv_ypi#GjX}C$MJ=tv))Q#hE{5h)+5zJ8TQnNmm7NXUp8NgCOqQ%o~3@~+RpCT ziflCVF3UKBOaF8}UNRM~%KdWO_YonFfFd7-rtl-gI;tO>p1H`OzT%%2(2ngJUuhsp zi(j~MQ{kN0Bu`N;uTX1tqv=&V=oo??DIIWtsuOD_J46F@Cmo?qj+aiKMJKY4gTB|| z)7!_PCjiy##eg{MiyvY6JpoYrAIvA8%)y~6Mm#CW2=^FBcxaaO-BpiH)n;*pZ zzJq@%QTdv7^k6h6qG+glsb1*E`A&lzKL#U8(N+wRf#sS9eB}g<@1?H z%@MtScEDkWN&V5<{QCz(>iR<8q~FYcMK@WSXr!y6^e>_3azLJ&6=*tQ5=ne?sg1r~ zO?M+-X7Y$8jm&SWeJ|XvXve~l7&Nle>j!<$|7P1Cd|BVtqueN30*P+(`F}1>7Lc+E zhAWSLE@5>#MMtQw0CFSW)k_;3m(S+h>jhDUGOI!nRTySCz#6?eW|GWemL=r(5^RQV zSyyzvh;^HC*Te#YoNyn+k=80oG7tP8E77flvvQhZ`}WRE;A3Bf;$(k`Jo+X1q`&8P zS|(`fcjE`G=q^NMf3RgOCe0UN)$D89!GxDKRFCE1OI%a0(_YFUA=;7sC(6^{2124o zR@XP#lNxElC}fXyyiBh~Y<8Mxc;w^|!3-%}jo+!F;jxTHl&(g<;7`p%)N{u(BCN}| zI>S)HzTJ zKv;juNFJDd6+g3J8L6-9pu|O^5NKiQ_dMt16e48_BNX0QbNYYbZxR3b=MLxt^6B8- V=Mc1O`p5s)Da&ifmA|wI`5#n5e98a- literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index f49054b900..02887b86a6 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -9,6 +9,7 @@ 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 ZillizLogo from "@/media/vectordbs/zilliz.png"; import PreLoader from "@/components/Preloader"; import ChangeWarningModal from "@/components/ChangeWarning"; import { MagnifyingGlass } from "@phosphor-icons/react"; @@ -19,6 +20,7 @@ import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions"; +import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -33,7 +35,6 @@ export default function GeneralVectorDatabase() { useEffect(() => { async function fetchKeys() { const _settings = await System.keys(); - console.log(_settings); setSettings(_settings); setSelectedVDB(_settings?.VectorDB || "lancedb"); setHasEmbeddings(_settings?.HasExistingEmbeddings || false); @@ -66,6 +67,14 @@ export default function GeneralVectorDatabase() { options: , description: "100% cloud-based vector database for enterprise use cases.", }, + { + name: "Zilliz Cloud", + value: "zilliz", + logo: ZillizLogo, + options: , + description: + "Cloud hosted vector database built for enterprise with SOC 2 compliance.", + }, { name: "QDrant", value: "qdrant", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 3b0046382a..ae57302763 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -10,6 +10,7 @@ import TogetherAILogo from "@/media/llmprovider/togetherai.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import MistralLogo from "@/media/llmprovider/mistral.jpeg"; +import ZillizLogo from "@/media/vectordbs/zilliz.png"; import ChromaLogo from "@/media/vectordbs/chroma.png"; import PineconeLogo from "@/media/vectordbs/pinecone.png"; import LanceDbLogo from "@/media/vectordbs/lancedb.png"; @@ -139,6 +140,13 @@ const VECTOR_DB_PRIVACY = { ], logo: MilvusLogo, }, + zilliz: { + name: "Zilliz Cloud", + description: [ + "Your vectors and document text are stored on your Zilliz cloud cluster.", + ], + logo: ZillizLogo, + }, lancedb: { name: "LanceDB", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx index 37e0e5b736..af0b5662d2 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx @@ -6,6 +6,7 @@ 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 ZillizLogo from "@/media/vectordbs/zilliz.png"; import System from "@/models/system"; import paths from "@/utils/paths"; import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions"; @@ -14,6 +15,7 @@ import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions"; import MilvusOptions from "@/components/VectorDBSelection/MilvusDBOptions"; +import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; @@ -68,6 +70,14 @@ export default function VectorDatabaseConnection({ options: , description: "100% cloud-based vector database for enterprise use cases.", }, + { + name: "Zilliz Cloud", + value: "zilliz", + logo: ZillizLogo, + options: , + description: + "Cloud hosted vector database built for enterprise with SOC 2 compliance.", + }, { name: "QDrant", value: "qdrant", diff --git a/server/.env.example b/server/.env.example index 26c51927cf..23e20bb138 100644 --- a/server/.env.example +++ b/server/.env.example @@ -96,6 +96,11 @@ VECTOR_DB="lancedb" # MILVUS_USERNAME= # MILVUS_PASSWORD= +# Enable all below if you are using vector database: Zilliz Cloud. +# VECTOR_DB="zilliz" +# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" +# ZILLIZ_API_TOKEN=api-token-here + # 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/models/systemSettings.js b/server/models/systemSettings.js index 1c4069ac9f..90de463f09 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -63,6 +63,12 @@ const SystemSettings = { MilvusPassword: !!process.env.MILVUS_PASSWORD, } : {}), + ...(vectorDB === "zilliz" + ? { + ZillizEndpoint: process.env.ZILLIZ_ENDPOINT, + ZillizApiToken: process.env.ZILLIZ_API_TOKEN, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 2eed9057ca..b72bb7977b 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -19,6 +19,9 @@ function getVectorDbClass() { case "milvus": const { Milvus } = require("../vectorDbProviders/milvus"); return Milvus; + case "zilliz": + const { Zilliz } = require("../vectorDbProviders/zilliz"); + return Zilliz; 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 f44b040b71..9e89047ff0 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -199,6 +199,16 @@ const KEY_MAPPING = { checks: [isNotEmpty], }, + // Zilliz Cloud Options + ZillizEndpoint: { + envKey: "ZILLIZ_ENDPOINT", + checks: [isValidURL], + }, + ZillizApiToken: { + envKey: "ZILLIZ_API_TOKEN", + checks: [isNotEmpty], + }, + // Together Ai Options TogetherAiApiKey: { envKey: "TOGETHER_AI_API_KEY", @@ -316,6 +326,7 @@ function supportedVectorDB(input = "") { "weaviate", "qdrant", "milvus", + "zilliz", ]; return supported.includes(input) ? null diff --git a/server/utils/vectorDbProviders/zilliz/index.js b/server/utils/vectorDbProviders/zilliz/index.js new file mode 100644 index 0000000000..b8493e1c28 --- /dev/null +++ b/server/utils/vectorDbProviders/zilliz/index.js @@ -0,0 +1,365 @@ +const { + DataType, + MetricType, + IndexType, + MilvusClient, +} = require("@zilliz/milvus2-sdk-node"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { v4: uuidv4 } = require("uuid"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +// Zilliz is basically a copy of Milvus DB class with a different constructor +// to connect to the cloud +const Zilliz = { + name: "Zilliz", + connect: async function () { + if (process.env.VECTOR_DB !== "zilliz") + throw new Error("Zilliz::Invalid ENV settings"); + + const client = new MilvusClient({ + address: process.env.ZILLIZ_ENDPOINT, + token: process.env.ZILLIZ_API_TOKEN, + }); + + const { isHealthy } = await client.checkHealth(); + if (!isHealthy) + throw new Error( + "Zilliz::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalVectors: async function () { + const { client } = await this.connect(); + const { collection_names } = await client.listCollections(); + const total = collection_names.reduce(async (acc, collection_name) => { + const statistics = await client.getCollectionStatistics({ + collection_name, + }); + return Number(acc) + Number(statistics?.data?.row_count ?? 0); + }, 0); + return total; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const statistics = await client.getCollectionStatistics({ + collection_name: _namespace, + }); + return Number(statistics?.data?.row_count ?? 0); + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client + .getCollectionStatistics({ collection_name: namespace }) + .catch(() => null); + return collection; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { value } = await client + .hasCollection({ collection_name: namespace }) + .catch((e) => { + console.error("Zilliz::namespaceExists", e.message); + return { value: false }; + }); + return value; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection({ collection_name: namespace }); + return true; + }, + // Zilliz requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + if (!dimensions) + throw new Error( + `Zilliz:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); + + await client.createCollection({ + collection_name: namespace, + fields: [ + { + name: "id", + description: "id", + data_type: DataType.VarChar, + max_length: 255, + is_primary_key: true, + }, + { + name: "vector", + description: "vector", + data_type: DataType.FloatVector, + dim: dimensions, + }, + { + name: "metadata", + decription: "metadata", + data_type: DataType.JSON, + }, + ], + }); + await client.createIndex({ + collection_name: namespace, + field_name: "vector", + index_type: IndexType.AUTOINDEX, + metric_type: MetricType.COSINE, + }); + await client.loadCollectionSync({ + collection_name: namespace, + }); + } + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + let vectorDimension = null; + const { pageContent, docId, ...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 { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + + await this.getOrCreateCollection(client, namespace, vectorDimension); + for (const chunk of chunks) { + // Before sending to Pinecone and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const id = uuidv4(); + documentVectors.push({ docId, vectorId: id }); + return { id, vector: chunk.values, metadata: chunk.metadata }; + }); + const insertResult = await client.insert({ + collection_name: namespace, + data: newChunks, + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await DocumentVectors.bulkInsert(documentVectors); + await client.flushSync({ collection_names: [namespace] }); + return true; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + 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); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; + const vectorRecord = { + id: uuidv4(), + values: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + + if (vectors.length > 0) { + const chunks = []; + const { client } = await this.connect(); + await this.getOrCreateCollection(client, namespace, vectorDimension); + + console.log("Inserting vectorized chunks into Zilliz."); + for (const chunk of toChunks(vectors, 100)) { + chunks.push(chunk); + const insertResult = await client.insert({ + collection_name: namespace, + data: chunk.map((item) => ({ + id: item.id, + vector: item.values, + metadata: chunk.metadata, + })), + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await storeVectorResult(chunks, fullFilePath); + await client.flushSync({ collection_names: [namespace] }); + } + + 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 }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + const queryIn = vectorIds.map((v) => `'${v}'`).join(","); + await client.deleteEntities({ + collection_name: namespace, + expr: `id in [${queryIn}]`, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + + // Even after flushing Zilliz can take some time to re-calc the count + // so all we can hope to do is flushSync so that the count can be correct + // on a later call. + await client.flushSync({ collection_names: [namespace] }); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + const response = await client.search({ + collection_name: namespace, + vectors: queryVector, + }); + response.results.forEach((match) => { + if (match.score < similarityThreshold) return; + result.contextTexts.push(match.metadata.text); + result.sourceDocuments.push(match); + result.scores.push(match.score); + }); + return result; + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + 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(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const statistics = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + const vectorCount = Number(statistics?.data?.row_count ?? 0); + return { + message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, +}; + +module.exports.Zilliz = Zilliz; From 658e7fa3909eb49d3ce1d106eeeed9f8f158b970 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Thu, 18 Jan 2024 11:40:48 -0800 Subject: [PATCH 023/298] chore: Better VectorDb and Embedder error messages (#620) * chore: propogate embedder and vectordb errors during document mutations * add default value for errors on addDocuments --- frontend/src/index.css | 4 ++++ server/endpoints/workspaces.js | 8 +++++--- server/models/documents.js | 15 +++++++++++---- .../utils/EmbeddingEngines/azureOpenAi/index.js | 16 ++++++++++++---- server/utils/EmbeddingEngines/localAi/index.js | 16 ++++++++++++---- server/utils/EmbeddingEngines/openAi/index.js | 16 ++++++++++++---- server/utils/vectorDbProviders/chroma/index.js | 7 +++---- server/utils/vectorDbProviders/lance/index.js | 7 +++---- server/utils/vectorDbProviders/milvus/index.js | 7 +++---- server/utils/vectorDbProviders/pinecone/index.js | 7 +++---- server/utils/vectorDbProviders/qdrant/index.js | 7 +++---- server/utils/vectorDbProviders/weaviate/index.js | 7 +++---- server/utils/vectorDbProviders/zilliz/index.js | 7 +++---- 13 files changed, 77 insertions(+), 47 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index e8d7e2d8ca..729cccb5ff 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -403,3 +403,7 @@ dialog::backdrop { .tooltip { @apply !bg-black !text-white !py-2 !px-3 !rounded-md; } + +.Toastify__toast-body { + white-space: pre-line; +} diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 7873ef76a1..7119297f6a 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -159,7 +159,7 @@ function workspaceEndpoints(app) { } await Document.removeDocuments(currWorkspace, deletes); - const { failed = [] } = await Document.addDocuments( + const { failedToEmbed = [], errors = [] } = await Document.addDocuments( currWorkspace, adds ); @@ -167,8 +167,10 @@ function workspaceEndpoints(app) { response.status(200).json({ workspace: updatedWorkspace, message: - failed.length > 0 - ? `${failed.length} documents could not be embedded.` + failedToEmbed.length > 0 + ? `${failedToEmbed.length} documents failed to add.\n\n${errors + .map((msg) => `${msg}`) + .join("\n\n")}` : null, }); } catch (e) { diff --git a/server/models/documents.js b/server/models/documents.js index 4505089d5c..8f3b88fbd8 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -39,6 +39,7 @@ const Document = { if (additions.length === 0) return { failed: [], embedded: [] }; const embedded = []; const failedToEmbed = []; + const errors = new Set(); for (const path of additions) { const data = await fileData(path); @@ -53,14 +54,20 @@ const Document = { workspaceId: workspace.id, metadata: JSON.stringify(metadata), }; - const vectorized = await VectorDb.addDocumentToNamespace( + + const { vectorized, error } = await VectorDb.addDocumentToNamespace( workspace.slug, { ...data, docId }, path ); + if (!vectorized) { - console.error("Failed to vectorize", path); - failedToEmbed.push(path); + console.error( + "Failed to vectorize", + metadata?.title || newDoc.filename + ); + failedToEmbed.push(metadata?.title || newDoc.filename); + errors.add(error); continue; } @@ -77,7 +84,7 @@ const Document = { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); - return { failed: failedToEmbed, embedded }; + return { failedToEmbed, errors: Array.from(errors), embedded }; }, removeDocuments: async function (workspace, removals = []) { diff --git a/server/utils/EmbeddingEngines/azureOpenAi/index.js b/server/utils/EmbeddingEngines/azureOpenAi/index.js index e80b4b734b..4193e860d1 100644 --- a/server/utils/EmbeddingEngines/azureOpenAi/index.js +++ b/server/utils/EmbeddingEngines/azureOpenAi/index.js @@ -46,7 +46,12 @@ class AzureOpenAiEmbedder { resolve({ data: res.data, error: null }); }) .catch((e) => { - resolve({ data: [], error: e?.error }); + e.type = + e?.response?.data?.error?.code || + e?.response?.status || + "failed_to_embed"; + e.message = e?.response?.data?.error?.message || e.message; + resolve({ data: [], error: e }); }); }) ); @@ -62,11 +67,14 @@ class AzureOpenAiEmbedder { .map((res) => res.error) .flat(); if (errors.length > 0) { + let uniqueErrors = new Set(); + errors.map((error) => + uniqueErrors.add(`[${error.type}]: ${error.message}`) + ); + return { data: [], - error: `(${errors.length}) Embedding Errors! ${errors - .map((error) => `[${error.type}]: ${error.message}`) - .join(", ")}`, + error: Array.from(uniqueErrors).join(", "), }; } return { diff --git a/server/utils/EmbeddingEngines/localAi/index.js b/server/utils/EmbeddingEngines/localAi/index.js index 1480755d76..2c9db2c734 100644 --- a/server/utils/EmbeddingEngines/localAi/index.js +++ b/server/utils/EmbeddingEngines/localAi/index.js @@ -41,7 +41,12 @@ class LocalAiEmbedder { resolve({ data: res.data?.data, error: null }); }) .catch((e) => { - resolve({ data: [], error: e?.error }); + e.type = + e?.response?.data?.error?.code || + e?.response?.status || + "failed_to_embed"; + e.message = e?.response?.data?.error?.message || e.message; + resolve({ data: [], error: e }); }); }) ); @@ -57,11 +62,14 @@ class LocalAiEmbedder { .map((res) => res.error) .flat(); if (errors.length > 0) { + let uniqueErrors = new Set(); + errors.map((error) => + uniqueErrors.add(`[${error.type}]: ${error.message}`) + ); + return { data: [], - error: `(${errors.length}) Embedding Errors! ${errors - .map((error) => `[${error.type}]: ${error.message}`) - .join(", ")}`, + error: Array.from(uniqueErrors).join(", "), }; } return { diff --git a/server/utils/EmbeddingEngines/openAi/index.js b/server/utils/EmbeddingEngines/openAi/index.js index 105be9d73a..1f9ba432a1 100644 --- a/server/utils/EmbeddingEngines/openAi/index.js +++ b/server/utils/EmbeddingEngines/openAi/index.js @@ -37,7 +37,12 @@ class OpenAiEmbedder { resolve({ data: res.data?.data, error: null }); }) .catch((e) => { - resolve({ data: [], error: e?.error }); + e.type = + e?.response?.data?.error?.code || + e?.response?.status || + "failed_to_embed"; + e.message = e?.response?.data?.error?.message || e.message; + resolve({ data: [], error: e }); }); }) ); @@ -53,11 +58,14 @@ class OpenAiEmbedder { .map((res) => res.error) .flat(); if (errors.length > 0) { + let uniqueErrors = new Set(); + errors.map((error) => + uniqueErrors.add(`[${error.type}]: ${error.message}`) + ); + return { data: [], - error: `(${errors.length}) Embedding Errors! ${errors - .map((error) => `[${error.type}]: ${error.message}`) - .join(", ")}`, + error: Array.from(uniqueErrors).join(", "), }; } return { diff --git a/server/utils/vectorDbProviders/chroma/index.js b/server/utils/vectorDbProviders/chroma/index.js index 878cf05f85..28af39e666 100644 --- a/server/utils/vectorDbProviders/chroma/index.js +++ b/server/utils/vectorDbProviders/chroma/index.js @@ -171,7 +171,7 @@ const Chroma = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } // If we are here then we are going to embed and store a novel document. @@ -242,11 +242,10 @@ const Chroma = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js index 5e58ef1c86..8f243cf9b2 100644 --- a/server/utils/vectorDbProviders/lance/index.js +++ b/server/utils/vectorDbProviders/lance/index.js @@ -172,7 +172,7 @@ const LanceDb = { await this.updateOrCreateCollection(client, submissions, namespace); await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } // If we are here then we are going to embed and store a novel document. @@ -229,11 +229,10 @@ const LanceDb = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, performSimilaritySearch: async function ({ diff --git a/server/utils/vectorDbProviders/milvus/index.js b/server/utils/vectorDbProviders/milvus/index.js index cc934a9a2b..79a1324139 100644 --- a/server/utils/vectorDbProviders/milvus/index.js +++ b/server/utils/vectorDbProviders/milvus/index.js @@ -167,7 +167,7 @@ const Milvus = { } await DocumentVectors.bulkInsert(documentVectors); await client.flushSync({ collection_names: [namespace] }); - return true; + return { vectorized: true, error: null }; } const textSplitter = new RecursiveCharacterTextSplitter({ @@ -231,11 +231,10 @@ const Milvus = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { diff --git a/server/utils/vectorDbProviders/pinecone/index.js b/server/utils/vectorDbProviders/pinecone/index.js index 7a7f862c23..594a9aaf3d 100644 --- a/server/utils/vectorDbProviders/pinecone/index.js +++ b/server/utils/vectorDbProviders/pinecone/index.js @@ -125,7 +125,7 @@ const Pinecone = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } // If we are here then we are going to embed and store a novel document. @@ -183,11 +183,10 @@ const Pinecone = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js index 2783cde932..70c069e843 100644 --- a/server/utils/vectorDbProviders/qdrant/index.js +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -190,7 +190,7 @@ const QDrant = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } // If we are here then we are going to embed and store a novel document. @@ -272,11 +272,10 @@ const QDrant = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { diff --git a/server/utils/vectorDbProviders/weaviate/index.js b/server/utils/vectorDbProviders/weaviate/index.js index 91faff64ef..ac89315af7 100644 --- a/server/utils/vectorDbProviders/weaviate/index.js +++ b/server/utils/vectorDbProviders/weaviate/index.js @@ -233,7 +233,7 @@ const Weaviate = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } // If we are here then we are going to embed and store a novel document. @@ -316,11 +316,10 @@ const Weaviate = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { diff --git a/server/utils/vectorDbProviders/zilliz/index.js b/server/utils/vectorDbProviders/zilliz/index.js index b8493e1c28..31afab35a3 100644 --- a/server/utils/vectorDbProviders/zilliz/index.js +++ b/server/utils/vectorDbProviders/zilliz/index.js @@ -168,7 +168,7 @@ const Zilliz = { } await DocumentVectors.bulkInsert(documentVectors); await client.flushSync({ collection_names: [namespace] }); - return true; + return { vectorized: true, error: null }; } const textSplitter = new RecursiveCharacterTextSplitter({ @@ -232,11 +232,10 @@ const Zilliz = { } await DocumentVectors.bulkInsert(documentVectors); - return true; + return { vectorized: true, error: null }; } catch (e) { - console.error(e); console.error("addDocumentToNamespace", e.message); - return false; + return { vectorized: false, error: e.message }; } }, deleteDocumentFromNamespace: async function (namespace, docId) { From 683cd69a975568d4197360601aed704c22f75008 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Thu, 18 Jan 2024 12:01:53 -0800 Subject: [PATCH 024/298] Display readable error message in chat failure (#621) --- .../ChatHistory/HistoricalMessage/index.jsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index c39220f37f..ba3b687b0e 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -39,12 +39,15 @@ const HistoricalMessage = forwardRef( /> {error ? ( - - Could not - respond to message. - +
+ + Could not + respond to message. + +

+ {error} +

+
) : ( )}
- {role === "assistant" && ( + {role === "assistant" && !error && (
From 56fa17caf27949818d34c367605371ceac803c74 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 18 Jan 2024 12:34:20 -0800 Subject: [PATCH 025/298] create configurable topN per workspace (#616) * create configurable topN per workspace * Update TopN UI text Fix fallbacks for all providers Add SQLite CHECK to TOPN value * merge with master Update zilliz provider for variable TopN --------- Co-authored-by: timothycarambat --- .../Modals/MangeWorkspace/Settings/index.jsx | 35 +++++++++++++++++++ server/models/workspace.js | 1 + .../20240118201333_init/migration.sql | 2 ++ server/prisma/schema.prisma | 1 + server/utils/chats/index.js | 1 + server/utils/chats/stream.js | 1 + .../utils/vectorDbProviders/chroma/index.js | 9 +++-- server/utils/vectorDbProviders/lance/index.js | 9 +++-- .../utils/vectorDbProviders/milvus/index.js | 8 +++-- .../utils/vectorDbProviders/pinecone/index.js | 9 +++-- .../utils/vectorDbProviders/qdrant/index.js | 9 +++-- .../utils/vectorDbProviders/weaviate/index.js | 9 +++-- .../utils/vectorDbProviders/zilliz/index.js | 8 +++-- 13 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 server/prisma/migrations/20240118201333_init/migration.sql diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx index da0e7b9f02..b288dc6c14 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -21,6 +21,9 @@ function castToType(key, value) { similarityThreshold: { cast: (value) => parseFloat(value), }, + topN: { + cast: (value) => Number(value), + }, }; if (!definitions.hasOwnProperty(key)) return value; @@ -236,6 +239,38 @@ export default function WorkspaceSettings({ active, workspace, settings }) { autoComplete="off" onChange={() => setHasChanges(true)} /> + +
+
+ +

+ This setting controls the maximum amount of context + snippets the will be sent to the LLM for per chat or + query. +
+ Recommended: 4 +

+
+ e.target.blur()} + defaultValue={workspace?.topN ?? 4} + className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" + placeholder="4" + required={true} + autoComplete="off" + onChange={() => setHasChanges(true)} + /> +
@@ -364,8 +414,21 @@ export function SidebarMobileHeader() { ); } -const Option = ({ btnText, icon, href }) => { +const Option = ({ + btnText, + icon, + href, + flex = false, + user = null, + allowedRole = [], +}) => { const isActive = window.location.pathname === href; + + // Option only for multi-user + if (!flex && !allowedRole.includes(user?.role)) return null; + + // Option is dual-mode, but user exists, we need to check permissions + if (flex && !!user && !allowedRole.includes(user?.role)) return null; return (
{ if (res.ok && res.status !== 204) return res.blob(); @@ -283,6 +284,7 @@ const System = { return await fetch(`${API_BASE}/system/welcome-messages`, { method: "GET", cache: "no-cache", + headers: baseHeaders(), }) .then((res) => { if (!res.ok) throw new Error("Could not fetch welcome messages."); diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx index d734759fc6..cd1c477327 100644 --- a/frontend/src/pages/Admin/Users/UserRow/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -3,9 +3,17 @@ import { titleCase } from "text-case"; import Admin from "@/models/admin"; import EditUserModal, { EditUserModalId } from "./EditUserModal"; import { DotsThreeOutline } from "@phosphor-icons/react"; +import showToast from "@/utils/toast"; + +const ModMap = { + admin: ["admin", "manager", "default"], + manager: ["manager", "default"], + default: [], +}; export default function UserRow({ currUser, user }) { const rowRef = useRef(null); + const canModify = ModMap[currUser?.role || "default"].includes(user.role); const [suspended, setSuspended] = useState(user.suspended === 1); const handleSuspend = async () => { if ( @@ -14,8 +22,19 @@ export default function UserRow({ currUser, user }) { ) ) return false; - setSuspended(!suspended); - await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 }); + + const { success, error } = await Admin.updateUser(user.id, { + suspended: suspended ? 0 : 1, + }); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + showToast( + `User ${!suspended ? "has been suspended" : "is no longer suspended"}.`, + "success", + { clear: true } + ); + setSuspended(!suspended); + } }; const handleDelete = async () => { if ( @@ -24,8 +43,12 @@ export default function UserRow({ currUser, user }) { ) ) return false; - rowRef?.current?.remove(); - await Admin.deleteUser(user.id); + const { success, error } = await Admin.deleteUser(user.id); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + rowRef?.current?.remove(); + showToast("User deleted from system.", "success", { clear: true }); + } }; return ( @@ -40,7 +63,7 @@ export default function UserRow({ currUser, user }) { {titleCase(user.role)} {user.createdAt} - {currUser?.role !== "default" && ( + {canModify && ( )} - {currUser?.id !== user.id && currUser?.role !== "default" && ( + {currUser?.id !== user.id && canModify && ( <>
- -
- - -
-
); } diff --git a/frontend/src/components/ModalWrapper/index.jsx b/frontend/src/components/ModalWrapper/index.jsx new file mode 100644 index 0000000000..37041f6348 --- /dev/null +++ b/frontend/src/components/ModalWrapper/index.jsx @@ -0,0 +1,9 @@ +export default function ModalWrapper({ children, isOpen }) { + if (!isOpen) return null; + + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/UserMenu/index.jsx b/frontend/src/components/UserMenu/index.jsx index 61e111da4e..e6f3c7cfb8 100644 --- a/frontend/src/components/UserMenu/index.jsx +++ b/frontend/src/components/UserMenu/index.jsx @@ -193,7 +193,7 @@ function AccountModal({ user, hideModal }) { return (
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index 9af36fc5a5..697c55214d 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -1,9 +1,10 @@ -import { memo, useState, useEffect, useRef } from "react"; +import { memo, useState } from "react"; import { X } from "@phosphor-icons/react"; import { v4 } from "uuid"; import { decode as HTMLDecode } from "he"; import { CaretRight, FileText } from "@phosphor-icons/react"; import truncate from "truncate"; +import ModalWrapper from "@/components/ModalWrapper"; function combineLikeSources(sources) { const combined = {}; @@ -98,27 +99,10 @@ function SkeletonLine() { function CitationDetailModal({ source, onClose }) { const { references, title, text } = source; - const dialogRef = useRef(null); - - useEffect(() => { - if (source && dialogRef.current) { - dialogRef.current.showModal(); - } - }, [source]); - - const handleModalClose = () => { - if (dialogRef.current) { - dialogRef.current.close(); - } - onClose(); - }; return ( - -
+ +

{truncate(title, 45)} @@ -129,7 +113,7 @@ function CitationDetailModal({ source, onClose }) {

)}

-
+ ); } diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx index 30bd494f34..cbe7dbdd05 100644 --- a/frontend/src/components/WorkspaceChat/index.jsx +++ b/frontend/src/components/WorkspaceChat/index.jsx @@ -3,6 +3,7 @@ import Workspace from "@/models/workspace"; import LoadingChat from "./LoadingChat"; import ChatContainer from "./ChatContainer"; import paths from "@/utils/paths"; +import ModalWrapper from "../ModalWrapper"; export default function WorkspaceChat({ loading, workspace }) { const [history, setHistory] = useState([]); @@ -28,11 +29,7 @@ export default function WorkspaceChat({ loading, workspace }) { return ( <> {loading === false && !workspace && ( - +

@@ -52,7 +49,7 @@ export default function WorkspaceChat({ loading, workspace }) {

- + )} diff --git a/frontend/src/hooks/useModal.js b/frontend/src/hooks/useModal.js new file mode 100644 index 0000000000..854b5d4a8c --- /dev/null +++ b/frontend/src/hooks/useModal.js @@ -0,0 +1,10 @@ +import { useState } from "react"; + +export function useModal() { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => setIsOpen(true); + const closeModal = () => setIsOpen(false); + + return { isOpen, openModal, closeModal }; +} diff --git a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx index 54ae7e076c..3aef87a658 100644 --- a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx +++ b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx @@ -2,14 +2,7 @@ import React, { useEffect, useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; -const DIALOG_ID = `new-invite-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewInviteModalId = DIALOG_ID; -export default function NewInviteModal() { +export default function NewInviteModal({ closeModal }) { const [invite, setInvite] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); @@ -39,74 +32,70 @@ export default function NewInviteModal() { }, [copied]); return ( - -
-
-
-

- Create new invite -

- -
-
-
-
- {error && ( -

Error: {error}

- )} - {invite && ( - - )} -

- After creation you will be able to copy the invite and send it - to a new user where they can create an account as a default - user. -

-
+
+
+
+

+ Create new invite +

+ +
+ +
+
+ {error &&

Error: {error}

} + {invite && ( + + )} +

+ After creation you will be able to copy the invite and send it + to a new user where they can create an account as a default + user. +

-
- {!invite ? ( - <> - - - - ) : ( +
+
+ {!invite ? ( + <> - )} -
- -
+ + + ) : ( + + )} +
+
-
+
); } diff --git a/frontend/src/pages/Admin/Invitations/index.jsx b/frontend/src/pages/Admin/Invitations/index.jsx index cf5b38d376..94b07f3300 100644 --- a/frontend/src/pages/Admin/Invitations/index.jsx +++ b/frontend/src/pages/Admin/Invitations/index.jsx @@ -7,9 +7,12 @@ import { EnvelopeSimple } from "@phosphor-icons/react"; import usePrefersDarkMode from "@/hooks/usePrefersDarkMode"; import Admin from "@/models/admin"; import InviteRow from "./InviteRow"; -import NewInviteModal, { NewInviteModalId } from "./NewInviteModal"; +import NewInviteModal from "./NewInviteModal"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function AdminInvites() { + const { isOpen, openModal, closeModal } = useModal(); return (
{!isMobile && } @@ -23,9 +26,7 @@ export default function AdminInvites() {

Invitations

- + + +
); diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index d8c26d1e31..9a7e2a6d03 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -4,14 +4,7 @@ import Admin from "@/models/admin"; import { userFromStorage } from "@/utils/request"; import { RoleHintDisplay } from ".."; -const DIALOG_ID = `new-user-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewUserModalId = DIALOG_ID; -export default function NewUserModal() { +export default function NewUserModal({ closeModal }) { const [error, setError] = useState(null); const [role, setRole] = useState("default"); const handleCreate = async (e) => { @@ -28,107 +21,103 @@ export default function NewUserModal() { const user = userFromStorage(); return ( - -
-
-
-

- Add user to instance -

+
+
+
+

+ Add user to instance +

+ +
+
+
+
+
+ + +
+
+ + +
+
+ + + +
+ {error &&

Error: {error}

} +

+ After creating a user they will need to login with their initial + login to get access. +

+
+
+
+
- -
-
-
- - -
-
- - -
-
- - - -
- {error && ( -

Error: {error}

- )} -

- After creating a user they will need to login with their - initial login to get access. -

-
-
-
- - -
-
-
+
-
+
); } diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 4c3c2686ae..959b04b4c6 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -3,16 +3,10 @@ import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import { RoleHintDisplay } from "../.."; -export const EditUserModalId = (user) => `edit-user-${user.id}-modal`; - -export default function EditUserModal({ currentUser, user }) { +export default function EditUserModal({ currentUser, user, closeModal }) { const [role, setRole] = useState(user.role); const [error, setError] = useState(null); - const hideModal = () => { - document.getElementById(EditUserModalId(user)).close(); - }; - const handleUpdate = async (e) => { setError(null); e.preventDefault(); @@ -28,103 +22,99 @@ export default function EditUserModal({ currentUser, user }) { }; return ( - -
-
-
-

- Edit {user.username} -

+
+
+
+

+ Edit {user.username} +

+ +
+
+
+
+
+ + +
+
+ + +
+
+ + + +
+ {error &&

Error: {error}

} +
+
+
+
- -
-
-
- - -
-
- - -
-
- - - -
- {error && ( -

Error: {error}

- )} -
-
-
- - -
-
-
+
-
+
); } diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx index cd1c477327..c58b8124b1 100644 --- a/frontend/src/pages/Admin/Users/UserRow/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -1,9 +1,11 @@ import { useRef, useState } from "react"; import { titleCase } from "text-case"; import Admin from "@/models/admin"; -import EditUserModal, { EditUserModalId } from "./EditUserModal"; +import EditUserModal from "./EditUserModal"; import { DotsThreeOutline } from "@phosphor-icons/react"; import showToast from "@/utils/toast"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; const ModMap = { admin: ["admin", "manager", "default"], @@ -15,6 +17,7 @@ export default function UserRow({ currUser, user }) { const rowRef = useRef(null); const canModify = ModMap[currUser?.role || "default"].includes(user.role); const [suspended, setSuspended] = useState(user.suspended === 1); + const { isOpen, openModal, closeModal } = useModal(); const handleSuspend = async () => { if ( !window.confirm( @@ -65,9 +68,7 @@ export default function UserRow({ currUser, user }) { {canModify && (
- + + +
); diff --git a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx index 21d9faed46..5179b3fb7a 100644 --- a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx @@ -1,14 +1,8 @@ import React, { useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; -const DIALOG_ID = `new-workspace-modal`; -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewWorkspaceModalId = DIALOG_ID; -export default function NewWorkspaceModal() { +export default function NewWorkspaceModal({ closeModal }) { const [error, setError] = useState(null); const handleCreate = async (e) => { setError(null); @@ -20,69 +14,65 @@ export default function NewWorkspaceModal() { }; return ( - -
-
-
-

- Create new workspace -

+
+
+
+

+ Create new workspace +

+ +
+
+
+
+
+ + +
+ {error &&

Error: {error}

} +

+ After creating this workspace only admins will be able to see + it. You can add users after it has been created. +

+
+
+
+
- -
-
-
- - -
- {error && ( -

Error: {error}

- )} -

- After creating this workspace only admins will be able to see - it. You can add users after it has been created. -

-
-
-
- - -
-
-
+
-
+
); } diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx index 052f845b40..cd8d5f0118 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx @@ -6,13 +6,13 @@ import { titleCase } from "text-case"; export const EditWorkspaceUsersModalId = (workspace) => `edit-workspace-${workspace.id}-modal`; -export default function EditWorkspaceUsersModal({ workspace, users }) { +export default function EditWorkspaceUsersModal({ + workspace, + users, + closeModal, +}) { const [error, setError] = useState(null); - const hideModal = () => { - document.getElementById(EditWorkspaceUsersModalId(workspace)).close(); - }; - const handleUpdate = async (e) => { setError(null); e.preventDefault(); @@ -35,122 +35,115 @@ export default function EditWorkspaceUsersModal({ workspace, users }) { }; return ( - -
-
-
-

- Edit {workspace.name} -

+
+
+
+

+ Edit {workspace.name} +

+ +
+
+
+
+ {users + .filter((user) => user.role !== "admin") + .map((user) => { + return ( +
{ + document + .getElementById( + `workspace-${workspace.id}-user-${user.id}` + ) + ?.click(); + }} + > + + +
+ ); + })} +
+ + +
+ {error &&

Error: {error}

} +
+
+
+
- -
-
- {users - .filter((user) => user.role !== "admin") - .map((user) => { - return ( -
{ - document - .getElementById( - `workspace-${workspace.id}-user-${user.id}` - ) - ?.click(); - }} - > - - -
- ); - })} -
- - -
- {error && ( -

Error: {error}

- )} -
-
-
- - -
-
-
+
-
+ ); } diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx index 8c8969622a..e755e18578 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx @@ -1,13 +1,14 @@ import { useRef } from "react"; import Admin from "@/models/admin"; import paths from "@/utils/paths"; -import EditWorkspaceUsersModal, { - EditWorkspaceUsersModalId, -} from "./EditWorkspaceUsersModal"; +import EditWorkspaceUsersModal from "./EditWorkspaceUsersModal"; import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function WorkspaceRow({ workspace, users }) { const rowRef = useRef(null); + const { isOpen, openModal, closeModal } = useModal(); const handleDelete = async () => { if ( !window.confirm( @@ -32,6 +33,7 @@ export default function WorkspaceRow({ workspace, users }) {
{workspace.slug} @@ -41,11 +43,7 @@ export default function WorkspaceRow({ workspace, users }) { {workspace.createdAt} - + + + ); } diff --git a/frontend/src/pages/Admin/Workspaces/index.jsx b/frontend/src/pages/Admin/Workspaces/index.jsx index c29c92ac29..8ca7410b0d 100644 --- a/frontend/src/pages/Admin/Workspaces/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/index.jsx @@ -7,9 +7,12 @@ import { BookOpen } from "@phosphor-icons/react"; import usePrefersDarkMode from "@/hooks/usePrefersDarkMode"; import Admin from "@/models/admin"; import WorkspaceRow from "./WorkspaceRow"; -import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal"; +import NewWorkspaceModal from "./NewWorkspaceModal"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function AdminWorkspaces() { + const { isOpen, openModal, closeModal } = useModal(); return (
{!isMobile && } @@ -25,9 +28,7 @@ export default function AdminWorkspaces() { Instance workspaces

- + + + ); diff --git a/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx index a4095ae9c7..cde8bdbe63 100644 --- a/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx @@ -5,14 +5,7 @@ import paths from "@/utils/paths"; import { userFromStorage } from "@/utils/request"; import System from "@/models/system"; -const DIALOG_ID = `new-api-key-modal`; - -function hideModal() { - document.getElementById(DIALOG_ID)?.close(); -} - -export const NewApiKeyModalId = DIALOG_ID; -export default function NewApiKeyModal() { +export default function NewApiKeyModal({ closeModal }) { const [apiKey, setApiKey] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); @@ -43,80 +36,77 @@ export default function NewApiKeyModal() { }, [copied]); return ( - -
-
-
-

- Create new API key -

- -
-
-
- +
+
+
+

+ Create new API key +

+ +
+ +
+
+ {error &&

Error: {error}

} + {apiKey && ( + + )} +

+ Once created the API key can be used to programmatically access + and configure this AnythingLLM instance. +

+ + Read the API documentation → +
-
- {!apiKey ? ( - <> - - - - ) : ( +
+
+ {!apiKey ? ( + <> - )} -
- -
+ + + ) : ( + + )} +
+
- +
); } diff --git a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx index 5ee5fd0a39..bf4ac51ef7 100644 --- a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx @@ -6,12 +6,15 @@ import "react-loading-skeleton/dist/skeleton.css"; import { PlusCircle } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import ApiKeyRow from "./ApiKeyRow"; -import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal"; +import NewApiKeyModal from "./NewApiKeyModal"; import paths from "@/utils/paths"; import { userFromStorage } from "@/utils/request"; import System from "@/models/system"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; export default function AdminApiKeys() { + const { isOpen, openModal, closeModal } = useModal(); return (
{!isMobile && } @@ -25,9 +28,7 @@ export default function AdminApiKeys() {

API Keys

- + + +
); diff --git a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx index 23fc39d9ae..3056e76593 100644 --- a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx @@ -2,9 +2,22 @@ import { useRef } from "react"; import truncate from "truncate"; import { X, Trash } from "@phosphor-icons/react"; import System from "@/models/system"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; export default function ChatRow({ chat }) { const rowRef = useRef(null); + const { + isOpen: isPromptOpen, + openModal: openPromptModal, + closeModal: closePromptModal, + } = useModal(); + const { + isOpen: isResponseOpen, + openModal: openResponseModal, + closeModal: closeResponseModal, + } = useModal(); + const handleDelete = async () => { if ( !window.confirm( @@ -30,17 +43,13 @@ export default function ChatRow({ chat }) { {chat.workspace?.name} { - document.getElementById(`chat-${chat.id}-prompt`)?.showModal(); - }} + onClick={openPromptModal} className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg" > {truncate(chat.prompt, 40)} { - document.getElementById(`chat-${chat.id}-response`)?.showModal(); - }} + onClick={openResponseModal} className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg" > {truncate(JSON.parse(chat.response)?.text, 40)} @@ -55,42 +64,38 @@ export default function ChatRow({ chat }) { - - + + + + + + ); } - -function hideModal(modalName) { - document.getElementById(modalName)?.close(); -} - -const TextPreview = ({ text, modalName }) => { +const TextPreview = ({ text, closeModal }) => { return ( - -
-
-
-

Viewing Text

- -
-
-
-              {text}
-            
-
+
+
+
+

Viewing Text

+ +
+
+
+            {text}
+          
-
+ ); }; diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index ddcb81319a..c6c62fef7b 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -15,6 +15,8 @@ import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions"; import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions"; import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem"; import { MagnifyingGlass } from "@phosphor-icons/react"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function GeneralEmbeddingPreference() { const [saving, setSaving] = useState(false); @@ -25,6 +27,7 @@ export default function GeneralEmbeddingPreference() { const [searchQuery, setSearchQuery] = useState(""); const [filteredEmbedders, setFilteredEmbedders] = useState([]); const [selectedEmbedder, setSelectedEmbedder] = useState(null); + const { isOpen, openModal, closeModal } = useModal(); const handleSubmit = async (e) => { e.preventDefault(); @@ -33,7 +36,7 @@ export default function GeneralEmbeddingPreference() { hasChanges && hasEmbeddings ) { - document.getElementById("confirmation-modal")?.showModal(); + openModal(); } else { await handleSaveSettings(); } @@ -56,7 +59,7 @@ export default function GeneralEmbeddingPreference() { setHasChanges(false); } setSaving(false); - document.getElementById("confirmation-modal")?.close(); + closeModal(); }; const updateChoice = (selection) => { @@ -116,11 +119,13 @@ export default function GeneralEmbeddingPreference() { return (
- document.getElementById("confirmation-modal")?.close()} - onConfirm={handleSaveSettings} - /> + + + {!isMobile && } {loading ? (
{ async function fetchKeys() { @@ -107,7 +110,7 @@ export default function GeneralVectorDatabase() { const handleSubmit = async (e) => { e.preventDefault(); if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) { - document.getElementById("confirmation-modal")?.showModal(); + openModal(); } else { await handleSaveSettings(); } @@ -130,7 +133,7 @@ export default function GeneralVectorDatabase() { setHasChanges(false); } setSaving(false); - document.getElementById("confirmation-modal")?.close(); + closeModal(); }; useEffect(() => { @@ -142,11 +145,13 @@ export default function GeneralVectorDatabase() { return (
- document.getElementById("confirmation-modal")?.close()} - onConfirm={handleSaveSettings} - /> + + + {!isMobile && } {loading ? (
-
-
-
-

- Create a new account -

-
-
-
-
-
- - -
-
- - -
- {error && ( -

Error: {error}

- )} -

- After creating your account you will be able to login with - these credentials and start using workspaces. -

+
+
+
+

+ Create a new account +

+
+ +
+
+
+ +
+
+ + +
+ {error &&

Error: {error}

} +

+ After creating your account you will be able to login with these + credentials and start using workspaces. +

-
- -
- -
+
+
+ +
+
- +
); } diff --git a/frontend/src/pages/Invite/index.jsx b/frontend/src/pages/Invite/index.jsx index e44ff23599..a7522a54e1 100644 --- a/frontend/src/pages/Invite/index.jsx +++ b/frontend/src/pages/Invite/index.jsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import { FullScreenLoader } from "@/components/Preloader"; import Invite from "@/models/invite"; import NewUserModal from "./NewUserModal"; +import ModalWrapper from "@/components/ModalWrapper"; export default function InvitePage() { const { code } = useParams(); @@ -47,7 +48,9 @@ export default function InvitePage() { return (
- + + +
); } From 09a0fe1c813dca85f3be8c2cd4f609a36919c273 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 23 Jan 2024 14:22:50 -0800 Subject: [PATCH 040/298] Add prisma reset dev command Patch background color for fullscreen loader --- frontend/src/components/Preloader.jsx | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Preloader.jsx b/frontend/src/components/Preloader.jsx index 066d5552b1..4a81437bd9 100644 --- a/frontend/src/components/Preloader.jsx +++ b/frontend/src/components/Preloader.jsx @@ -10,7 +10,7 @@ export function FullScreenLoader() { return (
diff --git a/package.json b/package.json index ad02aff6e0..6a5a753c4a 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,11 @@ "prisma:migrate": "cd server && npx prisma migrate dev --name init", "prisma:seed": "cd server && npx prisma db seed", "prisma:setup": "yarn prisma:generate && yarn prisma:migrate && yarn prisma:seed", + "prisma:reset": "cd server && npx prisma db push --force-reset", "prod:server": "cd server && yarn start", "prod:frontend": "cd frontend && yarn build", "generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs", "generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs" }, "private": false -} +} \ No newline at end of file From 171b1dd9aa1f365f98f5d4185b28c8172126c47e Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Wed, 24 Jan 2024 10:40:31 -0800 Subject: [PATCH 041/298] Add runtime metric to Telemetry --- server/models/telemetry.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/models/telemetry.js b/server/models/telemetry.js index d1f0b0381f..7c27aa69ce 100644 --- a/server/models/telemetry.js +++ b/server/models/telemetry.js @@ -28,11 +28,22 @@ const Telemetry = { return new PostHog(this.pubkey); }, - sendTelemetry: async function (event, properties = {}, subUserId = null) { + runtime: function () { + if (process.env.ANYTHING_LLM_RUNTIME === "docker") return "docker"; + if (process.env.NODE_ENV === "production") return "production"; + return "other"; + }, + + sendTelemetry: async function ( + event, + eventProperties = {}, + subUserId = null + ) { try { const { client, distinctId: systemId } = await this.connect(); if (!client) return; const distinctId = !!subUserId ? `${systemId}::${subUserId}` : systemId; + const properties = { ...eventProperties, runtime: this.runtime() }; console.log(`\x1b[32m[TELEMETRY SENT]\x1b[0m`, { event, distinctId, From 8377600211ad6d940055026f557d6a5591ae84a3 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 24 Jan 2024 13:08:22 -0800 Subject: [PATCH 042/298] Patch Azure text completion persistence (#647) --- server/utils/chats/stream.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 29386531e0..9bed8f39ff 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -269,6 +269,7 @@ function handleStreamResponses(response, stream, responseProps) { for (const choice of event.choices) { const delta = choice.delta?.content; if (!delta) continue; + fullText += delta; writeResponseChunk(response, { uuid, sources: [], From 978cad476ad7a4732cc8260d92ba5d1e1de68645 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Wed, 24 Jan 2024 14:27:52 -0800 Subject: [PATCH 043/298] update BARE_METAL setup --- BARE_METAL.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/BARE_METAL.md b/BARE_METAL.md index a3ff750c38..17ad84f40a 100644 --- a/BARE_METAL.md +++ b/BARE_METAL.md @@ -23,7 +23,12 @@ Here you can find the scripts and known working process to run AnythingLLM outsi 2. `cd anything-llm` and run `yarn setup`. This will install all dependencies to run in production as well as debug the application. -3. `cp server/.env.example server/.env` to create the basic ENV file for where instance settings will be read from on service start. This file is automatically managed and should not be edited manually. +3. `cp server/.env.example server/.env` to create the basic ENV file for where instance settings will be read from on service start. + +4. Ensure that the `server/.env` file has _at least_ these keys to start. These values will persist and this file will be automatically written and managed after your first successful boot. +``` +STORAGE_DIR="/your/absolute/path/to/server/.env" +``` ## To start the application @@ -45,10 +50,10 @@ cd server && npx prisma migrate deploy --schema=./prisma/schema.prisma ``` 4. Boot the server in production -`cd server && NODE_ENV=production index.js &` +`cd server && NODE_ENV=production node index.js &` 5. Boot the collection in another process -`cd collector && NODE_ENV=production index.js &` +`cd collector && NODE_ENV=production node index.js &` AnythingLLM should now be running on `http://localhost:3001`! From ca3decf4134df6eb1f0cd309b8d5879845764cab Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 26 Jan 2024 12:44:15 -0800 Subject: [PATCH 044/298] fix bug yaml --- .github/ISSUE_TEMPLATE/01_bug.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml index 02ff8a442e..2672252629 100644 --- a/.github/ISSUE_TEMPLATE/01_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -5,11 +5,8 @@ labels: [possible bug] body: - type: markdown attributes: - value: | - Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue. - - Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm - + value: | + Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue.\n\nWant help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm" - type: dropdown id: runtime attributes: From 39d07feaedaa68d80b822d627e7a7bb11e0a5db4 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 26 Jan 2024 12:45:05 -0800 Subject: [PATCH 045/298] fix bug yaml --- .github/ISSUE_TEMPLATE/01_bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml index 2672252629..d59f0e521d 100644 --- a/.github/ISSUE_TEMPLATE/01_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue.\n\nWant help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm" + Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue. Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm" - type: dropdown id: runtime attributes: From 21653b09fc447bfcd43317825c7fa4859b9fb5da Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Fri, 26 Jan 2024 13:03:50 -0800 Subject: [PATCH 046/298] [FEAT] add gpt-4-turbo-preview (#651) * add gpt-4-turbo-preview * add gpt-4-turbo-preview to valid models --- frontend/src/components/LLMSelection/OpenAiOptions/index.jsx | 1 + .../Settings/ChatModelPreference/useGetProviderModels.js | 1 + server/utils/AiProviders/openAi/index.js | 3 +++ 3 files changed, 5 insertions(+) diff --git a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx index ffc0f91e1f..b1718afe90 100644 --- a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx @@ -85,6 +85,7 @@ function OpenAIModelSelection({ apiKey, settings }) { "gpt-3.5-turbo", "gpt-3.5-turbo-1106", "gpt-4", + "gpt-4-turbo-preview", "gpt-4-1106-preview", "gpt-4-32k", ].map((model) => { diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js index 270f8a968e..1f8cce9889 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/ChatModelPreference/useGetProviderModels.js @@ -8,6 +8,7 @@ const PROVIDER_DEFAULT_MODELS = { "gpt-3.5-turbo", "gpt-3.5-turbo-1106", "gpt-4", + "gpt-4-turbo-preview", "gpt-4-1106-preview", "gpt-4-32k", ], diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index a25f69eade..120e728448 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -52,6 +52,8 @@ class OpenAiLLM { return 8192; case "gpt-4-1106-preview": return 128000; + case "gpt-4-turbo-preview": + return 128000; case "gpt-4-32k": return 32000; default: @@ -65,6 +67,7 @@ class OpenAiLLM { "gpt-3.5-turbo", "gpt-3.5-turbo-1106", "gpt-4-1106-preview", + "gpt-4-turbo-preview", "gpt-4-32k", ]; const isPreset = validModels.some((model) => modelName === model); From 5614e2ed3000489661c85f2e8af43b4271735209 Mon Sep 17 00:00:00 2001 From: Hakeem Abbas Date: Fri, 26 Jan 2024 13:07:53 -0800 Subject: [PATCH 047/298] feature: Integrate Astra as vectorDBProvider (#648) * feature: Integrate Astra as vectorDBProvider feature: Integrate Astra as vectorDBProvider * Update .env.example * Add env.example to docker example file Update spellcheck fo Astra Update Astra key for vector selection Update order of AstraDB options Resize Astra logo image to 330x330 Update methods of Astra to take in latest vectorDB params like TopN and more Update Astra interface to support default methods and avoid crash errors from 404 collections Update Astra interface to comply to max chunk insertion limitations Update Astra interface to dynamically set dimensionality from chunk 0 size on creation * reset workspaces --------- Co-authored-by: timothycarambat --- .vscode/settings.json | 1 + docker/.env.example | 5 + .../Modals/MangeWorkspace/Settings/index.jsx | 24 +- .../AstraDBOptions/index.jsx | 41 ++ frontend/src/media/vectordbs/astraDB.png | Bin 0 -> 1521 bytes .../GeneralSettings/VectorDatabase/index.jsx | 9 + .../Steps/DataHandling/index.jsx | 8 + .../Steps/VectorDatabaseConnection/index.jsx | 9 + server/.env.example | 5 + server/models/systemSettings.js | 6 + server/package.json | 1 + server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 12 + .../vectorDbProviders/astra/ASTRA_SETUP.md | 22 + server/utils/vectorDbProviders/astra/index.js | 380 ++++++++++++++++++ server/yarn.lock | 16 +- 16 files changed, 536 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/VectorDBSelection/AstraDBOptions/index.jsx create mode 100644 frontend/src/media/vectordbs/astraDB.png create mode 100644 server/utils/vectorDbProviders/astra/ASTRA_SETUP.md create mode 100644 server/utils/vectorDbProviders/astra/index.js diff --git a/.vscode/settings.json b/.vscode/settings.json index ab66c194b3..14c396fbe8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "Astra", "Dockerized", "Langchain", "Milvus", diff --git a/docker/.env.example b/docker/.env.example index 0adabbdf7d..858e4098be 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -103,6 +103,11 @@ GID='1000' # ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" # ZILLIZ_API_TOKEN=api-token-here +# Enable all below if you are using vector database: Astra DB. +# VECTOR_DB="astra" +# ASTRA_DB_APPLICATION_TOKEN= +# ASTRA_DB_ENDPOINT= + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx index b288dc6c14..a9471388fe 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -44,6 +44,7 @@ export default function WorkspaceSettings({ active, workspace, settings }) { const formEl = useRef(null); const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); + const [deleting, setDeleting] = useState(false); const defaults = recommendedSettings(settings?.LLMProvider); const handleUpdate = async (e) => { @@ -72,7 +73,15 @@ export default function WorkspaceSettings({ active, workspace, settings }) { ) ) return false; - await Workspace.delete(workspace.slug); + + setDeleting(true); + const success = await Workspace.delete(workspace.slug); + if (!success) { + showToast("Workspace could not be deleted!", "error", { clear: true }); + setDeleting(false); + return; + } + workspace.slug === slug ? (window.location = paths.home()) : window.location.reload(); @@ -310,7 +319,11 @@ export default function WorkspaceSettings({ active, workspace, settings }) {
- + {hasChanges && ( ); } diff --git a/frontend/src/components/VectorDBSelection/AstraDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/AstraDBOptions/index.jsx new file mode 100644 index 0000000000..11990dc44c --- /dev/null +++ b/frontend/src/components/VectorDBSelection/AstraDBOptions/index.jsx @@ -0,0 +1,41 @@ +export default function AstraDBOptions({ settings }) { + return ( +
+
+
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/frontend/src/media/vectordbs/astraDB.png b/frontend/src/media/vectordbs/astraDB.png new file mode 100644 index 0000000000000000000000000000000000000000..3403c72f2c35ffa793b4736013a5d24bcd2b71c1 GIT binary patch literal 1521 zcma)5X)qfI6i({Ct47_`)hr2#I!CRm+d4Z%H*vPa)u^*>Qc8#^tEHquU8%Z~Qpbim zx+%I?DpsnBXi3Vdpc7X#5<;y0+@0B(zL{^{yzhPAkN4)iylZYwa?xYH%zhfyfzxR8o<_Dui)WR$)E2&kx1Jr=yVcGbh-xOw*M z*^3u1+Su5fK7Cq3Lc-9{P*qhmGc)t(=;-k9aAIP@#l-~(1S%^l<8Zk3_4VN3;O*^g zPfyQNr%sW{Cd0vr|MwBs4U1Yio-{ zB7s047K=)m2MN z%i7u+fk3phv@9(x>FDTaYipaDn##z?C@LyeR8;W!{HduaD=Vve_wMcN?99x}h>3~$ z`1nw%R839Ifq{X^$w?Iz6*)P%XV0E-x!k$AIU^$@2M34U-QA|9CUtdnJRW~=a4B_W;Hc6k&%(p)6<28g|V@*;^N}ime?2oKxDwh{$~$#)%yHif>M&^$f6jK z5$_S!l8MK1u}#W&=`&9y&!B_F98M~nBtOM=qZjgYm@ECyO_omP=(_F-MlHex*8QQn zn+!T=j+RJq03P4Rcf~|_0!xO^gypS9bgt*1oYuPB2-(uOyx*w~kMxvbyv){5fC{8PLW-Nz~O6z*h6 zG%s4pz!C z3DT#csYDcClz)_ug2bhY;R z#U9njvei9yBFoLSXEjTq(zY#(KaiGRm~Ol_@fcOr=9W&dM3~lA6Mo4+tek(I1<9pN z-I;N@B}UQ|b}_H{QO6D9#UqeW)BZq-+e>7NxeM& ztAHVw6AXVM2+HU(YmvSSUwD#p`3Hzf>-yx4Hlb1}hrKv_83-A{96;Rh5m@-c!m6rZ zu+8$@8x(%&CmsFe)mBJ9dKBUHKDKad5M5y%dQ3 E2W{BMXaE2J literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index f5f697a72c..dd43fda570 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -10,6 +10,7 @@ import WeaviateLogo from "@/media/vectordbs/weaviate.png"; import QDrantLogo from "@/media/vectordbs/qdrant.png"; import MilvusLogo from "@/media/vectordbs/milvus.png"; import ZillizLogo from "@/media/vectordbs/zilliz.png"; +import AstraDBLogo from "@/media/vectordbs/astraDB.png"; import PreLoader from "@/components/Preloader"; import ChangeWarningModal from "@/components/ChangeWarning"; import { MagnifyingGlass } from "@phosphor-icons/react"; @@ -23,6 +24,7 @@ import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions"; import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; import { useModal } from "@/hooks/useModal"; import ModalWrapper from "@/components/ModalWrapper"; +import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -100,6 +102,13 @@ export default function GeneralVectorDatabase() { options: , description: "Open-source, highly scalable, and blazing fast.", }, + { + name: "AstraDB", + value: "astra", + logo: AstraDBLogo, + options: , + description: "Vector Search for Real-world GenAI.", + }, ]; const updateVectorChoice = (selection) => { diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index ae57302763..60a3b6da4d 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -11,6 +11,7 @@ import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import MistralLogo from "@/media/llmprovider/mistral.jpeg"; import ZillizLogo from "@/media/vectordbs/zilliz.png"; +import AstraDBLogo from "@/media/vectordbs/astraDB.png"; import ChromaLogo from "@/media/vectordbs/chroma.png"; import PineconeLogo from "@/media/vectordbs/pinecone.png"; import LanceDbLogo from "@/media/vectordbs/lancedb.png"; @@ -147,6 +148,13 @@ const VECTOR_DB_PRIVACY = { ], logo: ZillizLogo, }, + astra: { + name: "AstraDB", + description: [ + "Your vectors and document text are stored on your cloud AstraDB database.", + ], + logo: AstraDBLogo, + }, lancedb: { name: "LanceDB", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx index af0b5662d2..98034528d5 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx @@ -7,6 +7,7 @@ import WeaviateLogo from "@/media/vectordbs/weaviate.png"; import QDrantLogo from "@/media/vectordbs/qdrant.png"; import MilvusLogo from "@/media/vectordbs/milvus.png"; import ZillizLogo from "@/media/vectordbs/zilliz.png"; +import AstraDBLogo from "@/media/vectordbs/astraDB.png"; import System from "@/models/system"; import paths from "@/utils/paths"; import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions"; @@ -16,6 +17,7 @@ import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions" import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions"; import MilvusOptions from "@/components/VectorDBSelection/MilvusDBOptions"; import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; +import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; @@ -100,6 +102,13 @@ export default function VectorDatabaseConnection({ options: , description: "Open-source, highly scalable, and blazing fast.", }, + { + name: "AstraDB", + value: "astra", + logo: AstraDBLogo, + options: , + description: "Vector Search for Real-world GenAI.", + }, ]; function handleForward() { diff --git a/server/.env.example b/server/.env.example index e44748b41f..f497fea91d 100644 --- a/server/.env.example +++ b/server/.env.example @@ -76,6 +76,11 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # PINECONE_API_KEY= # PINECONE_INDEX= +# Enable all below if you are using vector database: Astra DB. +# VECTOR_DB="astra" +# ASTRA_DB_APPLICATION_TOKEN= +# ASTRA_DB_ENDPOINT= + # Enable all below if you are using vector database: LanceDB. VECTOR_DB="lancedb" diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index b4e93bde62..b8c46524cf 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -68,6 +68,12 @@ const SystemSettings = { ZillizApiToken: process.env.ZILLIZ_API_TOKEN, } : {}), + ...(vectorDB === "astra" + ? { + AstraDBApplicationToken: process?.env?.ASTRA_DB_APPLICATION_TOKEN, + AstraDBEndpoint: process?.env?.ASTRA_DB_ENDPOINT, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { diff --git a/server/package.json b/server/package.json index c8a41b795e..bf1b85c06e 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.8.1", "@azure/openai": "1.0.0-beta.10", + "@datastax/astra-db-ts": "^0.1.3", "@google/generative-ai": "^0.1.3", "@googleapis/youtube": "^9.0.0", "@pinecone-database/pinecone": "^2.0.1", diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index b72bb7977b..53a76faebf 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -22,6 +22,9 @@ function getVectorDbClass() { case "zilliz": const { Zilliz } = require("../vectorDbProviders/zilliz"); return Zilliz; + case "astra": + const { AstraDB } = require("../vectorDbProviders/astra"); + return AstraDB; 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 b061061e82..50b423474a 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -204,6 +204,17 @@ const KEY_MAPPING = { checks: [isNotEmpty], }, + // Astra DB Options + + AstraDBApplicationToken: { + envKey: "ASTRA_DB_APPLICATION_TOKEN", + checks: [isNotEmpty], + }, + AstraDBEndpoint: { + envKey: "ASTRA_DB_ENDPOINT", + checks: [isNotEmpty], + }, + // Together Ai Options TogetherAiApiKey: { envKey: "TOGETHER_AI_API_KEY", @@ -322,6 +333,7 @@ function supportedVectorDB(input = "") { "qdrant", "milvus", "zilliz", + "astra", ]; return supported.includes(input) ? null diff --git a/server/utils/vectorDbProviders/astra/ASTRA_SETUP.md b/server/utils/vectorDbProviders/astra/ASTRA_SETUP.md new file mode 100644 index 0000000000..e3749f0779 --- /dev/null +++ b/server/utils/vectorDbProviders/astra/ASTRA_SETUP.md @@ -0,0 +1,22 @@ +# How to setup Astra Vector Database for AnythingLLM + +[Official Astra DB Docs](https://docs.datastax.com/en/astra/astra-db-vector/get-started/quickstart.html) for reference. + +### How to get started + +**Requirements** + +- Astra Vector Database with active status. + +**Instructions** + +- [Create an Astra account or sign in to an existing Astra account](astra.datastax.com) +- Create an Astra Serverless(Vector) Database. +- Make sure DB is in active state. +- Get `API ENDPOINT`and `Application Token` from Overview screen + +``` +VECTOR_DB="astra" +ASTRA_DB_ENDPOINT=Astra DB API endpoint +ASTRA_DB_APPLICATION_TOKEN=AstraCS:.. +``` diff --git a/server/utils/vectorDbProviders/astra/index.js b/server/utils/vectorDbProviders/astra/index.js new file mode 100644 index 0000000000..df983d4f48 --- /dev/null +++ b/server/utils/vectorDbProviders/astra/index.js @@ -0,0 +1,380 @@ +const { AstraDB: AstraClient } = require("@datastax/astra-db-ts"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { v4: uuidv4 } = require("uuid"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +const AstraDB = { + name: "AstraDB", + connect: async function () { + if (process.env.VECTOR_DB !== "astra") + throw new Error("AstraDB::Invalid ENV settings"); + + const client = new AstraClient( + process?.env?.ASTRA_DB_APPLICATION_TOKEN, + process?.env?.ASTRA_DB_ENDPOINT + ); + return { client }; + }, + heartbeat: async function () { + return { heartbeat: Number(new Date()) }; + }, + // Astra interface will return a valid collection object even if the collection + // does not actually exist. So we run a simple check which will always throw + // when the table truly does not exist. Faster than iterating all collections. + isRealCollection: async function (astraCollection = null) { + if (!astraCollection) return false; + return await astraCollection + .countDocuments() + .then(() => true) + .catch(() => false); + }, + totalVectors: async function () { + const { client } = await this.connect(); + const collectionNames = await this.allNamespaces(client); + var totalVectors = 0; + for (const name of collectionNames) { + const collection = await client.collection(name).catch(() => null); + const count = await collection.countDocuments().catch(() => 0); + totalVectors += count ? count : 0; + } + return totalVectors; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const namespace = await this.namespace(client, _namespace); + return namespace?.vectorCount || 0; + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client.collection(namespace).catch(() => null); + if (!(await this.isRealCollection(collection))) return null; + + const count = await collection.countDocuments().catch((e) => { + console.error("Astra::namespaceExists", e.message); + return null; + }); + + return { + name: namespace, + ...collection, + vectorCount: typeof count === "number" ? count : 0, + }; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client.collection(namespace); + return await this.isRealCollection(collection); + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection(namespace); + return true; + }, + // AstraDB requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + if (!dimensions) + throw new Error( + `AstraDB:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); + + await client.createCollection(namespace, { + vector: { + dimension: dimensions, + metric: "cosine", + }, + }); + } + return await client.collection(namespace); + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + let vectorDimension = null; + const { pageContent, docId, ...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 { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); + if (!(await this.isRealCollection(collection))) + throw new Error("Failed to create new AstraDB collection!", { + namespace, + }); + + for (const chunk of chunks) { + // Before sending to Astra and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const _id = uuidv4(); + documentVectors.push({ docId, vectorId: _id }); + return { + _id: _id, + $vector: chunk.values, + metadata: chunk.metadata || {}, + }; + }); + + await collection.insertMany(newChunks); + } + await DocumentVectors.bulkInsert(documentVectors); + return { vectorized: true, error: null }; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + 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); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; + const vectorRecord = { + _id: uuidv4(), + $vector: vector, + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord._id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + const { client } = await this.connect(); + const collection = await this.getOrCreateCollection( + client, + namespace, + vectorDimension + ); + if (!(await this.isRealCollection(collection))) + throw new Error("Failed to create new AstraDB collection!", { + namespace, + }); + + if (vectors.length > 0) { + const chunks = []; + + console.log("Inserting vectorized chunks into Astra DB."); + + // AstraDB has maximum upsert size of 20 records per-request so we have to use a lower chunk size here + // in order to do the queries - this takes a lot more time than other providers but there + // is no way around it. This will save the vector-cache with the same layout, so we don't + // have to chunk again for cached files. + for (const chunk of toChunks(vectors, 20)) { + chunks.push( + chunk.map((c) => { + return { id: c._id, values: c.$vector, metadata: c.metadata }; + }) + ); + await collection.insertMany(chunk); + } + await storeVectorResult(chunks, fullFilePath); + } + + await DocumentVectors.bulkInsert(documentVectors); + return { vectorized: true, error: null }; + } catch (e) { + console.error("addDocumentToNamespace", e.message); + return { vectorized: false, error: e.message }; + } + }, + deleteDocumentFromNamespace: async function (namespace, docId) { + const { DocumentVectors } = require("../../../models/vectors"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error( + "Invalid namespace - has it been collected and populated yet?" + ); + const collection = await client.collection(namespace); + + const knownDocuments = await DocumentVectors.where({ docId }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + for (const id of vectorIds) { + await collection.deleteMany({ + _id: id, + }); + } + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + topN = 4, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: + "Invalid query - no namespace found for workspace in vector db!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold, + topN + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25, + topN = 4 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + + const collection = await client.collection(namespace); + const responses = await collection + .find( + {}, + { + sort: { $vector: queryVector }, + limit: topN, + includeSimilarity: true, + } + ) + .toArray(); + + responses.forEach((response) => { + if (response.$similarity < similarityThreshold) return; + result.contextTexts.push(response.metadata.text); + result.sourceDocuments.push(response); + result.scores.push(response.$similarity); + }); + return result; + }, + allNamespaces: async function (client) { + try { + let header = new Headers(); + header.append("Token", client?.httpClient?.applicationToken); + header.append("Content-Type", "application/json"); + + let raw = JSON.stringify({ + findCollections: {}, + }); + + let requestOptions = { + method: "POST", + headers: header, + body: raw, + redirect: "follow", + }; + + const call = await fetch(client?.httpClient?.baseUrl, requestOptions); + const resp = await call?.text(); + const collections = resp ? JSON.parse(resp)?.status?.collections : []; + return collections; + } catch (e) { + console.error("Astra::AllNamespace", e); + return []; + } + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + 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(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const details = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + return { + message: `Namespace ${namespace} was deleted along with ${ + details?.vectorCount || "all" + } vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + if (Object.keys(source).length > 0) { + const metadata = source.hasOwnProperty("metadata") + ? source.metadata + : source; + documents.push({ + ...metadata, + }); + } + } + + return documents; + }, +}; + +module.exports.AstraDB = AstraDB; diff --git a/server/yarn.lock b/server/yarn.lock index 67b4e11b67..ecc8f1a755 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -174,6 +174,15 @@ enabled "2.0.x" kuler "^2.0.0" +"@datastax/astra-db-ts@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@datastax/astra-db-ts/-/astra-db-ts-0.1.3.tgz#fcc25cda8d146c06278860054f09d687ff031568" + integrity sha512-7lnpym0HhUtfJVd8+vu6vYdDQpFyYof7TVLFVD2fgoIjUwj3EksFXmqDqicLAlLferZDllqSVthX9pXQ5Rdapw== + dependencies: + axios "^1.4.0" + bson "^6.2.0" + winston "^3.7.2" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1353,6 +1362,11 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +bson@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3" + integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q== + btoa-lite@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" @@ -5636,7 +5650,7 @@ winston-transport@^4.5.0: readable-stream "^3.6.0" triple-beam "^1.3.0" -winston@^3.9.0: +winston@^3.7.2, winston@^3.9.0: version "3.11.0" resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91" integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g== From 9d41ff58e2af6ff7d7a9df8a3de78df23518d516 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Mon, 29 Jan 2024 08:48:27 -0800 Subject: [PATCH 048/298] [FEAT] add support for new openai embedding models (#653) * add support for new openai models * QOL changes/improve logic for adding new openai embedding models * add example file inputs for Openai embedding ENV selection; * Fix if stmt conditional --------- Co-authored-by: timothycarambat --- docker/.env.example | 1 + .../OpenAiOptions/index.jsx | 25 +++++++++++++++---- .../Modals/MangeWorkspace/Documents/index.jsx | 19 +++++++++----- server/.env.example | 1 + server/utils/EmbeddingEngines/openAi/index.js | 3 ++- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 858e4098be..4213c3ff50 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -54,6 +54,7 @@ GID='1000' # Only used if you are using an LLM that does not natively support embedding (openai or Azure) # EMBEDDING_ENGINE='openai' # OPEN_AI_KEY=sk-xxxx +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' # EMBEDDING_ENGINE='azure' # AZURE_OPENAI_ENDPOINT= diff --git a/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx index dd00d67abd..15b5d307cc 100644 --- a/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx @@ -22,12 +22,27 @@ export default function OpenAiOptions({ settings }) { Model Preference
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx index ff7f0dd654..c5a66748c3 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx @@ -6,9 +6,14 @@ import Directory from "./Directory"; import showToast from "../../../../utils/toast"; import WorkspaceDirectory from "./WorkspaceDirectory"; -// OpenAI Cost per token for text-ada-embedding +// OpenAI Cost per token // ref: https://openai.com/pricing#:~:text=%C2%A0/%201K%20tokens-,Embedding%20models,-Build%20advanced%20search -const COST_PER_TOKEN = 0.0000001; // $0.0001 / 1K tokens + +const MODEL_COSTS = { + "text-embedding-ada-002": 0.0000001, // $0.0001 / 1K tokens + "text-embedding-3-small": 0.00000002, // $0.00002 / 1K tokens + "text-embedding-3-large": 0.00000013, // $0.00013 / 1K tokens +}; export default function DocumentSettings({ workspace, @@ -142,10 +147,12 @@ export default function DocumentSettings({ }); // Do not do cost estimation unless the embedding engine is OpenAi. - if ( - !systemSettings?.EmbeddingEngine || - systemSettings.EmbeddingEngine === "openai" - ) { + if (systemSettings?.EmbeddingEngine === "openai") { + const COST_PER_TOKEN = + MODEL_COSTS[ + systemSettings?.EmbeddingModelPref || "text-embedding-ada-002" + ]; + const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN; setEmbeddingsCost(dollarAmount); } diff --git a/server/.env.example b/server/.env.example index f497fea91d..96cbd58aec 100644 --- a/server/.env.example +++ b/server/.env.example @@ -51,6 +51,7 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # Only used if you are using an LLM that does not natively support embedding (openai or Azure) # EMBEDDING_ENGINE='openai' # OPEN_AI_KEY=sk-xxxx +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' # EMBEDDING_ENGINE='azure' # AZURE_OPENAI_ENDPOINT= diff --git a/server/utils/EmbeddingEngines/openAi/index.js b/server/utils/EmbeddingEngines/openAi/index.js index 1f9ba432a1..b52e78c6f4 100644 --- a/server/utils/EmbeddingEngines/openAi/index.js +++ b/server/utils/EmbeddingEngines/openAi/index.js @@ -9,6 +9,7 @@ class OpenAiEmbedder { }); const openai = new OpenAIApi(config); this.openai = openai; + this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002"; // Limit of how many strings we can process in a single pass to stay with resource or network limits this.maxConcurrentChunks = 500; @@ -30,7 +31,7 @@ class OpenAiEmbedder { new Promise((resolve) => { this.openai .createEmbedding({ - model: "text-embedding-ada-002", + model: this.model, input: chunk, }) .then((res) => { From dfab14a5d238003ac7c1f75b8bf818206bfc6f3f Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Mon, 29 Jan 2024 09:49:22 -0800 Subject: [PATCH 049/298] Patch lanceDB not deleting vectors from workspace (#655) patch lanceDB not deleting vectors from workspace documentVectors self-sanitize on delete of parent document --- server/models/documents.js | 3 +++ server/utils/vectorDbProviders/lance/index.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/models/documents.js b/server/models/documents.js index 8f3b88fbd8..bdb29dc77b 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -106,6 +106,9 @@ const Document = { await prisma.workspace_documents.delete({ where: { id: document.id, workspaceId: workspace.id }, }); + await prisma.document_vectors.deleteMany({ + where: { docId: document.docId }, + }); } catch (error) { console.error(error.message); } diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js index 67705c00e0..ecf10007f5 100644 --- a/server/utils/vectorDbProviders/lance/index.js +++ b/server/utils/vectorDbProviders/lance/index.js @@ -207,9 +207,9 @@ const LanceDb = { vectors.push(vectorRecord); submissions.push({ + ...vectorRecord.metadata, id: vectorRecord.id, vector: vectorRecord.values, - ...vectorRecord.metadata, }); documentVectors.push({ docId, vectorId: vectorRecord.id }); } From d927629c19342b0244120a8f80e87bc025eed91d Mon Sep 17 00:00:00 2001 From: Alex Leventer <3254549+alexleventer@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:47:03 -0800 Subject: [PATCH 050/298] Add Astra DB to list of supported vector stores (#657) Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d1fced317a..8641339512 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Some cool features of AnythingLLM **Supported Vector Databases:** - [LanceDB](https://github.com/lancedb/lancedb) (default) +- [Astra DB](https://www.datastax.com/products/datastax-astra) - [Pinecone](https://pinecone.io) - [Chroma](https://trychroma.com) - [Weaviate](https://weaviate.io) From 9d410496c09bb8df9808dea2b0d9119c454b3ce5 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 31 Jan 2024 13:38:21 -0800 Subject: [PATCH 051/298] [FIX]: Fix Chinese characters causing empty workspace slug (#660) if slug is empty on create workspace, generate a uuid as the workspace slug --- server/models/workspace.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/models/workspace.js b/server/models/workspace.js index c8e1247ee4..9cc142e74a 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -3,6 +3,7 @@ const slugify = require("slugify"); const { Document } = require("./documents"); const { WorkspaceUser } = require("./workspaceUsers"); const { ROLES } = require("../utils/middleware/multiUserProtected"); +const { v4: uuidv4 } = require("uuid"); const Workspace = { writable: [ @@ -22,6 +23,7 @@ const Workspace = { new: async function (name = null, creatorId = null) { if (!name) return { result: null, message: "name cannot be null" }; var slug = slugify(name, { lower: true }); + slug = slug || uuidv4(); const existingBySlug = await this.get({ slug }); if (existingBySlug !== null) { From 146385bf41e90aca17cc9beb0f4befa247a554e9 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 2 Feb 2024 15:29:38 -0800 Subject: [PATCH 052/298] no build on embed changes --- .github/workflows/build-and-push-image.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 12b274b753..2044c66baf 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -20,6 +20,7 @@ on: - '.vscode/**/*' - '**/.env.example' - '.github/ISSUE_TEMPLATE/**/*' + - 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced jobs: push_multi_platform_to_registries: From 1846a99b93939588b8d8d2a4a01eebbd109d3261 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Mon, 5 Feb 2024 14:21:34 -0800 Subject: [PATCH 053/298] [FEAT] Embedded AnythingLLM (#656) * WIP embedded app * WIP got response from backend in embedded app * WIP streaming prints to embedded app * implemented streaming and tailwind min for styling into embedded app * WIP embedded app history functional * load params from script tag into embedded app * rough in modularization of embed chat cleanup dev process for easier dev support move all chat to components todo: build process todo: backend support * remove eslint config * Implement models and cleanup embed chat endpoints Improve build process for embed prod minification and bundle size awareness WIP * forgot files * rename to embed folder * introduce chat modal styles * add middleware validations on embed chat * auto open param and default greeting * reset chat history * Admin embed config page * Admin Embed Chats mgmt page * update embed * nonpriv * more style support reopen if chat was last opened * update comments * remove unused imports * allow change of workspace for embedconfig * update failure to lookup message * update reset script * update instructions * Add more styling options Add sponsor text at bottom Support dynamic container height Loading animations * publish new embed script * Add back syntax highlighting and keep bundle small via dynamic script build * add hint * update readme * update copy model for snippet with link to styles --------- Co-authored-by: timothycarambat --- .vscode/settings.json | 3 + README.md | 4 +- embed/.gitignore | 25 + embed/.prettierignore | 9 + embed/README.md | 90 + embed/index.html | 13 + embed/jsconfig.json | 12 + embed/package.json | 43 + embed/scripts/updateHljs.mjs | 35 + embed/src/App.jsx | 52 + embed/src/assets/anything-llm-dark.png | Bin 0 -> 8413 bytes .../HistoricalMessage/Actions/index.jsx | 43 + .../ChatHistory/HistoricalMessage/index.jsx | 60 + .../ChatHistory/PromptReply/index.jsx | 65 + .../ChatContainer/ChatHistory/index.jsx | 123 + .../ChatContainer/PromptInput/index.jsx | 78 + .../ChatWindow/ChatContainer/index.jsx | 91 + .../components/ChatWindow/Header/index.jsx | 92 + embed/src/components/ChatWindow/index.jsx | 89 + embed/src/components/Head.jsx | 171 + embed/src/components/OpenButton/index.jsx | 33 + embed/src/components/SessionId/index.jsx | 10 + embed/src/components/Sponsor/index.jsx | 16 + embed/src/hooks/chat/useChatHistory.js | 27 + embed/src/hooks/useOpen.js | 16 + embed/src/hooks/useScriptAttributes.js | 56 + embed/src/hooks/useSessionId.js | 29 + embed/src/main.jsx | 22 + embed/src/models/chatService.js | 108 + embed/src/static/tailwind@3.4.1.js | 209 ++ embed/src/utils/chat/hljs.js | 88 + embed/src/utils/chat/index.js | 96 + embed/src/utils/chat/markdown.js | 47 + embed/src/utils/constants.js | 1 + embed/vite.config.js | 64 + embed/yarn.lock | 3035 +++++++++++++++++ .../embed/anythingllm-chat-widget.min.js | 38 + frontend/src/App.jsx | 12 + .../src/components/SettingsSidebar/index.jsx | 80 +- frontend/src/models/embed.js | 80 + .../EmbedChats/ChatRow/index.jsx | 130 + .../GeneralSettings/EmbedChats/index.jsx | 124 + .../EmbedRow/CodeSnippetModal/index.jsx | 123 + .../EmbedRow/EditEmbedModal/index.jsx | 121 + .../EmbedConfigs/EmbedRow/index.jsx | 140 + .../EmbedConfigs/NewEmbedModal/index.jsx | 328 ++ .../GeneralSettings/EmbedConfigs/index.jsx | 103 + frontend/src/utils/paths.js | 6 + package.json | 4 +- server/.gitignore | 1 + server/endpoints/embed/index.js | 101 + server/endpoints/embedManagement.js | 115 + server/index.js | 6 + server/models/embedChats.js | 162 + server/models/embedConfig.js | 239 ++ .../20240202002020_init/migration.sql | 37 + server/prisma/schema.prisma | 37 + server/utils/chats/embed.js | 249 ++ server/utils/chats/stream.js | 1 + server/utils/middleware/embedMiddleware.js | 151 + 60 files changed, 7328 insertions(+), 15 deletions(-) create mode 100644 embed/.gitignore create mode 100644 embed/.prettierignore create mode 100644 embed/README.md create mode 100644 embed/index.html create mode 100644 embed/jsconfig.json create mode 100644 embed/package.json create mode 100644 embed/scripts/updateHljs.mjs create mode 100644 embed/src/App.jsx create mode 100644 embed/src/assets/anything-llm-dark.png create mode 100644 embed/src/components/ChatWindow/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx create mode 100644 embed/src/components/ChatWindow/ChatContainer/ChatHistory/HistoricalMessage/index.jsx create mode 100644 embed/src/components/ChatWindow/ChatContainer/ChatHistory/PromptReply/index.jsx create mode 100644 embed/src/components/ChatWindow/ChatContainer/ChatHistory/index.jsx create mode 100644 embed/src/components/ChatWindow/ChatContainer/PromptInput/index.jsx create mode 100644 embed/src/components/ChatWindow/ChatContainer/index.jsx create mode 100644 embed/src/components/ChatWindow/Header/index.jsx create mode 100644 embed/src/components/ChatWindow/index.jsx create mode 100644 embed/src/components/Head.jsx create mode 100644 embed/src/components/OpenButton/index.jsx create mode 100644 embed/src/components/SessionId/index.jsx create mode 100644 embed/src/components/Sponsor/index.jsx create mode 100644 embed/src/hooks/chat/useChatHistory.js create mode 100644 embed/src/hooks/useOpen.js create mode 100644 embed/src/hooks/useScriptAttributes.js create mode 100644 embed/src/hooks/useSessionId.js create mode 100644 embed/src/main.jsx create mode 100644 embed/src/models/chatService.js create mode 100644 embed/src/static/tailwind@3.4.1.js create mode 100644 embed/src/utils/chat/hljs.js create mode 100644 embed/src/utils/chat/index.js create mode 100644 embed/src/utils/chat/markdown.js create mode 100644 embed/src/utils/constants.js create mode 100644 embed/vite.config.js create mode 100644 embed/yarn.lock create mode 100644 frontend/public/embed/anythingllm-chat-widget.min.js create mode 100644 frontend/src/models/embed.js create mode 100644 frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedChats/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/CodeSnippetModal/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/EditEmbedModal/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedConfigs/NewEmbedModal/index.jsx create mode 100644 frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx create mode 100644 server/endpoints/embed/index.js create mode 100644 server/endpoints/embedManagement.js create mode 100644 server/models/embedChats.js create mode 100644 server/models/embedConfig.js create mode 100644 server/prisma/migrations/20240202002020_init/migration.sql create mode 100644 server/utils/chats/embed.js create mode 100644 server/utils/middleware/embedMiddleware.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 14c396fbe8..ac8c94729a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,10 @@ { "cSpell.words": [ + "anythingllm", "Astra", "Dockerized", + "Embeddable", + "hljs", "Langchain", "Milvus", "Ollama", diff --git a/README.md b/README.md index 8641339512..04f123d21e 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,11 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace Some cool features of AnythingLLM - **Multi-user instance support and permissioning** +- **_New_** [Custom Embeddable Chat widget for your website](./embed/README.md) - Multiple document type support (PDF, TXT, DOCX, etc) - Manage documents in your vector database from a simple UI - Two chat modes `conversation` and `query`. Conversation retains previous questions and amendments. Query is simple QA against your documents -- In-chat citations linked to the original document source and text -- Simple technology stack for fast iteration +- In-chat citations - 100% Cloud deployment ready. - "Bring your own LLM" model. - Extremely efficient cost-saving measures for managing very large documents. You'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other document chatbot solutions. diff --git a/embed/.gitignore b/embed/.gitignore new file mode 100644 index 0000000000..4d3751d9a5 --- /dev/null +++ b/embed/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!yarn.lock +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/embed/.prettierignore b/embed/.prettierignore new file mode 100644 index 0000000000..d90a3c0893 --- /dev/null +++ b/embed/.prettierignore @@ -0,0 +1,9 @@ +# defaults +**/.git +**/.svn +**/.hg +**/node_modules + +**/dist +**/static/** +src/utils/chat/hljs.js diff --git a/embed/README.md b/embed/README.md new file mode 100644 index 0000000000..503348e9b1 --- /dev/null +++ b/embed/README.md @@ -0,0 +1,90 @@ +# AnythingLLM Embedded Chat Widget + +> [!WARNING] +> The use of the AnythingLLM embed is currently in beta. Please request a feature or +> report a bug via a Github Issue if you have any issues. + +> [!WARNING] +> The core AnythingLLM team publishes a pre-built version of the script that is bundled +> with the main application. You can find it at the frontend URL `/embed/anythingllm-chat-widget.min.js`. +> You should only be working in this repo if you are wanting to build your own custom embed. + +This folder of AnythingLLM contains the source code for how the embedded version of AnythingLLM works to provide a public facing interface of your workspace. + +The AnythingLLM Embedded chat widget allows you to expose a workspace and its embedded knowledge base as a chat bubble via a ` +``` + +### ` + +`; +} + +const ScriptTag = ({ embed }) => { + const [copied, setCopied] = useState(false); + const scriptHost = import.meta.env.DEV + ? "http://localhost:3000" + : window.location.origin; + const serverHost = import.meta.env.DEV + ? "http://localhost:3001" + : window.location.origin; + const snippet = createScriptTagSnippet(embed, scriptHost, serverHost); + + const handleClick = () => { + window.navigator.clipboard.writeText(snippet); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 2500); + showToast("Snippet copied to clipboard!", "success", { clear: true }); + }; + + return ( +
+ ); +}; diff --git a/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/EditEmbedModal/index.jsx b/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/EditEmbedModal/index.jsx new file mode 100644 index 0000000000..a1b15eef02 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/EditEmbedModal/index.jsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import { X } from "@phosphor-icons/react"; +import { + BooleanInput, + ChatModeSelection, + NumberInput, + PermittedDomains, + WorkspaceSelection, + enforceSubmissionSchema, +} from "../../NewEmbedModal"; +import Embed from "@/models/embed"; +import showToast from "@/utils/toast"; + +export default function EditEmbedModal({ embed, closeModal }) { + const [error, setError] = useState(null); + + const handleUpdate = async (e) => { + setError(null); + e.preventDefault(); + const form = new FormData(e.target); + const data = enforceSubmissionSchema(form); + const { success, error } = await Embed.updateEmbed(embed.id, data); + if (success) { + showToast("Embed updated successfully.", "success", { clear: true }); + setTimeout(() => { + window.location.reload(); + }, 800); + } + setError(error); + }; + + return ( +
+
+
+

+ Update embed #{embed.id} +

+ +
+
+
+
+ + + + + + + + + + {error &&

Error: {error}

} +

+ After creating an embed you will be provided a link that you can + publish on your website with a simple + + <script> + {" "} + tag. +

+
+
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/index.jsx b/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/index.jsx new file mode 100644 index 0000000000..24c9399155 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/EmbedConfigs/EmbedRow/index.jsx @@ -0,0 +1,140 @@ +import { useRef, useState } from "react"; +import { DotsThreeOutline, LinkSimple } from "@phosphor-icons/react"; +import showToast from "@/utils/toast"; +import { useModal } from "@/hooks/useModal"; +import ModalWrapper from "@/components/ModalWrapper"; +import Embed from "@/models/embed"; +import paths from "@/utils/paths"; +import { nFormatter } from "@/utils/numbers"; +import EditEmbedModal from "./EditEmbedModal"; +import CodeSnippetModal from "./CodeSnippetModal"; + +export default function EmbedRow({ embed }) { + const rowRef = useRef(null); + const [enabled, setEnabled] = useState(Number(embed.enabled) === 1); + const { + isOpen: isSettingsOpen, + openModal: openSettingsModal, + closeModal: closeSettingsModal, + } = useModal(); + const { + isOpen: isSnippetOpen, + openModal: openSnippetModal, + closeModal: closeSnippetModal, + } = useModal(); + + const handleSuspend = async () => { + if ( + !window.confirm( + `Are you sure you want to disabled this embed?\nOnce disabled the embed will no longer respond to any chat requests.` + ) + ) + return false; + + const { success, error } = await Embed.updateEmbed(embed.id, { + enabled: !enabled, + }); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + showToast( + `Embed ${enabled ? "has been disabled" : "is active"}.`, + "success", + { clear: true } + ); + setEnabled(!enabled); + } + }; + const handleDelete = async () => { + if ( + !window.confirm( + `Are you sure you want to delete this embed?\nOnce deleted this embed will no longer respond to chats or be active.\n\nThis action is irreversible.` + ) + ) + return false; + const { success, error } = await Embed.deleteEmbed(embed.id); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + rowRef?.current?.remove(); + showToast("Embed deleted from system.", "success", { clear: true }); + } + }; + + return ( + <> + + + + {embed.workspace.name} + + + + {nFormatter(embed._count.embed_chats)} + + + + + + + <> + + + + + + + + + + + + + + ); +} + +function ActiveDomains({ domainList }) { + if (!domainList) return

all

; + try { + const domains = JSON.parse(domainList); + return ( +
+ {domains.map((domain) => { + return

{domain}

; + })} +
+ ); + } catch { + return

all

; + } +} diff --git a/frontend/src/pages/GeneralSettings/EmbedConfigs/NewEmbedModal/index.jsx b/frontend/src/pages/GeneralSettings/EmbedConfigs/NewEmbedModal/index.jsx new file mode 100644 index 0000000000..b1ea67ec6d --- /dev/null +++ b/frontend/src/pages/GeneralSettings/EmbedConfigs/NewEmbedModal/index.jsx @@ -0,0 +1,328 @@ +import React, { useEffect, useState } from "react"; +import { X } from "@phosphor-icons/react"; +import Workspace from "@/models/workspace"; +import { TagsInput } from "react-tag-input-component"; +import Embed from "@/models/embed"; + +export function enforceSubmissionSchema(form) { + const data = {}; + for (var [key, value] of form.entries()) { + if (!value || value === null) continue; + data[key] = value; + if (value === "on") data[key] = true; + } + + // Always set value on nullable keys since empty or off will not send anything from form element. + if (!data.hasOwnProperty("allowlist_domains")) data.allowlist_domains = null; + if (!data.hasOwnProperty("allow_model_override")) + data.allow_model_override = false; + if (!data.hasOwnProperty("allow_temperature_override")) + data.allow_temperature_override = false; + if (!data.hasOwnProperty("allow_prompt_override")) + data.allow_prompt_override = false; + return data; +} + +export default function NewEmbedModal({ closeModal }) { + const [error, setError] = useState(null); + + const handleCreate = async (e) => { + setError(null); + e.preventDefault(); + const form = new FormData(e.target); + const data = enforceSubmissionSchema(form); + const { embed, error } = await Embed.newEmbed(data); + if (!!embed) window.location.reload(); + setError(error); + }; + + return ( +
+
+
+

+ Create new embed for workspace +

+ +
+
+
+
+ + + + + + + + + + {error &&

Error: {error}

} +

+ After creating an embed you will be provided a link that you can + publish on your website with a simple + + <script> + {" "} + tag. +

+
+
+
+ + +
+
+
+
+ ); +} + +export const WorkspaceSelection = ({ defaultValue = null }) => { + const [workspaces, setWorkspaces] = useState([]); + useEffect(() => { + async function fetchWorkspaces() { + const _workspaces = await Workspace.all(); + setWorkspaces(_workspaces); + } + fetchWorkspaces(); + }, []); + + return ( +
+
+ +

+ This is the workspace your chat window will be based on. All defaults + will be inherited from the workspace unless overridden by this config. +

+
+ +
+ ); +}; + +export const ChatModeSelection = ({ defaultValue = null }) => { + const [chatMode, setChatMode] = useState(defaultValue ?? "query"); + + return ( +
+
+ +

+ Set how your chatbot should operate. Query means it will only respond + if a document helps answer the query. +
+ Chat opens the chat to even general questions and can answer totally + unrelated queries to your workspace. +

+
+
+ + +
+
+ ); +}; + +export const PermittedDomains = ({ defaultValue = [] }) => { + const [domains, setDomains] = useState(defaultValue); + const handleChange = (data) => { + const validDomains = data + .map((input) => { + let url = input; + if (!url.includes("http://") && !url.includes("https://")) + url = `https://${url}`; + try { + new URL(url); + return url; + } catch { + return null; + } + }) + .filter((u) => !!u); + setDomains(validDomains); + }; + + return ( +
+
+ +

+ This filter will block any requests that come from a domain other than + the list below. +
+ Leaving this empty means anyone can use your embed on any site. +

+
+ + +
+ ); +}; + +export const NumberInput = ({ name, title, hint, defaultValue = 0 }) => { + return ( +
+
+ +

{hint}

+
+ e.target.blur()} + /> +
+ ); +}; + +export const BooleanInput = ({ name, title, hint, defaultValue = null }) => { + const [status, setStatus] = useState(defaultValue ?? false); + + return ( +
+
+ +

{hint}

+
+
+
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 358e520a18..74c159f4ac 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -6,7 +6,7 @@ import ManageWorkspace from "../../../Modals/MangeWorkspace"; import { ArrowDown } from "@phosphor-icons/react"; import debounce from "lodash.debounce"; -export default function ChatHistory({ history = [], workspace }) { +export default function ChatHistory({ history = [], workspace, sendCommand }) { const replyRef = useRef(null); const { showing, showModal, hideModal } = useManageWorkspaceModal(); const [isAtBottom, setIsAtBottom] = useState(true); @@ -46,25 +46,31 @@ export default function ChatHistory({ history = [], workspace }) { } }; + const handleSendSuggestedMessage = (heading, message) => { + sendCommand(`${heading} ${message}`, true); + }; + if (history.length === 0) { return ( -
-
+
+

Welcome to your new workspace.

-
-

- To get started either{" "} - - upload a document - - or send a chat. -

-
+

+ To get started either{" "} + + upload a document + + or send a chat. +

+
{showing && ( ); } + +function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) { + if (suggestions.length === 0) return null; + return ( +
+ {suggestions.map((suggestion, index) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 372c79a7c6..7a5a974af3 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -97,7 +97,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { > {isMobile && }
- + { + if (!res.ok) throw new Error("Could not fetch suggested messages."); + return res.json(); + }) + .then((res) => res.suggestedMessages) + .catch((e) => { + console.error(e); + return null; + }); + }, + setSuggestedMessages: async function (slug, messages) { + return fetch(`${API_BASE}/workspace/${slug}/suggested-messages`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ messages }), + }) + .then((res) => { + if (!res.ok) { + throw new Error( + res.statusText || "Error setting suggested messages." + ); + } + return { success: true, ...res.json() }; + }) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, }; export default Workspace; diff --git a/frontend/src/pages/WorkspaceChat/index.jsx b/frontend/src/pages/WorkspaceChat/index.jsx index 7db652a921..9575b64cef 100644 --- a/frontend/src/pages/WorkspaceChat/index.jsx +++ b/frontend/src/pages/WorkspaceChat/index.jsx @@ -27,7 +27,11 @@ function ShowWorkspaceChat() { async function getWorkspace() { if (!slug) return; const _workspace = await Workspace.bySlug(slug); - setWorkspace(_workspace); + const suggestedMessages = await Workspace.getSuggestedMessages(slug); + setWorkspace({ + ..._workspace, + suggestedMessages, + }); setLoading(false); } getWorkspace(); diff --git a/frontend/src/pages/WorkspaceSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/index.jsx new file mode 100644 index 0000000000..35743d137c --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/index.jsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { isMobile } from "react-device-detect"; +import showToast from "@/utils/toast"; +import { ArrowUUpLeft, Plus, X } from "@phosphor-icons/react"; +import Workspace from "@/models/workspace"; +import paths from "@/utils/paths"; + +export default function WorkspaceSettings() { + const [hasChanges, setHasChanges] = useState(false); + const [workspace, setWorkspace] = useState(null); + const [suggestedMessages, setSuggestedMessages] = useState([]); + const [editingIndex, setEditingIndex] = useState(-1); + const [newMessage, setNewMessage] = useState({ heading: "", message: "" }); + const { slug } = useParams(); + + useEffect(() => { + async function fetchWorkspace() { + if (!slug) return; + const workspace = await Workspace.bySlug(slug); + const suggestedMessages = await Workspace.getSuggestedMessages(slug); + setWorkspace(workspace); + setSuggestedMessages(suggestedMessages); + } + fetchWorkspace(); + }, [slug]); + + const handleSaveSuggestedMessages = async () => { + const validMessages = suggestedMessages.filter( + (msg) => + msg?.heading?.trim()?.length > 0 || msg?.message?.trim()?.length > 0 + ); + const { success, error } = await Workspace.setSuggestedMessages( + slug, + validMessages + ); + if (!success) { + showToast(`Failed to update welcome messages: ${error}`, "error"); + return; + } + showToast("Successfully updated welcome messages.", "success"); + setHasChanges(false); + }; + + const addMessage = () => { + setEditingIndex(-1); + if (suggestedMessages.length >= 4) { + showToast("Maximum of 4 messages allowed.", "warning"); + return; + } + const defaultMessage = { + heading: "Explain to me", + message: "the benefits of AnythingLLM", + }; + setNewMessage(defaultMessage); + setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]); + setHasChanges(true); + }; + + const removeMessage = (index) => { + const messages = [...suggestedMessages]; + messages.splice(index, 1); + setSuggestedMessages(messages); + setHasChanges(true); + }; + + const startEditing = (index) => { + setEditingIndex(index); + setNewMessage({ ...suggestedMessages[index] }); + }; + + const handleRemoveMessage = (index) => { + removeMessage(index); + setEditingIndex(-1); + }; + + const onEditChange = (e) => { + const updatedNewMessage = { + ...newMessage, + [e.target.name]: e.target.value, + }; + setNewMessage(updatedNewMessage); + const updatedMessages = suggestedMessages.map((message, index) => { + if (index === editingIndex) { + return { ...message, [e.target.name]: e.target.value }; + } + return message; + }); + + setSuggestedMessages(updatedMessages); + setHasChanges(true); + }; + + return ( +
+ + + +
+
+
+
+

+ Workspace Settings ({workspace?.name}) +

+
+

+ Customize your workspace. +

+
+
+
+

+ Suggested Chat Messages +

+

+ Customize the messages that will be suggested to your workspace + users. +

+
+ +
+ {suggestedMessages.map((suggestion, index) => ( +
+ + +
+ ))} +
+ {editingIndex >= 0 && ( +
+
+ + +
+
+ + +
+
+ )} + {suggestedMessages.length < 4 && ( + + )} + + {hasChanges && ( +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index a9669300ac..8fbaacecd2 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -55,6 +55,9 @@ export default { chat: (slug) => { return `/workspace/${slug}`; }, + additionalSettings: (slug) => { + return `/workspace/${slug}/settings`; + }, }, apiDocs: () => { return `${API_BASE}/docs`; diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 25e391036f..b04d23376f 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -17,6 +17,9 @@ const { flexUserRoleValid, ROLES, } = require("../utils/middleware/multiUserProtected"); +const { + WorkspaceSuggestedMessages, +} = require("../models/workspacesSuggestedMessages"); const { handleUploads } = setupMulter(); function workspaceEndpoints(app) { @@ -283,6 +286,53 @@ function workspaceEndpoints(app) { } } ); + + app.get( + "/workspace/:slug/suggested-messages", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async function (request, response) { + try { + const { slug } = request.params; + const suggestedMessages = + await WorkspaceSuggestedMessages.getMessages(slug); + response.status(200).json({ success: true, suggestedMessages }); + } catch (error) { + console.error("Error fetching suggested messages:", error); + response + .status(500) + .json({ success: false, message: "Internal server error" }); + } + } + ); + + app.post( + "/workspace/:slug/suggested-messages", + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + async (request, response) => { + try { + const { messages = [] } = reqBody(request); + const { slug } = request.params; + if (!Array.isArray(messages)) { + return response.status(400).json({ + success: false, + message: "Invalid message format. Expected an array of messages.", + }); + } + + await WorkspaceSuggestedMessages.saveAll(messages, slug); + return response.status(200).json({ + success: true, + message: "Suggested messages saved successfully.", + }); + } catch (error) { + console.error("Error processing the suggested messages:", error); + response.status(500).json({ + success: true, + message: "Error saving the suggested messages.", + }); + } + } + ); } module.exports = { workspaceEndpoints }; diff --git a/server/models/workspacesSuggestedMessages.js b/server/models/workspacesSuggestedMessages.js new file mode 100644 index 0000000000..ef35a5bb5a --- /dev/null +++ b/server/models/workspacesSuggestedMessages.js @@ -0,0 +1,83 @@ +const prisma = require("../utils/prisma"); + +const WorkspaceSuggestedMessages = { + get: async function (clause = {}) { + try { + const message = await prisma.workspace_suggested_messages.findFirst({ + where: clause, + }); + return message || null; + } catch (error) { + console.error(error.message); + return null; + } + }, + + where: async function (clause = {}, limit) { + try { + const messages = await prisma.workspace_suggested_messages.findMany({ + where: clause, + take: limit || undefined, + }); + return messages; + } catch (error) { + console.error(error.message); + return []; + } + }, + + saveAll: async function (messages, workspaceSlug) { + try { + const workspace = await prisma.workspaces.findUnique({ + where: { slug: workspaceSlug }, + }); + + if (!workspace) throw new Error("Workspace not found"); + + // Delete all existing messages for the workspace + await prisma.workspace_suggested_messages.deleteMany({ + where: { workspaceId: workspace.id }, + }); + + // Create new messages + // We create each message individually because prisma + // with sqlite does not support createMany() + for (const message of messages) { + await prisma.workspace_suggested_messages.create({ + data: { + workspaceId: workspace.id, + heading: message.heading, + message: message.message, + }, + }); + } + } catch (error) { + console.error("Failed to save all messages", error.message); + } + }, + + getMessages: async function (workspaceSlug) { + try { + const workspace = await prisma.workspaces.findUnique({ + where: { slug: workspaceSlug }, + }); + + if (!workspace) throw new Error("Workspace not found"); + + const messages = await prisma.workspace_suggested_messages.findMany({ + where: { workspaceId: workspace.id }, + orderBy: { createdAt: "asc" }, + }); + + return messages.map((msg) => ({ + heading: msg.heading, + message: msg.message, + })); + } catch (error) { + console.error("Failed to get all messages", error.message); + return []; + } + }, +}; + +module.exports.WorkspaceSuggestedMessages = WorkspaceSuggestedMessages; diff --git a/server/prisma/migrations/20240206181106_init/migration.sql b/server/prisma/migrations/20240206181106_init/migration.sql new file mode 100644 index 0000000000..9655c7b7ae --- /dev/null +++ b/server/prisma/migrations/20240206181106_init/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "workspace_suggested_messages" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "workspaceId" INTEGER NOT NULL, + "heading" TEXT NOT NULL, + "message" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "workspace_suggested_messages_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE INDEX "workspace_suggested_messages_workspaceId_idx" ON "workspace_suggested_messages"("workspaceId"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 314a8359d7..ede8a1fdeb 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -85,21 +85,34 @@ model welcome_messages { } model workspaces { - id Int @id @default(autoincrement()) - name String - slug String @unique - vectorTag String? - createdAt DateTime @default(now()) - openAiTemp Float? - openAiHistory Int @default(20) - lastUpdatedAt DateTime @default(now()) - openAiPrompt String? - similarityThreshold Float? @default(0.25) - chatModel String? - topN Int? @default(4) - workspace_users workspace_users[] - documents workspace_documents[] - embed_configs embed_configs[] + id Int @id @default(autoincrement()) + name String + slug String @unique + vectorTag String? + createdAt DateTime @default(now()) + openAiTemp Float? + openAiHistory Int @default(20) + lastUpdatedAt DateTime @default(now()) + openAiPrompt String? + similarityThreshold Float? @default(0.25) + chatModel String? + topN Int? @default(4) + workspace_users workspace_users[] + documents workspace_documents[] + workspace_suggested_messages workspace_suggested_messages[] + embed_configs embed_configs[] +} + +model workspace_suggested_messages { + id Int @id @default(autoincrement()) + workspaceId Int + heading String + message String + createdAt DateTime @default(now()) + lastUpdatedAt DateTime @default(now()) + workspace workspaces @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + + @@index([workspaceId]) } model workspace_chats { From b94da205a58770bcbfb5e29b7daaea7b1cf74a82 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 6 Feb 2024 11:30:49 -0800 Subject: [PATCH 056/298] fix truncate function to handle Chinese characters (#677) * fix truncate function to handle Chinese characters * revert changes to truncate function and cut off title using css * add CSS ellipse on long file name * fix uploaded middleTruncation as well as directory --------- Co-authored-by: timothycarambat --- .../Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx | 2 +- .../Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx index 61187f55b2..7e2259b228 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx @@ -78,7 +78,7 @@ export default function FileRow({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > -

+

{middleTruncate(item.title, 17)}

{showTooltip && ( diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx index 99493dbaf2..91e165d4c0 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx @@ -64,7 +64,7 @@ export default function WorkspaceFileRow({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > -

+

{middleTruncate(item.title, 17)}

{showTooltip && ( From 36558350e737d3baebde02fb574bc5a4ac2ea835 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 6 Feb 2024 13:11:44 -0800 Subject: [PATCH 057/298] Fix white screen bug when deleting chats under Workspace Chats menu (#681) * use filter instead of ref to delete row from workspace chat component on delete/fix backend invalid json error * remove ref from pagination on embed chats and fix white screen bug * remove unneeded import * normalize response object --------- Co-authored-by: timothycarambat --- .../src/pages/GeneralSettings/Chats/ChatRow/index.jsx | 11 +++-------- frontend/src/pages/GeneralSettings/Chats/index.jsx | 8 +++++++- .../GeneralSettings/EmbedChats/ChatRow/index.jsx | 11 +++-------- .../src/pages/GeneralSettings/EmbedChats/index.jsx | 8 +++++++- server/endpoints/system.js | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx index 3056e76593..5c0744f12f 100644 --- a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx @@ -1,12 +1,10 @@ -import { useRef } from "react"; import truncate from "truncate"; import { X, Trash } from "@phosphor-icons/react"; import System from "@/models/system"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; -export default function ChatRow({ chat }) { - const rowRef = useRef(null); +export default function ChatRow({ chat, onDelete }) { const { isOpen: isPromptOpen, openModal: openPromptModal, @@ -25,16 +23,13 @@ export default function ChatRow({ chat }) { ) ) return false; - rowRef?.current?.remove(); await System.deleteChat(chat.id); + onDelete(chat.id); }; return ( <> - + {chat.id} diff --git a/frontend/src/pages/GeneralSettings/Chats/index.jsx b/frontend/src/pages/GeneralSettings/Chats/index.jsx index f0ae8e9735..336aadccea 100644 --- a/frontend/src/pages/GeneralSettings/Chats/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/index.jsx @@ -142,6 +142,10 @@ function ChatsContainer() { setOffset(offset + 1); }; + const handleDeleteChat = (chatId) => { + setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId)); + }; + useEffect(() => { async function fetchChats() { const { chats: _chats, hasPages = false } = await System.chats(offset); @@ -196,7 +200,9 @@ function ChatsContainer() { {!!chats && - chats.map((chat) => )} + chats.map((chat) => ( + + ))}
diff --git a/frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx index cc0bc2a83a..a9c125cf4f 100644 --- a/frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx @@ -1,4 +1,3 @@ -import { useRef } from "react"; import truncate from "truncate"; import { X, Trash, LinkSimple } from "@phosphor-icons/react"; import ModalWrapper from "@/components/ModalWrapper"; @@ -6,8 +5,7 @@ import { useModal } from "@/hooks/useModal"; import paths from "@/utils/paths"; import Embed from "@/models/embed"; -export default function ChatRow({ chat }) { - const rowRef = useRef(null); +export default function ChatRow({ chat, onDelete }) { const { isOpen: isPromptOpen, openModal: openPromptModal, @@ -26,16 +24,13 @@ export default function ChatRow({ chat }) { ) ) return false; - rowRef?.current?.remove(); await Embed.deleteChat(chat.id); + onDelete(chat.id); }; return ( <> - + { + setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId)); + }; + useEffect(() => { async function fetchChats() { const { chats: _chats, hasPages = false } = await Embed.chats(offset); @@ -99,7 +103,9 @@ function ChatsContainer() { {!!chats && - chats.map((chat) => )} + chats.map((chat) => ( + + ))}
diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 4eb82fb0ac..100d0a4b68 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -774,7 +774,7 @@ function systemEndpoints(app) { try { const { id } = request.params; await WorkspaceChats.delete({ id: Number(id) }); - response.sendStatus(200).end(); + response.json({ success: true, error: null }); } catch (e) { console.error(e); response.sendStatus(500).end(); From 5d64f2606678cf0f87b1fbc69f07ec8a86929d95 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 6 Feb 2024 14:39:56 -0800 Subject: [PATCH 058/298] patch admin pwd update --- server/utils/helpers/admin/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/utils/helpers/admin/index.js b/server/utils/helpers/admin/index.js index 87a0127841..c114417dc8 100644 --- a/server/utils/helpers/admin/index.js +++ b/server/utils/helpers/admin/index.js @@ -20,9 +20,13 @@ function validRoleSelection(currentUser = {}, newUserParams = {}) { // Check to make sure with this update that includes a role change to an existing admin to a non-admin // that we still have at least one admin left or else they will lock themselves out. async function canModifyAdmin(userToModify, updates) { - // if updates don't include role property or the user being modified isn't an admin currently - skip. + // if updates don't include role property + // or the user being modified isn't an admin currently + // or the updates role is equal to the users current role. + // skip validation. if (!updates.hasOwnProperty("role")) return { valid: true, error: null }; if (userToModify.role !== ROLES.admin) return { valid: true, error: null }; + if (updates.role === userToModify.role) return { valid: true, error: null }; const adminCount = await User.count({ role: ROLES.admin }); if (adminCount - 1 <= 0) From d789920a194f56f6d4cf160edf43b99ae89f4203 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 6 Feb 2024 15:21:40 -0800 Subject: [PATCH 059/298] [FEAT] Automated audit logging (#667) * WIP event logging - new table for events and new settings view for viewing * WIP add logging * UI for log rows * rename files to Logging to prevent getting gitignore * add metadata for all logging events and colored badges in logs page * remove unneeded comment * cleanup namespace for logging * clean up backend calls * update logging to show to => from settings changes * add logging for invitations, created, deleted, and accepted * add logging for user created, updated, suspended, or removed * add logging for workspace deleted * add logging for chat logs exported * add logging for API keys, LLM, embedder, vector db, embed chat, and reset button * modify event logs * update to event log types * simplify rendering of event badges --------- Co-authored-by: timothycarambat --- frontend/src/App.jsx | 5 + .../src/components/SettingsSidebar/index.jsx | 21 ++- frontend/src/models/system.js | 23 +++ .../src/pages/Admin/Logging/LogRow/index.jsx | 105 +++++++++++++ frontend/src/pages/Admin/Logging/index.jsx | 138 ++++++++++++++++++ frontend/src/utils/paths.js | 3 + server/endpoints/admin.js | 44 ++++++ server/endpoints/api/admin/index.js | 7 +- server/endpoints/api/document/index.js | 41 +++--- server/endpoints/api/workspace/index.js | 16 ++ server/endpoints/chat.js | 10 ++ server/endpoints/embedManagement.js | 16 +- server/endpoints/invite.js | 9 ++ server/endpoints/system.js | 107 +++++++++++++- server/endpoints/workspaces.js | 38 ++++- server/models/apiKeys.js | 2 - server/models/documents.js | 21 ++- server/models/eventLogs.js | 129 ++++++++++++++++ server/models/user.js | 38 ++++- .../20240206211916_init/migration.sql | 11 ++ server/prisma/schema.prisma | 10 ++ server/utils/helpers/updateENV.js | 18 ++- 22 files changed, 778 insertions(+), 34 deletions(-) create mode 100644 frontend/src/pages/Admin/Logging/LogRow/index.jsx create mode 100644 frontend/src/pages/Admin/Logging/index.jsx create mode 100644 server/models/eventLogs.js create mode 100644 server/prisma/migrations/20240206211916_init/migration.sql diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a08365125b..7d4ee4c576 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,7 @@ const AdminUsers = lazy(() => import("@/pages/Admin/Users")); const AdminInvites = lazy(() => import("@/pages/Admin/Invitations")); const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces")); const AdminSystem = lazy(() => import("@/pages/Admin/System")); +const AdminLogs = lazy(() => import("@/pages/Admin/Logging")); const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats")); const GeneralAppearance = lazy( () => import("@/pages/GeneralSettings/Appearance") @@ -79,6 +80,10 @@ export default function App() { path="/settings/vector-database" element={} /> + } + /> } diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 2eba84a9d1..5fe81363c8 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -19,6 +19,7 @@ import { List, FileCode, Plugs, + Notepad, CodeBlock, Barcode, } from "@phosphor-icons/react"; @@ -63,7 +64,7 @@ export default function SettingsSidebar() { {/* Primary Body */}
-
+
@@ -299,7 +308,7 @@ export function SidebarMobileHeader() {
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 596348ede8..0ffbae6901 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -389,6 +389,29 @@ const System = { return []; }); }, + eventLogs: async (offset = 0) => { + return await fetch(`${API_BASE}/system/event-logs`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ offset }), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return []; + }); + }, + clearEventLogs: async () => { + return await fetch(`${API_BASE}/system/event-logs`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, deleteChat: async (chatId) => { return await fetch(`${API_BASE}/system/workspace-chats/${chatId}`, { method: "DELETE", diff --git a/frontend/src/pages/Admin/Logging/LogRow/index.jsx b/frontend/src/pages/Admin/Logging/LogRow/index.jsx new file mode 100644 index 0000000000..0ce89b35a9 --- /dev/null +++ b/frontend/src/pages/Admin/Logging/LogRow/index.jsx @@ -0,0 +1,105 @@ +import { CaretDown, CaretUp } from "@phosphor-icons/react"; +import { useEffect, useState } from "react"; + +export default function LogRow({ log }) { + const [expanded, setExpanded] = useState(false); + const [metadata, setMetadata] = useState(null); + const [hasMetadata, setHasMetadata] = useState(false); + + useEffect(() => { + function parseAndSetMetadata() { + try { + let data = JSON.parse(log.metadata); + setHasMetadata(Object.keys(data)?.length > 0); + setMetadata(data); + } catch {} + } + parseAndSetMetadata(); + }, [log.metadata]); + + const handleRowClick = () => { + if (log.metadata !== "{}") { + setExpanded(!expanded); + } + }; + + return ( + <> + + + + {log.user.username} + + + {log.occurredAt} + + {hasMetadata && ( + <> + {expanded ? ( + + +

hide

+ + ) : ( + + +

show

+ + )} + + )} + + + + ); +} + +const EventMetadata = ({ metadata, expanded = false }) => { + if (!metadata || !expanded) return null; + return ( + + + Event Metadata + + +
+
+            {JSON.stringify(metadata, null, 2)}
+          
+
+ + + ); +}; + +const EventBadge = ({ event }) => { + let colorTheme = { bg: "bg-sky-600/20", text: "text-sky-400 " }; + if (event.includes("update")) + colorTheme = { bg: "bg-yellow-600/20", text: "text-yellow-400 " }; + if (event.includes("failed_") || event.includes("deleted")) + colorTheme = { bg: "bg-red-600/20", text: "text-red-400 " }; + if (event === "login_event") + colorTheme = { bg: "bg-green-600/20", text: "text-green-400 " }; + + return ( + + + {event} + + + ); +}; diff --git a/frontend/src/pages/Admin/Logging/index.jsx b/frontend/src/pages/Admin/Logging/index.jsx new file mode 100644 index 0000000000..0219b54a39 --- /dev/null +++ b/frontend/src/pages/Admin/Logging/index.jsx @@ -0,0 +1,138 @@ +import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar"; +import useQuery from "@/hooks/useQuery"; +import System from "@/models/system"; +import { useEffect, useState } from "react"; +import { isMobile } from "react-device-detect"; +import * as Skeleton from "react-loading-skeleton"; +import LogRow from "./LogRow"; +import showToast from "@/utils/toast"; + +export default function AdminLogs() { + const handleResetLogs = async () => { + if ( + !window.confirm( + "Are you sure you want to clear all event logs? This action is irreversible." + ) + ) + return; + const { success, error } = await System.clearEventLogs(); + if (success) { + showToast("Event logs cleared successfully.", "success"); + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + showToast(`Failed to clear logs: ${error}`, "error"); + } + }; + return ( +
+ {!isMobile && } +
+ {isMobile && } +
+
+
+

Event Logs

+ +
+

+ View all actions and events happening on this instance for + monitoring. +

+
+ +
+
+
+ ); +} + +function LogsContainer() { + const query = useQuery(); + const [loading, setLoading] = useState(true); + const [logs, setLogs] = useState([]); + const [offset, setOffset] = useState(Number(query.get("offset") || 0)); + const [canNext, setCanNext] = useState(false); + + const handlePrevious = () => { + setOffset(Math.max(offset - 1, 0)); + }; + const handleNext = () => { + setOffset(offset + 1); + }; + + useEffect(() => { + async function fetchLogs() { + const { logs: _logs, hasPages = false } = await System.eventLogs(offset); + setLogs(_logs); + setCanNext(hasPages); + setLoading(false); + } + fetchLogs(); + }, [offset]); + + if (loading) { + return ( + + ); + } + + return ( + <> + + + + + + + + + + + {!!logs && logs.map((log) => )} + +
+ Event Type + + User + + Occurred At + + {" "} +
+
+ + +
+ + ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 8fbaacecd2..06428c608f 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -96,6 +96,9 @@ export default { apiKeys: () => { return "/settings/api-keys"; }, + logs: () => { + return "/settings/event-logs"; + }, embedSetup: () => { return `/settings/embed-config`; }, diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index b107a11b42..d9e1f9a0bf 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -1,7 +1,9 @@ const { ApiKey } = require("../models/apiKeys"); const { Document } = require("../models/documents"); +const { EventLogs } = require("../models/eventLogs"); const { Invite } = require("../models/invite"); const { SystemSettings } = require("../models/systemSettings"); +const { Telemetry } = require("../models/telemetry"); const { User } = require("../models/user"); const { DocumentVectors } = require("../models/vectors"); const { Workspace } = require("../models/workspace"); @@ -56,6 +58,14 @@ function adminEndpoints(app) { } const { user: newUser, error } = await User.create(newUserParams); + await EventLogs.logEvent( + "user_created", + { + userName: newUser.username, + createdBy: currUser.username, + }, + currUser.id + ); response.status(200).json({ user: newUser, error }); } catch (e) { console.error(e); @@ -121,6 +131,14 @@ function adminEndpoints(app) { } await User.delete({ id: Number(id) }); + await EventLogs.logEvent( + "user_deleted", + { + userName: user.username, + deletedBy: currUser.username, + }, + currUser.id + ); response.status(200).json({ success: true, error: null }); } catch (e) { console.error(e); @@ -150,6 +168,14 @@ function adminEndpoints(app) { try { const user = await userFromSession(request, response); const { invite, error } = await Invite.create(user.id); + await EventLogs.logEvent( + "invite_created", + { + inviteCode: invite.code, + createdBy: response.locals?.user?.username, + }, + response.locals?.user?.id + ); response.status(200).json({ invite, error }); } catch (e) { console.error(e); @@ -165,6 +191,11 @@ function adminEndpoints(app) { try { const { id } = request.params; const { success, error } = await Invite.deactivate(id); + await EventLogs.logEvent( + "invite_deleted", + { deletedBy: response.locals?.user?.username }, + response.locals?.user?.id + ); response.status(200).json({ success, error }); } catch (e) { console.error(e); @@ -323,6 +354,13 @@ function adminEndpoints(app) { try { const user = await userFromSession(request, response); const { apiKey, error } = await ApiKey.create(user.id); + + await Telemetry.sendTelemetry("api_key_created"); + await EventLogs.logEvent( + "api_key_created", + { createdBy: user?.username }, + user?.id + ); return response.status(200).json({ apiKey, error, @@ -341,6 +379,12 @@ function adminEndpoints(app) { try { const { id } = request.params; await ApiKey.delete({ id: Number(id) }); + + await EventLogs.logEvent( + "api_key_deleted", + { deletedBy: response.locals?.user?.username }, + response?.locals?.user?.id + ); return response.status(200).end(); } catch (e) { console.error(e); diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js index 1f2a5bae74..e91672e007 100644 --- a/server/endpoints/api/admin/index.js +++ b/server/endpoints/api/admin/index.js @@ -1,3 +1,4 @@ +const { EventLogs } = require("../../../models/eventLogs"); const { Invite } = require("../../../models/invite"); const { SystemSettings } = require("../../../models/systemSettings"); const { User } = require("../../../models/user"); @@ -259,7 +260,11 @@ function apiAdminEndpoints(app) { } const { id } = request.params; - await User.delete({ id }); + const user = await User.get({ id: Number(id) }); + await User.delete({ id: user.id }); + await EventLogs.logEvent("api_user_deleted", { + userName: user.username, + }); response.status(200).json({ success: true, error: null }); } catch (e) { console.error(e); diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js index 817043526b..b72debbdbb 100644 --- a/server/endpoints/api/document/index.js +++ b/server/endpoints/api/document/index.js @@ -12,6 +12,7 @@ const { findDocumentInDocuments, } = require("../../../utils/files"); const { reqBody } = require("../../../utils/http"); +const { EventLogs } = require("../../../models/eventLogs"); const { handleUploads } = setupMulter(); function apiDocumentEndpoints(app) { @@ -22,7 +23,7 @@ function apiDocumentEndpoints(app) { [validApiKey], handleUploads.single("file"), async (request, response) => { - /* + /* #swagger.tags = ['Documents'] #swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.' #swagger.requestBody = { @@ -68,9 +69,9 @@ function apiDocumentEndpoints(app) { ] } } - } + } } - } + } #swagger.responses[403] = { schema: { "$ref": "#/definitions/InvalidAPIKey" @@ -105,6 +106,9 @@ function apiDocumentEndpoints(app) { `Document ${originalname} uploaded processed and successfully. It is now available in documents.` ); await Telemetry.sendTelemetry("document_uploaded"); + await EventLogs.logEvent("api_document_uploaded", { + documentName: originalname, + }); response.status(200).json({ success: true, error: null, documents }); } catch (e) { console.log(e.message, e); @@ -117,7 +121,7 @@ function apiDocumentEndpoints(app) { "/v1/document/upload-link", [validApiKey], async (request, response) => { - /* + /* #swagger.tags = ['Documents'] #swagger.description = 'Upload a valid URL for AnythingLLM to scrape and prepare for embedding.' #swagger.requestBody = { @@ -132,7 +136,7 @@ function apiDocumentEndpoints(app) { "link": "https://useanything.com" } } - } + } } } #swagger.responses[200] = { @@ -161,9 +165,9 @@ function apiDocumentEndpoints(app) { ] } } - } + } } - } + } #swagger.responses[403] = { schema: { "$ref": "#/definitions/InvalidAPIKey" @@ -196,7 +200,10 @@ function apiDocumentEndpoints(app) { console.log( `Link ${link} uploaded processed and successfully. It is now available in documents.` ); - await Telemetry.sendTelemetry("document_uploaded"); + await Telemetry.sendTelemetry("link_uploaded"); + await EventLogs.logEvent("api_link_uploaded", { + link, + }); response.status(200).json({ success: true, error: null, documents }); } catch (e) { console.log(e.message, e); @@ -206,7 +213,7 @@ function apiDocumentEndpoints(app) { ); app.get("/v1/documents", [validApiKey], async (_, response) => { - /* + /* #swagger.tags = ['Documents'] #swagger.description = 'List of all locally-stored documents in instance' #swagger.responses[200] = { @@ -231,9 +238,9 @@ function apiDocumentEndpoints(app) { } } } - } + } } - } + } #swagger.responses[403] = { schema: { "$ref": "#/definitions/InvalidAPIKey" @@ -250,7 +257,7 @@ function apiDocumentEndpoints(app) { }); app.get("/v1/document/:docName", [validApiKey], async (request, response) => { - /* + /* #swagger.tags = ['Documents'] #swagger.description = 'Get a single document by its unique AnythingLLM document name' #swagger.parameters['docName'] = { @@ -281,9 +288,9 @@ function apiDocumentEndpoints(app) { } } } - } + } } - } + } #swagger.responses[403] = { schema: { "$ref": "#/definitions/InvalidAPIKey" @@ -308,7 +315,7 @@ function apiDocumentEndpoints(app) { "/v1/document/accepted-file-types", [validApiKey], async (_, response) => { - /* + /* #swagger.tags = ['Documents'] #swagger.description = 'Check available filetypes and MIMEs that can be uploaded.' #swagger.responses[200] = { @@ -337,9 +344,9 @@ function apiDocumentEndpoints(app) { } } } - } + } } - } + } #swagger.responses[403] = { schema: { "$ref": "#/definitions/InvalidAPIKey" diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index c1642ce4aa..885d0f1ae7 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -16,6 +16,7 @@ const { writeResponseChunk, VALID_CHAT_MODE, } = require("../../../utils/chats/stream"); +const { EventLogs } = require("../../../models/eventLogs"); function apiWorkspaceEndpoints(app) { if (!app) return; @@ -73,6 +74,9 @@ function apiWorkspaceEndpoints(app) { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + await EventLogs.logEvent("api_workspace_created", { + workspaceName: workspace?.name || "Unknown Workspace", + }); response.status(200).json({ workspace, message }); } catch (e) { console.log(e.message, e); @@ -206,6 +210,10 @@ function apiWorkspaceEndpoints(app) { await DocumentVectors.deleteForWorkspace(workspaceId); await Document.delete({ workspaceId: workspaceId }); await Workspace.delete({ id: workspaceId }); + + await EventLogs.logEvent("api_workspace_deleted", { + workspaceName: workspace?.name || "Unknown Workspace", + }); try { await VectorDb["delete-namespace"]({ namespace: slug }); } catch (e) { @@ -519,6 +527,10 @@ function apiWorkspaceEndpoints(app) { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + await EventLogs.logEvent("api_sent_chat", { + workspaceName: workspace?.name, + chatModel: workspace?.chatModel || "System Default", + }); response.status(200).json({ ...result }); } catch (e) { response.status(500).json({ @@ -637,6 +649,10 @@ function apiWorkspaceEndpoints(app) { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + await EventLogs.logEvent("api_sent_chat", { + workspaceName: workspace?.name, + chatModel: workspace?.chatModel || "System Default", + }); response.end(); } catch (e) { console.error(e); diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index 23739084a7..848a7a3633 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -14,6 +14,7 @@ const { ROLES, flexUserRoleValid, } = require("../utils/middleware/multiUserProtected"); +const { EventLogs } = require("../models/eventLogs"); function chatEndpoints(app) { if (!app) return; @@ -98,6 +99,15 @@ function chatEndpoints(app) { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + + await EventLogs.logEvent( + "sent_chat", + { + workspaceName: workspace?.name, + chatModel: workspace?.chatModel || "System Default", + }, + user?.id + ); response.end(); } catch (e) { console.error(e); diff --git a/server/endpoints/embedManagement.js b/server/endpoints/embedManagement.js index c3a27ce411..7ebab23e7b 100644 --- a/server/endpoints/embedManagement.js +++ b/server/endpoints/embedManagement.js @@ -1,5 +1,7 @@ const { EmbedChats } = require("../models/embedChats"); const { EmbedConfig } = require("../models/embedConfig"); +const { EventLogs } = require("../models/eventLogs"); +const { Workspace } = require("../models/workspace"); const { reqBody, userFromSession } = require("../utils/http"); const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware"); const { @@ -32,9 +34,14 @@ function embedManagementEndpoints(app) { [validatedRequest, flexUserRoleValid([ROLES.admin])], async (request, response) => { try { - const user = userFromSession(request, response); + const user = await userFromSession(request, response); const data = reqBody(request); const { embed, message: error } = await EmbedConfig.new(data, user?.id); + await EventLogs.logEvent( + "embed_created", + { embedId: embed.id }, + user?.id + ); response.status(200).json({ embed, error }); } catch (e) { console.error(e); @@ -48,9 +55,11 @@ function embedManagementEndpoints(app) { [validatedRequest, flexUserRoleValid([ROLES.admin]), validEmbedConfigId], async (request, response) => { try { + const user = await userFromSession(request, response); const { embedId } = request.params; const updates = reqBody(request); const { success, error } = await EmbedConfig.update(embedId, updates); + await EventLogs.logEvent("embed_updated", { embedId }, user?.id); response.status(200).json({ success, error }); } catch (e) { console.error(e); @@ -66,6 +75,11 @@ function embedManagementEndpoints(app) { try { const { embedId } = request.params; await EmbedConfig.delete({ id: Number(embedId) }); + await EventLogs.logEvent( + "embed_deleted", + { embedId }, + response?.locals?.user?.id + ); response.status(200).json({ success: true, error: null }); } catch (e) { console.error(e); diff --git a/server/endpoints/invite.js b/server/endpoints/invite.js index 4fd8d15450..38eb71de81 100644 --- a/server/endpoints/invite.js +++ b/server/endpoints/invite.js @@ -1,3 +1,4 @@ +const { EventLogs } = require("../models/eventLogs"); const { Invite } = require("../models/invite"); const { User } = require("../models/user"); const { reqBody } = require("../utils/http"); @@ -56,6 +57,14 @@ function inviteEndpoints(app) { } await Invite.markClaimed(invite.id, user); + await EventLogs.logEvent( + "invite_accepted", + { + username: user.username, + }, + user.id + ); + response.status(200).json({ success: true, error: null }); } catch (e) { console.error(e); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 100d0a4b68..823de7f1cb 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -48,6 +48,7 @@ const { prepareWorkspaceChatsForExport, exportChatsAsType, } = require("../utils/helpers/chat/convertTo"); +const { EventLogs } = require("../models/eventLogs"); function systemEndpoints(app) { if (!app) return; @@ -114,6 +115,14 @@ function systemEndpoints(app) { const existingUser = await User.get({ username }); if (!existingUser) { + await EventLogs.logEvent( + "failed_login_invalid_username", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + existingUser?.id + ); response.status(200).json({ user: null, valid: false, @@ -124,6 +133,14 @@ function systemEndpoints(app) { } if (!bcrypt.compareSync(password, existingUser.password)) { + await EventLogs.logEvent( + "failed_login_invalid_password", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + existingUser?.id + ); response.status(200).json({ user: null, valid: false, @@ -134,6 +151,14 @@ function systemEndpoints(app) { } if (existingUser.suspended) { + await EventLogs.logEvent( + "failed_login_account_suspended", + { + ip: request.ip || "Unknown IP", + username: username || "Unknown user", + }, + existingUser?.id + ); response.status(200).json({ user: null, valid: false, @@ -148,6 +173,16 @@ function systemEndpoints(app) { { multiUserMode: false }, existingUser?.id ); + + await EventLogs.logEvent( + "login_event", + { + ip: request.ip || "Unknown IP", + username: existingUser.username || "Unknown user", + }, + existingUser?.id + ); + response.status(200).json({ valid: true, user: existingUser, @@ -166,6 +201,10 @@ function systemEndpoints(app) { bcrypt.hashSync(process.env.AUTH_TOKEN, 10) ) ) { + await EventLogs.logEvent("failed_login_invalid_password", { + ip: request.ip || "Unknown IP", + multiUserMode: false, + }); response.status(401).json({ valid: false, token: null, @@ -175,6 +214,10 @@ function systemEndpoints(app) { } await Telemetry.sendTelemetry("login_event", { multiUserMode: false }); + await EventLogs.logEvent("login_event", { + ip: request.ip || "Unknown IP", + multiUserMode: false, + }); response.status(200).json({ valid: true, token: makeJWT({ p: password }, "30d"), @@ -288,7 +331,11 @@ function systemEndpoints(app) { async (request, response) => { try { const body = reqBody(request); - const { newValues, error } = await updateENV(body); + const { newValues, error } = await updateENV( + body, + false, + response?.locals?.user?.id + ); if (process.env.NODE_ENV === "production") await dumpENV(); response.status(200).json({ newValues, error }); } catch (e) { @@ -364,6 +411,7 @@ function systemEndpoints(app) { await Telemetry.sendTelemetry("enabled_multi_user_mode", { multiUserMode: true, }); + await EventLogs.logEvent("multi_user_mode_enabled", {}, user?.id); response.status(200).json({ success: !!user, error }); } catch (e) { await User.delete({}); @@ -694,6 +742,12 @@ function systemEndpoints(app) { } const { apiKey, error } = await ApiKey.create(); + await Telemetry.sendTelemetry("api_key_created"); + await EventLogs.logEvent( + "api_key_created", + {}, + response?.locals?.user?.id + ); return response.status(200).json({ apiKey, error, @@ -715,6 +769,11 @@ function systemEndpoints(app) { } await ApiKey.delete(); + await EventLogs.logEvent( + "api_key_deleted", + { deletedBy: response.locals?.user?.username }, + response?.locals?.user?.id + ); return response.status(200).end(); } catch (error) { console.error(error); @@ -744,6 +803,45 @@ function systemEndpoints(app) { } ); + app.post( + "/system/event-logs", + [validatedRequest, flexUserRoleValid([ROLES.admin])], + async (request, response) => { + try { + const { offset = 0, limit = 20 } = reqBody(request); + const logs = await EventLogs.whereWithData({}, limit, offset * limit, { + id: "desc", + }); + const totalLogs = await EventLogs.count(); + const hasPages = totalLogs > (offset + 1) * limit; + + response.status(200).json({ logs: logs, hasPages, totalLogs }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.delete( + "/system/event-logs", + [validatedRequest, flexUserRoleValid([ROLES.admin])], + async (_, response) => { + try { + await EventLogs.delete(); + await EventLogs.logEvent( + "event_logs_cleared", + {}, + response?.locals?.user?.id + ); + response.json({ success: true }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + app.post( "/system/workspace-chats", [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], @@ -790,6 +888,13 @@ function systemEndpoints(app) { const { type = "jsonl" } = request.query; const chats = await prepareWorkspaceChatsForExport(); const { contentType, data } = await exportChatsAsType(chats, type); + await EventLogs.logEvent( + "exported_chats", + { + type, + }, + response.locals.user?.id + ); response.setHeader("Content-Type", contentType); response.status(200).send(data); } catch (e) { diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index b04d23376f..574180627b 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -17,6 +17,7 @@ const { flexUserRoleValid, ROLES, } = require("../utils/middleware/multiUserProtected"); +const { EventLogs } = require("../models/eventLogs"); const { WorkspaceSuggestedMessages, } = require("../models/workspacesSuggestedMessages"); @@ -43,6 +44,14 @@ function workspaceEndpoints(app) { }, user?.id ); + + await EventLogs.logEvent( + "workspace_created", + { + workspaceName: workspace?.name || "Unknown Workspace", + }, + user?.id + ); if (onboardingComplete === true) await Telemetry.sendTelemetry("onboarding_complete"); @@ -112,6 +121,13 @@ function workspaceEndpoints(app) { `Document ${originalname} uploaded processed and successfully. It is now available in documents.` ); await Telemetry.sendTelemetry("document_uploaded"); + await EventLogs.logEvent( + "document_uploaded", + { + documentName: originalname, + }, + response.locals?.user?.id + ); response.status(200).json({ success: true, error: null }); } ); @@ -144,6 +160,11 @@ function workspaceEndpoints(app) { `Link ${link} uploaded processed and successfully. It is now available in documents.` ); await Telemetry.sendTelemetry("link_uploaded"); + await EventLogs.logEvent( + "link_uploaded", + { link }, + response.locals?.user?.id + ); response.status(200).json({ success: true, error: null }); } ); @@ -165,10 +186,15 @@ function workspaceEndpoints(app) { return; } - await Document.removeDocuments(currWorkspace, deletes); + await Document.removeDocuments( + currWorkspace, + deletes, + response.locals?.user?.id + ); const { failedToEmbed = [], errors = [] } = await Document.addDocuments( currWorkspace, - adds + adds, + response.locals?.user?.id ); const updatedWorkspace = await Workspace.get({ id: currWorkspace.id }); response.status(200).json({ @@ -209,6 +235,14 @@ function workspaceEndpoints(app) { await Document.delete({ workspaceId: Number(workspace.id) }); await Workspace.delete({ id: Number(workspace.id) }); + await EventLogs.logEvent( + "workspace_deleted", + { + workspaceName: workspace?.name || "Unknown Workspace", + }, + response.locals?.user?.id + ); + try { await VectorDb["delete-namespace"]({ namespace: slug }); } catch (e) { diff --git a/server/models/apiKeys.js b/server/models/apiKeys.js index b6242397ba..32727fec8d 100644 --- a/server/models/apiKeys.js +++ b/server/models/apiKeys.js @@ -1,4 +1,3 @@ -const { Telemetry } = require("./telemetry"); const prisma = require("../utils/prisma"); const ApiKey = { @@ -19,7 +18,6 @@ const ApiKey = { }, }); - await Telemetry.sendTelemetry("api_key_created"); return { apiKey, error: null }; } catch (error) { console.error("FAILED TO CREATE API KEY.", error.message); diff --git a/server/models/documents.js b/server/models/documents.js index bdb29dc77b..9f50aa9159 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -3,6 +3,7 @@ const { v4: uuidv4 } = require("uuid"); const { getVectorDbClass } = require("../utils/helpers"); const prisma = require("../utils/prisma"); const { Telemetry } = require("./telemetry"); +const { EventLogs } = require("./eventLogs"); const Document = { forWorkspace: async function (workspaceId = null) { @@ -34,7 +35,7 @@ const Document = { } }, - addDocuments: async function (workspace, additions = []) { + addDocuments: async function (workspace, additions = [], userId = null) { const VectorDb = getVectorDbClass(); if (additions.length === 0) return { failed: [], embedded: [] }; const embedded = []; @@ -84,10 +85,18 @@ const Document = { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + await EventLogs.logEvent( + "workspace_documents_added", + { + workspaceName: workspace?.name || "Unknown Workspace", + numberOfDocumentsAdded: additions.length, + }, + userId + ); return { failedToEmbed, errors: Array.from(errors), embedded }; }, - removeDocuments: async function (workspace, removals = []) { + removeDocuments: async function (workspace, removals = [], userId = null) { const VectorDb = getVectorDbClass(); if (removals.length === 0) return; @@ -119,6 +128,14 @@ const Document = { Embedder: process.env.EMBEDDING_ENGINE || "inherit", VectorDbSelection: process.env.VECTOR_DB || "pinecone", }); + await EventLogs.logEvent( + "workspace_documents_removed", + { + workspaceName: workspace?.name || "Unknown Workspace", + numberOfDocuments: removals.length, + }, + userId + ); return true; }, diff --git a/server/models/eventLogs.js b/server/models/eventLogs.js new file mode 100644 index 0000000000..51240431a7 --- /dev/null +++ b/server/models/eventLogs.js @@ -0,0 +1,129 @@ +const prisma = require("../utils/prisma"); + +const EventLogs = { + logEvent: async function (event, metadata = {}, userId = null) { + try { + const eventLog = await prisma.event_logs.create({ + data: { + event, + metadata: metadata ? JSON.stringify(metadata) : null, + userId: userId ? Number(userId) : null, + occurredAt: new Date(), + }, + }); + console.log(`\x1b[32m[Event Logged]\x1b[0m - ${event}`); + return { eventLog, message: null }; + } catch (error) { + console.error( + `\x1b[31m[Event Logging Failed]\x1b[0m - ${event}`, + error.message + ); + return { eventLog: null, message: error.message }; + } + }, + + getByEvent: async function (event, limit = null, orderBy = null) { + try { + const logs = await prisma.event_logs.findMany({ + where: { event }, + ...(limit !== null ? { take: limit } : {}), + ...(orderBy !== null + ? { orderBy } + : { orderBy: { occurredAt: "desc" } }), + }); + return logs; + } catch (error) { + console.error(error.message); + return []; + } + }, + + getByUserId: async function (userId, limit = null, orderBy = null) { + try { + const logs = await prisma.event_logs.findMany({ + where: { userId }, + ...(limit !== null ? { take: limit } : {}), + ...(orderBy !== null + ? { orderBy } + : { orderBy: { occurredAt: "desc" } }), + }); + return logs; + } catch (error) { + console.error(error.message); + return []; + } + }, + + where: async function ( + clause = {}, + limit = null, + orderBy = null, + offset = null + ) { + try { + const logs = await prisma.event_logs.findMany({ + where: clause, + ...(limit !== null ? { take: limit } : {}), + ...(offset !== null ? { skip: offset } : {}), + ...(orderBy !== null + ? { orderBy } + : { orderBy: { occurredAt: "desc" } }), + }); + return logs; + } catch (error) { + console.error(error.message); + return []; + } + }, + + whereWithData: async function ( + clause = {}, + limit = null, + offset = null, + orderBy = null + ) { + const { User } = require("./user"); + + try { + const results = await this.where(clause, limit, orderBy, offset); + + for (const res of results) { + const user = res.userId ? await User.get({ id: res.userId }) : null; + res.user = user + ? { username: user.username } + : { username: "unknown user" }; + } + + return results; + } catch (error) { + console.error(error.message); + return []; + } + }, + + count: async function (clause = {}) { + try { + const count = await prisma.event_logs.count({ + where: clause, + }); + return count; + } catch (error) { + console.error(error.message); + return 0; + } + }, + + delete: async function (clause = {}) { + try { + await prisma.event_logs.deleteMany({ + where: clause, + }); + return true; + } catch (error) { + console.error(error.message); + return false; + } + }, +}; + +module.exports = { EventLogs }; diff --git a/server/models/user.js b/server/models/user.js index 269219fc89..c447950caa 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,4 +1,5 @@ const prisma = require("../utils/prisma"); +const { EventLogs } = require("./eventLogs"); const User = { create: async function ({ username, password, role = "default" }) { @@ -24,25 +25,52 @@ const User = { } }, + // Log the changes to a user object, but omit sensitive fields + // that are not meant to be logged. + loggedChanges: function (updates, prev = {}) { + const changes = {}; + const sensitiveFields = ["password"]; + + Object.keys(updates).forEach((key) => { + if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) { + changes[key] = `${prev[key]} => ${updates[key]}`; + } + }); + + return changes; + }, + update: async function (userId, updates = {}) { try { - // Rehash new password if it exists as update field + const currentUser = await prisma.users.findUnique({ + where: { id: parseInt(userId) }, + }); + if (!currentUser) { + return { success: false, error: "User not found" }; + } + if (updates.hasOwnProperty("password")) { const passwordCheck = this.checkPasswordComplexity(updates.password); if (!passwordCheck.checkedOK) { return { success: false, error: passwordCheck.error }; } - const bcrypt = require("bcrypt"); updates.password = bcrypt.hashSync(updates.password, 10); - } else { - delete updates.password; } - await prisma.users.update({ + const user = await prisma.users.update({ where: { id: parseInt(userId) }, data: updates, }); + + await EventLogs.logEvent( + "user_updated", + { + username: user.username, + changes: this.loggedChanges(updates, currentUser), + }, + userId + ); return { success: true, error: null }; } catch (error) { console.error(error.message); diff --git a/server/prisma/migrations/20240206211916_init/migration.sql b/server/prisma/migrations/20240206211916_init/migration.sql new file mode 100644 index 0000000000..f2e882a0bb --- /dev/null +++ b/server/prisma/migrations/20240206211916_init/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "event_logs" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "event" TEXT NOT NULL, + "metadata" TEXT, + "userId" INTEGER, + "occurredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE INDEX "event_logs_event_idx" ON "event_logs"("event"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index ede8a1fdeb..1747db329c 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -181,3 +181,13 @@ model embed_chats { embed_config embed_configs @relation(fields: [embed_id], references: [id], onDelete: Cascade) users users? @relation(fields: [usersId], references: [id]) } + +model event_logs { + id Int @id @default(autoincrement()) + event String + metadata String? + userId Int? + occurredAt DateTime @default(now()) + + @@index([event]) +} diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index acd77b2fdb..f89a193f6e 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -430,7 +430,7 @@ async function wipeWorkspaceModelPreference(key, prev, next) { // read from an ENV file as this seems to be a complicating step for many so allowing people to write // to the process will at least alleviate that issue. It does not perform comprehensive validity checks or sanity checks // and is simply for debugging when the .env not found issue many come across. -async function updateENV(newENVs = {}, force = false) { +async function updateENV(newENVs = {}, force = false, userId = null) { let error = ""; const validKeys = Object.keys(KEY_MAPPING); const ENV_KEYS = Object.keys(newENVs).filter( @@ -458,9 +458,25 @@ async function updateENV(newENVs = {}, force = false) { await postUpdateFunc(key, prevValue, nextValue); } + await logChangesToEventLog(newValues, userId); return { newValues, error: error?.length > 0 ? error : false }; } +async function logChangesToEventLog(newValues = {}, userId = null) { + const { EventLogs } = require("../../models/eventLogs"); + const eventMapping = { + LLMProvider: "update_llm_provider", + EmbeddingEngine: "update_embedding_engine", + VectorDB: "update_vector_db", + }; + + for (const [key, eventName] of Object.entries(eventMapping)) { + if (!newValues.hasOwnProperty(key)) continue; + await EventLogs.logEvent(eventName, {}, userId); + } + return; +} + async function dumpENV() { const fs = require("fs"); const path = require("path"); From 357046e683dbf121ca719247f9073ecec2144140 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 6 Feb 2024 18:34:31 -0800 Subject: [PATCH 060/298] desktop soft launch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04f123d21e..26f9c282f8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

-👉 AnythingLLM for desktop! Sign up +👉 AnythingLLM for desktop is in **public beta**! Download Now

A full-stack application that enables you to turn any document, resource, or piece of content into context that any LLM can use as references during chatting. This application allows you to pick and choose which LLM or Vector Database you want to use as well as supporting multi-user management and permissions. From e2a6a2d6c741b53489b2a1dc0f74643e3d0662fe Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 6 Feb 2024 18:35:03 -0800 Subject: [PATCH 061/298] desktop soft launch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26f9c282f8..bd2d306854 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

-👉 AnythingLLM for desktop is in **public beta**! Download Now +👉 AnythingLLM for desktop is in public beta! Download Now

A full-stack application that enables you to turn any document, resource, or piece of content into context that any LLM can use as references during chatting. This application allows you to pick and choose which LLM or Vector Database you want to use as well as supporting multi-user management and permissions. From aca59406508d4ef7d1f917fc319419ae8081f2b5 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 7 Feb 2024 08:15:14 -0800 Subject: [PATCH 062/298] Refactor handleStream to LLM Classes (#685) --- server/utils/AiProviders/azureOpenAi/index.js | 38 ++- server/utils/AiProviders/gemini/index.js | 34 +- server/utils/AiProviders/huggingface/index.js | 113 ++++++- server/utils/AiProviders/lmStudio/index.js | 5 + server/utils/AiProviders/localAi/index.js | 5 + server/utils/AiProviders/mistral/index.js | 5 + server/utils/AiProviders/native/index.js | 36 +++ server/utils/AiProviders/ollama/index.js | 36 +++ server/utils/AiProviders/openAi/index.js | 5 + server/utils/AiProviders/togetherAi/index.js | 97 +++++- server/utils/chats/embed.js | 6 +- server/utils/chats/stream.js | 301 +----------------- 12 files changed, 374 insertions(+), 307 deletions(-) diff --git a/server/utils/AiProviders/azureOpenAi/index.js b/server/utils/AiProviders/azureOpenAi/index.js index 639ac102ed..eac47f0efb 100644 --- a/server/utils/AiProviders/azureOpenAi/index.js +++ b/server/utils/AiProviders/azureOpenAi/index.js @@ -1,5 +1,6 @@ const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi"); const { chatPrompt } = require("../../chats"); +const { writeResponseChunk } = require("../../chats/stream"); class AzureOpenAiLLM { constructor(embedder = null, _modelPreference = null) { @@ -135,7 +136,7 @@ class AzureOpenAiLLM { n: 1, } ); - return { type: "azureStream", stream }; + return stream; } async getChatCompletion(messages = [], { temperature = 0.7 }) { @@ -165,7 +166,40 @@ class AzureOpenAiLLM { n: 1, } ); - return { type: "azureStream", stream }; + return stream; + } + + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise(async (resolve) => { + let fullText = ""; + for await (const event of stream) { + for (const choice of event.choices) { + const delta = choice.delta?.content; + if (!delta) continue; + fullText += delta; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: delta, + close: false, + error: false, + }); + } + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + }); } // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 63549fb8dd..36c63df722 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -1,4 +1,5 @@ const { chatPrompt } = require("../../chats"); +const { writeResponseChunk } = require("../../chats/stream"); class GeminiLLM { constructor(embedder = null, modelPreference = null) { @@ -164,7 +165,7 @@ class GeminiLLM { if (!responseStream.stream) throw new Error("Could not stream response stream from Gemini."); - return { type: "geminiStream", ...responseStream }; + return responseStream.stream; } async streamGetChatCompletion(messages = [], _opts = {}) { @@ -183,7 +184,7 @@ class GeminiLLM { if (!responseStream.stream) throw new Error("Could not stream response stream from Gemini."); - return { type: "geminiStream", ...responseStream }; + return responseStream.stream; } async compressMessages(promptArgs = {}, rawHistory = []) { @@ -192,6 +193,35 @@ class GeminiLLM { return await messageArrayCompressor(this, messageArray, rawHistory); } + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise(async (resolve) => { + let fullText = ""; + for await (const chunk of stream) { + fullText += chunk.text(); + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: chunk.text(), + close: false, + error: false, + }); + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + }); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/huggingface/index.js b/server/utils/AiProviders/huggingface/index.js index 4faf9b30f0..8fcc2b47ef 100644 --- a/server/utils/AiProviders/huggingface/index.js +++ b/server/utils/AiProviders/huggingface/index.js @@ -1,6 +1,7 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); const { chatPrompt } = require("../../chats"); +const { writeResponseChunk } = require("../../chats/stream"); class HuggingFaceLLM { constructor(embedder = null, _modelPreference = null) { @@ -138,7 +139,7 @@ class HuggingFaceLLM { }, { responseType: "stream" } ); - return { type: "huggingFaceStream", stream: streamRequest }; + return streamRequest; } async getChatCompletion(messages = null, { temperature = 0.7 }) { @@ -162,7 +163,115 @@ class HuggingFaceLLM { }, { responseType: "stream" } ); - return { type: "huggingFaceStream", stream: streamRequest }; + return streamRequest; + } + + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise((resolve) => { + let fullText = ""; + let chunk = ""; + stream.data.on("data", (data) => { + const lines = data + ?.toString() + ?.split("\n") + .filter((line) => line.trim() !== ""); + + for (const line of lines) { + let validJSON = false; + const message = chunk + line.replace(/^data:/, ""); + if (message !== "[DONE]") { + // JSON chunk is incomplete and has not ended yet + // so we need to stitch it together. You would think JSON + // chunks would only come complete - but they don't! + try { + JSON.parse(message); + validJSON = true; + } catch { + console.log("Failed to parse message", message); + } + + if (!validJSON) { + // It can be possible that the chunk decoding is running away + // and the message chunk fails to append due to string length. + // In this case abort the chunk and reset so we can continue. + // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416 + try { + chunk += message; + } catch (e) { + console.error(`Chunk appending error`, e); + chunk = ""; + } + continue; + } else { + chunk = ""; + } + } + + if (message == "[DONE]") { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } else { + let error = null; + let finishReason = null; + let token = ""; + try { + const json = JSON.parse(message); + error = json?.error || null; + token = json?.choices?.[0]?.delta?.content; + finishReason = json?.choices?.[0]?.finish_reason || null; + } catch { + continue; + } + + if (!!error) { + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: null, + close: true, + error, + }); + resolve(""); + return; + } + + if (token) { + fullText += token; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + if (finishReason !== null) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } + } + } + }); + }); } // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js index 08950a7b96..fe8064b689 100644 --- a/server/utils/AiProviders/lmStudio/index.js +++ b/server/utils/AiProviders/lmStudio/index.js @@ -1,4 +1,5 @@ const { chatPrompt } = require("../../chats"); +const { handleDefaultStreamResponse } = require("../../chats/stream"); // hybrid of openAi LLM chat completion for LMStudio class LMStudioLLM { @@ -174,6 +175,10 @@ class LMStudioLLM { return streamRequest; } + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponse(response, stream, responseProps); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/localAi/index.js b/server/utils/AiProviders/localAi/index.js index 6d265cf828..2717c47f3a 100644 --- a/server/utils/AiProviders/localAi/index.js +++ b/server/utils/AiProviders/localAi/index.js @@ -1,4 +1,5 @@ const { chatPrompt } = require("../../chats"); +const { handleDefaultStreamResponse } = require("../../chats/stream"); class LocalAiLLM { constructor(embedder = null, modelPreference = null) { @@ -174,6 +175,10 @@ class LocalAiLLM { return streamRequest; } + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponse(response, stream, responseProps); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/mistral/index.js b/server/utils/AiProviders/mistral/index.js index a25185c763..785a8dd085 100644 --- a/server/utils/AiProviders/mistral/index.js +++ b/server/utils/AiProviders/mistral/index.js @@ -1,4 +1,5 @@ const { chatPrompt } = require("../../chats"); +const { handleDefaultStreamResponse } = require("../../chats/stream"); class MistralLLM { constructor(embedder = null, modelPreference = null) { @@ -164,6 +165,10 @@ class MistralLLM { return streamRequest; } + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponse(response, stream, responseProps); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index de1a97f3d7..4b96d02ef1 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -2,6 +2,7 @@ const fs = require("fs"); const path = require("path"); const { NativeEmbedder } = require("../../EmbeddingEngines/native"); const { chatPrompt } = require("../../chats"); +const { writeResponseChunk } = require("../../chats/stream"); // Docs: https://api.js.langchain.com/classes/chat_models_llama_cpp.ChatLlamaCpp.html const ChatLlamaCpp = (...args) => @@ -170,6 +171,41 @@ class NativeLLM { return responseStream; } + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise(async (resolve) => { + let fullText = ""; + for await (const chunk of stream) { + if (chunk === undefined) + throw new Error( + "Stream returned undefined chunk. Aborting reply - check model provider logs." + ); + + const content = chunk.hasOwnProperty("content") ? chunk.content : chunk; + fullText += content; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: content, + close: false, + error: false, + }); + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + }); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/ollama/index.js b/server/utils/AiProviders/ollama/index.js index af7fe8210f..9a16d245af 100644 --- a/server/utils/AiProviders/ollama/index.js +++ b/server/utils/AiProviders/ollama/index.js @@ -1,5 +1,6 @@ const { chatPrompt } = require("../../chats"); const { StringOutputParser } = require("langchain/schema/output_parser"); +const { writeResponseChunk } = require("../../chats/stream"); // Docs: https://github.com/jmorganca/ollama/blob/main/docs/api.md class OllamaAILLM { @@ -165,6 +166,41 @@ class OllamaAILLM { return stream; } + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise(async (resolve) => { + let fullText = ""; + for await (const chunk of stream) { + if (chunk === undefined) + throw new Error( + "Stream returned undefined chunk. Aborting reply - check model provider logs." + ); + + const content = chunk.hasOwnProperty("content") ? chunk.content : chunk; + fullText += content; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: content, + close: false, + error: false, + }); + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + }); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index 120e728448..dffd368241 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -1,5 +1,6 @@ const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); const { chatPrompt } = require("../../chats"); +const { handleDefaultStreamResponse } = require("../../chats/stream"); class OpenAiLLM { constructor(embedder = null, modelPreference = null) { @@ -222,6 +223,10 @@ class OpenAiLLM { return streamRequest; } + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponse(response, stream, responseProps); + } + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations async embedTextInput(textInput) { return await this.embedder.embedTextInput(textInput); diff --git a/server/utils/AiProviders/togetherAi/index.js b/server/utils/AiProviders/togetherAi/index.js index 341661f8db..7291622a73 100644 --- a/server/utils/AiProviders/togetherAi/index.js +++ b/server/utils/AiProviders/togetherAi/index.js @@ -1,4 +1,5 @@ const { chatPrompt } = require("../../chats"); +const { writeResponseChunk } = require("../../chats/stream"); function togetherAiModels() { const { MODELS } = require("./models.js"); @@ -141,7 +142,7 @@ class TogetherAiLLM { }, { responseType: "stream" } ); - return { type: "togetherAiStream", stream: streamRequest }; + return streamRequest; } async getChatCompletion(messages = null, { temperature = 0.7 }) { @@ -175,7 +176,99 @@ class TogetherAiLLM { }, { responseType: "stream" } ); - return { type: "togetherAiStream", stream: streamRequest }; + return streamRequest; + } + + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise((resolve) => { + let fullText = ""; + let chunk = ""; + stream.data.on("data", (data) => { + const lines = data + ?.toString() + ?.split("\n") + .filter((line) => line.trim() !== ""); + + for (const line of lines) { + let validJSON = false; + const message = chunk + line.replace(/^data: /, ""); + + if (message !== "[DONE]") { + // JSON chunk is incomplete and has not ended yet + // so we need to stitch it together. You would think JSON + // chunks would only come complete - but they don't! + try { + JSON.parse(message); + validJSON = true; + } catch {} + + if (!validJSON) { + // It can be possible that the chunk decoding is running away + // and the message chunk fails to append due to string length. + // In this case abort the chunk and reset so we can continue. + // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416 + try { + chunk += message; + } catch (e) { + console.error(`Chunk appending error`, e); + chunk = ""; + } + continue; + } else { + chunk = ""; + } + } + + if (message == "[DONE]") { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } else { + let finishReason = null; + let token = ""; + try { + const json = JSON.parse(message); + token = json?.choices?.[0]?.delta?.content; + finishReason = json?.choices?.[0]?.finish_reason || null; + } catch { + continue; + } + + if (token) { + fullText += token; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + if (finishReason !== null) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + } + } + } + }); + }); } // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations diff --git a/server/utils/chats/embed.js b/server/utils/chats/embed.js index 30fc524d3f..5a7b93b49d 100644 --- a/server/utils/chats/embed.js +++ b/server/utils/chats/embed.js @@ -1,7 +1,7 @@ const { v4: uuidv4 } = require("uuid"); const { getVectorDbClass, getLLMProvider } = require("../helpers"); const { chatPrompt, convertToPromptHistory } = require("."); -const { writeResponseChunk, handleStreamResponses } = require("./stream"); +const { writeResponseChunk } = require("./stream"); const { EmbedChats } = require("../../models/embedChats"); async function streamChatWithForEmbed( @@ -150,7 +150,7 @@ async function streamChatWithForEmbed( const stream = await LLMConnector.streamGetChatCompletion(messages, { temperature: embed.workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); - completeText = await handleStreamResponses(response, stream, { + completeText = await LLMConnector.handleStream(response, stream, { uuid, sources: [], }); @@ -227,7 +227,7 @@ async function streamEmptyEmbeddingChat({ embed.workspace, rawHistory ); - completeText = await handleStreamResponses(response, stream, { + completeText = await LLMConnector.handleStream(response, stream, { uuid, sources: [], }); diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 0ee448a5ee..d16f6e6029 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -156,7 +156,7 @@ async function streamChatWithWorkspace( const stream = await LLMConnector.streamGetChatCompletion(messages, { temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp, }); - completeText = await handleStreamResponses(response, stream, { + completeText = await LLMConnector.handleStream(response, stream, { uuid, sources, }); @@ -214,7 +214,7 @@ async function streamEmptyEmbeddingChat({ workspace, rawHistory ); - completeText = await handleStreamResponses(response, stream, { + completeText = await LLMConnector.handleStream(response, stream, { uuid, sources: [], }); @@ -229,301 +229,10 @@ async function streamEmptyEmbeddingChat({ return; } -// TODO: Refactor this implementation -function handleStreamResponses(response, stream, responseProps) { +// The default way to handle a stream response. Functions best with OpenAI. +function handleDefaultStreamResponse(response, stream, responseProps) { const { uuid = uuidv4(), sources = [] } = responseProps; - // Gemini likes to return a stream asyncIterator which will - // be a totally different object than other models. - if (stream?.type === "geminiStream") { - return new Promise(async (resolve) => { - let fullText = ""; - for await (const chunk of stream.stream) { - fullText += chunk.text(); - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: chunk.text(), - close: false, - error: false, - }); - } - - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - }); - } - - if (stream?.type === "azureStream") { - return new Promise(async (resolve) => { - let fullText = ""; - for await (const event of stream.stream) { - for (const choice of event.choices) { - const delta = choice.delta?.content; - if (!delta) continue; - fullText += delta; - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: delta, - close: false, - error: false, - }); - } - } - - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - }); - } - - if (stream.type === "togetherAiStream") { - return new Promise((resolve) => { - let fullText = ""; - let chunk = ""; - stream.stream.data.on("data", (data) => { - const lines = data - ?.toString() - ?.split("\n") - .filter((line) => line.trim() !== ""); - - for (const line of lines) { - let validJSON = false; - const message = chunk + line.replace(/^data: /, ""); - - if (message !== "[DONE]") { - // JSON chunk is incomplete and has not ended yet - // so we need to stitch it together. You would think JSON - // chunks would only come complete - but they don't! - try { - JSON.parse(message); - validJSON = true; - } catch {} - - if (!validJSON) { - // It can be possible that the chunk decoding is running away - // and the message chunk fails to append due to string length. - // In this case abort the chunk and reset so we can continue. - // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416 - try { - chunk += message; - } catch (e) { - console.error(`Chunk appending error`, e); - chunk = ""; - } - continue; - } else { - chunk = ""; - } - } - - if (message == "[DONE]") { - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - } else { - let finishReason = null; - let token = ""; - try { - const json = JSON.parse(message); - token = json?.choices?.[0]?.delta?.content; - finishReason = json?.choices?.[0]?.finish_reason || null; - } catch { - continue; - } - - if (token) { - fullText += token; - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: token, - close: false, - error: false, - }); - } - - if (finishReason !== null) { - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - } - } - } - }); - }); - } - - if (stream.type === "huggingFaceStream") { - return new Promise((resolve) => { - let fullText = ""; - let chunk = ""; - stream.stream.data.on("data", (data) => { - const lines = data - ?.toString() - ?.split("\n") - .filter((line) => line.trim() !== ""); - - for (const line of lines) { - let validJSON = false; - const message = chunk + line.replace(/^data:/, ""); - if (message !== "[DONE]") { - // JSON chunk is incomplete and has not ended yet - // so we need to stitch it together. You would think JSON - // chunks would only come complete - but they don't! - try { - JSON.parse(message); - validJSON = true; - } catch { - console.log("Failed to parse message", message); - } - - if (!validJSON) { - // It can be possible that the chunk decoding is running away - // and the message chunk fails to append due to string length. - // In this case abort the chunk and reset so we can continue. - // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416 - try { - chunk += message; - } catch (e) { - console.error(`Chunk appending error`, e); - chunk = ""; - } - continue; - } else { - chunk = ""; - } - } - - if (message == "[DONE]") { - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - } else { - let error = null; - let finishReason = null; - let token = ""; - try { - const json = JSON.parse(message); - error = json?.error || null; - token = json?.choices?.[0]?.delta?.content; - finishReason = json?.choices?.[0]?.finish_reason || null; - } catch { - continue; - } - - if (!!error) { - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: null, - close: true, - error, - }); - resolve(""); - return; - } - - if (token) { - fullText += token; - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: token, - close: false, - error: false, - }); - } - - if (finishReason !== null) { - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - } - } - } - }); - }); - } - - // If stream is not a regular OpenAI Stream (like if using native model, Ollama, or most LangChain interfaces) - // we can just iterate the stream content instead. - if (!stream.hasOwnProperty("data")) { - return new Promise(async (resolve) => { - let fullText = ""; - for await (const chunk of stream) { - if (chunk === undefined) - throw new Error( - "Stream returned undefined chunk. Aborting reply - check model provider logs." - ); - - const content = chunk.hasOwnProperty("content") ? chunk.content : chunk; - fullText += content; - writeResponseChunk(response, { - uuid, - sources: [], - type: "textResponseChunk", - textResponse: content, - close: false, - error: false, - }); - } - - writeResponseChunk(response, { - uuid, - sources, - type: "textResponseChunk", - textResponse: "", - close: true, - error: false, - }); - resolve(fullText); - }); - } - return new Promise((resolve) => { let fullText = ""; let chunk = ""; @@ -615,5 +324,5 @@ module.exports = { VALID_CHAT_MODE, streamChatWithWorkspace, writeResponseChunk, - handleStreamResponses, + handleDefaultStreamResponse, }; From f402ef4c3c409141a13afc8bed7d810e36661000 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 7 Feb 2024 08:17:48 -0800 Subject: [PATCH 063/298] [CHORE] Hide security tab on Settings Sidebar/Refactor SettingsSidebar component (#683) * hide security setting in settings sidebar when in multiusermode * refactor sidebar to handle mobile view inside SettingsSidebar component * forgot eventlogs on sidebar move footer on settingSidebar to component --------- Co-authored-by: timothycarambat --- .../src/components/SettingsSidebar/index.jsx | 653 +++++++----------- .../src/pages/Admin/Invitations/index.jsx | 5 +- frontend/src/pages/Admin/Logging/index.jsx | 5 +- frontend/src/pages/Admin/System/index.jsx | 5 +- frontend/src/pages/Admin/Users/index.jsx | 5 +- frontend/src/pages/Admin/Workspaces/index.jsx | 5 +- .../pages/GeneralSettings/ApiKeys/index.jsx | 5 +- .../GeneralSettings/Appearance/index.jsx | 6 +- .../src/pages/GeneralSettings/Chats/index.jsx | 5 +- .../Connectors/Github/index.jsx | 5 +- .../Connectors/Youtube/index.jsx | 5 +- .../GeneralSettings/DataConnectors/index.jsx | 5 +- .../GeneralSettings/EmbedChats/index.jsx | 5 +- .../GeneralSettings/EmbedConfigs/index.jsx | 5 +- .../EmbeddingPreference/index.jsx | 5 +- .../GeneralSettings/LLMPreference/index.jsx | 5 +- .../pages/GeneralSettings/Security/index.jsx | 5 +- .../GeneralSettings/VectorDatabase/index.jsx | 5 +- 18 files changed, 291 insertions(+), 448 deletions(-) diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 5fe81363c8..13c4abeabe 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -25,11 +25,104 @@ import { } from "@phosphor-icons/react"; import useUser from "@/hooks/useUser"; import { USER_BACKGROUND_COLOR } from "@/utils/constants"; +import { isMobile } from "react-device-detect"; export default function SettingsSidebar() { const { logo } = useLogo(); - const sidebarRef = useRef(null); const { user } = useUser(); + const sidebarRef = useRef(null); + const [showSidebar, setShowSidebar] = useState(false); + const [showBgOverlay, setShowBgOverlay] = useState(false); + + useEffect(() => { + function handleBg() { + if (showSidebar) { + setTimeout(() => { + setShowBgOverlay(true); + }, 300); + } else { + setShowBgOverlay(false); + } + } + handleBg(); + }, [showSidebar]); + + if (isMobile) { + return ( + <> +
+ +
+ Logo +
+
+
+
+
setShowSidebar(false)} + /> +
+
+ {/* Header Information */} +
+
+ Logo +
+
+ + + +
+
+ + {/* Primary Body */} +
+
+
+ +
+
+
+
+
+
+
+
+
+ + ); + } return ( <> @@ -62,162 +155,15 @@ export default function SettingsSidebar() { Settings
{/* Primary Body */} -
+
-
-
- {/* Footer */} -
-
- - - - - - - - - - {/* */} -
-
+
@@ -226,254 +172,38 @@ export default function SettingsSidebar() { ); } -export function SidebarMobileHeader() { - const { logo } = useLogo(); - const { user } = useUser(); - const sidebarRef = useRef(null); - const [showSidebar, setShowSidebar] = useState(false); - const [showBgOverlay, setShowBgOverlay] = useState(false); - - useEffect(() => { - function handleBg() { - if (showSidebar) { - setTimeout(() => { - setShowBgOverlay(true); - }, 300); - } else { - setShowBgOverlay(false); - } - } - handleBg(); - }, [showSidebar]); - +const Footer = () => { return ( - <> -
- -
- Logo -
-
-
-
-
setShowSidebar(false)} - /> -
+ + -
- {/* Header Information */} - - - {/* Primary Body */} -
-
-
-
-
-
- {/* Footer */} -
-
- - - - - - - - - - {/* */} -
-
-
-
-
-
+ + + + + + {/* */}
- +
); -} +}; const Option = ({ btnText, @@ -484,7 +214,10 @@ const Option = ({ user = null, allowedRole = [], subOptions = null, + hidden = false, }) => { + if (hidden) return null; + const hasActiveChild = childLinks.includes(window.location.pathname); const isActive = window.location.pathname === href; @@ -493,6 +226,7 @@ const Option = ({ // Option is dual-mode, but user exists, we need to check permissions if (flex && !!user && !allowedRole.includes(user?.role)) return null; + return ( <>
@@ -526,3 +260,130 @@ const Option = ({ ); }; + +const SidebarOptions = ({ user = null }) => ( + <> +