diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83792da7..58c42b62 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,7 @@ // Terraform support "ghcr.io/devcontainers/features/terraform:1": {}, // Just a wrap to install needed packages - "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "ghcr.io/devcontainers-contrib/features/apt-packages:1": { // Dependencies copied from ../docker/Dockerfile plus some dev stuff "packages": [ "build-essential", diff --git a/.prettierignore b/.prettierignore index faedf325..e3b0c14e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,7 @@ frontend/bundleinspector.html #server server/swagger/openapi.json + +#embed +**/static/** +embed/src/utils/chat/hljs.js diff --git a/.prettierrc b/.prettierrc index 3574c1df..5e2bccfe 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,7 +17,7 @@ } }, { - "files": "*.config.js", + "files": ["*.config.js"], "options": { "semi": false, "parser": "flow", diff --git a/README.md b/README.md index dfedb4f9..bc3e9fdd 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@
- English · 简体中文 + English · 简体中文 · 日本語
@@ -123,7 +123,7 @@ Some cool features of AnythingLLM - [Pinecone](https://pinecone.io) - [Chroma](https://trychroma.com) - [Weaviate](https://weaviate.io) -- [QDrant](https://qdrant.tech) +- [Qdrant](https://qdrant.tech) - [Milvus](https://milvus.io) - [Zilliz](https://zilliz.com) diff --git a/collector/package.json b/collector/package.json index 785604e3..938d65e1 100644 --- a/collector/package.json +++ b/collector/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "NODE_ENV=development nodemon --ignore hotdir --ignore storage --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./processSingleFile ./processLink ./utils index.js" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./processSingleFile ./processLink ./utils index.js" }, "dependencies": { "@googleapis/youtube": "^9.0.0", diff --git a/collector/utils/extensions/GithubRepo/RepoLoader/index.js b/collector/utils/extensions/GithubRepo/RepoLoader/index.js index dbe26fa2..c842f621 100644 --- a/collector/utils/extensions/GithubRepo/RepoLoader/index.js +++ b/collector/utils/extensions/GithubRepo/RepoLoader/index.js @@ -14,7 +14,11 @@ class RepoLoader { #validGithubUrl() { const UrlPattern = require("url-pattern"); const pattern = new UrlPattern( - "https\\://github.com/(:author)/(:project(*))" + "https\\://github.com/(:author)/(:project(*))", + { + // fixes project names with special characters (.github) + segmentValueCharset: "a-zA-Z0-9-._~%/+", + } ); const match = pattern.match(this.repo); if (!match) return false; diff --git a/collector/utils/files/mime.js b/collector/utils/files/mime.js index 635a6aa3..6cd88f82 100644 --- a/collector/utils/files/mime.js +++ b/collector/utils/files/mime.js @@ -23,6 +23,7 @@ class MimeDetector { { "text/plain": [ "ts", + "tsx", "py", "opts", "lock", @@ -35,6 +36,7 @@ class MimeDetector { "js", "lua", "pas", + "r", ], }, true diff --git a/docker/.env.example b/docker/.env.example index 6368a190..174a9d69 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -128,6 +128,12 @@ GID='1000' # VOYAGEAI_API_KEY= # EMBEDDING_MODEL_PREF='voyage-large-2-instruct' +# EMBEDDING_ENGINE='litellm' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' +# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# LITE_LLM_BASE_PATH='http://127.0.0.1:4000' +# LITE_LLM_API_KEY='sk-123abc' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/embed/.prettierignore b/embed/.prettierignore deleted file mode 100644 index d90a3c08..00000000 --- a/embed/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -# defaults -**/.git -**/.svn -**/.hg -**/node_modules - -**/dist -**/static/** -src/utils/chat/hljs.js diff --git a/embed/jsconfig.json b/embed/jsconfig.json index c8cc81fd..20cd368c 100644 --- a/embed/jsconfig.json +++ b/embed/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], - } - } -} \ No newline at end of file + "@/*": ["./src/*"], + }, + }, +} diff --git a/embed/package.json b/embed/package.json index eb399930..712af8e6 100644 --- a/embed/package.json +++ b/embed/package.json @@ -1,6 +1,7 @@ { "name": "anythingllm-embedded-chat", "private": false, + "license": "MIT", "type": "module", "scripts": { "dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"", @@ -8,7 +9,7 @@ "dev:build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js", "build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js && npx terser --compress -o dist/anythingllm-chat-widget.min.js -- dist/anythingllm-chat-widget.js", "build:publish": "yarn build && mkdir -p ../frontend/public/embed && cp -r dist/anythingllm-chat-widget.min.js ../frontend/public/embed/anythingllm-chat-widget.min.js", - "lint": "yarn prettier --write ./src" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src" }, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", diff --git a/embed/vite.config.js b/embed/vite.config.js index 21506422..9e23c70d 100644 --- a/embed/vite.config.js +++ b/embed/vite.config.js @@ -38,7 +38,7 @@ export default defineConfig({ rollupOptions: { external: [ // Reduces transformation time by 50% and we don't even use this variant, so we can ignore. - /@phosphor-icons\/react\/dist\/ssr/, + /@phosphor-icons\/react\/dist\/ssr/ ] }, commonjsOptions: { @@ -51,7 +51,7 @@ export default defineConfig({ emptyOutDir: true, inlineDynamicImports: true, assetsDir: "", - sourcemap: 'inline', + sourcemap: "inline" }, optimizeDeps: { esbuildOptions: { @@ -60,5 +60,5 @@ export default defineConfig({ }, plugins: [] } - }, + } }) diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index c8cc81fd..e21fc376 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], + "@/*": ["./src/*"] } } -} \ No newline at end of file +} diff --git a/frontend/package.json b/frontend/package.json index 11e612fc..8aa4dcfa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "start": "vite --open", "dev": "NODE_ENV=development vite --debug --host=0.0.0.0", "build": "vite build", - "lint": "yarn prettier --write ./src", + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src", "preview": "vite preview" }, "dependencies": { @@ -63,4 +63,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} +} \ No newline at end of file diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx index 8d311883..c5a1f190 100644 --- a/frontend/src/components/ChatBubble/index.jsx +++ b/frontend/src/components/ChatBubble/index.jsx @@ -1,5 +1,5 @@ import React from "react"; -import Jazzicon from "../UserIcon"; +import UserIcon from "../UserIcon"; import { userFromStorage } from "@/utils/request"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -11,8 +11,7 @@ export default function ChatBubble({ message, type, popMsg }) {
+ Be sure to select a valid embedding model. Chat models are not + embedding models. See{" "} + + this page + {" "} + for more information. +
+
+ AnythingLLM: あなたが探していたオールインワンAIアプリ。
+ ドキュメントとチャットし、AIエージェントを使用し、高度にカスタマイズ可能で、複数ユーザー対応、面倒な設定は不要です。
+
+ + + | + + + | + + ドキュメント + | + + ホストされたインスタンス + +
+ + + ++👉 デスクトップ用AnythingLLM(Mac、Windows、Linux対応)!今すぐダウンロード +
+ +これは、任意のドキュメント、リソース、またはコンテンツの断片を、チャット中にLLMが参照として使用できるコンテキストに変換できるフルスタックアプリケーションです。このアプリケーションを使用すると、使用するLLMまたはベクトルデータベースを選択し、マルチユーザー管理と権限をサポートできます。 + +![チャット](https://github.com/Mintplex-Labs/anything-llm/assets/16845892/cfc5f47c-bd91-4067-986c-f3f49621a859) + +- English · 简体中文 + English · 简体中文 · 简体中文
diff --git a/server/.env.example b/server/.env.example index f51d6177..6148d594 100644 --- a/server/.env.example +++ b/server/.env.example @@ -125,6 +125,12 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # VOYAGEAI_API_KEY= # EMBEDDING_MODEL_PREF='voyage-large-2-instruct' +# EMBEDDING_ENGINE='litellm' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' +# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# LITE_LLM_BASE_PATH='http://127.0.0.1:4000' +# LITE_LLM_API_KEY='sk-123abc' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js index c8e5e06c..7fdb92cc 100644 --- a/server/endpoints/api/system/index.js +++ b/server/endpoints/api/system/index.js @@ -206,6 +206,72 @@ function apiSystemEndpoints(app) { } } ); + app.delete( + "/v1/system/remove-documents", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['System Settings'] + #swagger.description = 'Permanently remove documents from the system.' + #swagger.requestBody = { + description: 'Array of document names to be removed permanently.', + required: true, + content: { + "application/json": { + schema: { + type: 'object', + properties: { + names: { + type: 'array', + items: { + type: 'string' + }, + example: [ + "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json" + ] + } + } + } + } + } + } + #swagger.responses[200] = { + description: 'Documents removed successfully.', + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + message: 'Documents removed successfully' + } + } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[500] = { + description: 'Internal Server Error' + } + */ + try { + const { names } = reqBody(request); + for await (const name of names) await purgeDocument(name); + response + .status(200) + .json({ success: true, message: "Documents removed successfully" }) + .end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); } module.exports = { apiSystemEndpoints }; diff --git a/server/endpoints/workspaceThreads.js b/server/endpoints/workspaceThreads.js index e2aead97..1c207e52 100644 --- a/server/endpoints/workspaceThreads.js +++ b/server/endpoints/workspaceThreads.js @@ -1,4 +1,9 @@ -const { multiUserMode, userFromSession, reqBody } = require("../utils/http"); +const { + multiUserMode, + userFromSession, + reqBody, + safeJsonParse, +} = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); const { @@ -168,6 +173,77 @@ function workspaceThreadEndpoints(app) { } } ); + + app.delete( + "/workspace/:slug/thread/:threadSlug/delete-edited-chats", + [ + validatedRequest, + flexUserRoleValid([ROLES.all]), + validWorkspaceAndThreadSlug, + ], + async (request, response) => { + try { + const { startingId } = reqBody(request); + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; + const thread = response.locals.thread; + + await WorkspaceChats.delete({ + workspaceId: Number(workspace.id), + thread_id: Number(thread.id), + user_id: user?.id, + id: { gte: Number(startingId) }, + }); + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/workspace/:slug/thread/:threadSlug/update-chat", + [ + validatedRequest, + flexUserRoleValid([ROLES.all]), + validWorkspaceAndThreadSlug, + ], + async (request, response) => { + try { + const { chatId, newText = null } = reqBody(request); + if (!newText || !String(newText).trim()) + throw new Error("Cannot save empty response"); + + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; + const thread = response.locals.thread; + const existingChat = await WorkspaceChats.get({ + workspaceId: workspace.id, + thread_id: thread.id, + user_id: user?.id, + id: Number(chatId), + }); + if (!existingChat) throw new Error("Invalid chat."); + + const chatResponse = safeJsonParse(existingChat.response, null); + if (!chatResponse) throw new Error("Failed to parse chat response"); + + await WorkspaceChats._update(existingChat.id, { + response: JSON.stringify({ + ...chatResponse, + text: String(newText), + }), + }); + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); } module.exports = { workspaceThreadEndpoints }; diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 2657eb97..6d6f29bb 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -380,7 +380,6 @@ function workspaceEndpoints(app) { const history = multiUserMode(response) ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id) : await WorkspaceChats.forWorkspace(workspace.id); - response.status(200).json({ history: convertToChatHistory(history) }); } catch (e) { console.log(e.message, e); @@ -420,6 +419,67 @@ function workspaceEndpoints(app) { } ); + app.delete( + "/workspace/:slug/delete-edited-chats", + [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + async (request, response) => { + try { + const { startingId } = reqBody(request); + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; + + await WorkspaceChats.delete({ + workspaceId: workspace.id, + thread_id: null, + user_id: user?.id, + id: { gte: Number(startingId) }, + }); + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/workspace/:slug/update-chat", + [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + async (request, response) => { + try { + const { chatId, newText = null } = reqBody(request); + if (!newText || !String(newText).trim()) + throw new Error("Cannot save empty response"); + + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; + const existingChat = await WorkspaceChats.get({ + workspaceId: workspace.id, + thread_id: null, + user_id: user?.id, + id: Number(chatId), + }); + if (!existingChat) throw new Error("Invalid chat."); + + const chatResponse = safeJsonParse(existingChat.response, null); + if (!chatResponse) throw new Error("Failed to parse chat response"); + + await WorkspaceChats._update(existingChat.id, { + response: JSON.stringify({ + ...chatResponse, + text: String(newText), + }), + }); + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + app.post( "/workspace/:slug/chat-feedback/:chatId", [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index c81992ca..bda40064 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -220,6 +220,24 @@ const WorkspaceChats = { console.error(error.message); } }, + + // Explicit update of settings + key validations. + // Only use this method when directly setting a key value + // that takes no user input for the keys being modified. + _update: async function (id = null, data = {}) { + if (!id) throw new Error("No workspace chat id provided for update"); + + try { + await prisma.workspace_chats.update({ + where: { id }, + data, + }); + return true; + } catch (error) { + console.error(error.message); + return false; + } + }, }; module.exports = { WorkspaceChats }; diff --git a/server/package.json b/server/package.json index 4f995470..1b0ba280 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./endpoints ./models ./utils index.js", + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./endpoints ./models ./utils index.js", "swagger": "node ./swagger/init.js", "sqlite:migrate": "cd ./utils/prisma && node migrateFromSqlite.js" }, @@ -32,7 +32,7 @@ "@langchain/textsplitters": "0.0.0", "@pinecone-database/pinecone": "^2.0.1", "@prisma/client": "5.3.1", - "@qdrant/js-client-rest": "^1.4.0", + "@qdrant/js-client-rest": "^1.9.0", "@xenova/transformers": "^2.14.0", "@zilliz/milvus2-sdk-node": "^2.3.5", "archiver": "^5.3.1", diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index ed6f1533..230f0ce6 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -2241,6 +2241,71 @@ } } } + }, + "/v1/system/remove-documents": { + "delete": { + "tags": [ + "System Settings" + ], + "description": "Permanently remove documents from the system.", + "parameters": [], + "responses": { + "200": { + "description": "Documents removed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "message": "Documents removed successfully" + } + } + } + } + }, + "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": "Array of document names to be removed permanently.", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json" + ] + } + } + } + } + } + } + } } }, "components": { diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 30c9ffa3..ef184580 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -91,6 +91,10 @@ class GeminiLLM { switch (this.model) { case "gemini-pro": return 30_720; + case "gemini-1.0-pro": + return 30_720; + case "gemini-1.5-flash-latest": + return 1_048_576; case "gemini-1.5-pro-latest": return 1_048_576; default: @@ -101,6 +105,7 @@ class GeminiLLM { isValidChatCompletionModel(modelName = "") { const validModels = [ "gemini-pro", + "gemini-1.0-pro", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", ]; diff --git a/server/utils/EmbeddingEngines/liteLLM/index.js b/server/utils/EmbeddingEngines/liteLLM/index.js new file mode 100644 index 00000000..cd22480b --- /dev/null +++ b/server/utils/EmbeddingEngines/liteLLM/index.js @@ -0,0 +1,93 @@ +const { toChunks, maximumChunkLength } = require("../../helpers"); + +class LiteLLMEmbedder { + constructor() { + const { OpenAI: OpenAIApi } = require("openai"); + if (!process.env.LITE_LLM_BASE_PATH) + throw new Error( + "LiteLLM must have a valid base path to use for the api." + ); + this.basePath = process.env.LITE_LLM_BASE_PATH; + this.openai = new OpenAIApi({ + baseURL: this.basePath, + apiKey: process.env.LITE_LLM_API_KEY ?? null, + }); + 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; + this.embeddingMaxChunkLength = maximumChunkLength(); + } + + async embedTextInput(textInput) { + const result = await this.embedChunks( + Array.isArray(textInput) ? textInput : [textInput] + ); + return result?.[0] || []; + } + + async embedChunks(textChunks = []) { + // Because there is a hard POST limit on how many chunks can be sent at once to LiteLLM (~8mb) + // we concurrently execute each max batch of text chunks possible. + // Refer to constructor maxConcurrentChunks for more info. + const embeddingRequests = []; + for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { + embeddingRequests.push( + new Promise((resolve) => { + this.openai.embeddings + .create({ + model: this.model, + input: chunk, + }) + .then((result) => { + resolve({ data: result?.data, error: null }); + }) + .catch((e) => { + 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 }); + }); + }) + ); + } + + const { data = [], error = null } = await Promise.all( + embeddingRequests + ).then((results) => { + // If any errors were returned from LiteLLM abort the entire sequence because the embeddings + // will be incomplete. + const errors = results + .filter((res) => !!res.error) + .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: Array.from(uniqueErrors).join(", "), + }; + } + return { + data: results.map((res) => res?.data || []).flat(), + error: null, + }; + }); + + if (!!error) throw new Error(`LiteLLM Failed to embed: ${error}`); + return data.length > 0 && + data.every((embd) => embd.hasOwnProperty("embedding")) + ? data.map((embd) => embd.embedding) + : null; + } +} + +module.exports = { + LiteLLMEmbedder, +}; diff --git a/server/utils/helpers/chat/responses.js b/server/utils/helpers/chat/responses.js index d07eae30..609b1819 100644 --- a/server/utils/helpers/chat/responses.js +++ b/server/utils/helpers/chat/responses.js @@ -174,6 +174,7 @@ function convertToChatHistory(history = []) { role: "user", content: prompt, sentAt: moment(createdAt).unix(), + chatId: id, }, { type: data?.type || "chart", diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index e60202a6..8f0df126 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -128,6 +128,9 @@ function getEmbeddingEngineSelection() { case "voyageai": const { VoyageAiEmbedder } = require("../EmbeddingEngines/voyageAi"); return new VoyageAiEmbedder(); + case "litellm": + const { LiteLLMEmbedder } = require("../EmbeddingEngines/liteLLM"); + return new LiteLLMEmbedder(); default: return new NativeEmbedder(); } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index d6900ae5..1a0e710a 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -532,6 +532,7 @@ function supportedTranscriptionProvider(input = "") { function validGeminiModel(input = "") { const validModels = [ "gemini-pro", + "gemini-1.0-pro", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", ]; @@ -576,6 +577,7 @@ function supportedEmbeddingModel(input = "") { "lmstudio", "cohere", "voyageai", + "litellm", ]; return supported.includes(input) ? null diff --git a/server/utils/vectorDbProviders/chroma/index.js b/server/utils/vectorDbProviders/chroma/index.js index 90956a94..5bea32bf 100644 --- a/server/utils/vectorDbProviders/chroma/index.js +++ b/server/utils/vectorDbProviders/chroma/index.js @@ -6,9 +6,55 @@ const { v4: uuidv4 } = require("uuid"); const { toChunks, getEmbeddingEngineSelection } = require("../../helpers"); const { parseAuthHeader } = require("../../http"); const { sourceIdentifier } = require("../../chats"); +const COLLECTION_REGEX = new RegExp( + /^(?!\d+\.\d+\.\d+\.\d+$)(?!.*\.\.)(?=^[a-zA-Z0-9][a-zA-Z0-9_-]{1,61}[a-zA-Z0-9]$).{3,63}$/ +); const Chroma = { name: "Chroma", + // Chroma DB has specific requirements for collection names: + // (1) Must contain 3-63 characters + // (2) Must start and end with an alphanumeric character + // (3) Can only contain alphanumeric characters, underscores, or hyphens + // (4) Cannot contain two consecutive periods (..) + // (5) Cannot be a valid IPv4 address + // We need to enforce these rules by normalizing the collection names + // before communicating with the Chroma DB. + normalize: function (inputString) { + if (COLLECTION_REGEX.test(inputString)) return inputString; + let normalized = inputString.replace(/[^a-zA-Z0-9_-]/g, "-"); + + // Replace consecutive periods with a single period (if any) + normalized = normalized.replace(/\.\.+/g, "."); + + // Ensure the name doesn't start with a non-alphanumeric character + if (normalized[0] && !/^[a-zA-Z0-9]$/.test(normalized[0])) { + normalized = "anythingllm-" + normalized.slice(1); + } + + // Ensure the name doesn't end with a non-alphanumeric character + if ( + normalized[normalized.length - 1] && + !/^[a-zA-Z0-9]$/.test(normalized[normalized.length - 1]) + ) { + normalized = normalized.slice(0, -1); + } + + // Ensure the length is between 3 and 63 characters + if (normalized.length < 3) { + normalized = `anythingllm-${normalized}`; + } else if (normalized.length > 63) { + // Recheck the norm'd name if sliced since its ending can still be invalid. + normalized = this.normalize(normalized.slice(0, 63)); + } + + // Ensure the name is not an IPv4 address + if (/^\d+\.\d+\.\d+\.\d+$/.test(normalized)) { + normalized = "-" + normalized.slice(1); + } + + return normalized; + }, connect: async function () { if (process.env.VECTOR_DB !== "chroma") throw new Error("Chroma::Invalid ENV settings"); @@ -59,7 +105,7 @@ const Chroma = { }, namespaceCount: async function (_namespace = null) { const { client } = await this.connect(); - const namespace = await this.namespace(client, _namespace); + const namespace = await this.namespace(client, this.normalize(_namespace)); return namespace?.vectorCount || 0; }, similarityResponse: async function ( @@ -70,7 +116,9 @@ const Chroma = { topN = 4, filterIdentifiers = [] ) { - const collection = await client.getCollection({ name: namespace }); + const collection = await client.getCollection({ + name: this.normalize(namespace), + }); const result = { contextTexts: [], sourceDocuments: [], @@ -106,7 +154,7 @@ const Chroma = { namespace: async function (client, namespace = null) { if (!namespace) throw new Error("No namespace value provided."); const collection = await client - .getCollection({ name: namespace }) + .getCollection({ name: this.normalize(namespace) }) .catch(() => null); if (!collection) return null; @@ -118,12 +166,12 @@ const Chroma = { hasNamespace: async function (namespace = null) { if (!namespace) return false; const { client } = await this.connect(); - return await this.namespaceExists(client, namespace); + return await this.namespaceExists(client, this.normalize(namespace)); }, namespaceExists: async function (client, namespace = null) { if (!namespace) throw new Error("No namespace value provided."); const collection = await client - .getCollection({ name: namespace }) + .getCollection({ name: this.normalize(namespace) }) .catch((e) => { console.error("ChromaDB::namespaceExists", e.message); return null; @@ -131,7 +179,7 @@ const Chroma = { return !!collection; }, deleteVectorsInNamespace: async function (client, namespace = null) { - await client.deleteCollection({ name: namespace }); + await client.deleteCollection({ name: this.normalize(namespace) }); return true; }, addDocumentToNamespace: async function ( @@ -149,7 +197,7 @@ const Chroma = { if (cacheResult.exists) { const { client } = await this.connect(); const collection = await client.getOrCreateCollection({ - name: namespace, + name: this.normalize(namespace), metadata: { "hnsw:space": "cosine" }, }); const { chunks } = cacheResult; @@ -245,7 +293,7 @@ const Chroma = { const { client } = await this.connect(); const collection = await client.getOrCreateCollection({ - name: namespace, + name: this.normalize(namespace), metadata: { "hnsw:space": "cosine" }, }); @@ -274,7 +322,7 @@ const Chroma = { const { client } = await this.connect(); if (!(await this.namespaceExists(client, namespace))) return; const collection = await client.getCollection({ - name: namespace, + name: this.normalize(namespace), }); const knownDocuments = await DocumentVectors.where({ docId }); @@ -299,7 +347,7 @@ const Chroma = { throw new Error("Invalid request to performSimilaritySearch."); const { client } = await this.connect(); - if (!(await this.namespaceExists(client, namespace))) { + if (!(await this.namespaceExists(client, this.normalize(namespace)))) { return { contextTexts: [], sources: [], @@ -330,9 +378,9 @@ const Chroma = { const { namespace = null } = reqBody; if (!namespace) throw new Error("namespace required"); const { client } = await this.connect(); - if (!(await this.namespaceExists(client, namespace))) + if (!(await this.namespaceExists(client, this.normalize(namespace)))) throw new Error("Namespace by that name does not exist."); - const stats = await this.namespace(client, namespace); + const stats = await this.namespace(client, this.normalize(namespace)); return stats ? stats : { message: "No stats were able to be fetched from DB for namespace" }; @@ -340,11 +388,11 @@ const Chroma = { "delete-namespace": async function (reqBody = {}) { const { namespace = null } = reqBody; const { client } = await this.connect(); - if (!(await this.namespaceExists(client, namespace))) + if (!(await this.namespaceExists(client, this.normalize(namespace)))) throw new Error("Namespace by that name does not exist."); - const details = await this.namespace(client, namespace); - await this.deleteVectorsInNamespace(client, namespace); + const details = await this.namespace(client, this.normalize(namespace)); + await this.deleteVectorsInNamespace(client, this.normalize(namespace)); return { message: `Namespace ${namespace} was deleted along with ${details?.vectorCount} vectors.`, }; diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js index ff55c06f..77945915 100644 --- a/server/utils/vectorDbProviders/qdrant/index.js +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -95,7 +95,7 @@ const QDrant = { return { name: namespace, ...collection, - vectorCount: collection.vectors_count, + vectorCount: (await client.count(namespace, { exact: true })).count, }; }, hasNamespace: async function (namespace = null) { diff --git a/server/yarn.lock b/server/yarn.lock index d274e574..c6cf4c2c 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1036,7 +1036,7 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@qdrant/js-client-rest@^1.4.0": +"@qdrant/js-client-rest@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@qdrant/js-client-rest/-/js-client-rest-1.9.0.tgz#deef8acb520f47f9db1c1517758ccf88c12e69fe" integrity sha512-YiX/IskbRCoAY2ujyPDI6FBcO0ygAS4pgkGaJ7DcrJFh4SZV2XHs+u0KM7mO72RWJn1eJQFF2PQwxG+401xxJg==