Files
memind/temporal-recall-service/rank.mjs
T
john 212ff3ff80 Add User Model Service and Temporal Recall for MeMind V0.1.
Introduce UMS ingest/snapshot pipeline, Context Planner with multi-source recall, runtime context injection, canonical user mapping, and session snapshot loading on auth/me.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 23:25:18 +08:00

92 lines
2.7 KiB
JavaScript

const SOURCE_QUALITY = {
calendar: 0.95,
chat: 0.85,
meinput: 0.82,
memory_v2: 0.78,
};
function parseMs(value) {
if (!value) return null;
const ms = new Date(value).getTime();
return Number.isNaN(ms) ? null : ms;
}
function temporalMatch(item, plan) {
const mode = plan.temporal_mode ?? 'AMBIGUOUS';
const mentionStart = parseMs(plan.time?.mention_range?.start);
const mentionEnd = parseMs(plan.time?.mention_range?.end);
const eventStart = parseMs(plan.time?.event_range?.start);
const eventEnd = parseMs(plan.time?.event_range?.end);
const observed = parseMs(item.observed_time);
const event = parseMs(item.event_time);
const inMention =
observed !== null && mentionStart !== null && mentionEnd !== null
? observed >= mentionStart && observed < mentionEnd
: 0.5;
const inEvent =
event !== null && eventStart !== null && eventEnd !== null
? event >= eventStart && event < eventEnd
: inMention;
switch (mode) {
case 'OCCURRED_IN':
return event !== null ? (inEvent ? 1 : 0.2) : inMention * 0.85;
case 'MENTIONED_IN':
case 'CREATED_IN':
return inMention ? 1 : 0.25;
case 'PLANNED_IN':
case 'DUE_IN':
return event !== null ? (inEvent ? 1 : 0.3) : inMention * 0.7;
default:
return Math.max(inMention, inEvent * 0.9);
}
}
function semanticMatch(item, expandedQueries = []) {
if (!expandedQueries?.length) return 0.75;
const text = `${item.title ?? ''} ${item.content ?? ''}`;
let hits = 0;
for (const q of expandedQueries) {
if (q && text.includes(q)) hits += 1;
}
return Math.min(1, 0.45 + hits * 0.12);
}
/**
* @param {object} item
* @param {object} plan
* @param {string[]} expandedQueries
*/
export function computeRecallScore(item, plan, expandedQueries = []) {
const source_quality = SOURCE_QUALITY[item.source] ?? 0.7;
const temporal_match = temporalMatch(item, plan);
const semantic_match = semanticMatch(item, expandedQueries);
const importance = Number(item.importance ?? 0.5);
const confidence = Number(item.confidence ?? 0.8);
const recall_score =
source_quality * temporal_match * semantic_match * importance * confidence;
return Number(Math.min(1, recall_score).toFixed(4));
}
/**
* @param {object[]} items
* @param {object} plan
*/
export function rankTimelineItems(items, plan) {
const retrievalQueries = Object.fromEntries(
(plan.retrievals ?? []).map((r) => [r.source, r.expanded_queries ?? []]),
);
return items
.map((item) => ({
...item,
recall_score: computeRecallScore(
item,
plan,
retrievalQueries[item.source] ?? [],
),
}))
.filter((item) => item.recall_score >= (plan.filters?.importance_min ?? 0.35) * 0.55)
.sort((a, b) => b.recall_score - a.recall_score);
}