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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 Z@F;oLc21yG$^U*NBQNtsx&~kz^gnxmi_HK4 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"