From e2aa598f0169b3dbc95bf7a1783592920e122c6d Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 20 Aug 2024 18:35:37 +0200 Subject: [PATCH] ollama_api: first version with a working ollama call! see #79 --- _locales/en/messages.json | 4 ++ api_webchat/controller.js | 39 +++++++++-- api_webchat/model-worker-ollama.js | 107 +++++++++++++++++++++++++++++ js/api/ollama.js | 25 +++++++ mzta-background.js | 12 ++-- 5 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 api_webchat/model-worker-ollama.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4bc7c081..c38b896b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -474,5 +474,9 @@ "ollama_empty_model": { "message": "You've not choosen a model for the Ollama API. Please choose one in the options page.", "description": "" + }, + "ollama_api_connecting": { + "message": "Will attempt to connect to the Ollama Local Server using the host", + "description": "" } } \ No newline at end of file diff --git a/api_webchat/controller.js b/api_webchat/controller.js index 8943784d..1619fc65 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -20,11 +20,25 @@ * The original code has been released under the Apache License, Version 2.0. */ +// Get the LLM to be used +const urlParams = new URLSearchParams(window.location.search); +const llm = urlParams.get('llm'); +console.log(">>>>>>>>>>> llm: " + llm); // The controller wires up all the components and workers together, // managing the dependencies. A kind of "DI" class. -const worker = new Worker('model-worker-openai.js', { type: 'module' }); +let worker = null; + +switch (llm) { + case "chatgpt_api": + worker = new Worker('model-worker-openai.js', { type: 'module' }); + break; + case "ollama_api": { + worker = new Worker('model-worker-ollama.js', { type: 'module' }); + break; + } +} const messagesArea = document.querySelector('messages-area'); messagesArea.init(worker); @@ -55,11 +69,24 @@ let promptData = null; // ============================== TESTING - END const params = new URLSearchParams(window.location.search); -let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''}); -// const openaiApiKey = params.get('openapi-key'); -//console.log(">>>>>>>>>>> chatgpt_api_key: " + prefs_api_key.chatgpt_api_key); -worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model}); -messagesArea.appendUserMessage(browser.i18n.getMessage("chagtp_api_connecting"), "info"); + +switch (llm) { + case "chatgpt_api": + let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''}); + //console.log(">>>>>>>>>>> chatgpt_api_key: " + prefs_api_key.chatgpt_api_key); + worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model}); + messagesArea.appendUserMessage(browser.i18n.getMessage("chagpt_api_connecting"), "info"); + break; + case "ollama_api": { + let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: ''}); + //console.log(">>>>>>>>>>> ollama_host: " + prefs_api_key.ollama_host); + worker.postMessage({ type: 'init', ollama_host: prefs_api.ollama_host, ollama_model: prefs_api.ollama_model}); + messagesArea.appendUserMessage(browser.i18n.getMessage("ollama_api_connecting") + " " + prefs_api.ollama_host + " ...", "info"); + break; + } +} + + // Event listeners for worker messages worker.onmessage = function(event) { diff --git a/api_webchat/model-worker-ollama.js b/api_webchat/model-worker-ollama.js new file mode 100644 index 00000000..56e35006 --- /dev/null +++ b/api_webchat/model-worker-ollama.js @@ -0,0 +1,107 @@ +/* + * 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 { Ollama } from '../js/api/ollama.js'; + +let ollama_host = null; +let ollama_model = ''; +let ollama = null; + +let conversationHistory = []; +let assistantResponseAccumulator = ''; + +self.onmessage = async function(event) { + if (event.data.type === 'init') { + ollama_host = event.data.ollama_host; + ollama_model = event.data.ollama_model; + //console.log(">>>>>>>>>>> ollama_host: " + ollama_host); + ollama = new Ollama(ollama_host, ollama_model, true); + } else if (event.data.type === 'chatMessage') { + conversationHistory.push({ role: 'user', content: event.data.message }); + + const response = await ollama.fetchResponse(conversationHistory); //4096); + postMessage({ type: 'messageSent' }); + + if (!response.ok) { + let error_message = ''; + let errorDetail = ''; + if(response.is_exception === true){ + error_message = response.error; + }else{ + const errorJSON = await response.json(); + errorDetail = JSON.stringify(errorJSON); + error_message = errorJSON.error; + //console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message)); + } + postMessage({ type: 'error', payload: "Ollama API request failed: " + error_message }); + throw new Error("[ThunderAI] Ollama API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + // lots of low-level Ollama response parsing stuff + const chunk = decoder.decode(value); console.log(">>>>>>>>>>>>> chunk: " + chunk); + const lines = chunk.split("\n"); + const parsedLines = lines + .map((line) => line.replace(/^chunk: /, "").trim()) // Remove the "chunk: " prefix + .filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]" + .map((line) => JSON.parse(line)); // Parse the JSON string + + for (const parsedLine of parsedLines) { + const { response } = parsedLine; + // Update the UI with the new content + if (response) { + assistantResponseAccumulator += response; + postMessage({ type: 'newToken', payload: { token: response } }); + } + } + } + } +}; + +function parsePartialResponse(responseText) { + // Logica di parsing dei dati parziali + // Potresti voler spezzare i chunk in linee o altri delimitatori + const lines = responseText.split("\n"); + + // Filtro le linee valide e faccio il parsing di ogni linea JSON + return lines + .filter(line => line.trim().length > 0) + .map(line => { + try { + return JSON.parse(line); + } catch (e) { + console.warn('Error parsing line:', line); + return null; + } + }) + .filter(parsed => parsed !== null); +} \ No newline at end of file diff --git a/js/api/ollama.js b/js/api/ollama.js index 85085a80..33733308 100644 --- a/js/api/ollama.js +++ b/js/api/ollama.js @@ -55,4 +55,29 @@ export class Ollama { return output; } + + + fetchResponse = async (messages) => { + try { + const response = await fetch(this.host + "/api/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: this.model, + prompt: messages.join(' '), + stream: this.stream, + }), + }); + return response; + }catch (error) { + console.error("[ThunderAI] Ollama API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "Ollama API request failed: " + error; + return output; + } + } } \ No newline at end of file diff --git a/mzta-background.js b/mzta-background.js index 90bd2736..733b0083 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -208,7 +208,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ case 'chatgpt_api': // We are using the ChatGPT API let newWindow2 = await browser.windows.create({ - url: browser.runtime.getURL('api_webchat/index.html'), + url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type), type: "popup", width: prefs.chatgpt_win_width, height: prefs.chatgpt_win_height @@ -239,8 +239,8 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ } let mailMessage = await browser.messageDisplay.getDisplayedMessage(curr_tabId); - let mailMessageId = -1; - if(mailMessage) mailMessageId = mailMessage.id; + let mailMessageId2 = -1; + if(mailMessage) mailMessageId2 = mailMessage.id; // check if the config is present, or give a message error if (prefs.chatgpt_api_key == '') { @@ -252,13 +252,13 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ return; } - browser.tabs.sendMessage(createdTab2.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab2.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId2, do_custom_text: do_custom_text}); break; // chatgpt_api case 'ollama_api': // We are using the Ollama API let newWindow3 = await browser.windows.create({ - url: browser.runtime.getURL('api_webchat/index.html'), + url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type), type: "popup", width: prefs.chatgpt_win_width, height: prefs.chatgpt_win_height @@ -302,7 +302,7 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ return; } - browser.tabs.sendMessage(createdTab3.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId, do_custom_text: do_custom_text}); + browser.tabs.sendMessage(createdTab3.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId3, do_custom_text: do_custom_text}); break; // ollama_api } }