diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83792da78..58c42b62d 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 faedf3258..e3b0c14e0 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 3574c1dfd..5e2bccfe4 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 dfedb4f95..bc3e9fdd8 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 785604e38..938d65e15 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 dbe26fa29..c842f621b 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 635a6aa32..6cd88f82e 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 23789af45..a38b4c5a2 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 ######## ########################################### @@ -232,4 +238,10 @@ GID='1000' # AGENT_GSE_CTX= #------ Serper.dev ----------- https://serper.dev/ -# AGENT_SERPER_DEV_KEY= \ No newline at end of file +# AGENT_SERPER_DEV_KEY= + +#------ Bing Search ----------- https://portal.azure.com/ +# AGENT_BING_SEARCH_API_KEY= + +#------ Serply.io ----------- https://serply.io/ +# AGENT_SERPLY_API_KEY= diff --git a/docker/HOW_TO_USE_DOCKER.md b/docker/HOW_TO_USE_DOCKER.md index 19a0920ef..f570dce90 100644 --- a/docker/HOW_TO_USE_DOCKER.md +++ b/docker/HOW_TO_USE_DOCKER.md @@ -86,6 +86,49 @@ mintplexlabs/anythingllm; +
+ Be sure to select a valid embedding model. Chat models are not + embedding models. See{" "} + + this page + {" "} + for more information. +
+- AnythingLLM + {customAppName || "AnythingLLM"}
- Sign in to your AnythingLLM account. + Sign in to your {customAppName || "AnythingLLM"} account.
- Sign in to your AnythingLLM instance. + Sign in to your {customAppName || "AnythingLLM"} instance.
- New Workspace -
-+ New Workspace +
++ You can get a Bing Web Search API subscription key{" "} + + from the Azure portal. + +
++ To set up a Bing Web Search API subscription: +
++ You can get a free API key{" "} + + from Serply.io. + +
+
+ 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 e38250beb..a88a8a039 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 ########
###########################################
@@ -228,4 +234,10 @@ TTS_PROVIDER="native"
# AGENT_GSE_CTX=
#------ Serper.dev ----------- https://serper.dev/
-# AGENT_SERPER_DEV_KEY=
\ No newline at end of file
+# AGENT_SERPER_DEV_KEY=
+
+#------ Bing Search ----------- https://portal.azure.com/
+# AGENT_BING_SEARCH_API_KEY=
+
+#------ Serply.io ----------- https://serply.io/
+# AGENT_SERPLY_API_KEY=
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 9b836b19a..59d645447 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -355,6 +355,9 @@ function adminEndpoints(app) {
?.value,
[]
) || [],
+ custom_app_name:
+ (await SystemSettings.get({ label: "custom_app_name" }))?.value ||
+ null,
};
response.status(200).json({ settings });
} catch (e) {
diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js
index c8e5e06cc..d12941623 100644
--- a/server/endpoints/api/system/index.js
+++ b/server/endpoints/api/system/index.js
@@ -1,5 +1,6 @@
const { EventLogs } = require("../../../models/eventLogs");
const { SystemSettings } = require("../../../models/systemSettings");
+const { purgeDocument } = require("../../../utils/files/purgeDocument");
const { getVectorDbClass } = require("../../../utils/helpers");
const {
prepareWorkspaceChatsForExport,
@@ -206,6 +207,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/chat.js b/server/endpoints/chat.js
index 7445c2134..c7e70265f 100644
--- a/server/endpoints/chat.js
+++ b/server/endpoints/chat.js
@@ -15,6 +15,8 @@ const {
validWorkspaceSlug,
} = require("../utils/middleware/validWorkspace");
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
+const { WorkspaceThread } = require("../models/workspaceThread");
+const truncate = require("truncate");
function chatEndpoints(app) {
if (!app) return;
@@ -196,6 +198,24 @@ function chatEndpoints(app) {
user,
thread
);
+
+ // If thread was renamed emit event to frontend via special `action` response.
+ await WorkspaceThread.autoRenameThread({
+ thread,
+ workspace,
+ user,
+ newName: truncate(message, 22),
+ onRename: (thread) => {
+ writeResponseChunk(response, {
+ action: "rename_thread",
+ thread: {
+ slug: thread.slug,
+ name: thread.name,
+ },
+ });
+ },
+ });
+
await Telemetry.sendTelemetry("sent_chat", {
multiUserMode: multiUserMode(response),
LLMSelection: process.env.LLM_PROVIDER || "openai",
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 0030ed68c..cc8ca8b58 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -531,17 +531,24 @@ function systemEndpoints(app) {
const defaultFilename = getDefaultFilename();
const logoPath = await determineLogoFilepath(defaultFilename);
const { found, buffer, size, mime } = fetchLogo(logoPath);
+
if (!found) {
response.sendStatus(204).end();
return;
}
+ const currentLogoFilename = await SystemSettings.currentLogoFilename();
response.writeHead(200, {
+ "Access-Control-Expose-Headers":
+ "Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length",
"Content-Type": mime || "image/png",
"Content-Disposition": `attachment; filename=${path.basename(
logoPath
)}`,
"Content-Length": size,
+ "X-Is-Custom-Logo":
+ currentLogoFilename !== null &&
+ currentLogoFilename !== defaultFilename,
});
response.end(Buffer.from(buffer, "base64"));
return;
@@ -578,6 +585,22 @@ function systemEndpoints(app) {
}
});
+ // No middleware protection in order to get this on the login page
+ app.get("/system/custom-app-name", async (_, response) => {
+ try {
+ const customAppName =
+ (
+ await SystemSettings.get({
+ label: "custom_app_name",
+ })
+ )?.value ?? null;
+ response.status(200).json({ customAppName: customAppName });
+ } catch (error) {
+ console.error("Error fetching custom app name:", error);
+ response.status(500).json({ message: "Internal server error" });
+ }
+ });
+
app.get(
"/system/pfp/:id",
[validatedRequest, flexUserRoleValid([ROLES.all])],
diff --git a/server/endpoints/workspaceThreads.js b/server/endpoints/workspaceThreads.js
index e2aead974..1c207e523 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 2657eb976..6d6f29bbd 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/systemSettings.js b/server/models/systemSettings.js
index dd5b10817..679cb9fb4 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -32,6 +32,7 @@ const SystemSettings = {
"agent_search_provider",
"default_agent_skills",
"agent_sql_connections",
+ "custom_app_name",
],
validations: {
footer_data: (updates) => {
@@ -74,7 +75,14 @@ const SystemSettings = {
agent_search_provider: (update) => {
try {
if (update === "none") return null;
- if (!["google-search-engine", "serper-dot-dev"].includes(update))
+ if (
+ ![
+ "google-search-engine",
+ "serper-dot-dev",
+ "bing-search",
+ "serply-engine",
+ ].includes(update)
+ )
throw new Error("Invalid SERP provider.");
return String(update);
} catch (e) {
@@ -175,6 +183,8 @@ const SystemSettings = {
AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null,
AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null,
AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null,
+ AgentBingSearchApiKey: process.env.AGENT_BING_SEARCH_API_KEY || null,
+ AgentSerplyApiKey: process.env.AGENT_SERPLY_API_KEY || null,
};
},
diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js
index c81992caa..bda40064d 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/models/workspaceThread.js b/server/models/workspaceThread.js
index a2a96f310..32e9f89b6 100644
--- a/server/models/workspaceThread.js
+++ b/server/models/workspaceThread.js
@@ -2,13 +2,14 @@ const prisma = require("../utils/prisma");
const { v4: uuidv4 } = require("uuid");
const WorkspaceThread = {
+ defaultName: "Thread",
writable: ["name"],
new: async function (workspace, userId = null) {
try {
const thread = await prisma.workspace_threads.create({
data: {
- name: "New thread",
+ name: this.defaultName,
slug: uuidv4(),
user_id: userId ? Number(userId) : null,
workspace_id: workspace.id,
@@ -84,6 +85,32 @@ const WorkspaceThread = {
return [];
}
},
+
+ // Will fire on first message (included or not) for a thread and rename the thread with the newName prop.
+ autoRenameThread: async function ({
+ workspace = null,
+ thread = null,
+ user = null,
+ newName = null,
+ onRename = null,
+ }) {
+ if (!workspace || !thread || !newName) return false;
+ if (thread.name !== this.defaultName) return false; // don't rename if already named.
+
+ const { WorkspaceChats } = require("./workspaceChats");
+ const chatCount = await WorkspaceChats.count({
+ workspaceId: workspace.id,
+ user_id: user?.id || null,
+ thread_id: thread.id,
+ });
+ if (chatCount !== 1) return { renamed: false, thread };
+ const { thread: updatedThread } = await this.update(thread, {
+ name: newName,
+ });
+
+ onRename?.(updatedThread);
+ return true;
+ },
};
module.exports = { WorkspaceThread };
diff --git a/server/package.json b/server/package.json
index 4f9954700..9cc27c8b0 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",
@@ -75,6 +75,7 @@
"sqlite3": "^5.1.6",
"swagger-autogen": "^2.23.5",
"swagger-ui-express": "^5.0.0",
+ "truncate": "^3.0.0",
"url-pattern": "^1.0.3",
"uuid": "^9.0.0",
"uuid-apikey": "^1.5.3",
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index ed6f15337..230f0ce66 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 30c9ffa35..ef1845801 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 000000000..cd22480b1
--- /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/agents/aibitat/index.js b/server/utils/agents/aibitat/index.js
index fa490edb2..0fe6eb510 100644
--- a/server/utils/agents/aibitat/index.js
+++ b/server/utils/agents/aibitat/index.js
@@ -41,6 +41,7 @@ class AIbitat {
...rest,
};
this.provider = this.defaultProvider.provider;
+ this.model = this.defaultProvider.model;
}
/**
diff --git a/server/utils/agents/aibitat/plugins/summarize.js b/server/utils/agents/aibitat/plugins/summarize.js
index de1657c9f..bd491f960 100644
--- a/server/utils/agents/aibitat/plugins/summarize.js
+++ b/server/utils/agents/aibitat/plugins/summarize.js
@@ -154,11 +154,12 @@ const docSummarizer = {
this.controller.abort();
});
- return await summarizeContent(
- this.super.provider,
- this.controller.signal,
- document.content
- );
+ return await summarizeContent({
+ provider: this.super.provider,
+ model: this.super.model,
+ controllerSignal: this.controller.signal,
+ content: document.content,
+ });
} catch (error) {
this.super.handlerProps.log(
`document-summarizer.summarizeDoc raised an error. ${error.message}`
diff --git a/server/utils/agents/aibitat/plugins/web-browsing.js b/server/utils/agents/aibitat/plugins/web-browsing.js
index 198b3ec55..81314f178 100644
--- a/server/utils/agents/aibitat/plugins/web-browsing.js
+++ b/server/utils/agents/aibitat/plugins/web-browsing.js
@@ -65,6 +65,12 @@ const webBrowsing = {
case "serper-dot-dev":
engine = "_serperDotDev";
break;
+ case "bing-search":
+ engine = "_bingWebSearch";
+ break;
+ case "serply-engine":
+ engine = "_serplyEngine";
+ break;
default:
engine = "_googleSearchEngine";
}
@@ -168,6 +174,123 @@ const webBrowsing = {
});
});
+ if (data.length === 0)
+ return `No information was found online for the search query.`;
+ return JSON.stringify(data);
+ },
+ _bingWebSearch: async function (query) {
+ if (!process.env.AGENT_BING_SEARCH_API_KEY) {
+ this.super.introspect(
+ `${this.caller}: I can't use Bing Web Search because the user has not defined the required API key.\nVisit: https://portal.azure.com/ to create the API key.`
+ );
+ return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
+ }
+
+ const searchURL = new URL(
+ "https://api.bing.microsoft.com/v7.0/search"
+ );
+ searchURL.searchParams.append("q", query);
+
+ this.super.introspect(
+ `${this.caller}: Using Bing Web Search to search for "${
+ query.length > 100 ? `${query.slice(0, 100)}...` : query
+ }"`
+ );
+
+ const searchResponse = await fetch(searchURL, {
+ headers: {
+ "Ocp-Apim-Subscription-Key":
+ process.env.AGENT_BING_SEARCH_API_KEY,
+ },
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ const searchResults = data.webPages?.value || [];
+ return searchResults.map((result) => ({
+ title: result.name,
+ link: result.url,
+ snippet: result.snippet,
+ }));
+ })
+ .catch((e) => {
+ console.log(e);
+ return [];
+ });
+
+ if (searchResponse.length === 0)
+ return `No information was found online for the search query.`;
+ return JSON.stringify(searchResponse);
+ },
+ _serplyEngine: async function (
+ query,
+ language = "en",
+ hl = "us",
+ limit = 100,
+ device_type = "desktop",
+ proxy_location = "US"
+ ) {
+ // query (str): The query to search for
+ // hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
+ // limit (int): The maximum number of results to return [10-100, defaults to 100]
+ // device_type: get results based on desktop/mobile (defaults to desktop)
+
+ if (!process.env.AGENT_SERPLY_API_KEY) {
+ this.super.introspect(
+ `${this.caller}: I can't use Serply.io searching because the user has not defined the required API key.\nVisit: https://serply.io to create the API key for free.`
+ );
+ return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
+ }
+
+ this.super.introspect(
+ `${this.caller}: Using Serply to search for "${
+ query.length > 100 ? `${query.slice(0, 100)}...` : query
+ }"`
+ );
+
+ const params = new URLSearchParams({
+ q: query,
+ language: language,
+ hl,
+ gl: proxy_location.toUpperCase(),
+ });
+ const url = `https://api.serply.io/v1/search/${params.toString()}`;
+ const { response, error } = await fetch(url, {
+ method: "GET",
+ headers: {
+ "X-API-KEY": process.env.AGENT_SERPLY_API_KEY,
+ "Content-Type": "application/json",
+ "User-Agent": "anything-llm",
+ "X-Proxy-Location": proxy_location,
+ "X-User-Agent": device_type,
+ },
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ if (data?.message === "Unauthorized") {
+ return {
+ response: null,
+ error:
+ "Unauthorized. Please double check your AGENT_SERPLY_API_KEY",
+ };
+ }
+ return { response: data, error: null };
+ })
+ .catch((e) => {
+ return { response: null, error: e.message };
+ });
+ if (error)
+ return `There was an error searching for content. ${error}`;
+
+ const data = [];
+ response.results?.forEach((searchResult) => {
+ const { title, link, description } = searchResult;
+ data.push({
+ title,
+ link,
+ snippet: description,
+ });
+ });
+
if (data.length === 0)
return `No information was found online for the search query.`;
return JSON.stringify(data);
diff --git a/server/utils/agents/aibitat/plugins/web-scraping.js b/server/utils/agents/aibitat/plugins/web-scraping.js
index f5c8d41f5..2ca69ec96 100644
--- a/server/utils/agents/aibitat/plugins/web-scraping.js
+++ b/server/utils/agents/aibitat/plugins/web-scraping.js
@@ -90,11 +90,13 @@ const webScraping = {
);
this.controller.abort();
});
- return summarizeContent(
- this.super.provider,
- this.controller.signal,
- content
- );
+
+ return summarizeContent({
+ provider: this.super.provider,
+ model: this.super.model,
+ controllerSignal: this.controller.signal,
+ content,
+ });
},
});
},
diff --git a/server/utils/agents/aibitat/providers/ai-provider.js b/server/utils/agents/aibitat/providers/ai-provider.js
index 91a81ebfa..b3a8b1791 100644
--- a/server/utils/agents/aibitat/providers/ai-provider.js
+++ b/server/utils/agents/aibitat/providers/ai-provider.js
@@ -2,8 +2,19 @@
* A service that provides an AI client to create a completion.
*/
+/**
+ * @typedef {Object} LangChainModelConfig
+ * @property {(string|null)} baseURL - Override the default base URL process.env for this provider
+ * @property {(string|null)} apiKey - Override the default process.env for this provider
+ * @property {(number|null)} temperature - Override the default temperature
+ * @property {(string|null)} model - Overrides model used for provider.
+ */
+
const { ChatOpenAI } = require("@langchain/openai");
const { ChatAnthropic } = require("@langchain/anthropic");
+const { ChatOllama } = require("@langchain/community/chat_models/ollama");
+const { toValidNumber } = require("../../../http");
+
const DEFAULT_WORKSPACE_PROMPT =
"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.";
@@ -27,8 +38,15 @@ class Provider {
return this._client;
}
+ /**
+ *
+ * @param {string} provider - the string key of the provider LLM being loaded.
+ * @param {LangChainModelConfig} config - Config to be used to override default connection object.
+ * @returns
+ */
static LangChainChatModel(provider = "openai", config = {}) {
switch (provider) {
+ // Cloud models
case "openai":
return new ChatOpenAI({
apiKey: process.env.OPEN_AI_KEY,
@@ -39,11 +57,108 @@ class Provider {
apiKey: process.env.ANTHROPIC_API_KEY,
...config,
});
- default:
+ case "groq":
return new ChatOpenAI({
- apiKey: process.env.OPEN_AI_KEY,
+ configuration: {
+ baseURL: "https://api.groq.com/openai/v1",
+ },
+ apiKey: process.env.GROQ_API_KEY,
...config,
});
+ case "mistral":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: "https://api.mistral.ai/v1",
+ },
+ apiKey: process.env.MISTRAL_API_KEY ?? null,
+ ...config,
+ });
+ case "openrouter":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: "https://openrouter.ai/api/v1",
+ defaultHeaders: {
+ "HTTP-Referer": "https://useanything.com",
+ "X-Title": "AnythingLLM",
+ },
+ },
+ apiKey: process.env.OPENROUTER_API_KEY ?? null,
+ ...config,
+ });
+ case "perplexity":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: "https://api.perplexity.ai",
+ },
+ apiKey: process.env.PERPLEXITY_API_KEY ?? null,
+ ...config,
+ });
+ case "togetherai":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: "https://api.together.xyz/v1",
+ },
+ apiKey: process.env.TOGETHER_AI_API_KEY ?? null,
+ ...config,
+ });
+ case "generic-openai":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: process.env.GENERIC_OPEN_AI_BASE_PATH,
+ },
+ apiKey: process.env.GENERIC_OPEN_AI_API_KEY,
+ maxTokens: toValidNumber(
+ process.env.GENERIC_OPEN_AI_MAX_TOKENS,
+ 1024
+ ),
+ ...config,
+ });
+
+ // OSS Model Runners
+ // case "anythingllm_ollama":
+ // return new ChatOllama({
+ // baseUrl: process.env.PLACEHOLDER,
+ // ...config,
+ // });
+ case "ollama":
+ return new ChatOllama({
+ baseUrl: process.env.OLLAMA_BASE_PATH,
+ ...config,
+ });
+ case "lmstudio":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: process.env.LMSTUDIO_BASE_PATH?.replace(/\/+$/, ""),
+ },
+ apiKey: "not-used", // Needs to be specified or else will assume OpenAI
+ ...config,
+ });
+ case "koboldcpp":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: process.env.KOBOLD_CPP_BASE_PATH,
+ },
+ apiKey: "not-used",
+ ...config,
+ });
+ case "localai":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: process.env.LOCAL_AI_BASE_PATH,
+ },
+ apiKey: process.env.LOCAL_AI_API_KEY ?? "not-used",
+ ...config,
+ });
+ case "textgenwebui":
+ return new ChatOpenAI({
+ configuration: {
+ baseURL: process.env.TEXT_GEN_WEB_UI_BASE_PATH,
+ },
+ apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? "not-used",
+ ...config,
+ });
+ default:
+ throw new Error(`Unsupported provider ${provider} for this task.`);
}
}
diff --git a/server/utils/agents/aibitat/providers/groq.js b/server/utils/agents/aibitat/providers/groq.js
index 01f69f7c1..9ca99065d 100644
--- a/server/utils/agents/aibitat/providers/groq.js
+++ b/server/utils/agents/aibitat/providers/groq.js
@@ -1,28 +1,52 @@
const OpenAI = require("openai");
const Provider = require("./ai-provider.js");
-const { RetryError } = require("../error.js");
+const InheritMultiple = require("./helpers/classes.js");
+const UnTooled = require("./helpers/untooled.js");
/**
- * The agent provider for the Groq provider.
- * Using OpenAI tool calling with groq really sucks right now
- * its just fast and bad. We should probably migrate this to Untooled to improve
- * coherence.
+ * The agent provider for the GroqAI provider.
+ * We wrap Groq in UnTooled because its tool-calling built in is quite bad and wasteful.
*/
-class GroqProvider extends Provider {
+class GroqProvider extends InheritMultiple([Provider, UnTooled]) {
model;
constructor(config = {}) {
const { model = "llama3-8b-8192" } = config;
+ super();
const client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: process.env.GROQ_API_KEY,
maxRetries: 3,
});
- super(client);
+
+ this._client = client;
this.model = model;
this.verbose = true;
}
+ get client() {
+ return this._client;
+ }
+
+ async #handleFunctionCallChat({ messages = [] }) {
+ return await this.client.chat.completions
+ .create({
+ model: this.model,
+ temperature: 0,
+ messages,
+ })
+ .then((result) => {
+ if (!result.hasOwnProperty("choices"))
+ throw new Error("GroqAI chat: No results!");
+ if (result.choices.length === 0)
+ throw new Error("GroqAI chat: No results length!");
+ return result.choices[0].message.content;
+ })
+ .catch((_) => {
+ return null;
+ });
+ }
+
/**
* Create a completion based on the received messages.
*
@@ -32,68 +56,49 @@ class GroqProvider extends Provider {
*/
async complete(messages, functions = null) {
try {
- const response = await this.client.chat.completions.create({
- model: this.model,
- // stream: true,
- messages,
- ...(Array.isArray(functions) && functions?.length > 0
- ? { functions }
- : {}),
- });
+ let completion;
+ if (functions.length > 0) {
+ const { toolCall, text } = await this.functionCall(
+ messages,
+ functions,
+ this.#handleFunctionCallChat.bind(this)
+ );
- // Right now, we only support one completion,
- // so we just take the first one in the list
- const completion = response.choices[0].message;
- const cost = this.getCost(response.usage);
- // treat function calls
- if (completion.function_call) {
- let functionArgs = {};
- try {
- functionArgs = JSON.parse(completion.function_call.arguments);
- } catch (error) {
- // call the complete function again in case it gets a json error
- return this.complete(
- [
- ...messages,
- {
- role: "function",
- name: completion.function_call.name,
- function_call: completion.function_call,
- content: error?.message,
- },
- ],
- functions
- );
+ if (toolCall !== null) {
+ this.providerLog(`Valid tool call found - running ${toolCall.name}.`);
+ this.deduplicator.trackRun(toolCall.name, toolCall.arguments);
+ return {
+ result: null,
+ functionCall: {
+ name: toolCall.name,
+ arguments: toolCall.arguments,
+ },
+ cost: 0,
+ };
}
-
- // console.log(completion, { functionArgs })
- return {
- result: null,
- functionCall: {
- name: completion.function_call.name,
- arguments: functionArgs,
- },
- cost,
- };
+ completion = { content: text };
}
+ if (!completion?.content) {
+ this.providerLog(
+ "Will assume chat completion without tool call inputs."
+ );
+ const response = await this.client.chat.completions.create({
+ model: this.model,
+ messages: this.cleanMsgs(messages),
+ });
+ completion = response.choices[0].message;
+ }
+
+ // The UnTooled class inherited Deduplicator is mostly useful to prevent the agent
+ // from calling the exact same function over and over in a loop within a single chat exchange
+ // _but_ we should enable it to call previously used tools in a new chat interaction.
+ this.deduplicator.reset("runs");
return {
result: completion.content,
- cost,
+ cost: 0,
};
} catch (error) {
- // If invalid Auth error we need to abort because no amount of waiting
- // will make auth better.
- if (error instanceof OpenAI.AuthenticationError) throw error;
-
- if (
- error instanceof OpenAI.RateLimitError ||
- error instanceof OpenAI.InternalServerError ||
- error instanceof OpenAI.APIError // Also will catch AuthenticationError!!!
- ) {
- throw new RetryError(error.message);
- }
-
throw error;
}
}
@@ -103,7 +108,7 @@ class GroqProvider extends Provider {
*
* @param _usage The completion to get the cost for.
* @returns The cost of the completion.
- * Stubbed since Groq has no cost basis.
+ * Stubbed since LMStudio has no cost basis.
*/
getCost(_usage) {
return 0;
diff --git a/server/utils/agents/aibitat/utils/summarize.js b/server/utils/agents/aibitat/utils/summarize.js
index 7f1852c02..fbee20533 100644
--- a/server/utils/agents/aibitat/utils/summarize.js
+++ b/server/utils/agents/aibitat/utils/summarize.js
@@ -3,26 +3,27 @@ const { PromptTemplate } = require("@langchain/core/prompts");
const { RecursiveCharacterTextSplitter } = require("@langchain/textsplitters");
const Provider = require("../providers/ai-provider");
/**
- * Summarize content using OpenAI's GPT-3.5 model.
- *
- * @param self The context of the caller function
- * @param content The content to summarize.
- * @returns The summarized content.
+ * @typedef {Object} LCSummarizationConfig
+ * @property {string} provider The LLM to use for summarization (inherited)
+ * @property {string} model The LLM Model to use for summarization (inherited)
+ * @property {AbortController['signal']} controllerSignal Abort controller to stop recursive summarization
+ * @property {string} content The text content of the text to summarize
*/
-const SUMMARY_MODEL = {
- anthropic: "claude-3-opus-20240229", // 200,000 tokens
- openai: "gpt-4o", // 128,000 tokens
-};
-
-async function summarizeContent(
+/**
+ * Summarize content using LLM LC-Chain call
+ * @param {LCSummarizationConfig} The LLM to use for summarization (inherited)
+ * @returns {Promise