import { searchCalendar } from './adapters/calendar.mjs'; import { searchChat } from './adapters/chat.mjs'; import { searchMeinput } from './adapters/meinput.mjs'; import { buildContextPlan } from './context-planner.mjs'; import { dedupeTimelineItems } from './dedupe.mjs'; import { rankTimelineItems } from './rank.mjs'; const SOURCE_HANDLERS = { calendar: searchCalendar, meinput: searchMeinput, chat: searchChat, }; /** * @param {import('mysql2/promise').Pool | null} pool * @param {{ plan: object, userId: string, sessionId?: string, limit?: number }} opts */ export async function executeTemporalRecall(pool, opts) { const started = Date.now(); const plan = opts.plan; const userId = opts.userId; const limit = Math.min(200, Math.max(1, Number(opts.limit ?? plan.output?.max_items ?? 50))); const skipBelow = 0.3; const sourcesQueried = []; const tasks = (plan.retrievals ?? []) .filter((r) => (r.weight ?? 0) >= (r.skip_below ?? skipBelow)) .map(async (retrieval) => { const handler = SOURCE_HANDLERS[retrieval.source]; if (!handler) return []; sourcesQueried.push(retrieval.source); const ctx = { userId, pool, retrieval, time: plan.time, temporalMode: plan.temporal_mode, sessionId: opts.sessionId, }; if (retrieval.source === 'chat') return searchChat(pool, ctx); return handler(ctx); }); const batches = await Promise.all(tasks); const raw = batches.flat(); const ranked = rankTimelineItems(raw, plan); const deduped = plan.output?.dedupe !== false ? dedupeTimelineItems(ranked) : ranked; const returned = deduped.slice(0, limit); let groups = null; if (plan.temporal_mode === 'AMBIGUOUS' && plan.output?.timeline) { const mentionStart = new Date(plan.time.mention_range.start).getTime(); const mentionEnd = new Date(plan.time.mention_range.end).getTime(); const occurred = []; const mentioned = []; for (const item of returned) { const obs = new Date(item.observed_time).getTime(); if (item.event_time) { const ev = new Date(item.event_time).getTime(); if (ev >= mentionStart && ev < mentionEnd) occurred.push(item); else mentioned.push(item); } else if (obs >= mentionStart && obs < mentionEnd) { mentioned.push(item); } else { occurred.push(item); } } groups = [ { label: 'occurred_in_range', items: occurred }, { label: 'mentioned_or_planned', items: mentioned }, ]; } return { query_type: plan.query_type, temporal_mode: plan.temporal_mode, time_range: plan.time.mention_range, groups, items: groups ? [] : returned, stats: { sources_queried: sourcesQueried, raw_count: raw.length, deduped_count: deduped.length, returned_count: returned.length, elapsed_ms: Date.now() - started, }, plan, }; } /** * @param {import('mysql2/promise').Pool | null} pool * @param {{ query?: string, plan?: object, user_id: string, now?: Date, session_id?: string, limit?: number }} input */ export async function queryTemporalRecall(pool, input) { const userId = input.user_id; const plan = input.plan ?? buildContextPlan({ query: input.query ?? '', user_id: userId, now: input.now, }); if (plan.context_needs?.temporal_recall === 'SKIP') { return { query_type: plan.query_type, temporal_mode: plan.temporal_mode, time_range: plan.time?.mention_range ?? null, groups: null, items: [], stats: { sources_queried: [], raw_count: 0, deduped_count: 0, returned_count: 0, elapsed_ms: 0 }, plan, }; } return executeTemporalRecall(pool, { plan, userId, sessionId: input.session_id, limit: input.limit, }); } export { buildContextPlan };