commit aee7c0f24f488cd7f5a835803b48dd0c0cb2fd5f parent 9ec33b3c877d2327facf6c6c32c7bb9c6dad1153 Author: Serban Stanca <sstanca@mozilla.com> Date: Thu, 1 Jan 2026 08:54:23 +0200 Revert "Bug 2001508 - Add ChatConversation.generatePrompt to integrate LLM context generating functions and update Chat.fetchWithHistory to use ChatConversation for conversation state and prompt submission r=tzhang,ai-frontend-reviewers,ai-models-reviewers,Mardak" for causing xpcshell failures in test_Chat.js. This reverts commit 88e18c79d67d9a1376de1de4d0e48cebfe5fd976. Diffstat:
15 files changed, 266 insertions(+), 435 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -2257,7 +2257,7 @@ pref("browser.aiwindow.insights", false); pref("browser.aiwindow.insightsLogLevel", "Warn"); pref("browser.aiwindow.firstrun.autoAdvanceMS", 3000); pref("browser.aiwindow.firstrun.modelChoice", ""); -pref("browser.aiwindow.model", "qwen3-235b-a22b-instruct-2507-maas"); +pref("browser.aiwindow.model", ""); // Block insecure active content on https pages pref("security.mixed_content.block_active_content", true); diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js @@ -335,6 +335,14 @@ var allowlist = [ { file: "moz-src:///browser/components/aiwindow/models/IntentClassifier.sys.mjs", }, + // Bug 2002840 - add function to return real time info injection message & tests (backed out due to unused file) + { + file: "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs", + }, + // Bug 2003623 - Add assistant system prompt + { + file: "moz-src:///browser/components/aiwindow/models/prompts/AssistantPrompts.sys.mjs", + }, // Bug 2004888 - [FirstRun] Create Firstrun.html opening firstrun welcome screen { file: "chrome://browser/content/aiwindow/firstrun.html", @@ -343,6 +351,10 @@ var allowlist = [ { file: "moz-src:///browser/components/aiwindow/models/InsightsHistoryScheduler.sys.mjs", }, + // Bug 2000987 - get user messages from chat source + { + file: "moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs", + }, // Bug 2003303 - Implement Title Generation (backed out due to unused file) { file: "moz-src:///browser/components/aiwindow/models/TitleGeneration.sys.mjs", diff --git a/browser/components/aiwindow/models/Chat.sys.mjs b/browser/components/aiwindow/models/Chat.sys.mjs @@ -4,8 +4,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ToolRoleOpts } from "moz-src:///browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs"; - /* eslint-disable-next-line mozilla/reject-import-system-module-from-non-system */ import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; import { openAIEngine } from "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs"; @@ -51,21 +49,21 @@ export const Chat = { * we execute them locally, append results to the conversation, and continue * streaming the model’s follow-up answer. Repeats until no more tool calls. * - * @param {ChatConversation} conversation + * @param {Array<{role:string, content?:string, tool_call_id?:string, tool_calls?:any}>} messages * @yields {string} Assistant text chunks */ - async *fetchWithHistory(conversation) { + async *fetchWithHistory(messages) { + const engineInstance = await openAIEngine.build(); // Note FXA token fetching disabled for now - this is still in progress // We can flip this switch on when more realiable const fxAccountToken = await this._getFxAccountToken(); - // @todo Bug 2007046 - // Update this with correct model id - const modelId = "qwen3-235b-a22b-instruct-2507-maas"; - - const toolRoleOpts = new ToolRoleOpts(modelId); - const currentTurn = conversation.currentTurnIndex(); - const engineInstance = await openAIEngine.build(); + // We'll mutate a local copy of the thread as we loop + // We also filter out empty assistant messages because + // these kinds of messages can produce unexpected model responses + let convo = Array.isArray(messages) + ? messages.filter(msg => !(msg.role == "assistant" && !msg.content)) + : []; // Helper to run the model once (streaming) on current convo const streamModelResponse = () => @@ -74,7 +72,7 @@ export const Chat = { fxAccountToken, tool_choice: "auto", tools: toolsConfig, - args: conversation.getMessagesInOpenAiFormat(), + args: convo, }); // Keep calling until the model finishes without requesting tools @@ -100,23 +98,25 @@ export const Chat = { } // 3) Build the assistant tool_calls message exactly as expected by the API - // - // @todo Bug 2006159 - Implement parallel tool calling - // Temporarily only include the first tool call due to quality issue + // Bug 2006159 - Implement parallel tool calling + // TODO: Temporarily only include the first tool call due to quality issue // with subsequent tool call responses, will include all later once above // ticket is resolved. - const tool_calls = pendingToolCalls.slice(0, 1).map(toolCall => ({ - id: toolCall.id, - type: "function", - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, - })); - conversation.addAssistantMessage("function", { tool_calls }); + const assistantToolMsg = { + role: "assistant", + tool_calls: pendingToolCalls.slice(0, 1).map(toolCall => ({ + id: toolCall.id, + type: "function", + function: { + name: toolCall.function.name, + arguments: toolCall.function.arguments, + }, + })), + }; // 4) Execute each tool locally and create a tool message with the result // TODO: Temporarily only execute the first tool call, will run all later + const toolResultMessages = []; for (const toolCall of pendingToolCalls) { const { id, function: functionSpec } = toolCall; const name = functionSpec?.name || ""; @@ -127,11 +127,11 @@ export const Chat = { ? JSON.parse(functionSpec.arguments) : {}; } catch { - const content = { + toolResultMessages.push({ + role: "tool", tool_call_id: id, - body: { error: "Invalid JSON arguments" }, - }; - conversation.addToolCallMessage(content, currentTurn, toolRoleOpts); + content: JSON.stringify({ error: "Invalid JSON arguments" }), + }); continue; } @@ -146,17 +146,31 @@ export const Chat = { result = await toolFunc(toolParams); // Create special tool call log message to show in the UI log panel - const content = { tool_call_id: id, body: result }; - conversation.addToolCallMessage(content, currentTurn, toolRoleOpts); + const assistantToolCallLogMsg = { + role: "assistant", + content: `Tool Call: ${name} with parameters: ${JSON.stringify( + toolParams + )}`, + type: "tool_call_log", + result, + }; + convo.push(assistantToolCallLogMsg); + yield assistantToolCallLogMsg; } catch (e) { result = { error: `Tool execution failed: ${String(e)}` }; - const content = { tool_call_id: id, body: result }; - conversation.addToolCallMessage(content, currentTurn, toolRoleOpts); } + toolResultMessages.push({ + role: "tool", + tool_call_id: id, + content: typeof result === "string" ? result : JSON.stringify(result), + }); + // Bug 2006159 - Implement parallel tool calling, remove after implemented break; } + + convo = [...convo, assistantToolMsg, ...toolResultMessages]; } }, }; diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js b/browser/components/aiwindow/models/tests/xpcshell/test_Chat.js @@ -1,14 +1,6 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -do_get_profile(); - -const { ChatConversation } = ChromeUtils.importESModule( - "moz-src:///browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs" -); -const { SYSTEM_PROMPT_TYPE, MESSAGE_ROLE } = ChromeUtils.importESModule( - "moz-src:///browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs" -); const { Chat } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs" ); @@ -119,22 +111,14 @@ add_task(async function test_Chat_fetchWithHistory_streams_and_forwards_args() { sb.stub(openAIEngine, "build").resolves(fakeEngine); // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token"); - const conversation = new ChatConversation({ - title: "chat title", - description: "chat desc", - pageUrl: new URL("https://www.firefox.com"), - pageMeta: {}, - }); - conversation.addSystemMessage( - SYSTEM_PROMPT_TYPE.TEXT, - "You are helpful", - 0 - ); - conversation.addUserMessage("Hi there", "https://www.firefox.com", 0); + const messages = [ + { role: "system", content: "You are helpful" }, + { role: "user", content: "Hi there" }, + ]; // Collect streamed output let acc = ""; - for await (const chunk of Chat.fetchWithHistory(conversation)) { + for await (const chunk of Chat.fetchWithHistory(messages)) { if (typeof chunk === "string") { acc += chunk; } @@ -146,8 +130,8 @@ add_task(async function test_Chat_fetchWithHistory_streams_and_forwards_args() { "Should concatenate streamed chunks" ); Assert.deepEqual( - [capturedArgs[0].body, capturedArgs[1].body], - [conversation.messages[0].body, conversation.messages[1].body], + capturedArgs, + messages, "Should forward messages as args to runWithGenerator()" ); Assert.deepEqual( @@ -197,34 +181,18 @@ add_task(async function test_Chat_fetchWithHistory_handles_tool_calls() { sb.stub(openAIEngine, "build").resolves(fakeEngine); // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token"); - const conversation = new ChatConversation({ - title: "chat title", - description: "chat desc", - pageUrl: new URL("https://www.firefox.com"), - pageMeta: {}, - }); - conversation.addUserMessage( - "Use the test tool", - "https://www.firefox.com", - 0 - ); + const messages = [{ role: "user", content: "Use the test tool" }]; let textOutput = ""; - const toolCallLogs = []; - for await (const chunk of Chat.fetchWithHistory(conversation)) { + let toolCallLogs = []; + for await (const chunk of Chat.fetchWithHistory(messages)) { if (typeof chunk === "string") { textOutput += chunk; - } else if (chunk?.role === MESSAGE_ROLE.TOOL) { + } else if (chunk?.type === "tool_call_log") { toolCallLogs.push(chunk); } } - const toolCalls = conversation.messages.filter( - message => - message.role === MESSAGE_ROLE.ASSISTANT && - message?.content?.type === "function" - ); - Assert.equal( textOutput, "I'll help you with that. Tool executed successfully!", @@ -232,9 +200,7 @@ add_task(async function test_Chat_fetchWithHistory_handles_tool_calls() { ); Assert.equal(toolCallLogs.length, 1, "Should have one tool call log"); Assert.ok( - toolCalls[0].content.body.tool_calls[0].function.name.includes( - "test_tool" - ), + toolCallLogs[0].content.includes("test_tool"), "Tool call log should mention tool name" ); Assert.ok(Chat.toolMap.test_tool.calledOnce, "Tool should be called once"); @@ -262,16 +228,10 @@ add_task( sb.stub(openAIEngine, "build").rejects(err); // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token"); - const conversation = new ChatConversation({ - title: "chat title", - description: "chat desc", - pageUrl: new URL("https://www.firefox.com"), - pageMeta: {}, - }); - conversation.addUserMessage("Hi", "https://www.firefox.com", 0); + const messages = [{ role: "user", content: "Hi" }]; const consume = async () => { - for await (const _chunk of Chat.fetchWithHistory(conversation)) { + for await (const _chunk of Chat.fetchWithHistory(messages)) { void _chunk; } }; @@ -324,20 +284,10 @@ add_task( sb.stub(openAIEngine, "build").resolves(fakeEngine); // sb.stub(Chat, "_getFxAccountToken").resolves("mock_token"); - const conversation = new ChatConversation({ - title: "chat title", - description: "chat desc", - pageUrl: new URL("https://www.firefox.com"), - pageMeta: {}, - }); - conversation.addUserMessage( - "Test bad JSON", - "https://www.firefox.com", - 0 - ); + const messages = [{ role: "user", content: "Test bad JSON" }]; let textOutput = ""; - for await (const chunk of Chat.fetchWithHistory(conversation)) { + for await (const chunk of Chat.fetchWithHistory(messages)) { if (typeof chunk === "string") { textOutput += chunk; } diff --git a/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs b/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs @@ -6,7 +6,7 @@ * JSWindowActor to pass data between AIChatContent singleton and content pages. */ export class AIChatContentParent extends JSWindowActorParent { - dispatchMessageToChatContent(response) { - this.sendAsyncMessage("AIChatContent:DispatchMessage", response); + async dispatchMessageToChatContent(response) { + return this.sendQuery("AIChatContent:DispatchMessage", response); } } diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs @@ -51,9 +51,10 @@ export class AIChatContent extends MozLitElement { handleUserPromptEvent(event) { const { content } = event.detail; - - this.conversationState.push({ role: "user", content }); - + this.conversationState = [ + ...this.conversationState, + { role: "user", content }, + ]; this.requestUpdate(); } @@ -64,10 +65,17 @@ export class AIChatContent extends MozLitElement { */ handleAIResponseEvent(event) { - const { ordinal } = event.detail; - - this.conversationState[ordinal] = event.detail; - + const { content, latestAssistantMessageIndex } = event.detail; + if (!this.conversationState[latestAssistantMessageIndex]) { + this.conversationState[latestAssistantMessageIndex] = { + role: "assistant", + content: "", + }; + } + this.conversationState[latestAssistantMessageIndex] = { + ...this.conversationState[latestAssistantMessageIndex], + content, + }; this.requestUpdate(); } @@ -78,12 +86,9 @@ export class AIChatContent extends MozLitElement { href="chrome://browser/content/aiwindow/components/ai-chat-content.css" /> <div class="chat-content-wrapper"> - ${this.conversationState.map(msg => { - return html`<ai-chat-message - .message=${msg.content.body} - .role=${msg.role} - ></ai-chat-message>`; - })} + ${this.conversationState.map( + msg => html`<ai-chat-message .message=${msg}></ai-chat-message>` + )} </div> `; } diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs @@ -14,8 +14,7 @@ export class AIChatMessage extends MozLitElement { */ static properties = { - role: { type: String }, - message: { type: String }, + message: { type: Object }, }; constructor() { @@ -34,9 +33,9 @@ export class AIChatMessage extends MozLitElement { /> <article> - <div class=${"message-" + this.role}> + <div class=${"message-" + this.message.role}> <!-- TODO: Add markdown parsing here --> - ${this.message} + ${this.message.content} </div> </article> `; diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs b/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs @@ -8,24 +8,16 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Chat: "moz-src:///browser/components/aiwindow/models/Chat.sys.mjs", - AIWindow: - "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs", - ChatConversation: - "moz-src:///browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs", - MESSAGE_ROLE: - "moz-src:///browser/components/aiwindow/ui/modules//ChatEnums.sys.mjs", - AssistantRoleOpts: - "moz-src:///browser/components/aiwindow/ui/modules/ChatMessage.sys.mjs", - getRoleLabel: - "moz-src:///browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs", }); -ChromeUtils.defineLazyGetter(lazy, "log", function () { - return console.createInstance({ - prefix: "ChatStore", - maxLogLevelPref: "browser.aiwindow.chatStore.loglevel", - }); -}); +/** + * State Management Strategy: + * + * - On initialization, this component will call `.renderState()` from ChatStore to hydrate the UI + * - Currently using temporary local state (`this.conversationState`) that only tracks the current conversation turn + * - When ChatStore is integrated, rely on it as the source of truth for full conversation history + * - When calling Chat.sys.mjs to fetch responses, supplement the request with complete history from ChatStore + */ /** * A custom element for managing AI Window @@ -33,17 +25,14 @@ ChromeUtils.defineLazyGetter(lazy, "log", function () { export class AIWindow extends MozLitElement { static properties = { userPrompt: { type: String }, + conversationState: { type: Array }, }; - #browser; - #conversation; - constructor() { super(); - + this._browser = null; this.userPrompt = ""; - this.#browser = null; - this.#conversation = new lazy.ChatConversation({}); + this.conversationState = []; } connectedCallback() { @@ -65,21 +54,21 @@ export class AIWindow extends MozLitElement { const container = this.renderRoot.querySelector("#browser-container"); container.appendChild(browser); - this.#browser = browser; + this._browser = browser; } /** - * Persists the current conversation state to the database. + * Adds a new message to the conversation history. * - * @private + * @param {object} chatEntry - A message object to add to the conversation + * @param {("system"|"user"|"assistant")} chatEntry.role - The role of the message sender + * @param {string} chatEntry.content - The text content of the message */ - async #updateConversation() { - await lazy.AIWindow.chatStore - .updateConversation(this.#conversation) - .catch(updateError => { - lazy.log.error(`Error updating conversation: ${updateError.message}`); - }); - } + + // TODO - can remove this method after ChatStore is integrated + #updateConversationState = chatEntry => { + this.conversationState = [...this.conversationState, chatEntry]; + }; /** * Fetches an AI response based on the current user prompt. @@ -96,39 +85,39 @@ export class AIWindow extends MozLitElement { } // Handle User Prompt - this.#dispatchMessageToChatContent({ - role: lazy.MESSAGE_ROLE.USER, - content: { - body: this.userPrompt, - }, + await this.#dispatchMessageToChatContent({ + role: "user", + content: this.userPrompt, }); - const nextTurnIndex = this.#conversation.currentTurnIndex() + 1; - try { - const stream = lazy.Chat.fetchWithHistory( - await this.#conversation.generatePrompt(this.userPrompt) - ); - this.#updateConversation(); - - this.userPrompt = ""; - - // @todo - // fill out these assistant message flags - const assistantRoleOpts = new lazy.AssistantRoleOpts(); - this.#conversation.addAssistantMessage( - "text", - "", - nextTurnIndex, - assistantRoleOpts - ); - - for await (const chunk of stream) { - const currentMessage = this.#conversation.messages.at(-1); - currentMessage.content.body += chunk; + // TODO - can remove this call after ChatStore is integrated + this.#updateConversationState({ role: "user", content: formattedPrompt }); + this.userPrompt = ""; - this.#updateConversation(); - this.#dispatchMessageToChatContent(currentMessage); + // Create an empty assistant placeholder. + // TODO - can remove this call after ChatStore is integrated + this.#updateConversationState({ role: "assistant", content: "" }); + const latestAssistantMessageIndex = this.conversationState.length - 1; + let acc = ""; + try { + // TODO - replace with ChatStore integration IE pass chatstore.getConversationState(this.userPrompt) + const stream = lazy.Chat.fetchWithHistory(this.conversationState); + for await (const chunk of stream) { + acc += chunk; + + // TODO - can remove this after ChatStore is integrated + this.conversationState[latestAssistantMessageIndex] = { + ...this.conversationState[latestAssistantMessageIndex], + content: acc, + }; + + // TODO - can pass chatstore.getLastturnIndex() instead of latestAssistantMessageIndex after ChatStore is integrated + await this.#dispatchMessageToChatContent({ + role: "assistant", + content: acc, + latestAssistantMessageIndex, + }); this.requestUpdate?.(); } } catch (e) { @@ -144,23 +133,23 @@ export class AIWindow extends MozLitElement { * @private */ - #getAIChatContentActor() { - if (!this.#browser) { - lazy.log.warn("AI browser not set, cannot get AIChatContent actor"); + async #getAIChatContentActor() { + if (!this._browser) { + console.warn("AI browser not set, cannot get AIChatContent actor"); return null; } - const windowGlobal = this.#browser.browsingContext?.currentWindowGlobal; + const windowGlobal = this._browser.browsingContext?.currentWindowGlobal; if (!windowGlobal) { - lazy.log.warn("No window global found for AI browser"); + console.warn("No window global found for AI browser"); return null; } try { return windowGlobal.getActor("AIChatContent"); } catch (error) { - lazy.log.error("Failed to get AIChatContent actor:", error); + console.error("Failed to get AIChatContent actor:", error); return null; } } @@ -168,19 +157,13 @@ export class AIWindow extends MozLitElement { /** * Dispatches a message to the AIChatContent actor. * - * @param {ChatMessage} message - message to dispatch to chat content actor + * @param {object} message - message to dispatch to chat content actor * @returns */ - #dispatchMessageToChatContent(message) { - const actor = this.#getAIChatContentActor(); - - if (typeof message.role !== "string") { - const roleLabel = lazy.getRoleLabel(message.role).toLowerCase(); - message.role = roleLabel; - } - - return actor.dispatchMessageToChatContent(message); + async #dispatchMessageToChatContent(message) { + const actor = await this.#getAIChatContentActor(); + return await actor.dispatchMessageToChatContent(message); } /** diff --git a/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs b/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs @@ -7,7 +7,6 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const AIWINDOW_URL = "chrome://browser/content/aiwindow/aiWindow.html"; const AIWINDOW_URI = Services.io.newURI(AIWINDOW_URL); - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { ChatStore: diff --git a/browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs b/browser/components/aiwindow/ui/modules/ChatConstants.sys.mjs @@ -23,9 +23,45 @@ export const DB_FILE_NAME = "chat-store.sqlite"; */ export const PREF_BRANCH = "browser.aiWindow.chatHistory"; -export { - CONVERSATION_STATUS, - MESSAGE_ROLE, - INSIGHTS_FLAG_SOURCE, - SYSTEM_PROMPT_TYPE, -} from "./ChatEnums.sys.mjs"; +/** + * @typedef ConversationStatus + * @property {number} ACTIVE - An active conversation + * @property {number} ARCHIVE - An archived conversation + * @property {number} DELETED - A deleted conversation + */ + +/** + * @type {ConversationStatus} + */ +export const CONVERSATION_STATUS = Object.freeze({ + ACTIVE: 0, + ARCHIVED: 1, + DELETED: 2, +}); + +/** + * @typedef {0 | 1 | 2 | 3} MessageRole + */ + +/** + * @enum {MessageRole} + */ +export const MESSAGE_ROLE = Object.freeze({ + USER: 0, + ASSISTANT: 1, + SYSTEM: 2, + TOOL: 3, +}); + +/** + * @typedef {0 | 1 | 2} InsightsFlagSource + */ + +/** + * @type {InsightsFlagSource} + */ +export const INSIGHTS_FLAG_SOURCE = Object.freeze({ + GLOBAL: 0, + CONVERSATION: 1, + MESSAGE_ONCE: 2, +}); diff --git a/browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs b/browser/components/aiwindow/ui/modules/ChatConversation.sys.mjs @@ -3,19 +3,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { assistantPrompt } from "moz-src:///browser/components/aiwindow/models/prompts/AssistantPrompts.sys.mjs"; - -import { - constructRelevantInsightsContextMessage, - constructRealTimeInfoInjectionMessage, -} from "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs"; - -import { makeGuid, getRoleLabel } from "./ChatUtils.sys.mjs"; -import { - CONVERSATION_STATUS, - MESSAGE_ROLE, - SYSTEM_PROMPT_TYPE, -} from "./ChatConstants.sys.mjs"; +import { makeGuid } from "./ChatUtils.sys.mjs"; +import { CONVERSATION_STATUS, MESSAGE_ROLE } from "./ChatConstants.sys.mjs"; import { AssistantRoleOpts, ChatMessage, @@ -154,9 +143,15 @@ export class ChatConversation { * * @param {string} contentBody - The user message content * @param {string?} [pageUrl=""] - The current page url when message was submitted + * @param {number?} [turnIndex=0] - The conversation turn/cycle * @param {UserRoleOpts} [userOpts=new UserRoleOpts()] - User message options */ - addUserMessage(contentBody, pageUrl = "", userOpts = new UserRoleOpts()) { + addUserMessage( + contentBody, + pageUrl = "", + turnIndex = 0, + userOpts = new UserRoleOpts() + ) { const content = { type: "text", body: contentBody, @@ -164,11 +159,7 @@ export class ChatConversation { let url = URL.parse(pageUrl); - let currentTurn = this.currentTurnIndex(); - const newTurnIndex = - this.#messages.length === 1 ? currentTurn : currentTurn + 1; - - this.addMessage(MESSAGE_ROLE.USER, content, url, newTurnIndex, userOpts); + this.addMessage(MESSAGE_ROLE.USER, content, url, turnIndex, userOpts); } /** @@ -176,15 +167,17 @@ export class ChatConversation { * * @param {string} type - The assistant message type: text|function * @param {string} contentBody - The assistant message content + * @param {number} turnIndex - The current conversation turn/cycle * @param {AssistantRoleOpts} [assistantOpts=new AssistantRoleOpts()] - ChatMessage options specific to assistant messages */ addAssistantMessage( type, contentBody, + turnIndex, assistantOpts = new AssistantRoleOpts() ) { const content = { - type, + type: "text", body: contentBody, }; @@ -192,7 +185,7 @@ export class ChatConversation { MESSAGE_ROLE.ASSISTANT, content, "", - this.currentTurnIndex(), + turnIndex, assistantOpts ); } @@ -201,16 +194,11 @@ export class ChatConversation { * Add a tool call message to the conversation * * @param {object} content - The tool call object to be saved as JSON + * @param {number} turnIndex - The current conversation turn/cycle * @param {ToolRoleOpts} [toolOpts=new ToolRoleOpts()] - Message opts for a tool role message */ - addToolCallMessage(content, toolOpts = new ToolRoleOpts()) { - this.addMessage( - MESSAGE_ROLE.TOOL, - content, - "", - this.currentTurnIndex(), - toolOpts - ); + addToolCallMessage(content, turnIndex, toolOpts = new ToolRoleOpts()) { + this.addMessage(MESSAGE_ROLE.TOOL, content, "", turnIndex, toolOpts); } /** @@ -218,44 +206,12 @@ export class ChatConversation { * * @param {string} type - The assistant message type: text|injected_insights|injected_real_time_info * @param {string} contentBody - The system message object to be saved as JSON + * @param {number} turnIndex - The current conversation turn/cycle */ - addSystemMessage(type, contentBody) { + addSystemMessage(type, contentBody, turnIndex) { const content = { type, body: contentBody }; - this.addMessage(MESSAGE_ROLE.SYSTEM, content, "", this.currentTurnIndex()); - } - - /** - * Takes a new prompt and generates LLM context messages before - * adding new user prompt to messages. - * - * @param {string} prompt - new user prompt - * @param {URL} pageUrl - The URL of the page when prompt was submitted - */ - async generatePrompt(prompt, pageUrl) { - if (!this.#messages.length) { - this.addSystemMessage(SYSTEM_PROMPT_TYPE.TEXT, assistantPrompt); - } - - const nextConversationTurn = this.currentTurnIndex() + 1; - - const realTime = await constructRealTimeInfoInjectionMessage(); - if (realTime.content) { - this.addSystemMessage(SYSTEM_PROMPT_TYPE.REAL_TIME, realTime.content); - } - - const insightsContext = await constructRelevantInsightsContextMessage(); - if (insightsContext?.content) { - this.addSystemMessage( - SYSTEM_PROMPT_TYPE.INSIGHTS, - insightsContext.content, - nextConversationTurn - ); - } - - this.addUserMessage(prompt, pageUrl, nextConversationTurn); - - return this; + this.addMessage(MESSAGE_ROLE.SYSTEM, content, "", turnIndex); } /** @@ -301,26 +257,6 @@ export class ChatConversation { return sites.length ? sites.pop() : null; } - /** - * Converts the persisted message data to OpenAI API format - * - * @returns {Array<{ role: string, content: string }>} - */ - getMessagesInOpenAiFormat() { - return this.#messages - .filter(message => { - return !( - message.role === MESSAGE_ROLE.ASSISTANT && !message?.content?.body - ); - }) - .map(message => { - return { - role: getRoleLabel(message.role).toLowerCase(), - content: message.content?.body ?? message.content, - }; - }); - } - #updateActiveBranchTipMessageId() { this.activeBranchTipMessageId = this.messages .filter(m => m.isActiveBranch) diff --git a/browser/components/aiwindow/ui/modules/ChatEnums.sys.mjs b/browser/components/aiwindow/ui/modules/ChatEnums.sys.mjs @@ -1,60 +0,0 @@ -/* - This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -/** - * @typedef ConversationStatus - * @property {number} ACTIVE - An active conversation - * @property {number} ARCHIVE - An archived conversation - * @property {number} DELETED - A deleted conversation - */ - -/** - * @type {ConversationStatus} - */ -export const CONVERSATION_STATUS = Object.freeze({ - ACTIVE: 0, - ARCHIVED: 1, - DELETED: 2, -}); - -/** - * @typedef {0 | 1 | 2 | 3} MessageRole - */ - -/** - * @enum {MessageRole} - */ -export const MESSAGE_ROLE = Object.freeze({ - USER: 0, - ASSISTANT: 1, - SYSTEM: 2, - TOOL: 3, -}); - -/** - * @typedef {0 | 1 | 2} InsightsFlagSource - */ - -/** - * @type {InsightsFlagSource} - */ -export const INSIGHTS_FLAG_SOURCE = Object.freeze({ - GLOBAL: 0, - CONVERSATION: 1, - MESSAGE_ONCE: 2, -}); - -/** - * @typedef { "text" | "injected_insights" | "injected_real_time_info" } SystemPromptType - */ - -/** - * @type {SystemPromptType} - */ -export const SYSTEM_PROMPT_TYPE = Object.freeze({ - TEXT: "text", - INSIGHTS: "injected_insights", - REAL_TIME: "injected_real_time_info", -}); diff --git a/browser/components/aiwindow/ui/modules/moz.build b/browser/components/aiwindow/ui/modules/moz.build @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Machine Learning: Frontend") diff --git a/browser/components/aiwindow/ui/moz.build b/browser/components/aiwindow/ui/moz.build @@ -16,7 +16,6 @@ MOZ_SRC_FILES += [ "modules/AIWindowMenu.sys.mjs", "modules/ChatConstants.sys.mjs", "modules/ChatConversation.sys.mjs", - "modules/ChatEnums.sys.mjs", "modules/ChatMessage.sys.mjs", "modules/ChatMigrations.sys.mjs", "modules/ChatSql.sys.mjs", diff --git a/browser/components/aiwindow/ui/test/xpcshell/test_ChatConversation.js b/browser/components/aiwindow/ui/test/xpcshell/test_ChatConversation.js @@ -147,13 +147,13 @@ add_task(function test_ChatConversation_addUserMessage() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; - conversation.addUserMessage(content, "https://www.mozilla.com"); + conversation.addUserMessage(content, "https://www.mozilla.com", 0); const message = conversation.messages[0]; Assert.withSoftAssertions(function (soft) { soft.equal(message.role, MESSAGE_ROLE.USER); - soft.equal(message.turnIndex, 1); + soft.equal(message.turnIndex, 0); soft.deepEqual(message.pageUrl, new URL("https://www.mozilla.com")); soft.deepEqual(message.content, { type: "text", @@ -166,7 +166,7 @@ add_task(function test_revisionRootMessageId_ChatConversation_addUserMessage() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; - conversation.addUserMessage(content, "https://www.firefox.com"); + conversation.addUserMessage(content, "https://www.firefox.com", 0); const message = conversation.messages[0]; @@ -180,6 +180,7 @@ add_task(function test_opts_ChatConversation_addUserMessage() { conversation.addUserMessage( content, "https://www.firefox.com", + 0, new UserRoleOpts("321") ); @@ -192,7 +193,7 @@ add_task(function test_ChatConversation_addAssistantMessage() { const conversation = new ChatConversation({}); const content = "response from assistant"; - conversation.addAssistantMessage("text", content); + conversation.addAssistantMessage("text", content, 0); const message = conversation.messages[0]; @@ -243,7 +244,7 @@ add_task(function test_opts_ChatConversation_addAssistantMessage() { ["insight"], ["search"] ); - conversation.addAssistantMessage("text", content, assistantOpts); + conversation.addAssistantMessage("text", content, 0, assistantOpts); const message = conversation.messages[0]; @@ -299,7 +300,7 @@ add_task(function test_ChatConversation_addToolCallMessage() { const content = { random: "tool call specific keys", }; - conversation.addToolCallMessage(content); + conversation.addToolCallMessage(content, 0); const message = conversation.messages[0]; @@ -320,7 +321,7 @@ add_task(function test_opts_ChatConversation_addToolCallMessage() { const content = { random: "tool call specific keys", }; - conversation.addToolCallMessage(content, new ToolRoleOpts("the-model-id")); + conversation.addToolCallMessage(content, 0, new ToolRoleOpts("the-model-id")); const message = conversation.messages[0]; @@ -345,7 +346,7 @@ add_task(function test_ChatConversation_addSystemMessage() { const content = { random: "system call specific keys", }; - conversation.addSystemMessage("text", content); + conversation.addSystemMessage("text", content, 0); const message = conversation.messages[0]; @@ -364,12 +365,12 @@ add_task(function test_ChatConversation_getSitesList() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; - conversation.addUserMessage(content, "https://www.mozilla.com"); - conversation.addUserMessage(content, "https://www.mozilla.com"); - conversation.addUserMessage(content, "https://www.firefox.com"); - conversation.addUserMessage(content, "https://www.cnn.com"); - conversation.addUserMessage(content, "https://www.espn.com"); - conversation.addUserMessage(content, "https://www.espn.com"); + conversation.addUserMessage(content, "https://www.mozilla.com", 0); + conversation.addUserMessage(content, "https://www.mozilla.com", 1); + conversation.addUserMessage(content, "https://www.firefox.com", 2); + conversation.addUserMessage(content, "https://www.cnn.com", 3); + conversation.addUserMessage(content, "https://www.espn.com", 4); + conversation.addUserMessage(content, "https://www.espn.com", 5); const sites = conversation.getSitesList(); @@ -385,12 +386,12 @@ add_task(function test_ChatConversation_getMostRecentPageVisited() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; - conversation.addUserMessage(content, "https://www.mozilla.com"); - conversation.addUserMessage(content, "https://www.mozilla.com"); - conversation.addUserMessage(content, "https://www.firefox.com"); - conversation.addUserMessage(content, "https://www.cnn.com"); - conversation.addUserMessage(content, "https://www.espn.com"); - conversation.addUserMessage(content, "https://www.espn.com"); + conversation.addUserMessage(content, "https://www.mozilla.com", 0); + conversation.addUserMessage(content, "https://www.mozilla.com", 1); + conversation.addUserMessage(content, "https://www.firefox.com", 2); + conversation.addUserMessage(content, "https://www.cnn.com", 3); + conversation.addUserMessage(content, "https://www.espn.com", 4); + conversation.addUserMessage(content, "https://www.espn.com", 5); const mostRecentPageVisited = conversation.getMostRecentPageVisited(); @@ -401,9 +402,9 @@ add_task(function test_noBrowsing_ChatConversation_getMostRecentPageVisited() { const conversation = new ChatConversation({}); const content = "user to assistant msg"; - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addUserMessage(content, ""); - conversation.addUserMessage(content, null); + conversation.addUserMessage(content, "about:aiwindow", 0); + conversation.addUserMessage(content, "", 1); + conversation.addUserMessage(content, null, 2); const mostRecentPageVisited = conversation.getMostRecentPageVisited(); @@ -415,12 +416,12 @@ add_task(function test_ChatConversation_renderState() { const content = "user to assistant msg"; - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addToolCallMessage("some content"); - conversation.addAssistantMessage("text", "a response"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addSystemMessage("text", "some system message"); - conversation.addAssistantMessage("text", "a response"); + conversation.addUserMessage(content, "about:aiwindow", 0); + conversation.addToolCallMessage("some content", 0); + conversation.addAssistantMessage("text", "a response", 0); + conversation.addUserMessage(content, "about:aiwindow", 1); + conversation.addSystemMessage("text", "some system message", 1); + conversation.addAssistantMessage("text", "a response", 1); const renderState = conversation.renderState(); @@ -437,65 +438,16 @@ add_task(function test_ChatConversation_currentTurnIndex() { const content = "user to assistant msg"; - conversation.addSystemMessage("text", "the system prompt"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addAssistantMessage("text", "a response"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addAssistantMessage("text", "a response"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addAssistantMessage("text", "a response"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addAssistantMessage("text", "a response"); - conversation.addUserMessage(content, "about:aiwindow"); - conversation.addAssistantMessage("text", "a response"); + conversation.addUserMessage(content, "about:aiwindow", 0); + conversation.addAssistantMessage("text", "a response", 0); + conversation.addUserMessage(content, "about:aiwindow", 2); + conversation.addAssistantMessage("text", "a response", 2); + conversation.addUserMessage(content, "about:aiwindow", 1); + conversation.addAssistantMessage("text", "a response", 1); + conversation.addUserMessage(content, "about:aiwindow", 4); + conversation.addAssistantMessage("text", "a response", 4); + conversation.addUserMessage(content, "about:aiwindow", 3); + conversation.addAssistantMessage("text", "a response", 3); Assert.deepEqual(conversation.currentTurnIndex(), 4); }); - -add_task(function test_ChatConversation_helpersTurnIndexing() { - const conversation = new ChatConversation({}); - - conversation.addSystemMessage("text", "the system prompt"); - conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); - conversation.addToolCallMessage({ some: "tool call details" }); - conversation.addAssistantMessage("text", "the llm response"); - conversation.addUserMessage( - "a user's second prompt", - "https://www.somesite.com" - ); - conversation.addToolCallMessage({ some: "more tool call details" }); - conversation.addAssistantMessage("text", "the second llm response"); - - Assert.withSoftAssertions(function (soft) { - soft.equal(conversation.messages.length, 7); - - soft.equal(conversation.messages[0].turnIndex, 0); - soft.equal(conversation.messages[1].turnIndex, 0); - soft.equal(conversation.messages[2].turnIndex, 0); - soft.equal(conversation.messages[3].turnIndex, 0); - soft.equal(conversation.messages[4].turnIndex, 1); - soft.equal(conversation.messages[5].turnIndex, 1); - soft.equal(conversation.messages[6].turnIndex, 1); - }); -}); - -add_task(function test_ChatConversation_getMessagesInOpenAiFormat() { - const conversation = new ChatConversation({}); - conversation.addSystemMessage("text", "the system prompt"); - conversation.addUserMessage("a user's prompt", "https://www.somesite.com"); - conversation.addToolCallMessage({ some: "tool call details" }); - conversation.addAssistantMessage("text", "the llm response"); - conversation.addUserMessage("a user's second prompt", "some question"); - conversation.addToolCallMessage({ some: "more tool call details" }); - conversation.addAssistantMessage("text", "the second llm response"); - - const openAiFormat = conversation.getMessagesInOpenAiFormat(); - - Assert.deepEqual(openAiFormat, [ - { role: "system", content: "the system prompt" }, - { role: "user", content: "https://www.somesite.com" }, - { role: "assistant", content: "the llm response" }, - { role: "user", content: "some question" }, - { role: "assistant", content: "the second llm response" }, - ]); -});