tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }