diff --git a/extension/src/apps-script/README.md b/extension/src/apps-script/README.md new file mode 100644 index 0000000..7ffec81 --- /dev/null +++ b/extension/src/apps-script/README.md @@ -0,0 +1,10 @@ +# Development Notes + +1. Copy the Code.gs, index.html files to the AppScript tab in doc/sheet: In the menu bar, `Extensions` -> `App Script`. + +2. If you need to run the backend locally, you have to local tunnel your server so that Google App Script has access to it +``` + ngrok http http://localhost:8000 +``` + +3. Replace the server URL in both web env file, and in the Code.gs file \ No newline at end of file diff --git a/extension/src/apps-script/gsheets/Code.gs b/extension/src/apps-script/gsheets/Code.gs index f6ba29e..a39fb95 100644 --- a/extension/src/apps-script/gsheets/Code.gs +++ b/extension/src/apps-script/gsheets/Code.gs @@ -1,23 +1,41 @@ -const LLM_RESPONSE_URL = "https://v1.minusxapi.com/planner/getLLMResponse"; +const BASE_URL = "https://v1.minusxapi.com"; +const LLM_RESPONSE_URL = BASE_URL + "/planner/getLLMResponse"; +const SCRAPE_URL = BASE_URL + "/web/scrape"; + + +function md5(inputString) { + return Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, inputString) + .reduce((output, byte) => output + (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0'), ''); +} + +function getCache(a1Notation){ + const key = "cell_" + a1Notation; + const cache = CacheService.getDocumentCache(); + const cachedInputOutput = cache.get(key); + if (cachedInputOutput === null){ + return null + } + else{ + return cachedInputOutput.split("") + } +} + +function updateCache(a1Notation, inputHash, output){ + const key = "cell_" + a1Notation; + const cache = CacheService.getDocumentCache(); + cache.put(key, inputHash + "" + output, 21600); +} function gsheetSetUserToken(token) { const scriptProperties = PropertiesService.getScriptProperties(); scriptProperties.setProperty('userToken', token); } -function MX_WEB(columns, query) { - // columns = [0.2, 0.3] - // query = "Is this less than majority chance?" +function queryContent(content, query){ const scriptProperties = PropertiesService.getScriptProperties(); const userToken = scriptProperties.getProperty('userToken'); - let dataArray; - if (Array.isArray(columns)) { - dataArray = columns.flat(); - } else { - dataArray = [columns]; - } systemMessage = `You are used as a google apps script function. You will be given an array of cell values and a query and your response will be used to set the current cell value. -${JSON.stringify(dataArray)} +${content} ${query}` let payload = { "messages": [ @@ -28,7 +46,7 @@ function MX_WEB(columns, query) { ], "actions": [], "llmSettings": { - "model": "gpt-4o-mini", + "model": "gpt-4o", "temperature": 0, "response_format": { "type": "text" diff --git a/extension/src/apps-script/gsheets/LiveFunctions.gs b/extension/src/apps-script/gsheets/LiveFunctions.gs new file mode 100644 index 0000000..7d1d6a9 --- /dev/null +++ b/extension/src/apps-script/gsheets/LiveFunctions.gs @@ -0,0 +1,107 @@ +/** + * Scrape the URL and answer the query + * + * @param {URL, query} Input the URL and the query. + * @return query response based on the contents of the URL + * @customfunction + */ + +function MX_WEBASK(url, query) { + const currentA1Notation = SpreadsheetApp.getActiveRange().getA1Notation(); + const hash = md5(url + "|" + query) + const cachedInputOutput = getCache(currentA1Notation); + let result; + if ((cachedInputOutput === null) || (cachedInputOutput[0] !== hash)) { + const webcontent = MX_WEBSCRAPE(url); + result = queryContent(webcontent, query); + } else { + result = cachedInputOutput[1]; + } + updateCache(currentA1Notation, hash, result); + return result; +} + +/** + * Scrape the URL + * + * @param {URL} Input the URL. + * @return The contents of the URL + * @customfunction + */ + +function MX_WEBSCRAPE(url) { + // Check if the parameter is a range by attempting to use getA1Notation + try { + if (url.getA1Notation) { + throw new Error("Input cannot be a range."); + } + } catch (e) { + // Not a range; proceed to check if it's a URL + } + + // Validate if the parameter is a URL + if (typeof url !== "string" || !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(url)) { + throw new Error("Input must be a valid URL."); + } + + // Fetch the URL and log the response + try { + const scriptProperties = PropertiesService.getScriptProperties(); + const userToken = scriptProperties.getProperty('userToken'); + let options = { + 'method': 'POST', + 'contentType': 'application/json', + 'payload': JSON.stringify({"URL": url}), + 'muteHttpExceptions': true, + 'headers': { + 'Authorization': `Bearer ${userToken}` + } + }; + let response = UrlFetchApp.fetch(SCRAPE_URL, options); + responseCode = response.getResponseCode() + let result = response.getContentText(); + if (responseCode == 200) { + return result + } else if (responseCode == 401) { + return 'Unauthorized. Please login to the MinusX sidebar' + } else if (responseCode == 402) { + return 'Credits Expired. Please add a membership to continue' + } else { + return `An error occured. Status Code: ${responseCode}` + } + } catch (e) { + Logger.log("Failed to fetch URL: " + e.message); + } +} + +/** + * Ask MinusX a question based on a selected range + * + * @param {columns, query} Input the range and query. + * @return The answer based on selected cells and query. + * @customfunction + */ + +function MX_ASK(columns, query) { + // columns = [0.2, 0.3] + // query = "Is this less than majority chance?" + let dataArray; + if (Array.isArray(columns)) { + dataArray = columns.flat(); + } else { + dataArray = [columns]; + } + const hash = md5(JSON.stringify(dataArray) + "|" + query) + const currentA1Notation = SpreadsheetApp.getActiveRange().getA1Notation(); + const cachedInputOutput = getCache(currentA1Notation); + let result; + if ((cachedInputOutput === null) || (cachedInputOutput[0] !== hash)) { + result = queryContent(JSON.stringify(dataArray), query); + } else { + result = cachedInputOutput[1]; + } + updateCache(currentA1Notation, hash, result); + return result; +} + +