commit 736918a3a718bf4ca1242c599213d2763b10140b
parent 30d7eab1713f0ef21a2b9021c01b1c7ff787f668
Author: Christopher DiPersio <cdipersio@mozilla.com>
Date: Wed, 10 Dec 2025 14:32:28 +0000
Bug 2005012 - Implement Insights Context Injection Function r=cgopal,ai-models-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D275647
Diffstat:
3 files changed, 231 insertions(+), 0 deletions(-)
diff --git a/browser/components/aiwindow/models/ChatUtils.sys.mjs b/browser/components/aiwindow/models/ChatUtils.sys.mjs
@@ -9,6 +9,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
PageDataService:
"moz-src:///browser/components/pagedata/PageDataService.sys.mjs",
+ InsightsManager:
+ "moz-src:///browser/components/aiwindow/models/InsightsManager.sys.mjs",
+ renderPrompt: "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs",
+ relevantInsightsContextPrompt:
+ "moz-src:///browser/components/aiwindow/models/prompts/insightsPrompts.sys.mjs",
});
/**
@@ -109,3 +114,38 @@ export async function constructRealTimeInfoInjectionMessage(depsOverride) {
content,
};
}
+
+/**
+ * Constructs the relevant insights context message to be inejcted before the user message.
+ *
+ * @param {string} message User message to find relevant insights for
+ * @returns {Promise<null|{role: string, tool_call_id: string, content: string}>} Relevant insights context message or null if no relevant insights
+ */
+export async function constructRelevantInsightsContextMessage(message) {
+ const relevantInsights =
+ await lazy.InsightsManager.getRelevantInsights(message);
+
+ // If there are relevant insights, render and return the context message
+ if (relevantInsights.length) {
+ const relevantInsightsList =
+ "- " +
+ relevantInsights
+ .map(insight => {
+ return insight.insight_summary;
+ })
+ .join("\n- ");
+ const content = await lazy.renderPrompt(
+ lazy.relevantInsightsContextPrompt,
+ {
+ relevantInsightsList,
+ }
+ );
+
+ return {
+ role: "system",
+ content,
+ };
+ }
+ // If there aren't any relevant insights, return null
+ return null;
+}
diff --git a/browser/components/aiwindow/models/prompts/insightsPrompts.sys.mjs b/browser/components/aiwindow/models/prompts/insightsPrompts.sys.mjs
@@ -201,3 +201,22 @@ Return ONLY JSON per the schema below.
"intents": ["<intent 1>", "<intent 2>", ...]
}
\`\`\``.trim();
+
+export const relevantInsightsContextPromptMetadata = {
+ version: "0.1",
+};
+
+export const relevantInsightsContextPrompt = `
+# Existing Insights
+
+Below is a list of existing insights:
+
+{relevantInsightsList}
+
+Use them to personalized your response using the following guidelines:
+
+1. Consider the user message below
+2. Choose SPECIFIC and RELEVANT insights from the list above to personalize your response to the user
+3. Write those SPECIFIC insights into your response to make it more helpful and tailored, then tag them AFTER your response using the format: \`§existing_insight: insight text§\`
+
+- NEVER tag insights you DID NOT USE in your response.`.trim();
diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js b/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js
@@ -8,13 +8,78 @@ const {
constructRealTimeInfoInjectionMessage,
getLocalIsoTime,
getCurrentTabMetadata,
+ constructRelevantInsightsContextMessage,
} = ChromeUtils.importESModule(
"moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs"
);
+const { InsightsManager } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/models/InsightsManager.sys.mjs"
+);
+const { InsightStore } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/services/InsightStore.sys.mjs"
+);
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
+/**
+ * Constants for test insights
+ */
+const TEST_INSIGHTS = [
+ {
+ insight_summary: "Loves drinking coffee",
+ category: "Food & Drink",
+ intent: "Plan / Organize",
+ score: 3,
+ },
+ {
+ insight_summary: "Buys dog food online",
+ category: "Pets & Animals",
+ intent: "Buy / Acquire",
+ score: 4,
+ },
+];
+
+/**
+ * Helper function bulk-add insights
+ */
+async function clearAndAddInsights() {
+ const insights = await InsightStore.getInsights();
+ for (const insight of insights) {
+ await InsightStore.hardDeleteInsight(insight.id);
+ }
+ for (const insight of TEST_INSIGHTS) {
+ await InsightStore.addInsight(insight);
+ }
+}
+
+/**
+ * Constants for preference keys and test values
+ */
+const PREF_API_KEY = "browser.aiwindow.apiKey";
+const PREF_ENDPOINT = "browser.aiwindow.endpoint";
+const PREF_MODEL = "browser.aiwindow.model";
+
+const API_KEY = "fake-key";
+const ENDPOINT = "https://api.fake-endpoint.com/v1";
+const MODEL = "fake-model";
+
+add_setup(async function () {
+ // Setup prefs used across multiple tests
+ Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
+ Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
+ Services.prefs.setStringPref(PREF_MODEL, MODEL);
+
+ // Clear prefs after testing
+ registerCleanupFunction(() => {
+ for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) {
+ if (Services.prefs.prefHasUserValue(pref)) {
+ Services.prefs.clearUserPref(pref);
+ }
+ }
+ });
+});
+
add_task(function test_getLocalIsoTime_returns_offset_timestamp() {
const sb = sinon.createSandbox();
const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
@@ -168,3 +233,110 @@ add_task(
}
}
);
+
+add_task(async function test_constructRelevantInsightsContextMessage() {
+ await clearAndAddInsights();
+
+ const sb = sinon.createSandbox();
+ try {
+ const fakeEngine = {
+ run() {
+ return {
+ finalOutput: `{
+ "categories": ["Food & Drink"],
+ "intents": ["Plan / Organize"]
+ }`,
+ };
+ },
+ };
+
+ // Stub the `ensureOpenAIEngine` method in InsightsManager
+ const stub = sb
+ .stub(InsightsManager, "ensureOpenAIEngine")
+ .returns(fakeEngine);
+
+ const relevantInsightsContextMessage =
+ await constructRelevantInsightsContextMessage("I love drinking coffee");
+ Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
+
+ // Check relevantInsightsContextMessage's top level structure
+ Assert.strictEqual(
+ typeof relevantInsightsContextMessage,
+ "object",
+ "Should return an object"
+ );
+ Assert.equal(
+ Object.keys(relevantInsightsContextMessage).length,
+ 2,
+ "Should have 2 keys"
+ );
+
+ // Check specific fields
+ Assert.equal(
+ relevantInsightsContextMessage.role,
+ "system",
+ "Should have role 'system'"
+ );
+ Assert.ok(
+ typeof relevantInsightsContextMessage.content === "string" &&
+ relevantInsightsContextMessage.content.length,
+ "Content should be a non-empty string"
+ );
+
+ const content = relevantInsightsContextMessage.content;
+ Assert.ok(
+ content.includes(
+ "Use them to personalized your response using the following guidelines:"
+ ),
+ "Relevant insights context prompt should pull from the correct base"
+ );
+ Assert.ok(
+ content.includes("- Loves drinking coffee"),
+ "Content should include relevant insight"
+ );
+ Assert.ok(
+ !content.includes("- Buys dog food online"),
+ "Content should not include non-relevant insight"
+ );
+ } finally {
+ sb.restore();
+ }
+});
+
+add_task(
+ async function test_constructRelevantInsightsContextMessage_no_relevant_insights() {
+ await clearAndAddInsights();
+
+ const sb = sinon.createSandbox();
+ try {
+ const fakeEngine = {
+ run() {
+ return {
+ finalOutput: `{
+ "categories": ["Health & Fitness"],
+ "intents": ["Plan / Organize"]
+ }`,
+ };
+ },
+ };
+
+ // Stub the `ensureOpenAIEngine` method in InsightsManager
+ const stub = sb
+ .stub(InsightsManager, "ensureOpenAIEngine")
+ .returns(fakeEngine);
+
+ const relevantInsightsContextMessage =
+ await constructRelevantInsightsContextMessage("I love drinking coffee");
+ Assert.ok(stub.calledOnce, "ensureOpenAIEngine should be called once");
+
+ // No relevant insights, so returned value should be null
+ Assert.equal(
+ relevantInsightsContextMessage,
+ null,
+ "Should return null when there are no relevant insights"
+ );
+ } finally {
+ sb.restore();
+ }
+ }
+);