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 }