Files
memind/temporal-recall-service/context-planner.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

134 lines
4.1 KiB
JavaScript

import {
analyzeKeywords,
expandQueries,
inferTargets,
inferTemporalMode,
isTemporalRecallQuery,
} from './keyword-rules.mjs';
import { parseTimeScope } from './time-parser.mjs';
function envCalendarEnabled() {
const raw = String(process.env.MEMIND_TEMPORAL_RECALL_CALENDAR_ENABLED ?? '1').trim().toLowerCase();
return ['1', 'true', 'yes', 'on'].includes(raw);
}
/**
* @param {{ query: string, user_id: string, now?: Date, planner_level?: string }} input
*/
export function buildContextPlan(input) {
const query = String(input.query ?? '').trim();
const now = input.now instanceof Date ? input.now : new Date(input.now ?? Date.now());
const planner_level = input.planner_level ?? 'auto';
if (!query) {
throw new Error('invalid_query');
}
if (planner_level === 'semantic') {
const err = new Error('semantic planner not implemented in v0.1');
err.code = 'not_implemented';
throw err;
}
const temporal = isTemporalRecallQuery(query);
if (!temporal) {
return {
query_type: 'general_chat',
time: {
mention_range: parseTimeScope('这周', now).mention_range,
event_range: null,
relative_label: 'this_week',
},
temporal_mode: 'AMBIGUOUS',
targets: [{ type: 'mention', weight: 0.5 }],
sources: { calendar: 0.2, chat: 0.4, meinput: 0.4, memory_v2: 0.3 },
retrievals: [],
filters: { importance_min: 0.35, status: 'any' },
output: { group_by: 'importance', dedupe: true, timeline: true, wide_recall: true, max_items: 50 },
context_needs: {
user_snapshot: 'REQUIRED',
temporal_recall: 'SKIP',
memory_retrieval: 'SKIP',
},
planner_meta: { level: 'cheap_router', rule_hits: ['query:general_chat'], confidence: 0.9 },
};
}
const time = parseTimeScope(query, now);
const kw = analyzeKeywords(query);
const targets = inferTargets(query, kw);
const temporal_mode = inferTemporalMode(query);
const rule_hits = [...time.rule_hits, ...kw.rule_hits, `temporal_mode:${temporal_mode}`];
const sources = { ...kw.sources };
if (temporal_mode === 'PLANNED_IN' || temporal_mode === 'DUE_IN') {
sources.calendar = Math.min(1, sources.calendar + 0.2);
}
if (temporal_mode === 'MENTIONED_IN' || temporal_mode === 'CREATED_IN') {
sources.meinput = Math.min(1, sources.meinput + 0.15);
sources.chat = Math.min(1, sources.chat + 0.15);
}
const skip_below = 0.3;
const retrievals = [];
for (const [source, weight] of Object.entries(sources)) {
if (weight < skip_below) continue;
if (source === 'calendar' && !envCalendarEnabled()) continue;
retrievals.push({
source,
query_type:
source === 'meinput'
? 'important_mentions'
: source === 'chat'
? 'commitments_and_decisions'
: source === 'calendar'
? 'events'
: 'time_bounded',
expanded_queries: expandQueries(source, query, kw.commitmentKeywords),
weight: Number(weight.toFixed(2)),
skip_below,
});
}
retrievals.sort((a, b) => b.weight - a.weight);
const memoryHint = /决定|定了|上次|记得|记住/.test(query);
const context_needs = {
user_snapshot: 'OPTIONAL',
temporal_recall: 'REQUIRED',
memory_retrieval: memoryHint ? 'OPTIONAL' : 'SKIP',
};
return {
query_type: 'personal_temporal_recall',
time: {
mention_range: time.mention_range,
event_range: time.event_range,
relative_label: time.relative_label,
},
temporal_mode,
targets,
sources: Object.fromEntries(
Object.entries(sources).map(([k, v]) => [k, Number(v.toFixed(2))]),
),
retrievals,
filters: {
importance_min: kw.importance_min,
status: /还没|未完成|没做|待/.test(query) ? 'unresolved' : 'any',
},
output: {
group_by: temporal_mode === 'AMBIGUOUS' ? 'importance' : 'time',
dedupe: true,
timeline: true,
wide_recall: true,
max_items: 50,
},
context_needs,
planner_meta: {
level: 'cheap_router',
rule_hits,
confidence: Math.min(0.95, 0.65 + retrievals.length * 0.08),
},
};
}