tor-browser

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

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:
Mbrowser/components/aiwindow/models/ChatUtils.sys.mjs | 40++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/models/prompts/insightsPrompts.sys.mjs | 19+++++++++++++++++++
Mbrowser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); + } + } +);