From d34569d86eba9c349b8cd5750da06cdc484a1f94 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 5 Jan 2025 23:24:17 +0100 Subject: [PATCH] starting to work on the gemini api. getting the models list in the options page. see #204 --- _locales/en/messages.json | 20 ++++ js/api/google_gemini.js | 75 ++++++++++++++ js/workers/model-worker-google_gemini.js | 124 +++++++++++++++++++++++ options/mzta-options.css | 13 +++ options/mzta-options.html | 28 +++++ options/mzta-options.js | 91 ++++++++++++++++- 6 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 js/api/google_gemini.js create mode 100644 js/workers/model-worker-google_gemini.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a41f2c60..629031f8 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -894,5 +894,25 @@ "prompt_add_tags_force_lang": { "message": "The tags must be written in", "description": "" + }, + "prefs_Connection_type_Google_Gemini_API": { + "message": "Google Gemini API", + "description": "" + }, + "prefs_GoogleGemini_API_Key": { + "message": "API Key", + "description": "" + }, + "GoogleGemini_Models": { + "message": "Google Gemini API Models", + "description": "" + }, + "GoogleGemini_Models_Fetch": { + "message": "Update Google Gemini Models list", + "description": "" + }, + "GoogleGemini_Models_Error_fetching": { + "message": "Error trying to fetch Google Gemini models", + "description": "" } } \ No newline at end of file diff --git a/js/api/google_gemini.js b/js/api/google_gemini.js new file mode 100644 index 00000000..ba8e7185 --- /dev/null +++ b/js/api/google_gemini.js @@ -0,0 +1,75 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + + +export class GoogleGemini { + + apiKey = ''; + model = ''; + stream = false; + + constructor(apiKey, model, stream) { + this.apiKey = apiKey; + this.model = model; + this.stream = stream; + } + + + fetchModels = async () => { + try{ + const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models?key=" + this.apiKey, { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + }); + + if (!response.ok) { + const errorDetail = await response.text(); + let err_msg = "[ThunderAI] Google Gemini API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail; + console.error(err_msg); + let output = {}; + output.ok = false; + output.error = errorDetail; + return output; + } + + let output = {}; + output.ok = true; + let output_response = await response.json(); + console.log("[ThunderAI] Google Gemini API response: " + JSON.stringify(output_response)); + output.response = output_response.models; + + return output; + }catch (error) { + console.error("[ThunderAI] Google Gemini API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "Google Gemini API request failed: " + error; + return output; + } + } + +//TODO + fetchResponse = async (messages, maxTokens = 0) => { + + } + +} \ No newline at end of file diff --git a/js/workers/model-worker-google_gemini.js b/js/workers/model-worker-google_gemini.js new file mode 100644 index 00000000..3f364682 --- /dev/null +++ b/js/workers/model-worker-google_gemini.js @@ -0,0 +1,124 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * This file contains a modified version of the code from the project at https://github.com/boxabirds/chatgpt-frontend-nobuild + * The original code has been released under the Apache License, Version 2.0. + */ + +import { OpenAI } from '../api/openai.js'; +import { taLogger } from '../mzta-logger.js'; + +let chatgpt_api_key = null; +let chatgpt_model = ''; +let openai = null; +let stopStreaming = false; +let i18nStrings = null; +let do_debug = false; +let taLog = null; + +let conversationHistory = []; +let assistantResponseAccumulator = ''; + +self.onmessage = async function(event) { + if (event.data.type === 'init') { + chatgpt_api_key = event.data.chatgpt_api_key; + chatgpt_model = event.data.chatgpt_model; + openai = new OpenAI(chatgpt_api_key, chatgpt_model, true); + do_debug = event.data.do_debug; + i18nStrings = event.data.i18nStrings; + taLog = new taLogger('model-worker-openai', do_debug); + } else if (event.data.type === 'chatMessage') { + conversationHistory.push({ role: 'user', content: event.data.message }); + + const response = await openai.fetchResponse(conversationHistory); //4096); + postMessage({ type: 'messageSent' }); + + if (!response.ok) { + let error_message = ''; + let errorDetail = ''; + if(response.is_exception === true){ + error_message = response.error; + }else{ + try{ + const errorJSON = await response.json(); + errorDetail = JSON.stringify(errorJSON); + error_message = errorJSON.error.message; + }catch(e){ + error_message = response.statusText; + } + taLog.log("error_message: " + JSON.stringify(error_message)); + } + postMessage({ type: 'error', payload: i18nStrings["chatgpt_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail }); + throw new Error("[ThunderAI] OpenAI ChatGPT API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ''; + + while (true) { + if (stopStreaming) { + stopStreaming = false; + reader.cancel(); + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + const { done, value } = await reader.read(); + if (done) { + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + // lots of low-level OpenAI response parsing stuff + const chunk = decoder.decode(value); + buffer += chunk; + taLog.log("buffer " + buffer); + const lines = buffer.split("\n"); + buffer = lines.pop(); + let parsedLines = []; + try{ + parsedLines = lines + .map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix + .filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]" + // .map((line) => JSON.parse(line)); // Parse the JSON string + .map((line) => { + taLog.log("line: " + JSON.stringify(line)); + return JSON.parse(line); + }); + }catch(e){ + taLog.error("Error parsing lines: " + e); + } + + for (const parsedLine of parsedLines) { + const { choices } = parsedLine; + const { delta } = choices[0]; + const { content } = delta; + // Update the UI with the new content + if (content) { + assistantResponseAccumulator += content; + postMessage({ type: 'newToken', payload: { token: content } }); + } + } + } + } else if (event.data.type === 'stop') { + stopStreaming = true; + } +}; diff --git a/options/mzta-options.css b/options/mzta-options.css index 77a05fef..3467488b 100644 --- a/options/mzta-options.css +++ b/options/mzta-options.css @@ -104,11 +104,20 @@ tr.conntype_openai_comp_api, tr.conntype_openai_comp_api2{ background-color: rgb(213, 169, 238); } +tr.conntype_google_gemini_api, tr.conntype_google_gemini_api2{ + background-color: rgb(233, 238, 169); +} + #chatgpt_model_fetch_loading{ display: none; font-style: italic; } +#google_gemini_model_fetch_loading{ + display: none; + font-style: italic; +} + .api_key-container { position: relative; width: 100%; @@ -218,6 +227,10 @@ input.option-input[type="text"]{ background-color: rgb(89, 20, 129); } + tr.conntype_google_gemini_api, tr.conntype_google_gemini_api2{ + background-color: rgb(72, 77, 3); + } + .api_key-container .toggle-icon img { filter: invert(1); } diff --git a/options/mzta-options.html b/options/mzta-options.html index dc54e2eb..5790b670 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -109,6 +109,7 @@ @@ -174,6 +175,33 @@ + + + +
+ + +
+ + + + + + + + __MSG_Loading__
+ + +