tor-browser

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

commit 5c5400ae890826cab4efc1c7c280e36d2e03415f
parent 898aa6ef1ce8a2739a0d0a9b489014a3e11cca7d
Author: Christopher DiPersio <cdipersio@mozilla.com>
Date:   Tue, 16 Dec 2025 15:13:07 +0000

Bug 2006073 - Implement initial insights list creation from Chat history r=cgopal,ai-models-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D276462

Diffstat:
Mbrowser/components/aiwindow/models/Insights.sys.mjs | 27+++++++++++++++++++++++----
Mbrowser/components/aiwindow/models/InsightsConstants.sys.mjs | 1+
Mbrowser/components/aiwindow/models/tests/xpcshell/test_Insights.js | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 135 insertions(+), 6 deletions(-)

diff --git a/browser/components/aiwindow/models/Insights.sys.mjs b/browser/components/aiwindow/models/Insights.sys.mjs @@ -28,6 +28,8 @@ import { renderPrompt } from "./Utils.sys.mjs"; import { HISTORY, + CONVERSATION, + ALL_SOURCES, CATEGORIES, CATEGORIES_LIST, INTENTS, @@ -193,6 +195,18 @@ export async function renderRecentHistoryForPrompt( return finalCSV.trim(); } +export async function renderRecentConversationForPrompt(conversationMessages) { + let finalCSV = ""; + if (conversationMessages.length) { + let conversationRecordsTable = ["Message"]; + for (const message of conversationMessages) { + conversationRecordsTable.push(`${message.content}`); + } + finalCSV += "# Chat History\n" + conversationRecordsTable.join("\n"); + } + return finalCSV.trim(); +} + /** * Builds the initial insights generation prompt, pulling profile information based on given source * @@ -200,6 +214,12 @@ export async function renderRecentHistoryForPrompt( * @returns {Promise<string>} Promise resolving the generated insights generation prompt with profile records injected */ export async function buildInitialInsightsGenerationPrompt(sources) { + if (ALL_SOURCES.intersection(new Set(Object.keys(sources))).size === 0) { + throw new Error( + `No valid sources provided to build insights generation prompt: ${Object.keys(sources).join(", ")}` + ); + } + let profileRecordsRenderedStr = ""; // Allow for multiple sources in the future @@ -211,10 +231,9 @@ export async function buildInitialInsightsGenerationPrompt(sources) { searchItems ); } - - if (!profileRecordsRenderedStr) { - throw new Error( - `No valid sources provided to build insights generation prompt: ${Object.keys(sources).join(", ")}` + if (sources.hasOwnProperty(CONVERSATION)) { + profileRecordsRenderedStr += await renderRecentConversationForPrompt( + sources[CONVERSATION] ); } diff --git a/browser/components/aiwindow/models/InsightsConstants.sys.mjs b/browser/components/aiwindow/models/InsightsConstants.sys.mjs @@ -4,6 +4,7 @@ export const HISTORY = "history"; export const CONVERSATION = "conversation"; +export const ALL_SOURCES = new Set([HISTORY, CONVERSATION]); /** * Insight categories diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_Insights.js b/browser/components/aiwindow/models/tests/xpcshell/test_Insights.js @@ -2,6 +2,11 @@ * 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/. */ +do_get_profile(); + +const { ChatStore, ChatMessage, MESSAGE_ROLE } = ChromeUtils.importESModule( + "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs" +); const { getRecentHistory, generateProfileInputs, @@ -10,7 +15,9 @@ const { } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/models/InsightsHistorySource.sys.mjs" ); - +const { getRecentChats } = ChromeUtils.importESModule( + "moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs" +); const { openAIEngine } = ChromeUtils.importESModule( "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs" ); @@ -26,6 +33,7 @@ const { formatListForPrompt, getFormattedInsightAttributeList, renderRecentHistoryForPrompt, + renderRecentConversationForPrompt, mapFilteredInsightsToInitialList, buildInitialInsightsGenerationPrompt, buildInsightsDeduplicationPrompt, @@ -103,6 +111,9 @@ async function buildFakeBrowserHistory() { await PlacesUtils.history.insertMany(seeded); } +/** + * Shortcut for full browser history aggregation pipeline + */ async function getBrowserHistoryAggregates() { const profileRecords = await getRecentHistory(); const profilePreparedInputs = await generateProfileInputs(profileRecords); @@ -110,11 +121,47 @@ async function getBrowserHistoryAggregates() { profilePreparedInputs ); - //const [domainItems, titleItems, searchItems] = return await topkAggregates(domainAgg, titleAgg, searchAgg); } /** + * Builds fake chat history data for testing + */ +async function buildFakeChatHistory() { + const fixedNow = 1_700_000_000_000; + + return [ + new ChatMessage({ + createdDate: fixedNow - 1_000, + ordinal: 1, + role: MESSAGE_ROLE.USER, + content: { type: "text", body: "I like dogs." }, + pageUrl: "https://example.com/1", + turnIndex: 0, + }), + new ChatMessage({ + createdDate: fixedNow - 10_000, + ordinal: 2, + role: MESSAGE_ROLE.USER, + content: { type: "text", body: "I also like cats." }, + pageUrl: "https://example.com/2", + turnIndex: 0, + }), + new ChatMessage({ + createdDate: fixedNow - 100_000, + ordinal: 3, + role: MESSAGE_ROLE.USER, + content: { + type: "text", + body: "Tell me a joke about my favorite animals.", + }, + pageUrl: "https://example.com/3", + turnIndex: 0, + }), + ]; +} + +/** * Tests building the prompt for initial insights generation */ add_task(async function test_buildInitialInsightsGenerationPrompt() { @@ -252,6 +299,68 @@ Internet for people, not profit — Mozilla,100`.trim() }); /** + * Tests building the prompt for initial insights generation with only chat data + */ +add_task(async function test_buildInitialInsightsGenerationPrompt_only_chat() { + const messages = await buildFakeChatHistory(); + const sb = sinon.createSandbox(); + const maxResults = 3; + const halfLifeDays = 7; + const startTime = 1_700_000_000_000 - 1_000_000; + + try { + // Stub the method + const stub = sb + .stub(ChatStore.prototype, "findMessagesByDate") + .callsFake(async () => { + return messages; + }); + + const recentMessages = await getRecentChats( + startTime, + maxResults, + halfLifeDays + ); + + // Assert stub was actually called + Assert.equal(stub.callCount, 1, "findMessagesByDate should be called once"); + + // Double check we get only the 3 expected messages back + Assert.equal(recentMessages.length, 3, "Should return 3 chat messages"); + + // Render the messages into CSV format and check correctness + const renderedConversationHistory = + await renderRecentConversationForPrompt(recentMessages); + Assert.equal( + renderedConversationHistory, + `# Chat History +Message +I like dogs. +I also like cats. +Tell me a joke about my favorite animals.`.trim(), + "Rendered conversation history should match expected CSV format" + ); + + // Build the actual prompt and check its contents + const sources = { conversation: recentMessages }; + const initialInsightsPrompt = + await buildInitialInsightsGenerationPrompt(sources); + Assert.ok( + initialInsightsPrompt.includes( + "You are an expert at extracting insights from user browser data." + ), + "Initial insights generation prompt should pull from the correct base" + ); + Assert.ok( + initialInsightsPrompt.includes(renderedConversationHistory), + "Prompt should include rendered conversation history" + ); + } finally { + sb.restore(); + } +}); + +/** * Tests building the prompt for insights deduplication */ add_task(async function test_buildInsightsDeduplicationPrompt() {