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); }