@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -74
- package/bin/gotry-inner.js +50 -1
- package/bin/gotry-stdio-ask.js +59 -0
- package/cordis.gotry-patch.yml +63 -5
- package/data/flights_2026.json +7 -4
- package/data/hotels_2026.json +102 -15
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +2 -1
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/anything.js +2 -2
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +11 -6
- package/dist/capabilities/incident-log.js +2 -1
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +56 -0
- package/dist/capabilities/session/transport.js +95 -0
- package/dist/capabilities/session-search.js +95 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/async-collect.js +26 -7
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/hbcli-tests.js +18 -2
- package/dist/scripts/ledger-tests.js +344 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/probe-poi-tests.js +9 -0
- package/dist/scripts/replay.js +73 -1
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-attach-wait.js +41 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-login.js +48 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +3 -1
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +242 -4
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +234 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/src/companions.js +112 -0
- package/dist/src/dsh-llm.js +27 -7
- package/dist/src/index.js +600 -121
- package/dist/src/loop.js +89 -25
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +6 -1
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +848 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +3 -1
- package/dist/src/wish-pool.js +27 -0
- package/package.json +6 -2
- package/ts/capabilities/hbcli.ts +6 -6
- package/ts/capabilities/incident-log.ts +6 -3
- package/ts/package.json +3 -0
- package/ts/scripts/skeleton-check.ts +5 -1
- package/ts/src/dsh-llm.ts +27 -7
- package/ts/src/index.ts +439 -91
- package/ts/src/loop.ts +109 -34
- package/ts/src/mock-llm.ts +13 -1
- package/ts/src/unified.ts +6 -1
- package/ts/cordis.gotry-patch.yml +0 -36
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
|
|
4
|
+
import { collectDeepPlanning, makeJournaledSolvePort, settleAsyncTicket } from '../src/loop.js';
|
|
5
|
+
import { solveUnified } from '../src/unified.js';
|
|
6
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
7
|
+
function rootOf(args) {
|
|
8
|
+
const i = args.indexOf('--state-root');
|
|
9
|
+
if (i >= 0 && args[i + 1]) return args[i + 1];
|
|
10
|
+
const first = args.find((a)=>!a.startsWith('--') && !/^[\d]+$/.test(a));
|
|
11
|
+
return first ?? '.';
|
|
12
|
+
}
|
|
13
|
+
function arg(name, fallback) {
|
|
14
|
+
const i = process.argv.indexOf(name);
|
|
15
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
16
|
+
}
|
|
17
|
+
function atomicWrite(path, text) {
|
|
18
|
+
const tmp = `${path}.tmp`;
|
|
19
|
+
writeFileSync(tmp, text, 'utf-8');
|
|
20
|
+
renameSync(tmp, path);
|
|
21
|
+
}
|
|
22
|
+
const HELP = '用法见文件头注释(npx tsx scripts/state-cli.ts <cmd> [root] ...)';
|
|
23
|
+
switch(cmd){
|
|
24
|
+
case 'migrate':
|
|
25
|
+
{
|
|
26
|
+
const root = rootOf(rest);
|
|
27
|
+
const before = openLedgerIfExists(root)?.countEvents() ?? 0;
|
|
28
|
+
const ledger = ensureLedger(root);
|
|
29
|
+
const backupDir = join(root, 'gotry-state', 'pre-ledger-backup');
|
|
30
|
+
console.log(`账本就绪:${ledger.dbPath}(events ${before} → ${ledger.countEvents()};存在旧文件时已快照至 ${backupDir}/)`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'export':
|
|
34
|
+
{
|
|
35
|
+
const root = rootOf(rest);
|
|
36
|
+
const ledger = ensureLedger(root);
|
|
37
|
+
const dir = join(root === '.' ? process.cwd() : root, 'gotry-state');
|
|
38
|
+
mkdirSync(dir, {
|
|
39
|
+
recursive: true
|
|
40
|
+
});
|
|
41
|
+
const motivation = ledger.readMotivation();
|
|
42
|
+
if (motivation) atomicWrite(join(dir, 'motivation-profile.json'), JSON.stringify(motivation, null, 2));
|
|
43
|
+
const pool = ledger.readWishPool();
|
|
44
|
+
if (pool.length) atomicWrite(join(dir, 'wish-pool.json'), JSON.stringify(pool, null, 2));
|
|
45
|
+
const companions = ledger.readCompanions();
|
|
46
|
+
if (companions.length) atomicWrite(join(dir, 'companions.json'), JSON.stringify(companions, null, 2));
|
|
47
|
+
const utility = ledger.readUtilityEvents();
|
|
48
|
+
if (utility.length) atomicWrite(join(dir, 'memory-utility.jsonl'), utility.map((e)=>JSON.stringify(e)).join('\n') + '\n');
|
|
49
|
+
const trips = ledger.readTrips();
|
|
50
|
+
if (trips.length) atomicWrite(join(dir, 'trips.jsonl'), trips.map((e)=>JSON.stringify(e)).join('\n') + '\n');
|
|
51
|
+
console.log(`视图已导出(单向,DB→文件;红线 6):${dir}/`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'log':
|
|
55
|
+
{
|
|
56
|
+
const root = rootOf(rest);
|
|
57
|
+
const limit = Number(arg('--limit', '20'));
|
|
58
|
+
const ledger = openLedgerIfExists(root);
|
|
59
|
+
if (!ledger) {
|
|
60
|
+
console.log('(无账本——未迁移 root,旧文件形态)');
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
for (const e of ledger.readEvents(undefined, limit)){
|
|
64
|
+
console.log(`${String(e.seq).padStart(5)} ${e.ts} ${e.actor.padEnd(28)} ${e.kind.padEnd(24)} ${e.subject_id}${e.idem_key ? ` #${e.idem_key}` : ''}`);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'stats':
|
|
69
|
+
{
|
|
70
|
+
const root = rootOf(rest);
|
|
71
|
+
const ledger = openLedgerIfExists(root);
|
|
72
|
+
if (!ledger) {
|
|
73
|
+
console.log('(无账本——未迁移 root)');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
console.log(`events=${ledger.countEvents()} wishes=${ledger.readWishPool().length} companions=${ledger.readCompanions().length} trips=${ledger.readTrips().length} utility=${ledger.readUtilityEvents().length} pendingRuns=${ledger.pendingWorkflowRuns().length} pendingWrites=${ledger.listPendingWrites().length}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'rebuild':
|
|
80
|
+
case 'rewind':
|
|
81
|
+
{
|
|
82
|
+
const root = rootOf(rest);
|
|
83
|
+
const ledger = openLedgerIfExists(root);
|
|
84
|
+
if (!ledger) {
|
|
85
|
+
console.error('无账本');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const toSeqRaw = rest.find((a)=>/^\d+$/.test(a));
|
|
89
|
+
const r = ledger.rebuildProjections(toSeqRaw ? Number(toSeqRaw) : undefined);
|
|
90
|
+
console.log(`fold 重建完成${toSeqRaw ? `(至 seq ${toSeqRaw};rebuild 无参即回最新)` : '(全量)'}:重放 ${r.events} 事件 → 愿望 ${r.wishes} / 同行人 ${r.companions}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'forget':
|
|
94
|
+
{
|
|
95
|
+
const root = rootOf(rest);
|
|
96
|
+
const ledger = openLedgerIfExists(root);
|
|
97
|
+
if (!ledger) {
|
|
98
|
+
console.error('无账本');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const subject = rest.find((a)=>!a.startsWith('--') && a !== root);
|
|
102
|
+
const id = rest[rest.indexOf(subject) + 1];
|
|
103
|
+
if (!subject || id === undefined) {
|
|
104
|
+
console.error(HELP);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const map = {
|
|
108
|
+
wish: {
|
|
109
|
+
kinds: [
|
|
110
|
+
'wish.imported',
|
|
111
|
+
'wish.added',
|
|
112
|
+
'wish.updated',
|
|
113
|
+
'memory_utility.event'
|
|
114
|
+
],
|
|
115
|
+
subjectId: id
|
|
116
|
+
},
|
|
117
|
+
companion: {
|
|
118
|
+
kinds: [
|
|
119
|
+
'companion.imported',
|
|
120
|
+
'companion.saved'
|
|
121
|
+
],
|
|
122
|
+
subjectId: id
|
|
123
|
+
},
|
|
124
|
+
motivation: {
|
|
125
|
+
kinds: [
|
|
126
|
+
'motivation.imported',
|
|
127
|
+
'motivation.patch'
|
|
128
|
+
],
|
|
129
|
+
subjectId: 'motivation'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const spec = map[subject];
|
|
133
|
+
if (!spec) {
|
|
134
|
+
console.error(`未知主体 ${subject}(wish|companion|motivation)`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const r = ledger.forgetSubject([
|
|
138
|
+
spec
|
|
139
|
+
]);
|
|
140
|
+
console.log(`已物理硬删 ${r.deleted} 条事件并重建投影(审计一行留痕;红线 6「可删除」)`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'tick':
|
|
144
|
+
{
|
|
145
|
+
const root = rootOf(rest);
|
|
146
|
+
const ledger = openLedgerIfExists(root);
|
|
147
|
+
if (!ledger) {
|
|
148
|
+
console.log('(无账本,无待办)');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const pending = ledger.pendingWorkflowRuns();
|
|
152
|
+
if (pending.length === 0) {
|
|
153
|
+
console.log('(无 pending 工单)');
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
for (const p of pending){
|
|
157
|
+
const run = ledger.getWorkflowRun(p.id);
|
|
158
|
+
const ticket = JSON.parse(run.ticket_json);
|
|
159
|
+
const state = JSON.parse(run.state_json);
|
|
160
|
+
const solve = makeJournaledSolvePort(ledger, p.id, solveUnified);
|
|
161
|
+
const { reply } = await collectDeepPlanning(state, ticket, solve);
|
|
162
|
+
await settleAsyncTicket(p.id, reply, root);
|
|
163
|
+
console.log(`工单 ${p.id} 已回收(durable 恢复语义:done 步骤零重执行)`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'whatif':
|
|
168
|
+
{
|
|
169
|
+
const root = rootOf(rest);
|
|
170
|
+
const ledger = openLedgerIfExists(root);
|
|
171
|
+
if (!ledger) {
|
|
172
|
+
console.error('无账本');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const dest = rest.find((a)=>a !== root && !a.startsWith('--'));
|
|
176
|
+
if (!dest) {
|
|
177
|
+
console.error(HELP);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
mkdirSync(dirname(dest), {
|
|
181
|
+
recursive: true
|
|
182
|
+
});
|
|
183
|
+
ledger.forkWhatIf(dest);
|
|
184
|
+
console.log(`what-if 分叉已生成:${dest}(预演在副本,正本零改动)`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'pw-list':
|
|
188
|
+
{
|
|
189
|
+
const root = rootOf(rest);
|
|
190
|
+
const ledger = openLedgerIfExists(root);
|
|
191
|
+
if (!ledger) {
|
|
192
|
+
console.log('(无账本)');
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
for (const w of ledger.listPendingWrites()){
|
|
196
|
+
console.log(`${w.status.padEnd(12)} ${w.idem_key} seam=${w.seam}${w.receipt ? ` receipt=${w.receipt}` : ''}`);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'pw-request':
|
|
201
|
+
case 'pw-confirm':
|
|
202
|
+
case 'pw-compensate':
|
|
203
|
+
{
|
|
204
|
+
const root = rootOf(rest);
|
|
205
|
+
const ledger = ensureLedger(root);
|
|
206
|
+
const positional = rest.filter((a)=>a !== root && !a.startsWith('--'));
|
|
207
|
+
if (positional.length < (cmd === 'pw-request' ? 3 : 2)) {
|
|
208
|
+
console.error(HELP);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
if (cmd === 'pw-request') {
|
|
212
|
+
const r = ledger.requestPendingWrite({
|
|
213
|
+
idemKey: positional[0],
|
|
214
|
+
seam: positional[1],
|
|
215
|
+
payload: JSON.parse(positional[2])
|
|
216
|
+
});
|
|
217
|
+
console.log(`登记:${JSON.stringify(r)}(L2:只登记不执行)`);
|
|
218
|
+
} else if (cmd === 'pw-confirm') {
|
|
219
|
+
const r = ledger.confirmPendingWrite(positional[0], positional[1]);
|
|
220
|
+
console.log(`确认:${JSON.stringify(r)}`);
|
|
221
|
+
} else {
|
|
222
|
+
const r = ledger.compensatePendingWrite(positional[0], positional[1]);
|
|
223
|
+
console.log(`补偿:${JSON.stringify(r)}`);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
console.error(cmd ? `未知命令 ${cmd}` : HELP);
|
|
229
|
+
console.error(HELP);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/state-cli.ts
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { buildTimeAnchor, parseAbsoluteDate } from '../src/time-anchor.js';
|
|
5
|
+
import { detectLanguage, flagExpiredSlots, scoreExtraction } from '../src/travel-slots.js';
|
|
6
|
+
import { resolveSlotDate, resolveSlots, specDateMismatches } from '../src/slot-spec.js';
|
|
7
|
+
import { createMockLlm } from '../src/mock-llm.js';
|
|
8
|
+
import { createOpenAICompatLlm } from '../src/dsh-llm.js';
|
|
9
|
+
const dataset = JSON.parse(readFileSync(join('..', 'data', 'time-slot-eval.json'), 'utf-8'));
|
|
10
|
+
const [ay, am, ad] = dataset.anchor_date.split('-').map(Number);
|
|
11
|
+
const ANCHOR_NOW = new Date(ay, am - 1, ad, 12);
|
|
12
|
+
{
|
|
13
|
+
const a = buildTimeAnchor(ANCHOR_NOW);
|
|
14
|
+
assert.equal(a.today, '2026-08-27', 'today');
|
|
15
|
+
assert.equal(a.todayWeekdayZh, '周四', '2026-08-27 是周四');
|
|
16
|
+
assert.match(a.tzLabel, /^UTC[+-]\d/, '时区标注格式');
|
|
17
|
+
const c = a.card;
|
|
18
|
+
assert.ok(c.includes('明天 2026-08-28 周五'), '明天');
|
|
19
|
+
assert.ok(c.includes('大后天 2026-08-30 周日'), '大后天');
|
|
20
|
+
assert.ok(c.includes('下周:周一 2026-08-31'), '下周一=08-31');
|
|
21
|
+
assert.ok(c.includes('周日 2026-09-06'), '下周日=09-06');
|
|
22
|
+
assert.ok(c.includes('周四 2026-09-10'), '下下周四=09-10');
|
|
23
|
+
assert.ok(c.includes('下个月 2026-09:初 09-01~09-10|中旬 09-11~09-20|下旬 09-21~09-30'), '下个月分段');
|
|
24
|
+
assert.ok(c.includes('下个季度初:2026-10-01 起'), '下个季度初');
|
|
25
|
+
assert.ok(c.includes('国庆 2026-10-01'), '国庆锚点');
|
|
26
|
+
assert.ok(c.includes('元旦 2027-01-01'), '元旦取下一次发生');
|
|
27
|
+
assert.ok(c.includes('春节 2027-02-06'), '春节锚点(2026 春节已过,取 2027)');
|
|
28
|
+
console.log('1. 锚点卡断言 OK(固定 2026-08-27:下周一/大后天/下下周四/月分段/季度/节日)');
|
|
29
|
+
const a2030 = buildTimeAnchor(new Date(2030, 5, 1, 12));
|
|
30
|
+
assert.ok(a2030.card.includes('春节 2031-01-23'), 'D-9 扩表:2030 年视视角春节取 2031-01-23');
|
|
31
|
+
console.log('1b. D-9 扩表断言 OK(2030 锚点 → 春节 2031-01-23,表覆盖至 2031)');
|
|
32
|
+
}{
|
|
33
|
+
const anchor = buildTimeAnchor(ANCHOR_NOW);
|
|
34
|
+
const base = {
|
|
35
|
+
schema_version: 'travel_slot_extraction.v1',
|
|
36
|
+
language: 'zh',
|
|
37
|
+
domains: [
|
|
38
|
+
'requisition'
|
|
39
|
+
],
|
|
40
|
+
slots: {
|
|
41
|
+
requisition: {
|
|
42
|
+
start_date: '8.1',
|
|
43
|
+
end_date: '8.10'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
missing_slots: []
|
|
47
|
+
};
|
|
48
|
+
const flagged = flagExpiredSlots(base, anchor);
|
|
49
|
+
assert.deepEqual(flagged.missing_slots, [
|
|
50
|
+
'requisition.start_date (date is expired)'
|
|
51
|
+
], '8.1 在 08-27 锚点下过期');
|
|
52
|
+
assert.equal(flagged.slots.requisition.start_date, '8.1', '槽位原值逐字保留');
|
|
53
|
+
assert.equal(flagged.missing_slots.length, 1, 'end_date 不重复判(只判主日期字段,对齐 golden)');
|
|
54
|
+
const again = flagExpiredSlots(flagged, anchor);
|
|
55
|
+
assert.equal(again.missing_slots.length, 1, '幂等不重复加');
|
|
56
|
+
const future = flagExpiredSlots({
|
|
57
|
+
...base,
|
|
58
|
+
slots: {
|
|
59
|
+
requisition: {
|
|
60
|
+
start_date: '9.1'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}, anchor);
|
|
64
|
+
assert.equal(future.missing_slots.length, 0, '9.1 不过期');
|
|
65
|
+
const relative = flagExpiredSlots({
|
|
66
|
+
...base,
|
|
67
|
+
slots: {
|
|
68
|
+
requisition: {
|
|
69
|
+
start_date: '本周三'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, anchor);
|
|
73
|
+
assert.equal(relative.missing_slots.length, 0, '相对表达(本周三)永不判过期');
|
|
74
|
+
const iso = flagExpiredSlots({
|
|
75
|
+
...base,
|
|
76
|
+
slots: {
|
|
77
|
+
requisition: {
|
|
78
|
+
start_date: '2026-08-01'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, anchor);
|
|
82
|
+
assert.equal(iso.missing_slots.length, 1, 'ISO 绝对日期判过期');
|
|
83
|
+
const zhDate = flagExpiredSlots({
|
|
84
|
+
...base,
|
|
85
|
+
slots: {
|
|
86
|
+
requisition: {
|
|
87
|
+
start_date: '8月5日'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, anchor);
|
|
91
|
+
assert.equal(zhDate.missing_slots.length, 1, '中文月日判过期');
|
|
92
|
+
const enDate = flagExpiredSlots({
|
|
93
|
+
...base,
|
|
94
|
+
slots: {
|
|
95
|
+
requisition: {
|
|
96
|
+
start_date: 'Aug-5'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}, anchor);
|
|
100
|
+
assert.equal(enDate.missing_slots.length, 0, '英文月名不判过期(逐字传递,对齐 golden time_english_date_range)');
|
|
101
|
+
const flight = flagExpiredSlots({
|
|
102
|
+
...base,
|
|
103
|
+
domains: [
|
|
104
|
+
'flight'
|
|
105
|
+
],
|
|
106
|
+
slots: {
|
|
107
|
+
flight: {
|
|
108
|
+
departure_date: '8.1'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, anchor);
|
|
112
|
+
assert.deepEqual(flight.missing_slots, [
|
|
113
|
+
'flight.departure_date (date is expired)'
|
|
114
|
+
], 'flight 主日期字段');
|
|
115
|
+
assert.equal(parseAbsoluteDate('下周一', 2026), null, 'parseAbsoluteDate 不碰相对表达');
|
|
116
|
+
for (const c of dataset.cases){
|
|
117
|
+
assert.equal(detectLanguage(c.utterance), c.expected.language, `detectLanguage ${c.id}`);
|
|
118
|
+
}
|
|
119
|
+
console.log('2. 过期校验断言 OK(8.1 过期/9.1 不/本周三不/ISO+中文月日判/英文不判/幂等)+ detectLanguage 全题对齐');
|
|
120
|
+
}{
|
|
121
|
+
const exp = dataset.cases[0].expected;
|
|
122
|
+
const identical = structuredClone(exp);
|
|
123
|
+
assert.equal(scoreExtraction(exp, identical).pass, true, '完全一致应 pass');
|
|
124
|
+
const wrongLang = {
|
|
125
|
+
...structuredClone(exp),
|
|
126
|
+
language: 'en'
|
|
127
|
+
};
|
|
128
|
+
assert.equal(scoreExtraction(exp, wrongLang).pass, false, 'language 不一致应 fail');
|
|
129
|
+
const wrongDomain = {
|
|
130
|
+
...structuredClone(exp),
|
|
131
|
+
domains: [
|
|
132
|
+
'flight'
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
assert.equal(scoreExtraction(exp, wrongDomain).pass, false, 'domains 不一致应 fail');
|
|
136
|
+
const wrongSlot = structuredClone(exp);
|
|
137
|
+
wrongSlot.slots.requisition.destination = '北京';
|
|
138
|
+
const r = scoreExtraction(exp, wrongSlot);
|
|
139
|
+
assert.equal(r.pass, false, '槽位值不一致应 fail');
|
|
140
|
+
assert.ok(r.diffs.some((d)=>d.includes('slots.requisition.destination')), 'diff 带路径');
|
|
141
|
+
const wrongMissing = {
|
|
142
|
+
...structuredClone(exp),
|
|
143
|
+
missing_slots: [
|
|
144
|
+
'requisition.start_date (date is expired)'
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
assert.equal(scoreExtraction(exp, wrongMissing).pass, false, 'missing_slots 不一致应 fail');
|
|
148
|
+
const extraKey = structuredClone(exp);
|
|
149
|
+
extraKey.slots.requisition['adults'] = 2;
|
|
150
|
+
const rk = scoreExtraction(exp, extraKey);
|
|
151
|
+
assert.equal(rk.pass, true, '多出键不 fail(非致命)');
|
|
152
|
+
assert.equal(rk.warnings.length, 1, '多出键记 warning');
|
|
153
|
+
console.log('3. 评分器自测 OK(pass/language/domains/槽位值/missing_slots/extra-key warning)');
|
|
154
|
+
}{
|
|
155
|
+
const script = dataset.cases.map((c)=>({
|
|
156
|
+
when: c.utterance,
|
|
157
|
+
extraction: c.expected
|
|
158
|
+
}));
|
|
159
|
+
const llm = createMockLlm(undefined, script);
|
|
160
|
+
let pass = 0;
|
|
161
|
+
for (const c of dataset.cases){
|
|
162
|
+
const ext = await llm.extractSlots([
|
|
163
|
+
{
|
|
164
|
+
role: 'user',
|
|
165
|
+
text: c.utterance
|
|
166
|
+
}
|
|
167
|
+
]);
|
|
168
|
+
assert.ok(ext, `mock 应命中 ${c.id}`);
|
|
169
|
+
const r = scoreExtraction(c.expected, ext);
|
|
170
|
+
assert.ok(r.pass, `mock 回放 ${c.id} 应 pass:${r.diffs.join(';')}`);
|
|
171
|
+
pass++;
|
|
172
|
+
}
|
|
173
|
+
console.log(`4. mock 回放管道 OK(${pass}/${dataset.cases.length}——评分管道自身正确)`);
|
|
174
|
+
}{
|
|
175
|
+
const anchor = buildTimeAnchor(ANCHOR_NOW);
|
|
176
|
+
const r = (expr)=>resolveSlotDate(expr, anchor);
|
|
177
|
+
assert.equal(r('2026-09-05').date, '2026-09-05', 'ISO');
|
|
178
|
+
assert.equal(r('8.20').date, '2026-08-20', '点分');
|
|
179
|
+
assert.equal(r('9月5日').date, '2026-09-05', '中文月日');
|
|
180
|
+
assert.equal(r('下周一').date, '2026-08-31', '下周一');
|
|
181
|
+
assert.equal(r('本周六').date, '2026-08-29', '本周六');
|
|
182
|
+
assert.equal(r('下下周四').date, '2026-09-10', '下下周四');
|
|
183
|
+
assert.equal(r('明天').date, '2026-08-28', '明天');
|
|
184
|
+
assert.equal(r('下个月中旬').date, '2026-09-11', '下个月中旬取首日');
|
|
185
|
+
assert.equal(r('下周五+3').date, '2026-09-07', '下周五+3');
|
|
186
|
+
assert.equal(r('8.20+2天').date, '2026-08-22', '8.20+2天');
|
|
187
|
+
assert.equal(r('近期').kind, 'unresolved', '模糊词');
|
|
188
|
+
assert.equal(r('这阵子').date, null, '模糊词不猜');
|
|
189
|
+
assert.equal(r('next Monday').kind, 'unresolved', '英文相对不解析');
|
|
190
|
+
assert.equal(r('next Monday').raw, 'next Monday', 'unresolved 保留原话');
|
|
191
|
+
const ext = {
|
|
192
|
+
schema_version: 'travel_slot_extraction.v1',
|
|
193
|
+
language: 'zh',
|
|
194
|
+
domains: [
|
|
195
|
+
'hotel',
|
|
196
|
+
'flight'
|
|
197
|
+
],
|
|
198
|
+
slots: {
|
|
199
|
+
hotel: {
|
|
200
|
+
action: 'search',
|
|
201
|
+
city: '上海',
|
|
202
|
+
check_in_date: '下周五',
|
|
203
|
+
check_out_date: '下周五+3'
|
|
204
|
+
},
|
|
205
|
+
flight: {
|
|
206
|
+
action: 'search',
|
|
207
|
+
origin: '上海',
|
|
208
|
+
destination: '杭州',
|
|
209
|
+
departure_date: '这阵子',
|
|
210
|
+
trip_type: 'one_way'
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
missing_slots: []
|
|
214
|
+
};
|
|
215
|
+
const resolved = resolveSlots(ext, anchor);
|
|
216
|
+
assert.equal(resolved.hotel?.check_in_date?.date, '2026-09-04', 'hotel 入住=下周五');
|
|
217
|
+
assert.equal(resolved.hotel?.check_out_date?.date, '2026-09-07', 'hotel 退房=下周五+3');
|
|
218
|
+
assert.deepEqual(resolved.unresolved, [
|
|
219
|
+
{
|
|
220
|
+
field: 'flight.departure_date',
|
|
221
|
+
raw: '这阵子'
|
|
222
|
+
}
|
|
223
|
+
], 'unresolved 清单');
|
|
224
|
+
assert.equal(specDateMismatches({
|
|
225
|
+
segments: [
|
|
226
|
+
{
|
|
227
|
+
date: '2026-09-04'
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}, resolved).length, 0, '一致则零 mismatch');
|
|
231
|
+
const mm = specDateMismatches({
|
|
232
|
+
segments: [
|
|
233
|
+
{
|
|
234
|
+
date: '2026-09-05'
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}, resolved);
|
|
238
|
+
assert.equal(mm.length, 1, '单日期段分歧暴露');
|
|
239
|
+
assert.equal(mm[0]?.specDate, '2026-09-05');
|
|
240
|
+
assert.equal(mm[0]?.slotDate, '2026-09-04');
|
|
241
|
+
assert.equal(specDateMismatches({
|
|
242
|
+
segments: [
|
|
243
|
+
{
|
|
244
|
+
date: '2026-09-04'
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
date: '2026-09-05'
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}, resolved).length, 0, '多段无逐段真值不判(巡检修正)');
|
|
251
|
+
const unresolvedOnly = {
|
|
252
|
+
unresolved: [
|
|
253
|
+
{
|
|
254
|
+
field: 'flight.departure_date',
|
|
255
|
+
raw: '近期'
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
};
|
|
259
|
+
assert.equal(specDateMismatches({
|
|
260
|
+
segments: [
|
|
261
|
+
{
|
|
262
|
+
date: '2026-09-05'
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}, unresolvedOnly).length, 0, '全 unresolved 不参与比对');
|
|
266
|
+
assert.equal(specDateMismatches({
|
|
267
|
+
segments: []
|
|
268
|
+
}, resolved).length, 0, 'spec 无日期不比对');
|
|
269
|
+
console.log('5. 槽位→日期解析 OK(绝对/词表/+N/unresolved 边界/整张解析/spec 一致性闸)');
|
|
270
|
+
}console.log('\nTIME-EVAL TESTS: 5/5 OK(确定性部分,CI 口径)');
|
|
271
|
+
if (process.argv.includes('--real')) {
|
|
272
|
+
try {
|
|
273
|
+
for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')){
|
|
274
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
275
|
+
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
const hasKey = Boolean(process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY']);
|
|
279
|
+
if (!hasKey) {
|
|
280
|
+
console.log('\n--real:无 LLM_API_KEY,跳过真模型巡检(ADR-8 回退原则)');
|
|
281
|
+
} else {
|
|
282
|
+
const clock = ()=>ANCHOR_NOW;
|
|
283
|
+
const llm = createOpenAICompatLlm(undefined, clock);
|
|
284
|
+
console.log(`\n=== 真模型巡检(${process.env['LLM_MODEL'] ?? 'MiniMax-M2'},锚点 ${dataset.anchor_date})===`);
|
|
285
|
+
let pass = 0;
|
|
286
|
+
let p0Pass = 0;
|
|
287
|
+
let p0Total = 0;
|
|
288
|
+
for (const c of dataset.cases){
|
|
289
|
+
if (c.priority === 'P0') p0Total++;
|
|
290
|
+
try {
|
|
291
|
+
const ext = await llm.extractSlots([
|
|
292
|
+
{
|
|
293
|
+
role: 'user',
|
|
294
|
+
text: c.utterance
|
|
295
|
+
}
|
|
296
|
+
]);
|
|
297
|
+
if (!ext) {
|
|
298
|
+
console.log(`FAIL ${c.id}(${c.priority}):模型未产出合法 v1 JSON`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const r = scoreExtraction(c.expected, ext);
|
|
302
|
+
if (r.pass) {
|
|
303
|
+
pass++;
|
|
304
|
+
if (c.priority === 'P0') p0Pass++;
|
|
305
|
+
console.log(`PASS ${c.id}${r.warnings.length ? `(warning: ${r.warnings.join(';')})` : ''}`);
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`FAIL ${c.id}(${c.priority}):\n ${r.diffs.join('\n ')}`);
|
|
308
|
+
}
|
|
309
|
+
} catch (e) {
|
|
310
|
+
console.log(`ERROR ${c.id}(${c.priority}):${e.message.slice(0, 120)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
console.log(`\nREAL ACCURACY: ${pass}/${dataset.cases.length}(P0: ${p0Pass}/${p0Total})——巡检报告口径,不设红线`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/time-eval-tests.ts
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { appendTrip, makeTripId, projectTimeline, timelineGapsForVerified, resolveTimelineDate } from '../src/travel-timeline.js';
|
|
3
|
+
import { buildTimeAnchor } from '../src/time-anchor.js';
|
|
4
|
+
const anchor = buildTimeAnchor(new Date(2026, 7, 28, 12));
|
|
5
|
+
let n = 0;
|
|
6
|
+
function pass(name, body) {
|
|
7
|
+
body();
|
|
8
|
+
console.log(` ${++n}. ${name} OK`);
|
|
9
|
+
}
|
|
10
|
+
pass('守门:必填与绝对日期(不猜)', ()=>{
|
|
11
|
+
const ev = {
|
|
12
|
+
destination: '大理',
|
|
13
|
+
start: '2025-10-01',
|
|
14
|
+
source: 'user-verbatim',
|
|
15
|
+
evidence: '用户原话'
|
|
16
|
+
};
|
|
17
|
+
assert.equal(appendTrip([], {
|
|
18
|
+
...ev,
|
|
19
|
+
destination: ''
|
|
20
|
+
}).appended, false, '无目的地拒');
|
|
21
|
+
assert.equal(appendTrip([], {
|
|
22
|
+
...ev,
|
|
23
|
+
evidence: ''
|
|
24
|
+
}).appended, false, '无 evidence 拒(溯源 P0)');
|
|
25
|
+
assert.ok(appendTrip([], {
|
|
26
|
+
...ev,
|
|
27
|
+
start: '下周一'
|
|
28
|
+
}).reason?.includes('YYYY-MM-DD'), '词表外日期拒收(不猜)');
|
|
29
|
+
assert.ok(appendTrip([], {
|
|
30
|
+
...ev,
|
|
31
|
+
end: '2025-09-30'
|
|
32
|
+
}).reason?.includes('end 早于 start'), 'end<start 拒');
|
|
33
|
+
});
|
|
34
|
+
pass('幂等:同目的地+start+source = 同一行程 no-op', ()=>{
|
|
35
|
+
const ev = {
|
|
36
|
+
destination: '大理',
|
|
37
|
+
start: '2025-10-01',
|
|
38
|
+
source: 'user-verbatim',
|
|
39
|
+
evidence: '原话'
|
|
40
|
+
};
|
|
41
|
+
const r1 = appendTrip([], ev);
|
|
42
|
+
assert.equal(r1.appended, true);
|
|
43
|
+
const r2 = appendTrip(r1.events, {
|
|
44
|
+
...ev,
|
|
45
|
+
evidence: '换个说法再说一遍'
|
|
46
|
+
});
|
|
47
|
+
assert.equal(r2.appended, false, '同 trip_id 重放 no-op');
|
|
48
|
+
assert.equal(r2.tripId, r1.tripId);
|
|
49
|
+
});
|
|
50
|
+
pass('重叠冲突即停:同目的地日期区间重叠拒收', ()=>{
|
|
51
|
+
const r1 = appendTrip([], {
|
|
52
|
+
destination: '大理',
|
|
53
|
+
start: '2025-10-01',
|
|
54
|
+
end: '2025-10-05',
|
|
55
|
+
source: 'user-verbatim',
|
|
56
|
+
evidence: '原话'
|
|
57
|
+
});
|
|
58
|
+
const r2 = appendTrip(r1.events, {
|
|
59
|
+
destination: '大理',
|
|
60
|
+
start: '2025-10-04',
|
|
61
|
+
source: 'user-verbatim',
|
|
62
|
+
evidence: '原话2'
|
|
63
|
+
});
|
|
64
|
+
assert.equal(r2.appended, false);
|
|
65
|
+
assert.ok(r2.reason?.includes('重叠'), '冲突理由说明');
|
|
66
|
+
});
|
|
67
|
+
pass('日期解析:锚点卡词表进、词表外 null(经 resolveTimelineDate)', ()=>{
|
|
68
|
+
assert.equal(resolveTimelineDate('2025-10-01', anchor), '2025-10-01');
|
|
69
|
+
assert.equal(resolveTimelineDate('去年国庆', anchor), null, '开放表达不猜');
|
|
70
|
+
});
|
|
71
|
+
pass('投影:start 倒序(最近在前)', ()=>{
|
|
72
|
+
let evs = [];
|
|
73
|
+
evs = appendTrip(evs, {
|
|
74
|
+
destination: '普吉',
|
|
75
|
+
start: '2026-07-10',
|
|
76
|
+
source: 'user-verbatim',
|
|
77
|
+
evidence: 'a'
|
|
78
|
+
}).events;
|
|
79
|
+
evs = appendTrip(evs, {
|
|
80
|
+
destination: '大理',
|
|
81
|
+
start: '2025-10-01',
|
|
82
|
+
source: 'user-verbatim',
|
|
83
|
+
evidence: 'b'
|
|
84
|
+
}).events;
|
|
85
|
+
const p = projectTimeline(evs);
|
|
86
|
+
assert.equal(p[0]?.destination, '普吉');
|
|
87
|
+
assert.equal(p.length, 2);
|
|
88
|
+
});
|
|
89
|
+
pass('交叉一致:verified wish 无 timeline = 缺口暴露(巡检口径,不自动补写)', ()=>{
|
|
90
|
+
const evs = appendTrip([], {
|
|
91
|
+
destination: '大理',
|
|
92
|
+
start: '2025-10-01',
|
|
93
|
+
source: 'wish-confirmed',
|
|
94
|
+
evidence: 'wish w1 confirm'
|
|
95
|
+
}).events;
|
|
96
|
+
const gaps = timelineGapsForVerified(evs, [
|
|
97
|
+
{
|
|
98
|
+
wishId: 'w1',
|
|
99
|
+
destination: '大理'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
wishId: 'w2',
|
|
103
|
+
destination: '清迈'
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
assert.deepEqual(gaps, [
|
|
107
|
+
{
|
|
108
|
+
wishId: 'w2'
|
|
109
|
+
}
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
pass('trip_id 语义派生:确定性可重放', ()=>{
|
|
113
|
+
assert.equal(makeTripId({
|
|
114
|
+
destination: '大 理',
|
|
115
|
+
start: '2025-10-01',
|
|
116
|
+
source: 'user-verbatim',
|
|
117
|
+
evidence: 'x'
|
|
118
|
+
}), '大_理|2025-10-01|user-verbatim');
|
|
119
|
+
});
|
|
120
|
+
console.log(`\nTRAVEL TIMELINE TESTS: ${n}/7 OK(memory-design P1 守门面)`);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/travel-timeline-tests.ts
|
|
@@ -16,7 +16,7 @@ const byLeg = Object.fromEntries(r1.legs.map((l)=>[
|
|
|
16
16
|
]));
|
|
17
17
|
assert.notEqual(byLeg['f4'].service, 'DZ6252');
|
|
18
18
|
assert.equal(byLeg['f5'].service, 'EK329');
|
|
19
|
-
assert.equal(byLeg['f5'].energy_pct,
|
|
19
|
+
assert.equal(byLeg['f5'].energy_pct, 79);
|
|
20
20
|
assert.ok(byLeg['f5'].wake.includes('前一日'));
|
|
21
21
|
assert.equal(byLeg['f5'].door_to_door, '11h20m');
|
|
22
22
|
if (byLeg['f3'].service === 'MU6088') {
|