Files
memind/scripts/simulate-schedule-capability-routing.mjs
T
john d58dc2a251 feat(wechat): add Intent Transaction Layer with unified task schema
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind
feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts
so reminders and automations get explicit user confirmation before persisting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 15:59:17 +08:00

334 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Simulate ~1000 user utterances across reminder / schedule / automation tiers.
* Reports routing accuracy, ambiguity, and agent fallthrough rate.
*/
import {
parseScheduleIntent,
shouldUseScheduleAssistant,
} from '../schedule-intent.mjs';
import {
isScheduledTaskIntent,
parseScheduledTaskIntent,
shouldUseScheduledTaskAutomation,
} from '../scheduled-task-intent.mjs';
const TZ = 'Asia/Shanghai';
const NOW = Date.UTC(2026, 7, 24, 4, 0, 0); // 2026-08-24 12:00 CST
const TIME_VARIANTS = [
'早上6点', '上午9点', '中午12点', '下午2点', '下午14:30', '1430', '晚上8点', '今晚9点半',
'明天早上7点', '明天下午3点', '后天上午10点', '今天15点', '明早6点半', '下午两点半',
];
const REMINDER_VERBS = ['提醒我', '设置提醒', '设个提醒', '闹钟', '叫我', '到点提醒'];
const REMINDER_TOPICS = [
'开会', '项目计划例会', '吃药', '接孩子', '交周报', '还信用卡', 'Standup',
'给老板发邮件', '健身', '订外卖', '复盘', '周会', '1对1',
];
const TODO_VERBS = ['帮我记一下', '记个待办', '添加待办', '设置一个代办', '先记一下'];
const TODO_TOPICS = ['跟进合同', '买牛奶', '回复张三', '整理发票', '还书', '修空调'];
const AUTO_TOPICS = [
'做今日新闻页面', '生成天气预报页面', '整理待办摘要页面', '推送销售日报',
'汇总行业资讯', '更新数据看板页面', '生成诗词页面',
];
const AUTO_RECURRENCE = ['每天', '每日', '每周一', '每周五', '定时'];
function cartesian(parts) {
return parts.reduce(
(acc, group) => acc.flatMap((prefix) => group.map((item) => [...prefix, item])),
[[]],
);
}
function buildCorpus() {
const buckets = {
reminder: [],
schedule: [],
automation: [],
edge: [],
};
for (const time of TIME_VARIANTS) {
for (const verb of REMINDER_VERBS) {
for (const topic of REMINDER_TOPICS.slice(0, 6)) {
buckets.reminder.push({
text: `${time}${verb}${topic}`,
expectedTier: 'reminder',
expectedPath: 'preflight_or_agent',
});
buckets.reminder.push({
text: `帮我设置提醒,${time}${topic}`,
expectedTier: 'reminder',
expectedPath: 'preflight_or_agent',
});
}
}
}
for (const verb of TODO_VERBS) {
for (const topic of TODO_TOPICS) {
buckets.schedule.push({
text: `${verb} ${topic}`,
expectedTier: 'schedule',
expectedPath: 'preflight',
});
buckets.schedule.push({
text: `${verb}${topic}`,
expectedTier: 'schedule',
expectedPath: 'preflight',
});
}
}
for (const time of ['早上7点', '每天8点', '每日6点半', '每天早上7点', '每天7点']) {
buckets.schedule.push({
text: `${time}把当天待办发给我`,
expectedTier: 'schedule',
expectedPath: 'preflight',
});
}
for (const threshold of [10, 20, 50, 100, 200]) {
buckets.schedule.push({
text: `余额低于${threshold}元提醒我`,
expectedTier: 'schedule',
expectedPath: 'preflight',
});
}
for (const rec of AUTO_RECURRENCE) {
for (const time of ['5点', '6点', '7点半', '8:00', '18点', '8点30分']) {
for (const topic of AUTO_TOPICS) {
buckets.automation.push({
text: `${rec}${time}帮我${topic}`,
expectedTier: 'automation',
expectedPath: 'preflight_or_agent',
});
}
}
}
buckets.edge.push(
{ text: '明天早上六点去跑步,五点半提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '双时间点' },
{ text: '每天6点帮我做今日新闻页面', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
{ text: '每天6点提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '循环提醒' },
{ text: '帮我生成一个唐诗页面', expectedTier: 'none', expectedPath: 'general' },
{ text: '取消我的定时任务', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
{ text: '看看我的待办', expectedTier: 'schedule', expectedPath: 'preflight' },
{ text: '设置提醒', expectedTier: 'reminder', expectedPath: 'clarify', note: '缺时间' },
{ text: '下午3点提醒我', expectedTier: 'reminder', expectedPath: 'clarify', note: '缺标题' },
{ text: '不是待办,下午2点提醒我交报告', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
{ text: '帮我设置一个代办明天早上六点去跑步记得在五点半的时候提醒我', expectedTier: 'reminder', expectedPath: 'agent' },
{ text: '每周一7点整理待办摘要', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
{ text: '今晚8点推送今日待办清单', expectedTier: 'schedule', expectedPath: 'agent_or_clarify', note: '待办清单' },
{ text: '定时任务:每天8点生成天气页面', expectedTier: 'automation', expectedPath: 'preflight_or_agent' },
{ text: '3小时后提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '相对时间' },
{ text: '半小时后叫我', expectedTier: 'reminder', expectedPath: 'agent', note: '相对时间' },
{ text: '周五下午3点项目评审提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '周几' },
{ text: '8月30号下午2点提醒我续费', expectedTier: 'reminder', expectedPath: 'agent', note: '具体日期' },
{ text: '提前15分钟提醒我开会', expectedTier: 'reminder', expectedPath: 'agent', note: '偏移提醒' },
{ text: '会议是3点,提前10分钟提醒我', expectedTier: 'reminder', expectedPath: 'agent', note: '事件+偏移' },
{ text: '查一下是否有执行的新闻任务', expectedTier: 'none', expectedPath: 'general', note: '误触' },
{ text: '设个提醒下午3点开会', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
{ text: '到点提醒下午2点交报告', expectedTier: 'reminder', expectedPath: 'preflight_or_agent' },
{ text: '帮我设置提醒,下午 14:30 分开会,项目计划例会', expectedTier: 'reminder', expectedPath: 'preflight' },
// --- Adversarial / ambiguous (expect clarify or correct layer, never wrong commit) ---
{ text: '明天提醒我自动生成日报', expectedTier: 'ambiguous', expectedPath: 'clarify', note: 'adv:提醒+生成' },
{ text: '帮我每天看看有没有新的招聘信息', expectedTier: 'automation', expectedPath: 'preflight_or_agent', note: 'adv:监控型自动化' },
{ text: '设置一个任务,如果余额低于100提醒我', expectedTier: 'schedule', expectedPath: 'preflight_or_agent', note: 'adv:condition trigger' },
{ text: '有没有我的新闻定时任务', expectedTier: 'none', expectedPath: 'general', note: 'adv:query guard' },
{ text: '每天8点提醒我跑步', expectedTier: 'reminder', expectedPath: 'agent', note: 'adv:recurring reminder' },
{ text: '每天8点帮我生成跑步报告', expectedTier: 'automation', expectedPath: 'preflight_or_agent', note: 'adv:notify vs act' },
);
const noisePrefixes = ['那个', '嗯', '麻烦', '请', '能不能'];
for (const prefix of noisePrefixes) {
for (const base of buckets.reminder.slice(0, 30)) {
buckets.edge.push({
text: `${prefix}${base.text}`,
expectedTier: 'reminder',
expectedPath: 'preflight_or_agent',
});
}
}
function sample(arr, n) {
if (arr.length <= n) return arr;
const step = arr.length / n;
const out = [];
for (let i = 0; i < n; i += 1) out.push(arr[Math.floor(i * step)]);
return out;
}
const target = {
reminder: 400,
schedule: 250,
automation: 280,
edge: 70,
};
const cases = [];
for (const [bucket, count] of Object.entries(target)) {
for (const [index, item] of sample(buckets[bucket], count).entries()) {
cases.push({ id: `${bucket}-${index}`, ...item });
}
}
return cases;
}
function classifyRoute(text) {
const schedTask = parseScheduledTaskIntent(text, { now: NOW, timezone: TZ });
const sched = parseScheduleIntent(text, { timezone: TZ, now: NOW });
if (isScheduledTaskIntent(schedTask)) {
if (schedTask.action === 'create_scheduled_task') {
return {
tier: 'automation',
path: schedTask.needsClarification?.length ? 'clarify' : 'preflight',
action: schedTask.action,
detail: schedTask,
};
}
return {
tier: 'automation',
path: 'preflight',
action: schedTask.action,
detail: schedTask,
};
}
if (sched.action === 'create_timed_reminder') {
return {
tier: 'reminder',
path: sched.needsClarification?.length ? 'clarify' : 'preflight',
action: sched.action,
detail: sched,
};
}
if (['create_todo', 'create_daily_todo_digest', 'create_balance_alert', 'query_schedule'].includes(sched.action)) {
return {
tier: 'schedule',
path: sched.needsClarification?.length ? 'clarify' : 'preflight',
action: sched.action,
detail: sched,
};
}
if (sched.action === 'schedule_agent') {
return { tier: 'reminder', path: 'agent', action: sched.action, detail: sched };
}
if (shouldUseScheduledTaskAutomation(text)) {
return { tier: 'automation', path: 'agent', action: 'none', detail: schedTask };
}
if (shouldUseScheduleAssistant(text)) {
return { tier: 'reminder', path: 'agent', action: sched.action, detail: sched };
}
return { tier: 'none', path: 'general', action: 'none', detail: sched };
}
function tierMatches(expectedTier, actualTier, text) {
if (expectedTier === 'ambiguous') return actualTier === 'reminder' || actualTier === 'automation';
if (expectedTier === actualTier) return true;
if (expectedTier === 'none' && actualTier === 'none') return true;
return false;
}
function pathMatches(expectedPath, actualPath) {
if (expectedPath === actualPath) return true;
if (expectedPath === 'preflight_or_agent' && (actualPath === 'preflight' || actualPath === 'agent')) return true;
if (expectedPath === 'agent_or_clarify' && (actualPath === 'agent' || actualPath === 'clarify')) return true;
if (expectedPath === 'clarify' && (actualPath === 'clarify' || actualPath === 'agent')) return true;
if (expectedPath === 'general' && actualPath === 'general') return true;
return false;
}
function main() {
const corpus = buildCorpus();
const stats = {
total: corpus.length,
tierCorrect: 0,
pathOk: 0,
byTier: {},
byPath: {},
misroutes: [],
clarifyCases: [],
agentFallback: [],
conflicts: [],
};
for (const item of corpus) {
const route = classifyRoute(item.text);
stats.byTier[route.tier] = (stats.byTier[route.tier] ?? 0) + 1;
stats.byPath[route.path] = (stats.byPath[route.path] ?? 0) + 1;
const tierOk = tierMatches(item.expectedTier, route.tier, item.text);
const pathOk = pathMatches(item.expectedPath ?? 'preflight_or_agent', route.path);
if (tierOk) stats.tierCorrect += 1;
if (pathOk) stats.pathOk += 1;
if (!tierOk) {
stats.misroutes.push({
text: item.text,
expectedTier: item.expectedTier,
actualTier: route.tier,
path: route.path,
action: route.action,
note: item.note ?? null,
});
}
if (route.path === 'clarify') {
stats.clarifyCases.push({ text: item.text, action: route.action, detail: route.detail });
}
if (route.path === 'agent') {
stats.agentFallback.push({ text: item.text, expectedTier: item.expectedTier, action: route.action });
}
if (shouldUseScheduledTaskAutomation(item.text) && shouldUseScheduleAssistant(item.text)) {
stats.conflicts.push(item.text);
}
}
const tierAccuracy = ((stats.tierCorrect / stats.total) * 100).toFixed(1);
const pathAccuracy = ((stats.pathOk / stats.total) * 100).toFixed(1);
const preflightRate = (
((stats.byPath.preflight ?? 0) / stats.total) * 100
).toFixed(1);
const agentRate = (
((stats.byPath.agent ?? 0) / stats.total) * 100
).toFixed(1);
const clarifyRate = (
((stats.byPath.clarify ?? 0) / stats.total) * 100
).toFixed(1);
const misrouteByReason = {};
for (const row of stats.misroutes) {
const key = `${row.expectedTier}->${row.actualTier}`;
misrouteByReason[key] = (misrouteByReason[key] ?? 0) + 1;
}
console.log(JSON.stringify({
summary: {
total: stats.total,
tierAccuracyPct: Number(tierAccuracy),
pathAccuracyPct: Number(pathAccuracy),
preflightRatePct: Number(preflightRate),
agentFallbackRatePct: Number(agentRate),
clarifyRatePct: Number(clarifyRate),
skillConflicts: stats.conflicts.length,
},
routeDistribution: stats.byTier,
pathDistribution: stats.byPath,
misroutePatterns: misrouteByReason,
topMisroutes: stats.misroutes.slice(0, 25),
sampleAgentFallback: stats.agentFallback.slice(0, 15),
sampleClarify: stats.clarifyCases.slice(0, 10),
}, null, 2));
}
main();