From 24227e48a73a63deadef4214e7a1ef85d3c3f40e Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Wed, 27 Dec 2023 17:08:03 -0800 Subject: [PATCH] Add LLM support for Google Gemini-Pro (#492) resolves #489 --- README.md | 1 + docker/.env.example | 4 + .../LLMSelection/GeminiLLMOptions/index.jsx | 43 ++++ frontend/src/media/llmprovider/gemini.png | Bin 0 -> 26348 bytes .../EmbeddingPreference/index.jsx | 8 +- .../GeneralSettings/LLMPreference/index.jsx | 22 +- .../GeneralSettings/VectorDatabase/index.jsx | 4 +- .../Steps/DataHandling/index.jsx | 9 + .../Steps/EmbeddingSelection/index.jsx | 4 +- .../Steps/LLMSelection/index.jsx | 16 +- server/.env.example | 4 + server/models/systemSettings.js | 14 ++ server/package.json | 3 +- server/utils/AiProviders/gemini/index.js | 200 ++++++++++++++++++ server/utils/chats/stream.js | 29 +++ server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 17 ++ server/yarn.lock | 5 + 18 files changed, 371 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/gemini.png create mode 100644 server/utils/AiProviders/gemini/index.js diff --git a/README.md b/README.md index 9ed7cc609..44e0557fa 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Some cool features of AnythingLLM - [OpenAI](https://openai.com) - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) - [Anthropic ClaudeV2](https://www.anthropic.com/) +- [Google Gemini Pro](https://ai.google.dev/) - [LM Studio (all models)](https://lmstudio.ai) - [LocalAi (all models)](https://localai.io/) diff --git a/docker/.env.example b/docker/.env.example index 8bbdd1dd6..cc9fa06fc 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -11,6 +11,10 @@ GID='1000' # OPEN_AI_KEY= # OPEN_MODEL_PREF='gpt-3.5-turbo' +# LLM_PROVIDER='gemini' +# GEMINI_API_KEY= +# GEMINI_LLM_MODEL_PREF='gemini-pro' + # LLM_PROVIDER='azure' # AZURE_OPENAI_ENDPOINT= # AZURE_OPENAI_KEY= diff --git a/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx new file mode 100644 index 000000000..4d09e0432 --- /dev/null +++ b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx @@ -0,0 +1,43 @@ +export default function GeminiLLMOptions({ settings }) { + return ( + <div className="w-full flex flex-col"> + <div className="w-full flex items-center gap-4"> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Google AI API Key + </label> + <input + type="password" + name="GeminiLLMApiKey" + className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5" + placeholder="Google Gemini API Key" + defaultValue={settings?.GeminiLLMApiKey ? "*".repeat(20) : ""} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Chat Model Selection + </label> + <select + name="GeminiLLMModelPref" + defaultValue={settings?.GeminiLLMModelPref || "gemini-pro"} + required={true} + className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5" + > + {["gemini-pro"].map((model) => { + return ( + <option key={model} value={model}> + {model} + </option> + ); + })} + </select> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/media/llmprovider/gemini.png b/frontend/src/media/llmprovider/gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..aa81cfd86f9766dee0ba7530deecd74fcf740f0d GIT binary patch literal 26348 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index 1abf3a4b4..d6224906c 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -46,10 +46,10 @@ export default function GeneralEmbeddingPreference() { const { error } = await System.updateSystem(settingsData); if (error) { - showToast(`Failed to save LLM settings: ${error}`, "error"); + showToast(`Failed to save embedding settings: ${error}`, "error"); setHasChanges(true); } else { - showToast("LLM preferences saved successfully.", "success"); + showToast("Embedding preferences saved successfully.", "success"); setHasChanges(false); } setSaving(false); @@ -132,7 +132,7 @@ export default function GeneralEmbeddingPreference() { <div className="text-white text-sm font-medium py-4"> Embedding Providers </div> - <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]"> + <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4"> <input hidden={true} name="EmbeddingEngine" @@ -174,7 +174,7 @@ export default function GeneralEmbeddingPreference() { onClick={updateChoice} /> </div> - <div className="mt-10 flex flex-wrap gap-4 max-w-[800px]"> + <div className="mt-10 flex flex-wrap gap-4"> {embeddingChoice === "native" && <NativeEmbeddingOptions />} {embeddingChoice === "openai" && ( <OpenAiOptions settings={settings} /> diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 1c18d1ff1..a0169fe15 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -7,6 +7,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import OpenAiLogo from "@/media/llmprovider/openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; +import GeminiLogo from "@/media/llmprovider/gemini.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import PreLoader from "@/components/Preloader"; @@ -17,6 +18,7 @@ import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions"; import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions"; import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions"; import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions"; +import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions"; export default function GeneralLLMPreference() { const [saving, setSaving] = useState(false); @@ -105,13 +107,13 @@ export default function GeneralLLMPreference() { <div className="text-white text-sm font-medium py-4"> LLM Providers </div> - <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]"> + <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4"> <input hidden={true} name="LLMProvider" value={llmChoice} /> <LLMProviderOption name="OpenAI" value="openai" link="openai.com" - description="The standard option for most non-commercial use. Provides both chat and embedding." + description="The standard option for most non-commercial use." checked={llmChoice === "openai"} image={OpenAiLogo} onClick={updateLLMChoice} @@ -120,7 +122,7 @@ export default function GeneralLLMPreference() { name="Azure OpenAI" value="azure" link="azure.microsoft.com" - description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding." + description="The enterprise option of OpenAI hosted on Azure services." checked={llmChoice === "azure"} image={AzureOpenAiLogo} onClick={updateLLMChoice} @@ -129,11 +131,20 @@ export default function GeneralLLMPreference() { name="Anthropic Claude 2" value="anthropic" link="anthropic.com" - description="A friendly AI Assistant hosted by Anthropic. Provides chat services only!" + description="A friendly AI Assistant hosted by Anthropic." checked={llmChoice === "anthropic"} image={AnthropicLogo} onClick={updateLLMChoice} /> + <LLMProviderOption + name="Google Gemini" + value="gemini" + link="ai.google.dev" + description="Google's largest and most capable AI model" + checked={llmChoice === "gemini"} + image={GeminiLogo} + onClick={updateLLMChoice} + /> <LLMProviderOption name="LM Studio" value="lmstudio" @@ -173,6 +184,9 @@ export default function GeneralLLMPreference() { {llmChoice === "anthropic" && ( <AnthropicAiOptions settings={settings} showAlert={true} /> )} + {llmChoice === "gemini" && ( + <GeminiLLMOptions settings={settings} /> + )} {llmChoice === "lmstudio" && ( <LMStudioOptions settings={settings} showAlert={true} /> )} diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 1635fef8b..2ddf1d5a7 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -55,10 +55,10 @@ export default function GeneralVectorDatabase() { const { error } = await System.updateSystem(settingsData); if (error) { - showToast(`Failed to save LLM settings: ${error}`, "error"); + showToast(`Failed to save vector database settings: ${error}`, "error"); setHasChanges(true); } else { - showToast("LLM preferences saved successfully.", "success"); + showToast("Vector database preferences saved successfully.", "success"); setHasChanges(false); } setSaving(false); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx index 98a1671cb..cd63d74d8 100644 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx @@ -4,6 +4,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import OpenAiLogo from "@/media/llmprovider/openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; +import GeminiLogo from "@/media/llmprovider/gemini.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import ChromaLogo from "@/media/vectordbs/chroma.png"; @@ -38,6 +39,14 @@ const LLM_SELECTION_PRIVACY = { ], logo: AnthropicLogo, }, + gemini: { + name: "Google Gemini", + description: [ + "Your chats are de-identified and used in training", + "Your prompts and document text are visible in responses to Google", + ], + logo: GeminiLogo, + }, lmstudio: { name: "LMStudio", description: [ diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx index 1f44c463b..98e1262a0 100644 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx @@ -76,7 +76,7 @@ function EmbeddingSelection({ nextStep, prevStep, currentStep }) { name="OpenAI" value="openai" link="openai.com" - description="The standard option for most non-commercial use. Provides both chat and embedding." + description="The standard option for most non-commercial use." checked={embeddingChoice === "openai"} image={OpenAiLogo} onClick={updateChoice} @@ -85,7 +85,7 @@ function EmbeddingSelection({ nextStep, prevStep, currentStep }) { name="Azure OpenAI" value="azure" link="azure.microsoft.com" - description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding." + description="The enterprise option of OpenAI hosted on Azure services." checked={embeddingChoice === "azure"} image={AzureOpenAiLogo} onClick={updateChoice} diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx index bb87486ba..f877e31db 100644 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx @@ -3,6 +3,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import OpenAiLogo from "@/media/llmprovider/openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; +import GeminiLogo from "@/media/llmprovider/gemini.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import System from "@/models/system"; @@ -14,6 +15,7 @@ import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions"; import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions"; import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions"; import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions"; +import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions"; function LLMSelection({ nextStep, prevStep, currentStep }) { const [llmChoice, setLLMChoice] = useState("openai"); @@ -71,7 +73,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { name="OpenAI" value="openai" link="openai.com" - description="The standard option for most non-commercial use. Provides both chat and embedding." + description="The standard option for most non-commercial use." checked={llmChoice === "openai"} image={OpenAiLogo} onClick={updateLLMChoice} @@ -80,7 +82,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { name="Azure OpenAI" value="azure" link="azure.microsoft.com" - description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding." + description="The enterprise option of OpenAI hosted on Azure services." checked={llmChoice === "azure"} image={AzureOpenAiLogo} onClick={updateLLMChoice} @@ -94,6 +96,15 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { image={AnthropicLogo} onClick={updateLLMChoice} /> + <LLMProviderOption + name="Google Gemini" + value="gemini" + link="ai.google.dev" + description="Google's largest and most capable AI model" + checked={llmChoice === "gemini"} + image={GeminiLogo} + onClick={updateLLMChoice} + /> <LLMProviderOption name="LM Studio" value="lmstudio" @@ -127,6 +138,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) { {llmChoice === "anthropic" && ( <AnthropicAiOptions settings={settings} /> )} + {llmChoice === "gemini" && <GeminiLLMOptions settings={settings} />} {llmChoice === "lmstudio" && ( <LMStudioOptions settings={settings} /> )} diff --git a/server/.env.example b/server/.env.example index a4bc9fe5b..f73e0e083 100644 --- a/server/.env.example +++ b/server/.env.example @@ -8,6 +8,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # OPEN_AI_KEY= # OPEN_MODEL_PREF='gpt-3.5-turbo' +# LLM_PROVIDER='gemini' +# GEMINI_API_KEY= +# GEMINI_LLM_MODEL_PREF='gemini-pro' + # LLM_PROVIDER='azure' # AZURE_OPENAI_ENDPOINT= # AZURE_OPENAI_KEY= diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 068359bb0..b5dfeb700 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -87,6 +87,20 @@ const SystemSettings = { } : {}), + ...(llmProvider === "gemini" + ? { + GeminiLLMApiKey: !!process.env.GEMINI_API_KEY, + GeminiLLMModelPref: + process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro", + + // For embedding credentials when Gemini is selected. + OpenAiKey: !!process.env.OPEN_AI_KEY, + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), + ...(llmProvider === "lmstudio" ? { LMStudioBasePath: process.env.LMSTUDIO_BASE_PATH, diff --git a/server/package.json b/server/package.json index 1100adbc3..4f84327a3 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.8.1", "@azure/openai": "^1.0.0-beta.3", + "@google/generative-ai": "^0.1.3", "@googleapis/youtube": "^9.0.0", "@pinecone-database/pinecone": "^0.1.6", "@prisma/client": "5.3.0", @@ -65,4 +66,4 @@ "nodemon": "^2.0.22", "prettier": "^2.4.1" } -} \ No newline at end of file +} diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js new file mode 100644 index 000000000..d0a76c550 --- /dev/null +++ b/server/utils/AiProviders/gemini/index.js @@ -0,0 +1,200 @@ +const { v4 } = require("uuid"); +const { chatPrompt } = require("../../chats"); + +class GeminiLLM { + constructor(embedder = null) { + if (!process.env.GEMINI_API_KEY) + throw new Error("No Gemini API key was set."); + + // Docs: https://ai.google.dev/tutorials/node_quickstart + const { GoogleGenerativeAI } = require("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + this.model = process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro"; + this.gemini = genAI.getGenerativeModel({ model: this.model }); + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + throw new Error( + "INVALID GEMINI LLM SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Gemini as your LLM." + ); + this.embedder = embedder; + this.answerKey = v4().split("-")[0]; + } + + streamingEnabled() { + return "streamChat" in this && "streamGetChatCompletion" in this; + } + + promptWindowLimit() { + switch (this.model) { + case "gemini-pro": + return 30_720; + default: + return 30_720; // assume a gemini-pro model + } + } + + isValidChatCompletionModel(modelName = "") { + const validModels = ["gemini-pro"]; + return validModels.includes(modelName); + } + + // Moderation cannot be done with Gemini. + // Not implemented so must be stubbed + async isSafe(_input = "") { + return { safe: true, reasons: [] }; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt} +Context: + ${contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("")}`, + }; + return [ + prompt, + { role: "assistant", content: "Okay." }, + ...chatHistory, + { role: "USER_PROMPT", content: userPrompt }, + ]; + } + + // This will take an OpenAi format message array and only pluck valid roles from it. + formatMessages(messages = []) { + // Gemini roles are either user || model. + // and all "content" is relabeled to "parts" + return messages + .map((message) => { + if (message.role === "system") + return { role: "user", parts: message.content }; + if (message.role === "user") + return { role: "user", parts: message.content }; + if (message.role === "assistant") + return { role: "model", parts: message.content }; + return null; + }) + .filter((msg) => !!msg); + } + + 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( + `Gemini chat: ${this.model} is not valid for chat completion!` + ); + + const prompt = messages.find( + (chat) => chat.role === "USER_PROMPT" + )?.content; + const chatThread = this.gemini.startChat({ + history: this.formatMessages(messages), + }); + 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 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 { type: "geminiStream", ...responseStream }; + } + + async streamGetChatCompletion(messages = [], _opts = {}) { + if (!this.isValidChatCompletionModel(this.model)) + throw new Error( + `Gemini chat: ${this.model} is not valid for chat completion!` + ); + + const prompt = messages.find( + (chat) => chat.role === "USER_PROMPT" + )?.content; + const chatThread = this.gemini.startChat({ + history: this.formatMessages(messages), + }); + const responseStream = await chatThread.sendMessageStream(prompt); + if (!responseStream.stream) + throw new Error("Could not stream response stream from Gemini."); + + return { type: "geminiStream", ...responseStream }; + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } + + // 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); + } +} + +module.exports = { + GeminiLLM, +}; diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 4eb9cf022..5bdb7a1f0 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -202,6 +202,35 @@ async function streamEmptyEmbeddingChat({ function handleStreamResponses(response, stream, responseProps) { const { uuid = uuidv4(), sources = [] } = responseProps; + // Gemini likes to return a stream asyncIterator which will + // be a totally different object than other models. + if (stream?.type === "geminiStream") { + return new Promise(async (resolve) => { + let fullText = ""; + for await (const chunk of stream.stream) { + fullText += chunk.text(); + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: chunk.text(), + close: false, + error: false, + }); + } + + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + resolve(fullText); + }); + } + // If stream is not a regular OpenAI Stream (like if using native model) // we can just iterate the stream content instead. if (!stream.hasOwnProperty("data")) { diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 3b7f4ccc2..115df4003 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -34,6 +34,9 @@ function getLLMProvider() { case "anthropic": const { AnthropicLLM } = require("../AiProviders/anthropic"); return new AnthropicLLM(embedder); + case "gemini": + const { GeminiLLM } = require("../AiProviders/gemini"); + return new GeminiLLM(embedder); case "lmstudio": const { LMStudioLLM } = require("../AiProviders/lmStudio"); return new LMStudioLLM(embedder); diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 3a8ea55dd..fe4f4f5c9 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -44,6 +44,15 @@ const KEY_MAPPING = { checks: [isNotEmpty, validAnthropicModel], }, + GeminiLLMApiKey: { + envKey: "GEMINI_API_KEY", + checks: [isNotEmpty], + }, + GeminiLLMModelPref: { + envKey: "GEMINI_LLM_MODEL_PREF", + checks: [isNotEmpty, validGeminiModel], + }, + // LMStudio Settings LMStudioBasePath: { envKey: "LMSTUDIO_BASE_PATH", @@ -204,12 +213,20 @@ function supportedLLM(input = "") { "openai", "azure", "anthropic", + "gemini", "lmstudio", "localai", "native", ].includes(input); } +function validGeminiModel(input = "") { + const validModels = ["gemini-pro"]; + return validModels.includes(input) + ? null + : `Invalid Model type. Must be one of ${validModels.join(", ")}.`; +} + function validAnthropicModel(input = "") { const validModels = ["claude-2", "claude-instant-1"]; return validModels.includes(input) diff --git a/server/yarn.lock b/server/yarn.lock index caffe137a..f9a621f69 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -140,6 +140,11 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@google/generative-ai@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.1.3.tgz#8e529d4d86c85b64d297b4abf1a653d613a09a9f" + integrity sha512-Cm4uJX1sKarpm1mje/MiOIinM7zdUUrQp/5/qGPAgznbdd/B9zup5ehT6c1qGqycFcSopTA1J1HpqHS5kJR8hQ== + "@googleapis/youtube@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@googleapis/youtube/-/youtube-9.0.0.tgz#e45f6f5f7eac198c6391782b94b3ca54bacf0b63" -- GitLab