tor-browser

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

ChatConversation.sys.mjs (9232B)


      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 https://mozilla.org/MPL/2.0/. */
      5 
      6 import { assistantPrompt } from "moz-src:///browser/components/aiwindow/models/prompts/AssistantPrompts.sys.mjs";
      7 
      8 import {
      9  constructRelevantMemoriesContextMessage,
     10  constructRealTimeInfoInjectionMessage,
     11 } from "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs";
     12 
     13 import { makeGuid, getRoleLabel } from "./ChatUtils.sys.mjs";
     14 import {
     15  CONVERSATION_STATUS,
     16  MESSAGE_ROLE,
     17  SYSTEM_PROMPT_TYPE,
     18 } from "./ChatConstants.sys.mjs";
     19 import {
     20  AssistantRoleOpts,
     21  ChatMessage,
     22  ToolRoleOpts,
     23  UserRoleOpts,
     24 } from "./ChatMessage.sys.mjs";
     25 
     26 const CHAT_ROLES = [MESSAGE_ROLE.USER, MESSAGE_ROLE.ASSISTANT];
     27 
     28 /**
     29 * A conversation containing messages.
     30 */
     31 export class ChatConversation {
     32  id;
     33  title;
     34  description;
     35  pageUrl;
     36  pageMeta;
     37  createdDate;
     38  updatedDate;
     39  status;
     40  #messages;
     41  activeBranchTipMessageId;
     42 
     43  /**
     44   * @param {object} params
     45   * @param {string} [params.id]
     46   * @param {string} params.title
     47   * @param {string} params.description
     48   * @param {URL} params.pageUrl
     49   * @param {object} params.pageMeta
     50   * @param {number} [params.createdDate]
     51   * @param {number} [params.updatedDate]
     52   * @param {CONVERSATION_STATUS} [params.status]
     53   * @param {Array<ChatMessage>} [params.messages]
     54   */
     55  constructor(params) {
     56    const {
     57      id = makeGuid(),
     58      title,
     59      description,
     60      pageUrl,
     61      pageMeta,
     62      createdDate = Date.now(),
     63      updatedDate = Date.now(),
     64      messages = [],
     65    } = params;
     66 
     67    this.id = id;
     68    this.title = title;
     69    this.description = description;
     70    this.pageUrl = pageUrl;
     71    this.pageMeta = pageMeta;
     72    this.createdDate = createdDate;
     73    this.updatedDate = updatedDate;
     74    this.#messages = messages;
     75 
     76    // NOTE: Destructuring params.status causes a linter error
     77    this.status = params.status || CONVERSATION_STATUS.ACTIVE;
     78  }
     79 
     80  /**
     81   * Returns a filtered messages array consisting only of the messages
     82   * that are meant to be rendered as the chat conversation.
     83   *
     84   * @returns {Array<ChatMessage>}
     85   */
     86  renderState() {
     87    const messages = this.#messages.filter(message => {
     88      return CHAT_ROLES.includes(message.role);
     89    });
     90 
     91    return messages;
     92  }
     93 
     94  /**
     95   * Returns the current turn index for the conversation
     96   *
     97   * @returns {number}
     98   */
     99  currentTurnIndex() {
    100    return this.#messages.reduce((turnIndex, message) => {
    101      return Math.max(turnIndex, message.turnIndex);
    102    }, 0);
    103  }
    104 
    105  /**
    106   * Adds a message to the conversation
    107   *
    108   * @param {ConversationRole} role - The type of conversation message
    109   * @param {object} content - The conversation message contents
    110   * @param {URL} pageUrl - The current page url when message was submitted
    111   * @param {number} turnIndex - The current conversation turn/cycle
    112   * @param {AssistantRoleOpts|ToolRoleOpts|UserRoleOpts} opts - Additional opts for the message
    113   */
    114  addMessage(role, content, pageUrl, turnIndex, opts = {}) {
    115    if (role < 0 || role > MESSAGE_ROLE.TOOL) {
    116      return;
    117    }
    118 
    119    if (turnIndex < 0) {
    120      turnIndex = 0;
    121    }
    122 
    123    let parentMessageId = null;
    124    if (this?.messages?.length) {
    125      const lastMessageIndex = this.messages.length - 1;
    126      parentMessageId = this.messages[lastMessageIndex].id;
    127    }
    128 
    129    const convId = this.id;
    130    const currentMessages = this?.messages || [];
    131    const ordinal = currentMessages.length ? currentMessages.length + 1 : 1;
    132 
    133    const message_data = {
    134      parentMessageId,
    135      content,
    136      ordinal,
    137      pageUrl,
    138      turnIndex,
    139      role,
    140      convId,
    141      ...opts,
    142    };
    143 
    144    const newMessage = new ChatMessage(message_data);
    145 
    146    this.messages.push(newMessage);
    147  }
    148 
    149  /**
    150   * Add a user message to the conversation
    151   *
    152   * @todo Bug 2005424
    153   * Limit/filter out data uris from message data
    154   *
    155   * @param {string} contentBody - The user message content
    156   * @param {string?} [pageUrl=""] - The current page url when message was submitted
    157   * @param {UserRoleOpts} [userOpts=new UserRoleOpts()] - User message options
    158   */
    159  addUserMessage(contentBody, pageUrl = "", userOpts = new UserRoleOpts()) {
    160    const content = {
    161      type: "text",
    162      body: contentBody,
    163    };
    164 
    165    let url = URL.parse(pageUrl);
    166 
    167    let currentTurn = this.currentTurnIndex();
    168    const newTurnIndex =
    169      this.#messages.length === 1 ? currentTurn : currentTurn + 1;
    170 
    171    this.addMessage(MESSAGE_ROLE.USER, content, url, newTurnIndex, userOpts);
    172  }
    173 
    174  /**
    175   * Add an assistant message to the conversation
    176   *
    177   * @param {string} type - The assistant message type: text|function
    178   * @param {string} contentBody - The assistant message content
    179   * @param {AssistantRoleOpts} [assistantOpts=new AssistantRoleOpts()] - ChatMessage options specific to assistant messages
    180   */
    181  addAssistantMessage(
    182    type,
    183    contentBody,
    184    assistantOpts = new AssistantRoleOpts()
    185  ) {
    186    const content = {
    187      type,
    188      body: contentBody,
    189    };
    190 
    191    this.addMessage(
    192      MESSAGE_ROLE.ASSISTANT,
    193      content,
    194      "",
    195      this.currentTurnIndex(),
    196      assistantOpts
    197    );
    198  }
    199 
    200  /**
    201   * Add a tool call message to the conversation
    202   *
    203   * @param {object} content - The tool call object to be saved as JSON
    204   * @param {ToolRoleOpts} [toolOpts=new ToolRoleOpts()] - Message opts for a tool role message
    205   */
    206  addToolCallMessage(content, toolOpts = new ToolRoleOpts()) {
    207    this.addMessage(
    208      MESSAGE_ROLE.TOOL,
    209      content,
    210      "",
    211      this.currentTurnIndex(),
    212      toolOpts
    213    );
    214  }
    215 
    216  /**
    217   * Add a system message to the conversation
    218   *
    219   * @param {string} type - The assistant message type: text|injected_insights|injected_real_time_info
    220   * @param {string} contentBody - The system message object to be saved as JSON
    221   */
    222  addSystemMessage(type, contentBody) {
    223    const content = { type, body: contentBody };
    224 
    225    this.addMessage(MESSAGE_ROLE.SYSTEM, content, "", this.currentTurnIndex());
    226  }
    227 
    228  /**
    229   * Takes a new prompt and generates LLM context messages before
    230   * adding new user prompt to messages.
    231   *
    232   * @param {string} prompt - new user prompt
    233   * @param {URL} pageUrl - The URL of the page when prompt was submitted
    234   */
    235  async generatePrompt(prompt, pageUrl) {
    236    if (!this.#messages.length) {
    237      // TODO: Bug 2008865
    238      // switch to use remote settings prompt accessed via engine.loadPrompt(feature)
    239      this.addSystemMessage(SYSTEM_PROMPT_TYPE.TEXT, assistantPrompt);
    240    }
    241 
    242    const nextConversationTurn = this.currentTurnIndex() + 1;
    243 
    244    const realTime = await constructRealTimeInfoInjectionMessage();
    245    if (realTime.content) {
    246      this.addSystemMessage(SYSTEM_PROMPT_TYPE.REAL_TIME, realTime.content);
    247    }
    248 
    249    const insightsContext = await constructRelevantMemoriesContextMessage();
    250    if (insightsContext?.content) {
    251      this.addSystemMessage(
    252        SYSTEM_PROMPT_TYPE.INSIGHTS,
    253        insightsContext.content,
    254        nextConversationTurn
    255      );
    256    }
    257 
    258    this.addUserMessage(prompt, pageUrl, nextConversationTurn);
    259 
    260    return this;
    261  }
    262 
    263  /**
    264   * Retrieves the list of visited sites during a conversation in visited order.
    265   * Primarily used to retrieve external URLs that the user had a conversation
    266   * around to display in Chat History view.
    267   *
    268   * @param {boolean} [includeInternal=false] - Whether to include internal Firefox URLs
    269   *
    270   * @returns {Array<URL>} - Ordered list of visited page URLs for this conversation
    271   */
    272  getSitesList(includeInternal = false) {
    273    const seen = new Set();
    274    const deduped = [];
    275 
    276    this.messages.forEach(message => {
    277      if (!message.pageUrl) {
    278        return;
    279      }
    280 
    281      if (!includeInternal && !message.pageUrl.protocol.startsWith("http")) {
    282        return;
    283      }
    284 
    285      if (!seen.has(message.pageUrl.href)) {
    286        seen.add(message.pageUrl.href);
    287        deduped.push(message.pageUrl);
    288      }
    289    });
    290 
    291    return deduped;
    292  }
    293 
    294  /**
    295   * Returns the most recently visited external sites during this conversation, or null
    296   * if no external sites have been visited.
    297   *
    298   * @returns {URL|null}
    299   */
    300  getMostRecentPageVisited() {
    301    const sites = this.getSitesList();
    302 
    303    return sites.length ? sites.pop() : null;
    304  }
    305 
    306  /**
    307   * Converts the persisted message data to OpenAI API format
    308   *
    309   * @returns {Array<{ role: string, content: string }>}
    310   */
    311  getMessagesInOpenAiFormat() {
    312    return this.#messages
    313      .filter(message => {
    314        return !(
    315          message.role === MESSAGE_ROLE.ASSISTANT && !message?.content?.body
    316        );
    317      })
    318      .map(message => {
    319        return {
    320          role: getRoleLabel(message.role).toLowerCase(),
    321          content: message.content?.body ?? message.content,
    322        };
    323      });
    324  }
    325 
    326  #updateActiveBranchTipMessageId() {
    327    this.activeBranchTipMessageId = this.messages
    328      .filter(m => m.isActiveBranch)
    329      .sort((a, b) => b.ordinal - a.ordinal)
    330      .shift()?.id;
    331  }
    332 
    333  set messages(value) {
    334    this.#messages = value;
    335    this.#updateActiveBranchTipMessageId();
    336  }
    337 
    338  get messages() {
    339    return this.#messages;
    340  }
    341 }