tor-browser

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

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:
Mbrowser/base/content/test/static/browser_all_files_referenced.js | 4++++
Abrowser/components/aiwindow/models/InsightsChatSource.sys.mjs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/models/moz.build | 1+
Abrowser/components/aiwindow/models/tests/xpcshell/test_InsightsChatSource.js | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/models/tests/xpcshell/xpcshell.toml | 2++
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"]