From b69bf7cc275d05211a8bb7ab05db271a0660519b Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Wed, 1 May 2024 16:13:20 -0700 Subject: [PATCH 01/13] Clearing of events does not reload anymore updating workspace name does not result in reload anymore set event log page size to 10 fix css issues with charts --- .../ChatHistory/Chartable/index.jsx | 6 +- frontend/src/pages/Admin/Logging/index.jsx | 72 +++++++++++-------- .../GeneralAppearance/index.jsx | 1 - server/endpoints/system.js | 2 +- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx index 8217fe95b..6a6e6b130 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx @@ -107,7 +107,7 @@ export function Chartable({ props, workspace }) { ); case "line": return ( -
+

{title}

-
+
{renderChart()}
-
+
{renderChart()}
diff --git a/frontend/src/pages/Admin/Logging/index.jsx b/frontend/src/pages/Admin/Logging/index.jsx index 69a81ab56..498247849 100644 --- a/frontend/src/pages/Admin/Logging/index.jsx +++ b/frontend/src/pages/Admin/Logging/index.jsx @@ -9,6 +9,22 @@ import showToast from "@/utils/toast"; import CTAButton from "@/components/lib/CTAButton"; export default function AdminLogs() { + 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); + + useEffect(() => { + async function fetchLogs() { + const { logs: _logs, hasPages = false } = await System.eventLogs(offset); + setLogs(_logs); + setCanNext(hasPages); + setLoading(false); + } + fetchLogs(); + }, [offset]); + const handleResetLogs = async () => { if ( !window.confirm( @@ -19,13 +35,22 @@ export default function AdminLogs() { const { success, error } = await System.clearEventLogs(); if (success) { showToast("Event logs cleared successfully.", "success"); - setTimeout(() => { - window.location.reload(); - }, 1000); + setLogs([]); + setCanNext(false); + setOffset(0); } else { showToast(`Failed to clear logs: ${error}`, "error"); } }; + + const handlePrevious = () => { + setOffset(Math.max(offset - 1, 0)); + }; + + const handleNext = () => { + setOffset(offset + 1); + }; + return (
@@ -53,37 +78,28 @@ export default function AdminLogs() { Clear Event Logs
- +
); } -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]); - +function LogsContainer({ + loading, + logs, + offset, + canNext, + handleNext, + handlePrevious, +}) { if (loading) { return ( window.location.reload(), 1_500); } else { showToast(`Error: ${message}`, "error", { clear: true }); } diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 3ea7fb24c..60d51e35f 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -913,7 +913,7 @@ function systemEndpoints(app) { [validatedRequest, flexUserRoleValid([ROLES.admin])], async (request, response) => { try { - const { offset = 0, limit = 20 } = reqBody(request); + const { offset = 0, limit = 10 } = reqBody(request); const logs = await EventLogs.whereWithData({}, limit, offset * limit, { id: "desc", }); From 43e54a19dcebbc223ac5d203bcb500c1a3054d0f Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 1 May 2024 16:22:10 -0700 Subject: [PATCH 02/13] [FEAT] Workspace settings gear icon UX improvement (#1254) take user back to workspace chat from gear icon --- .../src/components/Sidebar/ActiveWorkspaces/index.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx index 0a867e889..cf059b2fb 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx @@ -150,9 +150,13 @@ export default function ActiveWorkspaces() { handleGearMouseEnter(workspace.id)} onMouseLeave={() => handleGearMouseLeave(workspace.id)} className="rounded-md flex items-center justify-center text-[#A7A8A9] hover:text-white ml-auto" From a156a1e58c9c3190305a4abfd6f5dee29e0b425a Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 1 May 2024 16:26:14 -0700 Subject: [PATCH 03/13] [FEAT] Remove custom logo onboarding screen (#1252) remove custom logo onboarding screen --- .../OnboardingFlow/Steps/CustomLogo/index.jsx | 140 ------------------ .../Steps/LLMPreference/index.jsx | 2 +- .../OnboardingFlow/Steps/UserSetup/index.jsx | 2 +- .../src/pages/OnboardingFlow/Steps/index.jsx | 2 - frontend/src/utils/paths.js | 3 - 5 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx diff --git a/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx deleted file mode 100644 index 6a79f854a..000000000 --- a/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import useLogo from "@/hooks/useLogo"; -import System from "@/models/system"; -import showToast from "@/utils/toast"; -import { Plus } from "@phosphor-icons/react"; -import React, { useState, useEffect } from "react"; -import AnythingLLM from "@/media/logo/anything-llm.png"; -import paths from "@/utils/paths"; -import { useNavigate } from "react-router-dom"; - -const TITLE = "Custom Logo"; -const DESCRIPTION = - "Upload your custom logo to make your chatbot yours. Optional."; - -export default function CustomLogo({ setHeader, setForwardBtn, setBackBtn }) { - const navigate = useNavigate(); - function handleForward() { - navigate(paths.onboarding.userSetup()); - } - - function handleBack() { - navigate(paths.onboarding.llmPreference()); - } - - useEffect(() => { - setHeader({ title: TITLE, description: DESCRIPTION }); - setForwardBtn({ showing: true, disabled: false, onClick: handleForward }); - setBackBtn({ showing: true, disabled: false, onClick: handleBack }); - }, []); - - const { logo: _initLogo, setLogo: _setLogo } = useLogo(); - const [logo, setLogo] = useState(""); - const [isDefaultLogo, setIsDefaultLogo] = useState(true); - - useEffect(() => { - async function logoInit() { - setLogo(_initLogo || ""); - const _isDefaultLogo = await System.isDefaultLogo(); - setIsDefaultLogo(_isDefaultLogo); - } - logoInit(); - }, [_initLogo]); - - const handleFileUpload = async (event) => { - const file = event.target.files[0]; - if (!file) return false; - - const objectURL = URL.createObjectURL(file); - setLogo(objectURL); - - const formData = new FormData(); - formData.append("logo", file); - const { success, error } = await System.uploadLogo(formData); - if (!success) { - showToast(`Failed to upload logo: ${error}`, "error"); - setLogo(_initLogo); - return; - } - - const logoURL = await System.fetchLogo(); - _setLogo(logoURL); - setIsDefaultLogo(false); - }; - - const handleRemoveLogo = async () => { - setLogo(""); - setIsDefaultLogo(true); - - const { success, error } = await System.removeCustomLogo(); - if (!success) { - console.error("Failed to remove logo:", error); - showToast(`Failed to remove logo: ${error}`, "error"); - const logoURL = await System.fetchLogo(); - setLogo(logoURL); - setIsDefaultLogo(false); - return; - } - - const logoURL = await System.fetchLogo(); - _setLogo(logoURL); - }; - - return ( -
-
- {isDefaultLogo ? ( - - ) : ( -
- (e.target.src = AnythingLLM)} - /> -
- )} - {!isDefaultLogo ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index b9e0f5bb1..0b756fc2c 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -203,7 +203,7 @@ export default function LLMPreference({ showToast(`Failed to save LLM settings: ${error}`, "error"); return; } - navigate(paths.onboarding.customLogo()); + navigate(paths.onboarding.userSetup()); }; useEffect(() => { diff --git a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx index 2e619e395..6cc41428a 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx @@ -29,7 +29,7 @@ export default function UserSetup({ setHeader, setForwardBtn, setBackBtn }) { } function handleBack() { - navigate(paths.onboarding.customLogo()); + navigate(paths.onboarding.llmPreference()); } useEffect(() => { diff --git a/frontend/src/pages/OnboardingFlow/Steps/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/index.jsx index f223c0268..903395a77 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/index.jsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { isMobile } from "react-device-detect"; import Home from "./Home"; import LLMPreference from "./LLMPreference"; -import CustomLogo from "./CustomLogo"; import UserSetup from "./UserSetup"; import DataHandling from "./DataHandling"; import Survey from "./Survey"; @@ -12,7 +11,6 @@ import CreateWorkspace from "./CreateWorkspace"; const OnboardingSteps = { home: Home, "llm-preference": LLMPreference, - "custom-logo": CustomLogo, "user-setup": UserSetup, "data-handling": DataHandling, survey: Survey, diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 339ecf439..4dc4d5285 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -23,9 +23,6 @@ export default { vectorDatabase: () => { return "/onboarding/vector-database"; }, - customLogo: () => { - return "/onboarding/custom-logo"; - }, userSetup: () => { return "/onboarding/user-setup"; }, From 9feaad79cc69c826001b36d0e129da403a695d23 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Wed, 1 May 2024 16:52:28 -0700 Subject: [PATCH 04/13] [CHORE] Remove sendChat and streamChat in all LLM providers (#1260) * remove sendChat and streamChat functions/references in all LLM providers * remove unused imports --------- Co-authored-by: timothycarambat --- server/utils/AiProviders/anthropic/index.js | 28 +-- server/utils/AiProviders/azureOpenAi/index.js | 63 +----- server/utils/AiProviders/gemini/index.js | 53 +---- .../utils/AiProviders/genericOpenAi/index.js | 52 +---- server/utils/AiProviders/groq/index.js | 62 +----- server/utils/AiProviders/huggingface/index.js | 52 +---- server/utils/AiProviders/lmStudio/index.js | 62 +----- server/utils/AiProviders/localAi/index.js | 62 +----- server/utils/AiProviders/mistral/index.js | 61 +----- server/utils/AiProviders/native/index.js | 42 +--- server/utils/AiProviders/ollama/index.js | 50 +---- server/utils/AiProviders/openAi/index.js | 62 +----- server/utils/AiProviders/openRouter/index.js | 200 +----------------- server/utils/AiProviders/perplexity/index.js | 62 +----- server/utils/AiProviders/togetherAi/index.js | 62 +----- 15 files changed, 15 insertions(+), 958 deletions(-) diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 6a8ad3c42..d5ee1f9d3 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -1,5 +1,4 @@ const { v4 } = require("uuid"); -const { chatPrompt } = require("../../chats"); const { writeResponseChunk, clientAbortedHandler, @@ -33,7 +32,7 @@ class AnthropicLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -110,31 +109,6 @@ class AnthropicLLM { } } - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.isValidChatCompletionModel(this.model)) - throw new Error( - `Anthropic chat: ${this.model} is not valid for chat completion!` - ); - - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - - const streamRequest = await this.anthropic.messages.stream({ - model: this.model, - max_tokens: 4096, - system: messages[0].content, // Strip out the system message - messages: messages.slice(1), // Pop off the system message - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - }); - return streamRequest; - } - async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { if (!this.isValidChatCompletionModel(this.model)) throw new Error( diff --git a/server/utils/AiProviders/azureOpenAi/index.js b/server/utils/AiProviders/azureOpenAi/index.js index 21fc5cd91..a2ab556db 100644 --- a/server/utils/AiProviders/azureOpenAi/index.js +++ b/server/utils/AiProviders/azureOpenAi/index.js @@ -1,5 +1,4 @@ const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi"); -const { chatPrompt } = require("../../chats"); const { writeResponseChunk, clientAbortedHandler, @@ -45,7 +44,7 @@ class AzureOpenAiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Sure the user selected a proper value for the token limit @@ -82,66 +81,6 @@ class AzureOpenAiLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.model) - throw new Error( - "No OPEN_MODEL_PREF ENV defined. This must the name of a deployment on your Azure account for an LLM chat model like GPT-3.5." - ); - - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - const textResponse = await this.openai - .getChatCompletions(this.model, messages, { - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - }) - .then((res) => { - if (!res.hasOwnProperty("choices")) - throw new Error("AzureOpenAI chat: No results!"); - if (res.choices.length === 0) - throw new Error("AzureOpenAI chat: No results length!"); - return res.choices[0].message.content; - }) - .catch((error) => { - console.log(error); - throw new Error( - `AzureOpenAI::getChatCompletions failed with: ${error.message}` - ); - }); - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.model) - throw new Error( - "No OPEN_MODEL_PREF ENV defined. This must the name of a deployment on your Azure account for an LLM chat model like GPT-3.5." - ); - - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - const stream = await this.openai.streamChatCompletions( - this.model, - messages, - { - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - } - ); - return stream; - } - async getChatCompletion(messages = [], { temperature = 0.7 }) { if (!this.model) throw new Error( diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js index 354c1899e..b9eb26c3c 100644 --- a/server/utils/AiProviders/gemini/index.js +++ b/server/utils/AiProviders/gemini/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { writeResponseChunk, clientAbortedHandler, @@ -48,7 +47,7 @@ class GeminiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -118,32 +117,6 @@ class GeminiLLM { return allMessages; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.isValidChatCompletionModel(this.model)) - throw new Error( - `Gemini chat: ${this.model} is not valid for chat completion!` - ); - - const compressedHistory = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - chatHistory, - }, - rawHistory - ); - - const chatThread = this.gemini.startChat({ - history: this.formatMessages(compressedHistory), - }); - const result = await chatThread.sendMessage(prompt); - const response = result.response; - const responseText = response.text(); - - if (!responseText) throw new Error("Gemini: No response could be parsed."); - - return responseText; - } - async getChatCompletion(messages = [], _opts = {}) { if (!this.isValidChatCompletionModel(this.model)) throw new Error( @@ -165,30 +138,6 @@ class GeminiLLM { return responseText; } - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.isValidChatCompletionModel(this.model)) - throw new Error( - `Gemini chat: ${this.model} is not valid for chat completion!` - ); - - const compressedHistory = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - chatHistory, - }, - rawHistory - ); - - const chatThread = this.gemini.startChat({ - history: this.formatMessages(compressedHistory), - }); - const responseStream = await chatThread.sendMessageStream(prompt); - if (!responseStream.stream) - throw new Error("Could not stream response stream from Gemini."); - - return responseStream.stream; - } - async streamGetChatCompletion(messages = [], _opts = {}) { if (!this.isValidChatCompletionModel(this.model)) throw new Error( diff --git a/server/utils/AiProviders/genericOpenAi/index.js b/server/utils/AiProviders/genericOpenAi/index.js index cf293c3e7..8c171b679 100644 --- a/server/utils/AiProviders/genericOpenAi/index.js +++ b/server/utils/AiProviders/genericOpenAi/index.js @@ -1,5 +1,4 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -53,7 +52,7 @@ class GenericOpenAiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -89,55 +88,6 @@ class GenericOpenAiLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("GenericOpenAI chat: No results!"); - if (result.choices.length === 0) - throw new Error("GenericOpenAI chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `GenericOpenAI::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { const result = await this.openai.chat.completions .create({ diff --git a/server/utils/AiProviders/groq/index.js b/server/utils/AiProviders/groq/index.js index add064af4..01d92f006 100644 --- a/server/utils/AiProviders/groq/index.js +++ b/server/utils/AiProviders/groq/index.js @@ -1,5 +1,4 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -38,7 +37,7 @@ class GroqLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -91,65 +90,6 @@ class GroqLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `Groq chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .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((error) => { - throw new Error( - `GroqAI::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `GroqAI:streamChat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( diff --git a/server/utils/AiProviders/huggingface/index.js b/server/utils/AiProviders/huggingface/index.js index 22f9c2fd4..6a79880c8 100644 --- a/server/utils/AiProviders/huggingface/index.js +++ b/server/utils/AiProviders/huggingface/index.js @@ -1,6 +1,5 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -48,7 +47,7 @@ class HuggingFaceLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -90,55 +89,6 @@ class HuggingFaceLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("HuggingFace chat: No results!"); - if (result.choices.length === 0) - throw new Error("HuggingFace chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `HuggingFace::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { const result = await this.openai.createChatCompletion({ model: this.model, diff --git a/server/utils/AiProviders/lmStudio/index.js b/server/utils/AiProviders/lmStudio/index.js index 98cbbcaa5..48f689fbc 100644 --- a/server/utils/AiProviders/lmStudio/index.js +++ b/server/utils/AiProviders/lmStudio/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -49,7 +48,7 @@ class LMStudioLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -85,65 +84,6 @@ class LMStudioLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.model) - throw new Error( - `LMStudio chat: ${this.model} is not valid or defined for chat completion!` - ); - - const textResponse = await this.lmstudio.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("LMStudio chat: No results!"); - if (result.choices.length === 0) - throw new Error("LMStudio chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `LMStudio::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!this.model) - throw new Error( - `LMStudio chat: ${this.model} is not valid or defined for chat completion!` - ); - - const streamRequest = await this.lmstudio.chat.completions.create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - stream: true, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!this.model) throw new Error( diff --git a/server/utils/AiProviders/localAi/index.js b/server/utils/AiProviders/localAi/index.js index 4a8921af8..504775285 100644 --- a/server/utils/AiProviders/localAi/index.js +++ b/server/utils/AiProviders/localAi/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -41,7 +40,7 @@ class LocalAiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -75,65 +74,6 @@ class LocalAiLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `LocalAI chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("LocalAI chat: No results!"); - if (result.choices.length === 0) - throw new Error("LocalAI chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `LocalAI::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `LocalAI chat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( diff --git a/server/utils/AiProviders/mistral/index.js b/server/utils/AiProviders/mistral/index.js index 7b60f3fed..8410d4cb6 100644 --- a/server/utils/AiProviders/mistral/index.js +++ b/server/utils/AiProviders/mistral/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -42,7 +41,7 @@ class MistralLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -70,64 +69,6 @@ class MistralLLM { 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.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("Mistral chat: No results!"); - if (result.choices.length === 0) - throw new Error("Mistral chat: No results length!"); - return result.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.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( diff --git a/server/utils/AiProviders/native/index.js b/server/utils/AiProviders/native/index.js index 07d8918cf..e13b68a2f 100644 --- a/server/utils/AiProviders/native/index.js +++ b/server/utils/AiProviders/native/index.js @@ -1,7 +1,6 @@ const fs = require("fs"); const path = require("path"); const { NativeEmbedder } = require("../../EmbeddingEngines/native"); -const { chatPrompt } = require("../../chats"); const { writeResponseChunk, clientAbortedHandler, @@ -94,7 +93,7 @@ class NativeLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -123,45 +122,6 @@ class NativeLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - try { - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - - const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - }); - const response = await model.call(messages); - return response.content; - } catch (error) { - throw new Error( - `NativeLLM::createChatCompletion failed with: ${error.message}` - ); - } - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const model = await this.#llamaClient({ - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - }); - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - const responseStream = await model.stream(messages); - return responseStream; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { const model = await this.#llamaClient({ temperature }); const response = await model.call(messages); diff --git a/server/utils/AiProviders/ollama/index.js b/server/utils/AiProviders/ollama/index.js index a19315254..73269d6d2 100644 --- a/server/utils/AiProviders/ollama/index.js +++ b/server/utils/AiProviders/ollama/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { StringOutputParser } = require("@langchain/core/output_parsers"); const { writeResponseChunk, @@ -74,7 +73,7 @@ class OllamaAILLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -108,53 +107,6 @@ class OllamaAILLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - - const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - }); - const textResponse = await model - .pipe(new StringOutputParser()) - .invoke(this.#convertToLangchainPrototypes(messages)) - .catch((e) => { - throw new Error( - `Ollama::getChatCompletion failed to communicate with Ollama. ${e.message}` - ); - }); - - if (!textResponse || !textResponse.length) - throw new Error(`Ollama::sendChat text response was empty.`); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - const messages = await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ); - - const model = this.#ollamaClient({ - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - }); - const stream = await model - .pipe(new StringOutputParser()) - .stream(this.#convertToLangchainPrototypes(messages)); - return stream; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { const model = this.#ollamaClient({ temperature }); const textResponse = await model diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index d69ec11ee..3a4d997ce 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -1,5 +1,4 @@ const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi"); -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -41,7 +40,7 @@ class OpenAiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -122,65 +121,6 @@ class OpenAiLLM { return { safe: false, reasons }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `OpenAI chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("OpenAI chat: No results!"); - if (result.choices.length === 0) - throw new Error("OpenAI chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `OpenAI::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `OpenAI chat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( diff --git a/server/utils/AiProviders/openRouter/index.js b/server/utils/AiProviders/openRouter/index.js index 8fb078fbc..a83010835 100644 --- a/server/utils/AiProviders/openRouter/index.js +++ b/server/utils/AiProviders/openRouter/index.js @@ -1,10 +1,8 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); -const { chatPrompt } = require("../../chats"); const { v4: uuidv4 } = require("uuid"); const { writeResponseChunk, clientAbortedHandler, - handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); const fs = require("fs"); const path = require("path"); @@ -99,7 +97,7 @@ class OpenRouterLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -131,65 +129,6 @@ class OpenRouterLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `OpenRouter chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("OpenRouter chat: No results!"); - if (result.choices.length === 0) - throw new Error("OpenRouter chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `OpenRouter::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `OpenRouter chat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( @@ -304,143 +243,6 @@ class OpenRouterLLM { }); } - // handleStream(response, stream, responseProps) { - // const timeoutThresholdMs = 500; - // const { uuid = uuidv4(), sources = [] } = responseProps; - - // return new Promise((resolve) => { - // let fullText = ""; - // let chunk = ""; - // let lastChunkTime = null; // null when first token is still not received. - - // // Establish listener to early-abort a streaming response - // // in case things go sideways or the user does not like the response. - // // We preserve the generated text but continue as if chat was completed - // // to preserve previously generated content. - // const handleAbort = () => clientAbortedHandler(resolve, fullText); - // response.on("close", handleAbort); - - // // NOTICE: Not all OpenRouter models will return a stop reason - // // which keeps the connection open and so the model never finalizes the stream - // // like the traditional OpenAI response schema does. So in the case the response stream - // // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with - // // no new chunks then we kill the stream and assume it to be complete. OpenRouter is quite fast - // // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if - // // we find it is too aggressive. - // const timeoutCheck = setInterval(() => { - // if (lastChunkTime === null) return; - - // const now = Number(new Date()); - // const diffMs = now - lastChunkTime; - // if (diffMs >= timeoutThresholdMs) { - // console.log( - // `OpenRouter stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.` - // ); - // writeResponseChunk(response, { - // uuid, - // sources, - // type: "textResponseChunk", - // textResponse: "", - // close: true, - // error: false, - // }); - // clearInterval(timeoutCheck); - // response.removeListener("close", handleAbort); - // resolve(fullText); - // } - // }, 500); - - // 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: /, ""); - - // // 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]") { - // lastChunkTime = Number(new Date()); - // writeResponseChunk(response, { - // uuid, - // sources, - // type: "textResponseChunk", - // textResponse: "", - // close: true, - // error: false, - // }); - // clearInterval(timeoutCheck); - // response.removeListener("close", handleAbort); - // 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; - // lastChunkTime = Number(new Date()); - // writeResponseChunk(response, { - // uuid, - // sources: [], - // type: "textResponseChunk", - // textResponse: token, - // close: false, - // error: false, - // }); - // } - - // if (finishReason !== null) { - // lastChunkTime = Number(new Date()); - // writeResponseChunk(response, { - // uuid, - // sources, - // type: "textResponseChunk", - // textResponse: "", - // close: true, - // error: false, - // }); - // clearInterval(timeoutCheck); - // response.removeListener("close", handleAbort); - // 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/perplexity/index.js b/server/utils/AiProviders/perplexity/index.js index 71b74e9e3..a17ec43f5 100644 --- a/server/utils/AiProviders/perplexity/index.js +++ b/server/utils/AiProviders/perplexity/index.js @@ -1,5 +1,4 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -50,7 +49,7 @@ class PerplexityLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } promptWindowLimit() { @@ -81,65 +80,6 @@ class PerplexityLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `Perplexity chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("Perplexity chat: No results!"); - if (result.choices.length === 0) - throw new Error("Perplexity chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `Perplexity::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `Perplexity chat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( diff --git a/server/utils/AiProviders/togetherAi/index.js b/server/utils/AiProviders/togetherAi/index.js index 6d91e9a98..577a4b742 100644 --- a/server/utils/AiProviders/togetherAi/index.js +++ b/server/utils/AiProviders/togetherAi/index.js @@ -1,4 +1,3 @@ -const { chatPrompt } = require("../../chats"); const { handleDefaultStreamResponseV2, } = require("../../helpers/chat/responses"); @@ -49,7 +48,7 @@ class TogetherAiLLM { } streamingEnabled() { - return "streamChat" in this && "streamGetChatCompletion" in this; + return "streamGetChatCompletion" in this; } // Ensure the user set a value for the token limit @@ -82,65 +81,6 @@ class TogetherAiLLM { return { safe: true, reasons: [] }; } - async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `Together AI chat: ${this.model} is not valid for chat completion!` - ); - - const textResponse = await this.openai.chat.completions - .create({ - model: this.model, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("Together AI chat: No results!"); - if (result.choices.length === 0) - throw new Error("Together AI chat: No results length!"); - return result.choices[0].message.content; - }) - .catch((error) => { - throw new Error( - `TogetherAI::createChatCompletion failed with: ${error.message}` - ); - }); - - return textResponse; - } - - async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { - if (!(await this.isValidChatCompletionModel(this.model))) - throw new Error( - `TogetherAI chat: ${this.model} is not valid for chat completion!` - ); - - const streamRequest = await this.openai.chat.completions.create({ - model: this.model, - stream: true, - temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), - n: 1, - messages: await this.compressMessages( - { - systemPrompt: chatPrompt(workspace), - userPrompt: prompt, - chatHistory, - }, - rawHistory - ), - }); - return streamRequest; - } - async getChatCompletion(messages = null, { temperature = 0.7 }) { if (!(await this.isValidChatCompletionModel(this.model))) throw new Error( From 47b7df4fc3fc25ec99859988d2de393043a2ff1d Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 1 May 2024 20:39:58 -0700 Subject: [PATCH 05/13] Clear chat window on `/reset` (#1261) clear chat window on /reset --- frontend/src/utils/chat/index.js | 7 +++++++ server/utils/chats/commands/reset.js | 1 + 2 files changed, 8 insertions(+) diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index a2b18c7f4..c5730dbe0 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -17,6 +17,7 @@ export default function handleChat( error, close, chatId = null, + action = null, } = chatResult; if (type === "abort" || type === "statusResponse") { @@ -132,6 +133,12 @@ export default function handleChat( setChatHistory([..._chatHistory]); setLoadingResponse(false); } + + // Action Handling via special 'action' attribute on response. + if (action === "reset_chat") { + // Chat was reset, keep reset message and clear everything else. + setChatHistory([_chatHistory.pop()]); + } } export function chatPrompt(workspace) { diff --git a/server/utils/chats/commands/reset.js b/server/utils/chats/commands/reset.js index a23eef7aa..f2bd4562c 100644 --- a/server/utils/chats/commands/reset.js +++ b/server/utils/chats/commands/reset.js @@ -23,6 +23,7 @@ async function resetMemory( sources: [], close: true, error: false, + action: "reset_chat", }; } From c2277906adf83a29aa84c3106764c0b3f4197fb3 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 2 May 2024 09:55:06 -0700 Subject: [PATCH 06/13] turn off HTML for renderer --- embed/src/utils/chat/markdown.js | 2 +- frontend/src/utils/chat/markdown.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/embed/src/utils/chat/markdown.js b/embed/src/utils/chat/markdown.js index d82c4fa7c..bb21e6925 100644 --- a/embed/src/utils/chat/markdown.js +++ b/embed/src/utils/chat/markdown.js @@ -4,7 +4,7 @@ import { staticHljs as hljs } from "./hljs"; import { v4 } from "uuid"; const markdown = markdownIt({ - html: true, + html: false, typographer: true, highlight: function (code, lang) { const uuid = v4(); diff --git a/frontend/src/utils/chat/markdown.js b/frontend/src/utils/chat/markdown.js index ff4af77bc..ae1db23cb 100644 --- a/frontend/src/utils/chat/markdown.js +++ b/frontend/src/utils/chat/markdown.js @@ -5,7 +5,7 @@ import "highlight.js/styles/github-dark-dimmed.min.css"; import { v4 } from "uuid"; const markdown = markdownIt({ - html: true, + html: false, typographer: true, highlight: function (code, lang) { const uuid = v4(); From 244ce2e307af5fcaa59cd202fe0e1b2324d7467b Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Thu, 2 May 2024 10:15:11 -0700 Subject: [PATCH 07/13] Prevent concurrent downloads on first-doc upload (#1267) --- server/utils/EmbeddingEngines/native/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/utils/EmbeddingEngines/native/index.js b/server/utils/EmbeddingEngines/native/index.js index ae73c4896..5494c8869 100644 --- a/server/utils/EmbeddingEngines/native/index.js +++ b/server/utils/EmbeddingEngines/native/index.js @@ -107,14 +107,21 @@ class NativeEmbedder { ); let fetchResponse = await this.#fetchWithHost(); - if (fetchResponse.pipeline !== null) return fetchResponse.pipeline; + if (fetchResponse.pipeline !== null) { + this.modelDownloaded = true; + return fetchResponse.pipeline; + } this.log( `Failed to download model from primary URL. Using fallback ${fetchResponse.retry}` ); if (!!fetchResponse.retry) fetchResponse = await this.#fetchWithHost(fetchResponse.retry); - if (fetchResponse.pipeline !== null) return fetchResponse.pipeline; + if (fetchResponse.pipeline !== null) { + this.modelDownloaded = true; + return fetchResponse.pipeline; + } + throw fetchResponse.error; } From d02013fd71f454279ca89631fcb2748b8178f70e Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 2 May 2024 10:27:09 -0700 Subject: [PATCH 08/13] [FIX] Document pinning does not count in query mode (#1250) * if document is pinned, do not give queryRefusalResponse message * forgot embed.js patch --------- Co-authored-by: timothycarambat --- server/utils/chats/embed.js | 6 +++++- server/utils/chats/index.js | 8 ++++++-- server/utils/chats/stream.js | 8 ++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/server/utils/chats/embed.js b/server/utils/chats/embed.js index 533ea0c34..98b096fb1 100644 --- a/server/utils/chats/embed.js +++ b/server/utils/chats/embed.js @@ -131,7 +131,11 @@ async function streamChatWithForEmbed( // 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) { + if ( + chatMode === "query" && + sources.length === 0 && + pinnedDocIdentifiers.length === 0 + ) { writeResponseChunk(response, { id: uuid, type: "textResponse", diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 38ce6c9bd..76f98e0df 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -140,9 +140,13 @@ async function chatWithWorkspace( contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts]; sources = [...sources, ...vectorSearchResults.sources]; - // If in query mode and no sources are found, do not + // If in query mode and no sources are found from the vector search and no pinned documents, do not // let the LLM try to hallucinate a response or use general knowledge and exit early - if (chatMode === "query" && sources.length === 0) { + if ( + chatMode === "query" && + vectorSearchResults.sources.length === 0 && + pinnedDocIdentifiers.length === 0 + ) { return { id: uuid, type: "textResponse", diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 57f326664..ba4dea163 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -160,9 +160,13 @@ async function streamChatWithWorkspace( contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts]; sources = [...sources, ...vectorSearchResults.sources]; - // If in query mode and no sources are found, do not + // If in query mode and no sources are found from the vector search and no pinned documents, do not // let the LLM try to hallucinate a response or use general knowledge and exit early - if (chatMode === "query" && sources.length === 0) { + if ( + chatMode === "query" && + sources.length === 0 && + pinnedDocIdentifiers.length === 0 + ) { writeResponseChunk(response, { id: uuid, type: "textResponse", From 3caebc47b46dde67acf0702233a802f3895891b4 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 2 May 2024 10:35:50 -0700 Subject: [PATCH 09/13] [FEAT] Cohere LLM and embedder support (#1233) * getChatCompletion working WIP streaming * WIP * working streaming WIP abort stream * implement cohere embedder support * remove inputType option from cohere embedder * fix cohere LLM from not aborting stream when canceled by user * Patch Cohere implemention * add cohere to onboarding --------- Co-authored-by: timothycarambat --- docker/.env.example | 8 + .../CohereOptions/index.jsx | 55 +++++ .../LLMSelection/CohereAiOptions/index.jsx | 49 ++++ frontend/src/hooks/useGetProvidersModels.js | 8 + frontend/src/media/llmprovider/cohere.png | Bin 0 -> 142073 bytes .../EmbeddingPreference/index.jsx | 10 + .../GeneralSettings/LLMPreference/index.jsx | 10 + .../Steps/DataHandling/index.jsx | 15 ++ .../Steps/LLMPreference/index.jsx | 10 + server/.env.example | 8 + server/models/systemSettings.js | 4 + server/package.json | 1 + server/utils/AiProviders/cohere/index.js | 226 ++++++++++++++++++ server/utils/EmbeddingEngines/cohere/index.js | 86 +++++++ server/utils/helpers/index.js | 6 + server/utils/helpers/updateENV.js | 12 + server/yarn.lock | 43 +++- 17 files changed, 541 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/EmbeddingSelection/CohereOptions/index.jsx create mode 100644 frontend/src/components/LLMSelection/CohereAiOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/cohere.png create mode 100644 server/utils/AiProviders/cohere/index.js create mode 100644 server/utils/EmbeddingEngines/cohere/index.js diff --git a/docker/.env.example b/docker/.env.example index 3a0a68c52..20120b5b5 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -72,6 +72,10 @@ GID='1000' # GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096 # GENERIC_OPEN_AI_API_KEY=sk-123abc +# LLM_PROVIDER='cohere' +# COHERE_API_KEY= +# COHERE_MODEL_PREF='command-r' + ########################################### ######## Embedding API SElECTION ########## ########################################### @@ -100,6 +104,10 @@ GID='1000' # EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf' # EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# EMBEDDING_ENGINE='cohere' +# COHERE_API_KEY= +# EMBEDDING_MODEL_PREF='embed-english-v3.0' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/frontend/src/components/EmbeddingSelection/CohereOptions/index.jsx b/frontend/src/components/EmbeddingSelection/CohereOptions/index.jsx new file mode 100644 index 000000000..7cb092374 --- /dev/null +++ b/frontend/src/components/EmbeddingSelection/CohereOptions/index.jsx @@ -0,0 +1,55 @@ +export default function CohereEmbeddingOptions({ settings }) { + return ( +
+
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/LLMSelection/CohereAiOptions/index.jsx b/frontend/src/components/LLMSelection/CohereAiOptions/index.jsx new file mode 100644 index 000000000..a20c8b81f --- /dev/null +++ b/frontend/src/components/LLMSelection/CohereAiOptions/index.jsx @@ -0,0 +1,49 @@ +export default function CohereAiOptions({ settings }) { + return ( +
+
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/hooks/useGetProvidersModels.js b/frontend/src/hooks/useGetProvidersModels.js index fb35230c9..dfd468111 100644 --- a/frontend/src/hooks/useGetProvidersModels.js +++ b/frontend/src/hooks/useGetProvidersModels.js @@ -26,6 +26,14 @@ const PROVIDER_DEFAULT_MODELS = { "gemma-7b-it", ], native: [], + cohere: [ + "command-r", + "command-r-plus", + "command", + "command-light", + "command-nightly", + "command-light-nightly", + ], }; // For togetherAi, which has a large model list - we subgroup the options diff --git a/frontend/src/media/llmprovider/cohere.png b/frontend/src/media/llmprovider/cohere.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec86e49b572f0f51ac8252baa8800fe4b83eb08 GIT binary patch literal 142073 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�_}OiQXy(2Y^gdt|ex9kK86RCO)U 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 , + description: "Run powerful embedding models from Cohere.", + }, ]; export default function GeneralEmbeddingPreference() { diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 271d91974..ce37bd480 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -18,6 +18,7 @@ import HuggingFaceLogo from "@/media/llmprovider/huggingface.png"; import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; +import CohereLogo from "@/media/llmprovider/cohere.png"; import PreLoader from "@/components/Preloader"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; @@ -34,6 +35,7 @@ import HuggingFaceOptions from "@/components/LLMSelection/HuggingFaceOptions"; import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions"; import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions"; import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions"; +import CohereAiOptions from "@/components/LLMSelection/CohereAiOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; @@ -152,6 +154,14 @@ export const AVAILABLE_LLM_PROVIDERS = [ "The fastest LLM inferencing available for real-time AI applications.", requiredConfig: ["GroqApiKey"], }, + { + name: "Cohere", + value: "cohere", + logo: CohereLogo, + options: (settings) => , + description: "Run Cohere's powerful Command models.", + requiredConfig: ["CohereApiKey"], + }, { name: "Generic OpenAI", value: "generic-openai", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 548272fe0..d0613b8c3 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -15,6 +15,7 @@ import HuggingFaceLogo from "@/media/llmprovider/huggingface.png"; import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; +import CohereLogo from "@/media/llmprovider/cohere.png"; import ZillizLogo from "@/media/vectordbs/zilliz.png"; import AstraDBLogo from "@/media/vectordbs/astraDB.png"; import ChromaLogo from "@/media/vectordbs/chroma.png"; @@ -144,6 +145,13 @@ export const LLM_SELECTION_PRIVACY = { ], logo: GenericOpenAiLogo, }, + cohere: { + name: "Cohere", + description: [ + "Data is shared according to the terms of service of cohere.com and your localities privacy laws.", + ], + logo: CohereLogo, + }, }; export const VECTOR_DB_PRIVACY = { @@ -252,6 +260,13 @@ export const EMBEDDING_ENGINE_PRIVACY = { ], logo: LMStudioLogo, }, + cohere: { + name: "Cohere", + description: [ + "Data is shared according to the terms of service of cohere.com and your localities privacy laws.", + ], + logo: CohereLogo, + }, }; export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) { diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 0b756fc2c..0e73c399f 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -15,6 +15,7 @@ import HuggingFaceLogo from "@/media/llmprovider/huggingface.png"; import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; +import CohereLogo from "@/media/llmprovider/cohere.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions"; @@ -30,6 +31,8 @@ import TogetherAiOptions from "@/components/LLMSelection/TogetherAiOptions"; import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions"; import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions"; import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions"; +import CohereAiOptions from "@/components/LLMSelection/CohereAiOptions"; + import LLMItem from "@/components/LLMSelection/LLMItem"; import System from "@/models/system"; import paths from "@/utils/paths"; @@ -136,6 +139,13 @@ const LLMS = [ description: "The fastest LLM inferencing available for real-time AI applications.", }, + { + name: "Cohere", + value: "cohere", + logo: CohereLogo, + options: (settings) => , + description: "Run Cohere's powerful Command models.", + }, { name: "Generic OpenAI", value: "generic-openai", diff --git a/server/.env.example b/server/.env.example index 244d37e54..e515cc888 100644 --- a/server/.env.example +++ b/server/.env.example @@ -69,6 +69,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096 # GENERIC_OPEN_AI_API_KEY=sk-123abc +# LLM_PROVIDER='cohere' +# COHERE_API_KEY= +# COHERE_MODEL_PREF='command-r' + ########################################### ######## Embedding API SElECTION ########## ########################################### @@ -97,6 +101,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf' # EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# EMBEDDING_ENGINE='cohere' +# COHERE_API_KEY= +# EMBEDDING_MODEL_PREF='embed-english-v3.0' + ########################################### ######## Vector Database Selection ######## ########################################### diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index bdec2af3d..dfbdb882f 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -364,6 +364,10 @@ const SystemSettings = { GenericOpenAiModelPref: process.env.GENERIC_OPEN_AI_MODEL_PREF, GenericOpenAiTokenLimit: process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT, GenericOpenAiKey: !!process.env.GENERIC_OPEN_AI_API_KEY, + + // Cohere API Keys + CohereApiKey: !!process.env.COHERE_API_KEY, + CohereModelPref: process.env.COHERE_MODEL_PREF, }; }, }; diff --git a/server/package.json b/server/package.json index 5549ba713..cb5ed8141 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,7 @@ "chalk": "^4", "check-disk-space": "^3.4.0", "chromadb": "^1.5.2", + "cohere-ai": "^7.9.5", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", diff --git a/server/utils/AiProviders/cohere/index.js b/server/utils/AiProviders/cohere/index.js new file mode 100644 index 000000000..a97a15fca --- /dev/null +++ b/server/utils/AiProviders/cohere/index.js @@ -0,0 +1,226 @@ +const { v4 } = require("uuid"); +const { writeResponseChunk } = require("../../helpers/chat/responses"); +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); + +class CohereLLM { + constructor(embedder = null) { + const { CohereClient } = require("cohere-ai"); + if (!process.env.COHERE_API_KEY) + throw new Error("No Cohere API key was set."); + + const cohere = new CohereClient({ + token: process.env.COHERE_API_KEY, + }); + + this.cohere = cohere; + this.model = process.env.COHERE_MODEL_PREF; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + this.embedder = !!embedder ? embedder : new NativeEmbedder(); + } + + #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("") + ); + } + + #convertChatHistoryCohere(chatHistory = []) { + let cohereHistory = []; + chatHistory.forEach((message) => { + switch (message.role) { + case "system": + cohereHistory.push({ role: "SYSTEM", message: message.content }); + break; + case "user": + cohereHistory.push({ role: "USER", message: message.content }); + break; + case "assistant": + cohereHistory.push({ role: "CHATBOT", message: message.content }); + break; + } + }); + + return cohereHistory; + } + + streamingEnabled() { + return "streamGetChatCompletion" in this; + } + + promptWindowLimit() { + switch (this.model) { + case "command-r": + return 128_000; + case "command-r-plus": + return 128_000; + case "command": + return 4_096; + case "command-light": + return 4_096; + case "command-nightly": + return 8_192; + case "command-light-nightly": + return 8_192; + default: + return 4_096; + } + } + + async isValidChatCompletionModel(model = "") { + const validModels = [ + "command-r", + "command-r-plus", + "command", + "command-light", + "command-nightly", + "command-light-nightly", + ]; + return validModels.includes(model); + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_input = "") { + // Not implemented so must be stubbed + return { safe: true, reasons: [] }; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Cohere chat: ${this.model} is not valid for chat completion!` + ); + + const message = messages[messages.length - 1].content; // Get the last message + const cohereHistory = this.#convertChatHistoryCohere(messages.slice(0, -1)); // Remove the last message and convert to Cohere + + const chat = await this.cohere.chat({ + model: this.model, + message: message, + chatHistory: cohereHistory, + temperature, + }); + + if (!chat.hasOwnProperty("text")) return null; + return chat.text; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `Cohere chat: ${this.model} is not valid for chat completion!` + ); + + const message = messages[messages.length - 1].content; // Get the last message + const cohereHistory = this.#convertChatHistoryCohere(messages.slice(0, -1)); // Remove the last message and convert to Cohere + + const stream = await this.cohere.chatStream({ + model: this.model, + message: message, + chatHistory: cohereHistory, + temperature, + }); + + return { type: "stream", stream: stream }; + } + + async handleStream(response, stream, responseProps) { + return new Promise(async (resolve) => { + let fullText = ""; + const { uuid = v4(), sources = [] } = responseProps; + + const handleAbort = () => { + writeResponseChunk(response, { + uuid, + sources, + type: "abort", + textResponse: fullText, + close: true, + error: false, + }); + response.removeListener("close", handleAbort); + resolve(fullText); + }; + response.on("close", handleAbort); + + try { + for await (const chat of stream.stream) { + if (chat.eventType === "text-generation") { + const text = chat.text; + fullText += text; + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: text, + close: false, + error: false, + }); + } + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + response.removeListener("close", handleAbort); + resolve(fullText); + } catch (error) { + writeResponseChunk(response, { + uuid, + sources, + type: "abort", + textResponse: null, + close: true, + error: error.message, + }); + response.removeListener("close", handleAbort); + resolve(fullText); + } + }); + } + + // 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 = { + CohereLLM, +}; diff --git a/server/utils/EmbeddingEngines/cohere/index.js b/server/utils/EmbeddingEngines/cohere/index.js new file mode 100644 index 000000000..0dfb61d0d --- /dev/null +++ b/server/utils/EmbeddingEngines/cohere/index.js @@ -0,0 +1,86 @@ +const { toChunks } = require("../../helpers"); + +class CohereEmbedder { + constructor() { + if (!process.env.COHERE_API_KEY) + throw new Error("No Cohere API key was set."); + + const { CohereClient } = require("cohere-ai"); + const cohere = new CohereClient({ + token: process.env.COHERE_API_KEY, + }); + + this.cohere = cohere; + this.model = process.env.EMBEDDING_MODEL_PREF || "embed-english-v3.0"; + this.inputType = "search_document"; + + // Limit of how many strings we can process in a single pass to stay with resource or network limits + this.maxConcurrentChunks = 96; // Cohere's limit per request is 96 + this.embeddingMaxChunkLength = 1945; // https://docs.cohere.com/docs/embed-2 - assume a token is roughly 4 letters with some padding + } + + async embedTextInput(textInput) { + this.inputType = "search_query"; + const result = await this.embedChunks([textInput]); + return result?.[0] || []; + } + + async embedChunks(textChunks = []) { + const embeddingRequests = []; + this.inputType = "search_document"; + + for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { + embeddingRequests.push( + new Promise((resolve) => { + this.cohere + .embed({ + texts: chunk, + model: this.model, + inputType: this.inputType, + }) + .then((res) => { + resolve({ data: res.embeddings, 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) => { + 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(`Cohere Failed to embed: ${error}`); + + return data.length > 0 ? data : null; + } +} + +module.exports = { + CohereEmbedder, +}; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index c8cdd870f..5d88040dc 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -77,6 +77,9 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "groq": const { GroqLLM } = require("../AiProviders/groq"); return new GroqLLM(embedder, model); + case "cohere": + const { CohereLLM } = require("../AiProviders/cohere"); + return new CohereLLM(embedder, model); case "generic-openai": const { GenericOpenAiLLM } = require("../AiProviders/genericOpenAi"); return new GenericOpenAiLLM(embedder, model); @@ -110,6 +113,9 @@ function getEmbeddingEngineSelection() { case "lmstudio": const { LMStudioEmbedder } = require("../EmbeddingEngines/lmstudio"); return new LMStudioEmbedder(); + case "cohere": + const { CohereEmbedder } = require("../EmbeddingEngines/cohere"); + return new CohereEmbedder(); default: return null; } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 5e629baf8..45f2fd546 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -290,6 +290,16 @@ const KEY_MAPPING = { checks: [isNotEmpty], }, + // Cohere Options + CohereApiKey: { + envKey: "COHERE_API_KEY", + checks: [isNotEmpty], + }, + CohereModelPref: { + envKey: "COHERE_MODEL_PREF", + checks: [isNotEmpty], + }, + // Whisper (transcription) providers WhisperProvider: { envKey: "WHISPER_PROVIDER", @@ -393,6 +403,7 @@ function supportedLLM(input = "") { "perplexity", "openrouter", "groq", + "cohere", "generic-openai", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; @@ -434,6 +445,7 @@ function supportedEmbeddingModel(input = "") { "native", "ollama", "lmstudio", + "cohere", ]; return supported.includes(input) ? null diff --git a/server/yarn.lock b/server/yarn.lock index 49c202af6..e7ee9051f 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1817,6 +1817,17 @@ cmake-js@^7.2.1: which "^2.0.2" yargs "^17.7.2" +cohere-ai@^7.9.5: + version "7.9.5" + resolved "https://registry.yarnpkg.com/cohere-ai/-/cohere-ai-7.9.5.tgz#05a592fe19decb8692d1b19d93ac835d7f816b8b" + integrity sha512-tr8LUR3Q46agFpfEwaYwzYO4qAuN0/R/8YroG4bc86LadOacBAabctZUq0zfCdLiL7gB4yWJs4QCzfpRH3rQuw== + dependencies: + form-data "4.0.0" + js-base64 "3.7.2" + node-fetch "2.7.0" + qs "6.11.2" + url-join "4.0.1" + color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2846,19 +2857,19 @@ form-data-encoder@1.7.2: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== +form-data@4.0.0, form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -3652,6 +3663,11 @@ joi@^17.11.0: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" +js-base64@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" + integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== + js-tiktoken@^1.0.11, js-tiktoken@^1.0.7, js-tiktoken@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.11.tgz#d7d707b849f703841112660d9d55169424a35344" @@ -4324,7 +4340,7 @@ node-domexception@1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9: +node-fetch@2.7.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -4947,6 +4963,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@^6.7.0: version "6.12.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" @@ -5862,7 +5885,7 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" -url-join@^4.0.1: +url-join@4.0.1, url-join@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== From fc77b468006749af434a123625ee3a8b4a78fb45 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Thu, 2 May 2024 12:12:44 -0700 Subject: [PATCH 10/13] [FEAT] KoboldCPP LLM Support (#1268) * koboldcpp LLM support * update .env.examples for koboldcpp support * update LLM preference order update koboldcpp comments --------- Co-authored-by: timothycarambat --- docker/.env.example | 5 + .../LLMSelection/KoboldCPPOptions/index.jsx | 112 +++++++++++ frontend/src/media/llmprovider/koboldcpp.png | Bin 0 -> 7110 bytes .../GeneralSettings/LLMPreference/index.jsx | 14 ++ .../Steps/DataHandling/index.jsx | 8 + .../Steps/LLMPreference/index.jsx | 9 + server/.env.example | 5 + server/models/systemSettings.js | 5 + server/utils/AiProviders/koboldCPP/index.js | 180 ++++++++++++++++++ server/utils/helpers/customModels.js | 25 +++ server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 15 ++ 12 files changed, 381 insertions(+) create mode 100644 frontend/src/components/LLMSelection/KoboldCPPOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/koboldcpp.png create mode 100644 server/utils/AiProviders/koboldCPP/index.js diff --git a/docker/.env.example b/docker/.env.example index 20120b5b5..e10ace026 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -66,6 +66,11 @@ GID='1000' # GROQ_API_KEY=gsk_abcxyz # GROQ_MODEL_PREF=llama3-8b-8192 +# LLM_PROVIDER='koboldcpp' +# KOBOLD_CPP_BASE_PATH='http://127.0.0.1:5000/v1' +# KOBOLD_CPP_MODEL_PREF='koboldcpp/codellama-7b-instruct.Q4_K_S' +# KOBOLD_CPP_MODEL_TOKEN_LIMIT=4096 + # LLM_PROVIDER='generic-openai' # GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1' # GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo' diff --git a/frontend/src/components/LLMSelection/KoboldCPPOptions/index.jsx b/frontend/src/components/LLMSelection/KoboldCPPOptions/index.jsx new file mode 100644 index 000000000..7e5e20aef --- /dev/null +++ b/frontend/src/components/LLMSelection/KoboldCPPOptions/index.jsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from "react"; +import System from "@/models/system"; + +export default function KoboldCPPOptions({ settings }) { + const [basePathValue, setBasePathValue] = useState( + settings?.KoboldCPPBasePath + ); + const [basePath, setBasePath] = useState(settings?.KoboldCPPBasePath); + + return ( +
+
+ + setBasePathValue(e.target.value)} + onBlur={() => setBasePath(basePathValue)} + /> +
+ +
+ + e.target.blur()} + defaultValue={settings?.KoboldCPPTokenLimit} + required={true} + autoComplete="off" + /> +
+
+ ); +} + +function KoboldCPPModelSelection({ settings, basePath = null }) { + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!basePath || !basePath.includes("/v1")) { + setCustomModels([]); + setLoading(false); + return; + } + setLoading(true); + const { models } = await System.customModels("koboldcpp", null, basePath); + setCustomModels(models || []); + setLoading(false); + } + findCustomModels(); + }, [basePath]); + + if (loading || customModels.length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/media/llmprovider/koboldcpp.png b/frontend/src/media/llmprovider/koboldcpp.png new file mode 100644 index 0000000000000000000000000000000000000000..5724f04ab3a1bc40ac4e56c1fe9ee61fc67ba3ea GIT binary patch literal 7110 zcmeHMXIPV4wnkA9rKmKOW+fse1VJD)k*YM6DjrHm0t6BgNCJdV6hx6K7E}-rQF;-i z7ZGVn_W;tn0up-f+#TnPqt3ne$K3m4ehd#G$^Q1YzP;97>wVv~xqDe(i*4_by$lQt zY!|iFuY&hA`fCpp_!}@`t_0p#2-??(3=GUR^w$nKkEh)X47+A9MrI^4JzY2wkCQ;4 z@HS`(cN_uCW?)cshl7teGzoFk9fx%y!rhf1-)F$VXZkP{qP+L$Uq6a2C|medbS!X; z1w$ZV@J>hRrV%!HGD!&n`7!HnOF|)kJcdAa!G3=T3JFDH(Kxgdi3o*Bz9=Vatz(lA+h89B)x_k6$jPq!H1$w+&;Tgv}f=|9H)G5vqo z=3m40cUON6>;Kb66Mm{E`YwMjRQmpc3t)fG{<6Pd46aHh+2dW54bcdvquL0PEe4GR z5&Y@OZ{Gdia}i+0FNyeLiS7u(Kcyg;4F8@kco!o)9;>W|aB@W)J$)7JghIQZT|^Y2 zzt8yJ*86u5`fUz?qo2|l`UB3CncOdTFfj0VUQ|~#a(_12OEoe!7G$1*K9)Ul@>W>w zVG$Pd+S3l~j!ZS!{d#Ago&0pXrg>*{ee!_=R}+mcTw%uTA}0?s3Jowed!p_JpH!~q z7kH?9`R*0c)tC!6Ed5t2cGumfHWv2Oom%%&*)CjCUZ0?r^i!uhsj(r(YHDhX0b_Q0 z?VkmXzW7}8oRj6B%m|=I8Y8HXGk-m#?+9bv$BbGtIkt4`vPzl>4o;#BQ}>ui4w% z2l-iEzaHeXNwfOU)THI?3`tZ@mU5Ca{;5H6-Q7L_|cM zNFsd8=L{3|2cO!-T)`!#rLjUwW_-)>LJR~Km-l_8)Y%?lQ*Kh`LW`-X>7CYAqn8N@ zL$TW%5C=y`ZW@gQUzq6R^ke3z7R@a2?cW%Y)k8Z>H;i<-I6L2risDmLRGgt%Z6CRL z^X6E0er7q+)5|M7Ha2sWKQuIyKqmVa7b}3F!O_t>;U~0Sc&#r*cH6T&@bg}tvS@8> zJtUTynQ4ks%WOwZbY!hG`E13T;Ts}lSXY}KT4vUCp074#_|Z&g`cZS&#Yu@tZzBU97(f`UWfdPgAwSVHpPT!2~sMPwj358914ztlg}t`ZxE~C z^CJxvPhd!~))Uz;K?vU0)G!YY4r=Btma%p2@0lb#(<=9CX^E~WfVf|-G-%K5JxVX5iS7rv~oQH08c008^ zpYAQ@4G#|wBr~xcK@lYH3R=2N^{8rV?|Bg)AJ=sRhE4 zn#0EM_3PI?EG)3K2sOQ^-JA7K&+cJkGu{ySGC3KNem(zvUtgcE(g~d>iWI3lT(|9# zs{lX0q}LkhO+K}(qr=R|$SB8r-QVBeEE{$AS~vE=bE`6Mfq2c33lvIWIecnrO54Do z1{tPgTOJdBsyp^m(kJq?CgVy2C>y3pFn~#zgxi*9UUDzIy&C<^$A)Ok;*~!g+F|FYq%zC_#-6N zNYMm0bBG}=EzKHS$At52pjoF$&a`01yZCJGa9eR3cg$V{O)*mJXWdsKW z1y!sq(zt2r8XAXlKim`){P^*soaf4Gn`p2K>$(W>LsF=KD>#lL(tG@v*rZ-jRc8Lw zfr>=*7dThETuM(*KXB+!KuL+BANkUyOSv4#PQVEZ^ z`Af5mmT#{yEyD3?eMjJ=F$O-qihe%_(WM*$k!UEEnVNcbl|Mx6;KBol!ErG$-C0Of zY^>1d^y|#gjh+?`_V(u~6#3fP+UDkF>pWMs
P7))|!6M3{zpr^o@(b(A77H67z z`!>_VM~?%Reb2t0d1K*3ddmESbe(pXG( z^gJn*d2_ribq_nciRS}%cX#WWPyze)jOdG_XQ0rUKyJ~QF2~IBiVDW1r6oE(0Sud% zoSa=~F&GY!?Ry7q)$?dMmjc#w;lj?HjEn$HS?%oX6qh=X05C6PT9)n?5J+EXslVsz zyR)*Y3eoWzG1i*Q35G8v|7E z?Afz{kr5_P5;>0T>;R(73J7sm40YF+CJn{hTwOIuq@3j5b5VLx=R_h@W-B>`bFGYx z{ej)warvqC@#Qg2VcmE6{oanRd#b8-V=x$ZkQvuKL+9pP>Z4>i96zV2qS5F#bq>~pL}}3@g#&#)&?Zw3J9p+>D1KJ_3|}ZK)@d0QgRFg z3gsY<)T^4CN0#FsL`Fs?|@(pUDT3y_bvoa|<@_-#-{E zq#|gV@dwOo^plp542>v|E=YQHue+ne8Z37V@L5oIQ*G`0%DbG--~K!mA1W#$QeHxt zzZV{UlT7<8q8zt?1ipS!98YPd%`TU13v6#Kmxaf~u)Db}+tN;*I(6*KnZ0doZISCc zdZ;{}?{Bft>A@u39DUWSwyy2~KYuW&GKUnat5<#V`#knpT3V)%+q^qj1>i<4@(m)g zvb@~f+y&HS2|8#jOA(UaBn3KZ=vWt;09kW&YNc6+UkL4iAx!dnC zEN9$_j*iabeW4nq9JckiY(s-gGW^&WtpM{_u6Ra}L8rbOidW%xcuiAv`xy_y`9 z!zqyEs;a6VYHK~LUs)L$ae+vNIL&((yH39M-^Y9J>C?Nw2A|hPw1Qjk-qp?4gqvW8 zGpx!o%o4>v>)yI`D`%_#hOD;t2=xI%ASGX{AuB5jwvN+{${-u)>sJFK<|M4E>p4s6 zq|Ad0eMu=Pdwo_agp8Pjbjb^2pP088hlhvt*eUUuWm`v}r3(k&Zf{ZJ!!~CsIIeTK zi03TSw6*0LH{zT?(Wzsx*wh$0=dHA5dZRy|>y{n^RajLAEyFH6`!d;`52MVX^!9B7 z0vQ+>cy68v;x%0|uOFmYKRGdB^jPm4ptox+DenOLIa;1t`fwu5J~CX6GWYTmzEadQ zV*u~79+gmJo;kXigH5??Vq$9)g^L3cq9Y5*9OE&bY@(n=AV~5^{9(W3+Pwj5BaSu> zJ~Fk0jbox8iJGSL&1hnkB0j|c2IKJD5Yu2Ce|$pb<{VM9YUJtY**+}zxbGM{Y@ z*DDx(fDl{j{o4-S0-(Z-qj{Y^E@DwgWIWu?b9P9hVH3AciMmu7r*>yxdb zyBovip{vW9=hPeMy}2e6;!34Z5FhWKN{JE#4l+M_^r)khnqLl=kdUBPA;6uM;9Jqr z0sz6nA3gF10;O{xi%ceK>*%yBz>loQS3ab5YU2v+bf(baao6*m00bQi6;Md{hTuYe zBf*1@$m|6tEGsL+VFrC# zaB#?)3377!l~R|FLm-Krb4|+Yx{Gi7+uM1;&d8W;RrBGkL0)!bqjlbV)zHuY(CIgn zKgKNNd4ulMbz3{TJI1LdgFp!}C2}rLb||)^J4$~ ze7qJGn>H?6CcnD6x;YS0S6jQcv9Xbk>4}LDCe}kYwl>%E^cO0@@%WBZH3A zG1Mt}R(`qWwE3^M%}}jg<-WTMJXfWz8X9ubA!Crv^!&~nOWk;G(~b}U1(RGAWOXoX zr8+G+xjfggohyukh2=`Am7s!aTKRLv@fJN$EJZdH00l7Ahy8wK=N~4>OeZdJ_P5eH z?SYSh!wj!py$T2kX;U<@W_N7Qz_oqa>IYDzt*`HtyA*;5r;-4l1Je%XsQsLr@hf5( zUD6FL1}fPJKgatBRh5mWxBA^Hv@uPDyGm>Trc_rCb0CCzB%4a|b({Y@+ zc@Yv8DMjYN=G8WS&Om>6y!BFu@mjjpRs>}0%1{uLH7{Phu7b&d(MMljpKem<(r5t? z#|g80UlHvsCKJ#3I)&0$de&dd<}6d=*Va7f2+hpSe;F4RjQn&32q`y^TF*g05x6)$ zKED6pL0|A$>2gBeTwSvjYhi3xZ6aWt(-DrH$%g4dmzL2UrIsyBH!o$X(eB&iGDGT} zmH6~x6c5M5!~~K71j9U62$eN8e2cft04(u9Ez=6$O7p~8Qb(TJz4lsnZNoPJQroqA zw-O=q+qZ9bwS>G{2b@Js(j?{1@9yE-bG_I#z1+pXz@SZbJ3%jsi+9a)dviI^FEB4J zZx1W0k>Z`MT4ulLhQ^!#}SCU*XS&Np_V8A6ZK^hQ2yZf?#3@vcTbPn-K%2fDkpa+|$WDz!@M z@V^lIIT%@3ggk%#+*T`9M@NTF09D~w6&00TyLO#+ zGJO~m6S~~*Q`UL-2EAIyM0W8^#sW9dmpKdVt+Z8S#cj3<>U3#h^3>P5$1v&=c4d9N zqO&3&y#s=Ce4NcA4Lrtp8%*-W= zpRY%*Z7;T4#k)=S0cU+PGofke>GU>bqNmW1e8UmwwdQXmQ2I3hELlJ6XltL>)MU~O zK6Ep~qWA^C)XvUMJv>&;V_^)@s&*XEdIcy;wut-%k0aUE`6r1yq9wy)$cJer-6ncXP* zZAx02QE%ROO&@hyNzcGWrpRmk9KCaC&8q?mqNc!kn4yQ7WVgAt7?{5UG%hP&9vDdH zbM$n7ta<{81u{<(w3t=7TjGT=w>!GJj-NWU8wi)yoR2SHN6Ok7N-gFpG8!Jg18L25 zLNoYs`s2vRKuk-5%qF!n8$GkQD05wfE(a*R6rIklE}O;iHoapc5~(sm{5k+K<~Pwu zpn5b_<}f&1NL#8YU+L{$E_!j6Qe}@HKVILrlwzEE`Hw%aORhq;I6)HVE9mZRt}mOW zN8P*Eob&vBTN`(*_geC`_gkQbkITpy&*xj~2r{;PBWY`EyAK%bS*Epf02OH4NjfGh ztT}6P{^Iu5X84mQNd*Rx#~j{p(fzUkq|#ABd2^93Y{N`Xuff3eBJMh7{yF0^Pr%Dp zuQ2BB2M!;;`|u$b!*W6rC<94!?L(ePnNiS;78J-$^p$oeS=cJCjh!gjTu$8MX9^*d5LZADk22%0<{k{A5ZCd;JIM~>#e0T5Dg--+Kb{?re ztgfkfrZLHWaC$ln_=y~_S2Tqu7hzt*pEBagr+I<890kqIg0yHhwbB<+h zZcY;O=Se-jaiy)5VbGT|fmU7;STmJqc`o$C~^#s^|&^g(5 zvQ&Y%&(6*kxX&I3z#2Fj>ti{;9dzdobD#G*j$g@x%gTH8)p+YGbrZIT4#76xaT?8e zZ2|L4k#bPhsmDGtl1L<~CF(~vm-+YraaAF , + description: "Run local LLMs using koboldcpp.", + requiredConfig: [ + "KoboldCPPModelPref", + "KoboldCPPBasePath", + "KoboldCPPTokenLimit", + ], + }, { name: "Cohere", value: "cohere", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index d0613b8c3..6e8a18974 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -15,6 +15,7 @@ import HuggingFaceLogo from "@/media/llmprovider/huggingface.png"; import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; +import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png"; import CohereLogo from "@/media/llmprovider/cohere.png"; import ZillizLogo from "@/media/vectordbs/zilliz.png"; import AstraDBLogo from "@/media/vectordbs/astraDB.png"; @@ -138,6 +139,13 @@ export const LLM_SELECTION_PRIVACY = { ], logo: GroqLogo, }, + koboldcpp: { + name: "KoboldCPP", + description: [ + "Your model and chats are only accessible on the server running KoboldCPP", + ], + logo: KoboldCPPLogo, + }, "generic-openai": { name: "Generic OpenAI compatible service", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 0e73c399f..4cf3c221e 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -15,6 +15,7 @@ import HuggingFaceLogo from "@/media/llmprovider/huggingface.png"; import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; +import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png"; import CohereLogo from "@/media/llmprovider/cohere.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; @@ -38,6 +39,7 @@ import System from "@/models/system"; import paths from "@/utils/paths"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; +import KoboldCPPOptions from "@/components/LLMSelection/KoboldCPPOptions"; const TITLE = "LLM Preference"; const DESCRIPTION = @@ -102,6 +104,13 @@ const LLMS = [ options: (settings) => , description: "Run LLMs locally on your own machine.", }, + { + name: "KoboldCPP", + value: "koboldcpp", + logo: KoboldCPPLogo, + options: (settings) => , + description: "Run local LLMs using koboldcpp.", + }, { name: "Together AI", value: "togetherai", diff --git a/server/.env.example b/server/.env.example index e515cc888..c8f05340a 100644 --- a/server/.env.example +++ b/server/.env.example @@ -63,6 +63,11 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # GROQ_API_KEY=gsk_abcxyz # GROQ_MODEL_PREF=llama3-8b-8192 +# LLM_PROVIDER='koboldcpp' +# KOBOLD_CPP_BASE_PATH='http://127.0.0.1:5000/v1' +# KOBOLD_CPP_MODEL_PREF='koboldcpp/codellama-7b-instruct.Q4_K_S' +# KOBOLD_CPP_MODEL_TOKEN_LIMIT=4096 + # LLM_PROVIDER='generic-openai' # GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1' # GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo' diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index dfbdb882f..f7782d26a 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -359,6 +359,11 @@ const SystemSettings = { HuggingFaceLLMAccessToken: !!process.env.HUGGING_FACE_LLM_API_KEY, HuggingFaceLLMTokenLimit: process.env.HUGGING_FACE_LLM_TOKEN_LIMIT, + // KoboldCPP Keys + KoboldCPPModelPref: process.env.KOBOLD_CPP_MODEL_PREF, + KoboldCPPBasePath: process.env.KOBOLD_CPP_BASE_PATH, + KoboldCPPTokenLimit: process.env.KOBOLD_CPP_MODEL_TOKEN_LIMIT, + // Generic OpenAI Keys GenericOpenAiBasePath: process.env.GENERIC_OPEN_AI_BASE_PATH, GenericOpenAiModelPref: process.env.GENERIC_OPEN_AI_MODEL_PREF, diff --git a/server/utils/AiProviders/koboldCPP/index.js b/server/utils/AiProviders/koboldCPP/index.js new file mode 100644 index 000000000..4b1ff3f61 --- /dev/null +++ b/server/utils/AiProviders/koboldCPP/index.js @@ -0,0 +1,180 @@ +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); +const { + clientAbortedHandler, + writeResponseChunk, +} = require("../../helpers/chat/responses"); +const { v4: uuidv4 } = require("uuid"); + +class KoboldCPPLLM { + constructor(embedder = null, modelPreference = null) { + const { OpenAI: OpenAIApi } = require("openai"); + if (!process.env.KOBOLD_CPP_BASE_PATH) + throw new Error( + "KoboldCPP must have a valid base path to use for the api." + ); + + this.basePath = process.env.KOBOLD_CPP_BASE_PATH; + this.openai = new OpenAIApi({ + baseURL: this.basePath, + apiKey: null, + }); + this.model = modelPreference ?? process.env.KOBOLD_CPP_MODEL_PREF ?? null; + if (!this.model) throw new Error("KoboldCPP must have a valid model set."); + 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 KoboldCPPLLM - falling back to NativeEmbedder for embedding!" + ); + this.embedder = !embedder ? new NativeEmbedder() : embedder; + this.defaultTemp = 0.7; + this.log(`Inference API: ${this.basePath} Model: ${this.model}`); + } + + log(text, ...args) { + console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); + } + + #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 "streamGetChatCompletion" in this; + } + + // Ensure the user set a value for the token limit + // and if undefined - assume 4096 window. + promptWindowLimit() { + const limit = process.env.KOBOLD_CPP_MODEL_TOKEN_LIMIT || 4096; + if (!limit || isNaN(Number(limit))) + throw new Error("No token context limit was set."); + return Number(limit); + } + + // Short circuit since we have no idea if the model is valid or not + // in pre-flight for generic endpoints + 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(_input = "") { + // Not implemented so must be stubbed + return { safe: true, reasons: [] }; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + const result = await this.openai.chat.completions + .create({ + model: this.model, + messages, + temperature, + }) + .catch((e) => { + throw new Error(e.response.data.error.message); + }); + + if (!result.hasOwnProperty("choices") || result.choices.length === 0) + return null; + return result.choices[0].message.content; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + const streamRequest = await this.openai.chat.completions.create({ + model: this.model, + stream: true, + messages, + temperature, + }); + return streamRequest; + } + + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + + // Custom handler for KoboldCPP stream responses + return new Promise(async (resolve) => { + let fullText = ""; + const handleAbort = () => clientAbortedHandler(resolve, fullText); + response.on("close", handleAbort); + + for await (const chunk of stream) { + const message = chunk?.choices?.[0]; + const token = message?.delta?.content; + + if (token) { + fullText += token; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + // KoboldCPP finishes with "length" or "stop" + if ( + message.finish_reason !== "null" && + (message.finish_reason === "length" || + message.finish_reason === "stop") + ) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + response.removeListener("close", handleAbort); + resolve(fullText); + } + } + }); + } + + // 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 = { + KoboldCPPLLM, +}; diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 1bb54170a..ce690ae47 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -14,6 +14,7 @@ const SUPPORT_CUSTOM_MODELS = [ "perplexity", "openrouter", "lmstudio", + "koboldcpp", ]; async function getCustomModels(provider = "", apiKey = null, basePath = null) { @@ -39,6 +40,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { return await getOpenRouterModels(); case "lmstudio": return await getLMStudioModels(basePath); + case "koboldcpp": + return await getKoboldCPPModels(basePath); default: return { models: [], error: "Invalid provider for custom models" }; } @@ -171,6 +174,28 @@ async function getLMStudioModels(basePath = null) { } } +async function getKoboldCPPModels(basePath = null) { + try { + const { OpenAI: OpenAIApi } = require("openai"); + const openai = new OpenAIApi({ + baseURL: basePath || process.env.LMSTUDIO_BASE_PATH, + apiKey: null, + }); + const models = await openai.models + .list() + .then((results) => results.data) + .catch((e) => { + console.error(`KoboldCPP:listModels`, e.message); + return []; + }); + + return { models, error: null }; + } catch (e) { + console.error(`KoboldCPP:getKoboldCPPModels`, e.message); + return { models: [], error: "Could not fetch KoboldCPP Models" }; + } +} + async function ollamaAIModels(basePath = null) { let url; try { diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 5d88040dc..ba65e3dfb 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -77,6 +77,9 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "groq": const { GroqLLM } = require("../AiProviders/groq"); return new GroqLLM(embedder, model); + case "koboldcpp": + const { KoboldCPPLLM } = require("../AiProviders/koboldCPP"); + return new KoboldCPPLLM(embedder, model); case "cohere": const { CohereLLM } = require("../AiProviders/cohere"); return new CohereLLM(embedder, model); diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 45f2fd546..19cdfe2b2 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -132,6 +132,20 @@ const KEY_MAPPING = { checks: [nonZero], }, + // KoboldCPP Settings + KoboldCPPBasePath: { + envKey: "KOBOLD_CPP_BASE_PATH", + checks: [isNotEmpty, isValidURL], + }, + KoboldCPPModelPref: { + envKey: "KOBOLD_CPP_MODEL_PREF", + checks: [isNotEmpty], + }, + KoboldCPPTokenLimit: { + envKey: "KOBOLD_CPP_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, + // Generic OpenAI InferenceSettings GenericOpenAiBasePath: { envKey: "GENERIC_OPEN_AI_BASE_PATH", @@ -403,6 +417,7 @@ function supportedLLM(input = "") { "perplexity", "openrouter", "groq", + "koboldcpp", "cohere", "generic-openai", ].includes(input); From 1aa8e5766f6ae7ce216006a8f963244cf7061517 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 2 May 2024 13:05:20 -0700 Subject: [PATCH 11/13] duplicate key (no impact) --- collector/utils/WhisperProviders/OpenAiWhisper.js | 1 - 1 file changed, 1 deletion(-) diff --git a/collector/utils/WhisperProviders/OpenAiWhisper.js b/collector/utils/WhisperProviders/OpenAiWhisper.js index 8460ffea0..fc163eddf 100644 --- a/collector/utils/WhisperProviders/OpenAiWhisper.js +++ b/collector/utils/WhisperProviders/OpenAiWhisper.js @@ -22,7 +22,6 @@ class OpenAiWhisper { .create({ file: fs.createReadStream(fullFilePath), model: this.model, - model: "whisper-1", response_format: "text", temperature: this.temperature, }) From 2d215acb75f13253b3a662a4bad89e62160bdebe Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 2 May 2024 14:03:10 -0700 Subject: [PATCH 12/13] patch storage dirs for extensions --- collector/utils/extensions/Confluence/index.js | 16 +++++++++++----- collector/utils/extensions/GithubRepo/index.js | 16 +++++++++++----- .../utils/extensions/YoutubeTranscript/index.js | 16 +++++++++++----- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/collector/utils/extensions/Confluence/index.js b/collector/utils/extensions/Confluence/index.js index 1ea642e1a..5a473f654 100644 --- a/collector/utils/extensions/Confluence/index.js +++ b/collector/utils/extensions/Confluence/index.js @@ -66,11 +66,17 @@ async function loadConfluence({ pageUrl, username, accessToken }) { const outFolder = slugify( `${subdomain}-confluence-${v4().slice(0, 4)}` ).toLowerCase(); - const outFolderPath = path.resolve( - __dirname, - `../../../../server/storage/documents/${outFolder}` - ); - fs.mkdirSync(outFolderPath); + + const outFolderPath = + process.env.NODE_ENV === "development" + ? path.resolve( + __dirname, + `../../../../server/storage/documents/${outFolder}` + ) + : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`); + + if (!fs.existsSync(outFolderPath)) + fs.mkdirSync(outFolderPath, { recursive: true }); docs.forEach((doc) => { const data = { diff --git a/collector/utils/extensions/GithubRepo/index.js b/collector/utils/extensions/GithubRepo/index.js index e5925f1d4..a694a8cdd 100644 --- a/collector/utils/extensions/GithubRepo/index.js +++ b/collector/utils/extensions/GithubRepo/index.js @@ -31,11 +31,17 @@ async function loadGithubRepo(args) { const outFolder = slugify( `${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}` ).toLowerCase(); - const outFolderPath = path.resolve( - __dirname, - `../../../../server/storage/documents/${outFolder}` - ); - fs.mkdirSync(outFolderPath); + + const outFolderPath = + process.env.NODE_ENV === "development" + ? path.resolve( + __dirname, + `../../../../server/storage/documents/${outFolder}` + ) + : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`); + + if (!fs.existsSync(outFolderPath)) + fs.mkdirSync(outFolderPath, { recursive: true }); for (const doc of docs) { if (!doc.pageContent) continue; diff --git a/collector/utils/extensions/YoutubeTranscript/index.js b/collector/utils/extensions/YoutubeTranscript/index.js index 10c08b61b..e5fa336ba 100644 --- a/collector/utils/extensions/YoutubeTranscript/index.js +++ b/collector/utils/extensions/YoutubeTranscript/index.js @@ -67,11 +67,17 @@ async function loadYouTubeTranscript({ url }) { const outFolder = slugify( `${metadata.author} YouTube transcripts` ).toLowerCase(); - const outFolderPath = path.resolve( - __dirname, - `../../../../server/storage/documents/${outFolder}` - ); - if (!fs.existsSync(outFolderPath)) fs.mkdirSync(outFolderPath); + + const outFolderPath = + process.env.NODE_ENV === "development" + ? path.resolve( + __dirname, + `../../../../server/storage/documents/${outFolder}` + ) + : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`); + + if (!fs.existsSync(outFolderPath)) + fs.mkdirSync(outFolderPath, { recursive: true }); const data = { id: v4(), From 0eb16f2c60f13f38d220e6f8e26473b02f164697 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 2 May 2024 15:51:56 -0700 Subject: [PATCH 13/13] fix readme bold lol --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a56d24ac1..d5e4c3013 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- AnythingLLM: The all-in-one AI app you were looking for.
+ AnythingLLM: The all-in-one AI app you were looking for.
Chat with your docs, use AI Agents, hyper-configurable, multi-user, & no fustrating set up required.