@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.8

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.
Files changed (44) hide show
  1. package/README.md +27 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +56 -5
  5. package/data/flights_2026.json +7 -4
  6. package/data/hotels_2026.json +102 -15
  7. package/data/time-slot-eval.json +384 -0
  8. package/data/yunnan-pack.json +2 -1
  9. package/dist/capabilities/agent-reach-bridge.py +90 -0
  10. package/dist/capabilities/anything.js +2 -2
  11. package/dist/capabilities/hbcli.js +11 -6
  12. package/dist/capabilities/incident-log.js +2 -1
  13. package/dist/scripts/agent-reach-tests.js +1 -1
  14. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  15. package/dist/scripts/hbcli-tests.js +18 -2
  16. package/dist/scripts/memory-capture-tests.js +77 -0
  17. package/dist/scripts/memory-metrics.js +38 -0
  18. package/dist/scripts/nudge-digest.js +93 -0
  19. package/dist/scripts/probe-poi-tests.js +9 -0
  20. package/dist/scripts/replay.js +73 -1
  21. package/dist/scripts/skeleton-check.js +3 -1
  22. package/dist/scripts/skills-contract-tests.js +85 -0
  23. package/dist/scripts/smoke.js +201 -3
  24. package/dist/scripts/time-eval-tests.js +318 -0
  25. package/dist/src/dsh-llm.js +27 -7
  26. package/dist/src/index.js +342 -92
  27. package/dist/src/loop.js +37 -10
  28. package/dist/src/memory-capture.js +40 -0
  29. package/dist/src/memory-utility.js +55 -0
  30. package/dist/src/mock-llm.js +6 -1
  31. package/dist/src/slot-spec.js +165 -0
  32. package/dist/src/time-anchor.js +100 -0
  33. package/dist/src/tool-packet.js +12 -0
  34. package/dist/src/travel-slots.js +144 -0
  35. package/dist/src/wish-pool.js +27 -0
  36. package/package.json +6 -2
  37. package/ts/capabilities/hbcli.ts +6 -6
  38. package/ts/capabilities/incident-log.ts +6 -3
  39. package/ts/scripts/skeleton-check.ts +5 -1
  40. package/ts/src/dsh-llm.ts +27 -7
  41. package/ts/src/index.ts +292 -77
  42. package/ts/src/loop.ts +52 -17
  43. package/ts/src/mock-llm.ts +13 -1
  44. package/ts/cordis.gotry-patch.yml +0 -36
@@ -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
@@ -1,17 +1,19 @@
1
1
  import { parseFlightPackToSpec } from './unified.js';
2
- const MODEL = process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
3
- const BASE = (process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '');
2
+ import { buildTimeAnchor } from './time-anchor.js';
3
+ import { buildSlotSystem, flagExpiredSlots, normalizeExtraction } from './travel-slots.js';
4
+ const model = ()=>process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
5
+ const base = ()=>(process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '');
4
6
  async function chat(messages, json) {
5
7
  const key = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
6
8
  if (!key) throw new Error('LLM_API_KEY 未设置(兼容 DEEPSEEK_API_KEY 别名)——真 LLM 路径不可用,请回退 mock(ADR-8)');
7
- const res = await fetch(`${BASE}/chat/completions`, {
9
+ const res = await fetch(`${base()}/chat/completions`, {
8
10
  method: 'POST',
9
11
  headers: {
10
12
  'Content-Type': 'application/json',
11
13
  Authorization: `Bearer ${key}`
12
14
  },
13
15
  body: JSON.stringify({
14
- model: MODEL,
16
+ model: model(),
15
17
  messages,
16
18
  ...json ? {
17
19
  response_format: {
@@ -46,9 +48,10 @@ const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程
46
48
  输出 JSON:{"scenario":"erhai|workation|yunnan|generic","segments":[{"id":"f1","role":"choice|fixed","route":"HKG->HKT","dateHint":"2026-07-18","anchors":{"arriveByMin":885}}]}
47
49
  规则:每个跨城移动一段;锚点只放用户明说或必然的(如"当天到"→arriveByMin 23:59=1439);时刻用当日分钟。
48
50
  scenario 判定:「洱海/大理/千岛湖/太湖+选目的地」→erhai(候选集);「普吉/workation/远程办公+多城链」→workation(五段链);「云南/大理丽江」→yunnan;不确定→generic。只输出 JSON。`;
49
- export function createOpenAICompatLlm(flightPackPath) {
51
+ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
50
52
  const pack = flightPackPath;
51
53
  const historyText = (h)=>h.map((t)=>`${t.role === 'user' ? '用户' : '助手'}: ${t.text}`).join('\n');
54
+ const anchorContext = ()=>`时间锚点卡:\n${buildTimeAnchor(clock()).card}`;
52
55
  return {
53
56
  async extractFacts (history) {
54
57
  const out = await chat([
@@ -58,7 +61,7 @@ export function createOpenAICompatLlm(flightPackPath) {
58
61
  },
59
62
  {
60
63
  role: 'user',
61
- content: historyText(history)
64
+ content: `${anchorContext()}\n\n${historyText(history)}`
62
65
  }
63
66
  ], true);
64
67
  const obj = parseJsonBlock(out);
@@ -78,7 +81,7 @@ export function createOpenAICompatLlm(flightPackPath) {
78
81
  };
79
82
  },
80
83
  async extractSpec (history, state) {
81
- const context = `已断言日历:${JSON.stringify(state.calendar.assertedWeekdays)}\nprofile:${JSON.stringify(state.profile)}`;
84
+ const context = `${anchorContext()}\n已断言日历:${JSON.stringify(state.calendar.assertedWeekdays)}\nprofile:${JSON.stringify(state.profile)}`;
82
85
  const out = await chat([
83
86
  {
84
87
  role: 'system',
@@ -134,6 +137,23 @@ export function createOpenAICompatLlm(flightPackPath) {
134
137
  packSpec.budgetCny = 9000;
135
138
  return packSpec;
136
139
  },
140
+ async extractSlots (history, now) {
141
+ const anchor = buildTimeAnchor(now ?? clock());
142
+ const out = await chat([
143
+ {
144
+ role: 'system',
145
+ content: buildSlotSystem(anchor)
146
+ },
147
+ {
148
+ role: 'user',
149
+ content: historyText(history)
150
+ }
151
+ ], true);
152
+ const obj = parseJsonBlock(out);
153
+ if (!obj) return null;
154
+ const ext = normalizeExtraction(obj, history.map((t)=>t.text).join('\n'));
155
+ return ext ? flagExpiredSlots(ext, anchor) : null;
156
+ },
137
157
  async polishQuestion (q) {
138
158
  const out = await chat([
139
159
  {