ChatUtils.sys.mjs (7091B)
1 /** 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 */ 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 10 PageDataService: 11 "moz-src:///browser/components/pagedata/PageDataService.sys.mjs", 12 MemoriesManager: 13 "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs", 14 renderPrompt: "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs", 15 relevantMemoriesContextPrompt: 16 "moz-src:///browser/components/aiwindow/models/prompts/MemoriesPrompts.sys.mjs", 17 }); 18 19 /** 20 * Get the current local time in ISO format with timezone offset. 21 * 22 * @returns {string} 23 */ 24 export function getLocalIsoTime() { 25 try { 26 const date = new Date(); 27 const pad = n => String(n).padStart(2, "0"); 28 return ( 29 `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + 30 `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` 31 ); 32 } catch { 33 return null; 34 } 35 } 36 37 function resolveTabMetadataDependencies(overrides = {}) { 38 return { 39 BrowserWindowTracker: 40 overrides.BrowserWindowTracker ?? lazy.BrowserWindowTracker, 41 PageDataService: overrides.PageDataService ?? lazy.PageDataService, 42 }; 43 } 44 45 /** 46 * Get current tab metadata: url, title, description if available. 47 * 48 * @param {object} [depsOverride] 49 * @returns {Promise<{url: string, title: string, description: string}>} 50 */ 51 export async function getCurrentTabMetadata(depsOverride) { 52 const { BrowserWindowTracker, PageDataService } = 53 resolveTabMetadataDependencies(depsOverride); 54 const win = BrowserWindowTracker.getTopWindow(); 55 const browser = win?.gBrowser?.selectedBrowser; 56 if (!browser) { 57 return { url: "", title: "", description: "" }; 58 } 59 60 const url = browser.currentURI?.spec || ""; 61 const title = browser.contentTitle || browser.documentTitle || ""; 62 63 let description = ""; 64 if (url) { 65 const cachedData = PageDataService.getCached(url); 66 if (cachedData?.description) { 67 description = cachedData.description; 68 } else { 69 try { 70 const actor = 71 browser.browsingContext?.currentWindowGlobal?.getActor("PageData"); 72 if (actor) { 73 const pageData = await actor.collectPageData(); 74 description = pageData?.description || ""; 75 } 76 } catch (e) { 77 console.error( 78 "Failed to collect page description data from current tab:", 79 e 80 ); 81 } 82 } 83 } 84 85 return { url, title, description }; 86 } 87 88 /** 89 * Construct real time information injection message, to be inserted before 90 * the memories injection message and the user message in the conversation 91 * messages list. 92 * 93 * @param {object} [depsOverride] 94 * @returns {Promise<{role: string, content: string}>} 95 */ 96 export async function constructRealTimeInfoInjectionMessage(depsOverride) { 97 const { url, title, description } = await getCurrentTabMetadata(depsOverride); 98 const isoTimestamp = getLocalIsoTime(); 99 const datePart = isoTimestamp?.split("T")[0] ?? ""; 100 const locale = Services.locale.appLocaleAsBCP47; 101 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 102 const hasTabInfo = Boolean(url || title || description); 103 const tabSection = hasTabInfo 104 ? [ 105 `Current active browser tab details:`, 106 `- URL: ${url}`, 107 `- Title: ${title}`, 108 `- Description: ${description}`, 109 ] 110 : [`No active browser tab.`]; 111 112 const content = [ 113 `Below are some real-time context details you can use to inform your response:`, 114 `Locale: ${locale}`, 115 `Timezone: ${timezone}`, 116 `Current date & time in ISO format: ${isoTimestamp}`, 117 `Today's date: ${datePart || "Unavailable"}`, 118 ``, 119 ...tabSection, 120 ].join("\n"); 121 122 return { 123 role: "system", 124 content, 125 }; 126 } 127 128 /** 129 * Constructs the relevant memories context message to be inejcted before the user message. 130 * 131 * @param {string} message User message to find relevant memories for 132 * @returns {Promise<null|{role: string, tool_call_id: string, content: string}>} Relevant memories context message or null if no relevant memories 133 */ 134 export async function constructRelevantMemoriesContextMessage(message) { 135 const relevantMemories = 136 await lazy.MemoriesManager.getRelevantMemories(message); 137 138 // If there are relevant memories, render and return the context message 139 if (relevantMemories.length) { 140 const relevantMemoriesList = 141 "- " + 142 relevantMemories 143 .map(memory => { 144 return memory.memory_summary; 145 }) 146 .join("\n- "); 147 const content = await lazy.renderPrompt( 148 lazy.relevantMemoriesContextPrompt, 149 { 150 relevantMemoriesList, 151 } 152 ); 153 154 return { 155 role: "system", 156 content, 157 }; 158 } 159 // If there aren't any relevant memories, return null 160 return null; 161 } 162 163 /** 164 * Response parsing funtions to detect special tagged information like memories and search terms. 165 * Also return the cleaned content after removing all the taggings. 166 * 167 * @param {string} content 168 * @returns {Promise<object>} 169 */ 170 export async function parseContentWithTokens(content) { 171 const searchRegex = /§search:\s*([^§]+)§/gi; 172 const memoriesRegex = /§existing_memory:\s*([^§]+)§/gi; 173 174 const searchTokens = detectTokens(content, searchRegex, "query"); 175 const memoriesTokens = detectTokens(content, memoriesRegex, "memories"); 176 // Sort all tokens in reverse index order for easier removal 177 const allTokens = [...searchTokens, ...memoriesTokens].sort( 178 (a, b) => b.startIndex - a.startIndex 179 ); 180 181 if (allTokens.length === 0) { 182 return { 183 cleanContent: content, 184 searchQueries: [], 185 usedMemories: [], 186 }; 187 } 188 189 // Clean content by removing tagged information 190 let cleanContent = content; 191 const searchQueries = []; 192 const usedMemories = []; 193 194 for (const token of allTokens) { 195 if (token.query) { 196 searchQueries.unshift(token.query); 197 } else if (token.memories) { 198 usedMemories.unshift(token.memories); 199 // TODO: do we need customEvent to dispatch used memories as we iterate? 200 } 201 cleanContent = 202 cleanContent.slice(0, token.startIndex) + 203 cleanContent.slice(token.endIndex); 204 } 205 206 return { 207 cleanContent: cleanContent.trim(), 208 searchQueries, 209 usedMemories, 210 }; 211 } 212 213 /** 214 * Given the content and the regex pattern to search, find all occurrence of matches. 215 * 216 * @param {string} content 217 * @param {RegExp} regexPattern 218 * @param {string} key 219 * @returns {Array<object>} 220 */ 221 export function detectTokens(content, regexPattern, key) { 222 const matches = []; 223 let match; 224 while ((match = regexPattern.exec(content)) !== null) { 225 matches.push({ 226 fullMatch: match[0], 227 [key]: match[1].trim(), 228 startIndex: match.index, 229 endIndex: match.index + match[0].length, 230 }); 231 } 232 return matches; 233 }