commit 2f8ce2d3d1e117ebda8d02d185565b981cfa53ce
parent 7de886f333b2e9fadeff38ec4a715fbdbc2a9566
Author: Tom Zhang <tzhang@mozilla.com>
Date: Mon, 1 Dec 2025 18:57:49 +0000
Bug 2002840 - add function to return real time info injection message & tests r=ai-models-reviewers,cgopal
Differential Revision: https://phabricator.services.mozilla.com/D274355
Diffstat:
5 files changed, 292 insertions(+), 1 deletion(-)
diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -341,6 +341,10 @@ 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.mjs",
+ },
];
if (AppConstants.NIGHTLY_BUILD) {
diff --git a/browser/components/aiwindow/models/ChatUtils.mjs b/browser/components/aiwindow/models/ChatUtils.mjs
@@ -0,0 +1,111 @@
+/**
+ * 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/.
+ */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ PageDataService:
+ "moz-src:///browser/components/pagedata/PageDataService.sys.mjs",
+});
+
+/**
+ * Get the current local time in ISO format with timezone offset.
+ *
+ * @returns {string}
+ */
+export function getLocalIsoTime() {
+ try {
+ const date = new Date();
+ const tzOffsetMinutes = date.getTimezoneOffset();
+ const adjusted = new Date(date.getTime() - tzOffsetMinutes * 60000)
+ .toISOString()
+ .slice(0, 19); // Keep up to seconds
+ const sign = tzOffsetMinutes <= 0 ? "+" : "-";
+ const hours = String(Math.floor(Math.abs(tzOffsetMinutes) / 60)).padStart(
+ 2,
+ "0"
+ );
+ const minutes = String(Math.abs(tzOffsetMinutes) % 60).padStart(2, "0");
+ return `${adjusted}${sign}${hours}:${minutes}`;
+ } catch {
+ return null;
+ }
+}
+
+function resolveTabMetadataDependencies(overrides = {}) {
+ return {
+ BrowserWindowTracker:
+ overrides.BrowserWindowTracker ?? lazy.BrowserWindowTracker,
+ PageDataService: overrides.PageDataService ?? lazy.PageDataService,
+ };
+}
+
+/**
+ * Get current tab metadata: url, title, description if available.
+ *
+ * @param {object} [depsOverride]
+ * @returns {Promise<{url: string, title: string, description: string}>}
+ */
+export async function getCurrentTabMetadata(depsOverride) {
+ const { BrowserWindowTracker, PageDataService } =
+ resolveTabMetadataDependencies(depsOverride);
+ const win = BrowserWindowTracker.getTopWindow();
+ const browser = win?.gBrowser?.selectedBrowser;
+ if (!browser) {
+ return { url: "", title: "", description: "" };
+ }
+
+ const url = browser.currentURI?.spec || "";
+ const title = browser.contentTitle || browser.documentTitle || "";
+
+ let description = "";
+ if (url) {
+ description =
+ PageDataService.getCached(url)?.description ||
+ (await PageDataService.fetchPageData(url))?.description ||
+ "";
+ }
+
+ return { url, title, description };
+}
+
+/**
+ * Construct real time information injection message, to be inserted before
+ * the insights injection message and the user message in the conversation
+ * messages list.
+ *
+ * @param {object} [depsOverride]
+ * @returns {Promise<{role: string, content: string}>}
+ */
+export async function constructRealTimeInfoInjectionMessage(depsOverride) {
+ const { url, title, description } = await getCurrentTabMetadata(depsOverride);
+ const isoTimestamp = getLocalIsoTime();
+ const datePart = isoTimestamp?.split("T")[0] ?? "";
+ const locale = Services.locale.appLocaleAsBCP47;
+ const hasTabInfo = Boolean(url || title || description);
+ const tabSection = hasTabInfo
+ ? [
+ `Current active browser tab details:`,
+ `- URL: ${url}`,
+ `- Title: ${title}`,
+ `- Description: ${description}`,
+ ]
+ : [`No active browser tab.`];
+
+ const content = [
+ `Below are some real-time context details you can use to inform your response:`,
+ `Locale: ${locale}`,
+ `Current date & time in ISO format: ${isoTimestamp}`,
+ `Today's date: ${datePart || "Unavailable"}`,
+ ``,
+ ...tabSection,
+ ].join("\n");
+
+ return {
+ role: "system",
+ content,
+ };
+}
diff --git a/browser/components/aiwindow/models/moz.build b/browser/components/aiwindow/models/moz.build
@@ -5,7 +5,11 @@
with Files("**"):
BUG_COMPONENT = ("Core", "Machine Learning: General")
-MOZ_SRC_FILES += ["InsightsHistorySource.sys.mjs", "IntentClassifier.sys.mjs"]
+MOZ_SRC_FILES += [
+ "ChatUtils.mjs",
+ "InsightsHistorySource.sys.mjs",
+ "IntentClassifier.sys.mjs",
+]
XPCSHELL_TESTS_MANIFESTS += [
"tests/xpcshell/xpcshell.toml",
diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js b/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js
@@ -0,0 +1,170 @@
+/**
+ * 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/.
+ */
+
+const {
+ constructRealTimeInfoInjectionMessage,
+ getLocalIsoTime,
+ getCurrentTabMetadata,
+} = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/models/ChatUtils.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+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) });
+ try {
+ const iso = getLocalIsoTime();
+ Assert.ok(
+ typeof iso === "string" && !!iso.length,
+ "Should return a non-empty string"
+ );
+ Assert.ok(
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(iso),
+ "Should include date, time (up to seconds), and timezone offset"
+ );
+ } finally {
+ clock.restore();
+ sb.restore();
+ }
+});
+
+add_task(async function test_getCurrentTabMetadata_fetch_fallback() {
+ const sb = sinon.createSandbox();
+ const tracker = { getTopWindow: sb.stub() };
+ const pageData = {
+ getCached: sb.stub(),
+ fetchPageData: sb.stub(),
+ };
+ const fakeBrowser = {
+ currentURI: { spec: "https://example.com/article" },
+ contentTitle: "",
+ documentTitle: "Example Article",
+ };
+
+ tracker.getTopWindow.returns({
+ gBrowser: { selectedBrowser: fakeBrowser },
+ });
+ pageData.getCached.returns(null);
+ const fetchStub = pageData.fetchPageData.resolves({
+ description: "Fetched description",
+ });
+
+ try {
+ const result = await getCurrentTabMetadata({
+ BrowserWindowTracker: tracker,
+ PageDataService: pageData,
+ });
+ Assert.deepEqual(result, {
+ url: "https://example.com/article",
+ title: "Example Article",
+ description: "Fetched description",
+ });
+ Assert.ok(fetchStub.calledOnce, "Should fetch description when not cached");
+ } finally {
+ sb.restore();
+ }
+});
+
+add_task(
+ async function test_constructRealTimeInfoInjectionMessage_with_tab_info() {
+ const sb = sinon.createSandbox();
+ const tracker = { getTopWindow: sb.stub() };
+ const pageData = {
+ getCached: sb.stub(),
+ fetchPageData: sb.stub(),
+ };
+ const locale = Services.locale.appLocaleAsBCP47;
+ const fakeBrowser = {
+ currentURI: { spec: "https://mozilla.org" },
+ contentTitle: "Mozilla",
+ documentTitle: "Mozilla",
+ };
+
+ tracker.getTopWindow.returns({
+ gBrowser: { selectedBrowser: fakeBrowser },
+ });
+ pageData.getCached.returns({
+ description: "Internet for people",
+ });
+ const fetchStub = pageData.fetchPageData;
+ const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
+
+ try {
+ const message = await constructRealTimeInfoInjectionMessage({
+ BrowserWindowTracker: tracker,
+ PageDataService: pageData,
+ });
+ Assert.equal(message.role, "system", "Should return system role");
+ Assert.ok(
+ message.content.includes(`Locale: ${locale}`),
+ "Should include locale"
+ );
+ Assert.ok(
+ message.content.includes("Current active browser tab details:"),
+ "Should include tab details heading"
+ );
+ Assert.ok(
+ message.content.includes("- URL: https://mozilla.org"),
+ "Should include tab URL"
+ );
+ Assert.ok(
+ message.content.includes("- Title: Mozilla"),
+ "Should include tab title"
+ );
+ Assert.ok(
+ message.content.includes("- Description: Internet for people"),
+ "Should include tab description"
+ );
+ Assert.ok(
+ fetchStub.notCalled,
+ "Should not fetch when cached data exists"
+ );
+ } finally {
+ clock.restore();
+ sb.restore();
+ }
+ }
+);
+
+add_task(
+ async function test_constructRealTimeInfoInjectionMessage_without_tab_info() {
+ const sb = sinon.createSandbox();
+ const tracker = { getTopWindow: sb.stub() };
+ const pageData = {
+ getCached: sb.stub(),
+ fetchPageData: sb.stub(),
+ };
+ const locale = Services.locale.appLocaleAsBCP47;
+
+ tracker.getTopWindow.returns(null);
+ const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) });
+
+ try {
+ const message = await constructRealTimeInfoInjectionMessage({
+ BrowserWindowTracker: tracker,
+ PageDataService: pageData,
+ });
+ Assert.ok(
+ message.content.includes("No active browser tab."),
+ "Should mention missing tab info"
+ );
+ Assert.ok(
+ !message.content.includes("- URL:"),
+ "Should not include empty tab fields"
+ );
+ Assert.ok(
+ message.content.includes(`Locale: ${locale}`),
+ "Should include system locale"
+ );
+ } finally {
+ clock.restore();
+ sb.restore();
+ }
+ }
+);
diff --git a/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml b/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml
@@ -4,6 +4,8 @@ head = "head.js"
firefox-appdir = "browser"
support-files = []
+["test_ChatUtils.js"]
+
["test_InsightsHistorySource.js"]
["test_intent_classifier.js"]