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:
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() {