commit 5c53e0d884d08a75f3cb03afd7d8ac3c8bcb3f86
parent 64712f35a3cfd3dc6cfa118d8c660fc15910670c
Author: Chidam Gopal <cgopal@mozilla.com>
Date: Fri, 12 Dec 2025 19:10:12 +0000
Bug 2000987 - Extract user messages from ChatStore r=cdipersio,ai-models-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D274865
Diffstat:
5 files changed, 276 insertions(+), 0 deletions(-)
diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -368,6 +368,10 @@ var allowlist = [
{
file: "moz-src:///browser/components/aiwindow/models/InsightsDriftDetector.sys.mjs",
},
+ // Bug 2000987 - get user messages from chat source
+ {
+ file: "moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs",
+ },
];
if (AppConstants.NIGHTLY_BUILD) {
diff --git a/browser/components/aiwindow/models/InsightsChatSource.sys.mjs b/browser/components/aiwindow/models/InsightsChatSource.sys.mjs
@@ -0,0 +1,105 @@
+/* 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/. */
+
+/**
+ * This module handles the user message extraction from chat store
+ */
+
+import {
+ ChatStore,
+ MESSAGE_ROLE,
+} from "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs";
+
+// Chat fetch defaults
+const DEFAULT_MAX_RESULTS = 50;
+const DEFAULT_HALF_LIFE_DAYS = 7;
+const MS_PER_SEC = 1_000;
+const SEC_PER_MIN = 60;
+const MINS_PER_HOUR = 60;
+const HOURS_PER_DAY = 24;
+
+/**
+ * Fetch recent user chat messages from the ChatStore and compute a freshness
+ * score for each one.
+ *
+ * Messages are fetched between `startTime` and "now" (Date.now()) and limited
+ * to the most recent `maxResults` entries. A per-message `freshness_score`
+ * in [0, 1] is computed using an exponential half-life decay over age in days.
+ *
+ * @param {number} [startTime=0]
+ * Inclusive start time in milliseconds since Unix epoch.
+ * @param {number} [maxResults=DEFAULT_MAX_RESULTS]
+ * Maximum number of most recent messages to return.
+ * @param {number} [halfLifeDays=DEFAULT_HALF_LIFE_DAYS]
+ * Half-life in days for the freshness decay function.
+ * @returns {Promise<Array<{
+ * createdDate: number,
+ * role: string,
+ * content: any,
+ * pageUrl: string | null,
+ * freshness_score: number
+ * }>>}
+ * Promise resolving to an array of mapped chat message objects.
+ */
+export async function getRecentChats(
+ startTime = 0,
+ maxResults = DEFAULT_MAX_RESULTS,
+ halfLifeDays = DEFAULT_HALF_LIFE_DAYS
+) {
+ const endTime = Date.now();
+ const chatStore = new ChatStore();
+ const messages = await chatStore.findMessagesByDate(
+ startTime,
+ endTime,
+ MESSAGE_ROLE.USER,
+ maxResults
+ );
+
+ const chatMessages = messages.map(msg => {
+ const createdDate = msg.createdDate;
+ const freshness_score = computeFreshnessScore(createdDate, halfLifeDays);
+ return {
+ createdDate,
+ role: msg.role,
+ content: msg.content?.body ?? null,
+ pageUrl: msg.pageUrl,
+ freshness_score,
+ };
+ });
+
+ return chatMessages;
+}
+
+/**
+ * Compute a freshness score for a message based on its age, using an
+ * exponential decay with a configurable half-life.
+ *
+ * The score is:
+ * -> 1.0 for messages with ageDays <= 0 (now or in the future)
+ * -> exp(-ln(2) * (ageDays / halfLifeDays)) for older messages,
+ * clamped into the [0, 1] range.
+ *
+ * @param {number|Date} createdDate
+ * Message creation time, either as a millisecond timestamp or a Date.
+ * @param {number} [halfLifeDays=DEFAULT_HALF_LIFE_DAYS]
+ * Half-life in days; larger values decay more slowly.
+ * @returns {number}
+ * Freshness score in the range [0, 1].
+ */
+export function computeFreshnessScore(
+ createdDate,
+ halfLifeDays = DEFAULT_HALF_LIFE_DAYS
+) {
+ const now = Date.now();
+ const createdMs =
+ typeof createdDate === "number" ? createdDate : createdDate.getTime();
+ const ageMs = now - createdMs;
+ const ageDays =
+ ageMs / (MS_PER_SEC * SEC_PER_MIN * MINS_PER_HOUR * HOURS_PER_DAY);
+ if (ageDays <= 0) {
+ return 1;
+ }
+ const raw = Math.exp(-Math.LN2 * (ageDays / halfLifeDays));
+ return Math.max(0, Math.min(1, raw));
+}
diff --git a/browser/components/aiwindow/models/moz.build b/browser/components/aiwindow/models/moz.build
@@ -13,6 +13,7 @@ MOZ_SRC_FILES += [
"Chat.sys.mjs",
"ChatUtils.sys.mjs",
"Insights.sys.mjs",
+ "InsightsChatSource.sys.mjs",
"InsightsConstants.sys.mjs",
"InsightsDriftDetector.sys.mjs",
"InsightsHistorySource.sys.mjs",
diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_InsightsChatSource.js b/browser/components/aiwindow/models/tests/xpcshell/test_InsightsChatSource.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+do_get_profile();
+
+const { getRecentChats, computeFreshnessScore } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/models/InsightsChatSource.sys.mjs"
+);
+const { ChatStore, ChatMessage, MESSAGE_ROLE } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const MS_PER_DAY = 1000 * 60 * 60 * 24;
+let sandbox;
+
+add_setup(function () {
+ sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+// past date check
+add_task(function test_computeFreshnessScore_past_date_check() {
+ const createdDate = Date.now() - 10 * MS_PER_DAY;
+ const score = computeFreshnessScore(createdDate, 7);
+
+ Assert.less(score, 0.5, "Freshness score should be < 0.5");
+});
+
+// future date check
+add_task(function test_computeFreshnessScore_future_date_check() {
+ const createdDate = Date.now() + 1 * MS_PER_DAY;
+ const score = computeFreshnessScore(createdDate, 7);
+ Assert.equal(score, 1, "Freshness score should be 1");
+});
+
+// current date check
+add_task(function test_computeFreshnessScore_current_date_check() {
+ const createdDate = Date.now();
+ const score = computeFreshnessScore(createdDate, 7);
+ Assert.equal(score, 1, "Freshness score should be 1");
+});
+
+// approx halflife check
+add_task(function test_computeFreshnessScore_halflife_approx_check() {
+ const createdDate = Date.now() - 7 * MS_PER_DAY;
+ const score = computeFreshnessScore(createdDate, 7);
+ // making sure that score in between 0.49 & 0.51 (closer to halflife)
+ Assert.less(score, 0.51, "Freshness score should be < 0.51");
+ Assert.greater(score, 0.49, "Freshness score should be > 0.49");
+});
+
+// older vs recent score check
+add_task(function test_computeFreshnessScore_older_vs_recent_check() {
+ const olderDate = Date.now() - 30 * MS_PER_DAY;
+ const recentDate = Date.now() - 1 * MS_PER_DAY;
+ const olderScore = computeFreshnessScore(olderDate, 7);
+ const recentScore = computeFreshnessScore(recentDate, 7);
+ Assert.less(olderScore, recentScore, "Older score should be < recent score");
+});
+
+add_task(async function test_getRecentChats_basic_mapping_and_limit() {
+ const fixedNow = 1_700_000_000_000;
+
+ const clock = sandbox.useFakeTimers({ now: fixedNow });
+
+ const messages = [
+ new ChatMessage({
+ createdDate: fixedNow - 1_000,
+ ordinal: 1,
+ role: MESSAGE_ROLE.USER,
+ content: { type: "text", body: "msg1" },
+ pageUrl: "https://example.com/1",
+ turnIndex: 0,
+ }),
+ new ChatMessage({
+ createdDate: fixedNow - 10_000,
+ ordinal: 2,
+ role: MESSAGE_ROLE.USER,
+ content: { type: "text", body: "msg2" },
+ pageUrl: "https://example.com/2",
+ turnIndex: 0,
+ }),
+ new ChatMessage({
+ createdDate: fixedNow - 100_000,
+ ordinal: 3,
+ role: MESSAGE_ROLE.USER,
+ content: { type: "text", body: "msg3" },
+ pageUrl: "https://example.com/3",
+ turnIndex: 0,
+ }),
+ ];
+
+ messages.forEach(msg => {
+ Assert.ok(
+ "createdDate" in msg,
+ "Test stub message should have createdDate (camelCase)"
+ );
+ Assert.ok(
+ msg.content &&
+ typeof msg.content === "object" &&
+ !Array.isArray(msg.content),
+ "msg.content should be an object, not an array"
+ );
+ Assert.ok("body" in msg.content, "msg.content should have a body field");
+ });
+
+ const maxResults = 3;
+ const halfLifeDays = 7;
+ const startTime = fixedNow - 1_000_000;
+
+ // Stub the method
+ const stub = sandbox
+ .stub(ChatStore.prototype, "findMessagesByDate")
+ .callsFake(async (startTimeArg, endTimeArg, roleArg, limitArg) => {
+ Assert.equal(
+ roleArg,
+ MESSAGE_ROLE.USER,
+ "Role passed to findMessagesByDate should be USER"
+ );
+ Assert.greaterOrEqual(
+ endTimeArg,
+ startTimeArg,
+ "endTime should be >= startTime"
+ );
+ Assert.equal(limitArg, maxResults, "limit should match maxResults");
+ return messages;
+ });
+
+ const result = await getRecentChats(startTime, maxResults, halfLifeDays);
+
+ // Assert stub was actually called
+ Assert.equal(stub.callCount, 1, "findMessagesByDate should be called once");
+
+ const [startTimeArg, , roleArg] = stub.firstCall.args;
+ Assert.equal(roleArg, MESSAGE_ROLE.USER, "Role should be USER");
+ Assert.equal(
+ startTimeArg,
+ fixedNow - 1_000_000,
+ "startTime should be fixedNow - 1_000_000"
+ );
+
+ Assert.equal(result.length, maxResults, "Should respect maxResults");
+
+ const first = result[0];
+ const second = result[1];
+
+ Assert.equal(first.content, "msg1");
+ Assert.equal(second.content, "msg2");
+
+ Assert.ok("freshness_score" in first);
+ Assert.greater(
+ first.freshness_score,
+ second.freshness_score,
+ "More recent message should have higher freshness_score"
+ );
+
+ clock.restore?.();
+});
diff --git a/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml b/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml
@@ -12,6 +12,8 @@ support-files = []
["test_Insights.js"]
+["test_InsightsChatSource.js"]
+
["test_InsightsDriftDetector.js"]
["test_InsightsHistorySource.js"]