212ff3ff80
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>
134 lines
4.1 KiB
JavaScript
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),
|
|
},
|
|
};
|
|
}
|