@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
package/dist/src/loop.js CHANGED
@@ -228,6 +228,26 @@ export async function runTurn(state, userMsg, llm, history = [], solve = null) {
228
228
  } else {
229
229
  const invalid = spec ? validateSpec(spec) : '翻译器未产出 spec';
230
230
  if (spec && !invalid) {
231
+ const ext = await llm.extractSlots([
232
+ ...history,
233
+ {
234
+ role: 'user',
235
+ text: userMsg
236
+ }
237
+ ]);
238
+ if (ext) {
239
+ const { resolveSlots, specDateMismatches } = await import('./slot-spec.js');
240
+ const { buildTimeAnchor } = await import('./time-anchor.js');
241
+ const mismatches = specDateMismatches(spec, resolveSlots(ext, buildTimeAnchor()));
242
+ if (mismatches.length > 0) {
243
+ const lines = mismatches.map((m)=>`- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`);
244
+ parts.push(`⚠️ 日期分歧,确认后我再算(以你为准):\n${lines.join('\n')}`);
245
+ return {
246
+ reply: parts.join('\n\n'),
247
+ state
248
+ };
249
+ }
250
+ }
231
251
  state.spec = spec;
232
252
  state.solve = await solve(spec);
233
253
  parts.push(llm.render ? await llm.render(state) : renderSolve(state));
@@ -277,17 +297,24 @@ export async function settleAsyncTicket(ticketId, reply) {
277
297
  export function probePoi(msg) {
278
298
  const trimmed = msg.trim();
279
299
  if (!trimmed) return null;
280
- const isShort = trimmed.length <= 24 && !/[??!!。.,,]/.test(trimmed);
281
- if (isShort) {
282
- const stripped = trimmed.replace(/^(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*/, '').trim();
283
- if (stripped.length >= 2) return stripped.slice(0, 24);
284
- }
300
+ const cutAtPunct = (s)=>s.split(/[,,。.;;!!??]/)[0]?.trim() ?? '';
285
301
  const m = trimmed.match(/(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*(.{2,24})/);
286
- if (m) return m[2].trim().slice(0, 24);
287
- if (/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/.test(trimmed)) {
288
- const place = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/);
289
- if (place) return place[1];
290
- return trimmed.replace(/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/, '').trim().slice(0, 24) || trimmed.slice(0, 24);
302
+ if (m?.[2]) {
303
+ const obj = cutAtPunct(m[2]);
304
+ if (obj.length >= 2) return obj.slice(0, 24);
305
+ }
306
+ if (/(酒店|民宿|客栈|饭店)/.test(trimmed)) {
307
+ const parts = trimmed.split(/(酒店|民宿|客栈|饭店)/);
308
+ const after = cutAtPunct((parts[2] ?? '').replace(/^[的::\s]+/, ''));
309
+ if (after.length >= 2 && /[A-Za-z0-9]/.test(after)) return after.slice(0, 24);
310
+ const nounAt = trimmed.search(/(酒店|民宿|客栈|饭店)/);
311
+ const pre = trimmed.slice(Math.max(0, nounAt - 6), nounAt).match(/[一-龥]{2,6}$/);
312
+ if (pre && !/(机票|车票|和|做|订|的)$/.test(pre[0])) return pre[0];
313
+ }
314
+ const q = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:有什么|玩什么|有哪些)/);
315
+ if (q?.[1]) return q[1];
316
+ if (trimmed.length <= 12 && !/[,,。.;;!!??::]/.test(trimmed) && !/[是有在要想得到说订吧呢]/.test(trimmed)) {
317
+ return trimmed.slice(0, 24);
291
318
  }
292
319
  return null;
293
320
  }
@@ -0,0 +1,40 @@
1
+ export function mergeProfile(current, patch) {
2
+ if (!patch) return null;
3
+ const base = {
4
+ weights: current?.weights ?? {},
5
+ evidence: current?.evidence ?? [],
6
+ hard: current?.hard ?? {}
7
+ };
8
+ const newEvidence = (patch.evidence ?? []).filter(Boolean).filter((e)=>!base.evidence.includes(e));
9
+ const patchWeights = patch.weights;
10
+ const weightsChanged = !!patchWeights && Object.keys(patchWeights).length > 0 && JSON.stringify(patchWeights) !== JSON.stringify(base.weights);
11
+ const hardChanged = !!patch.hard && Object.keys(patch.hard).length > 0 && JSON.stringify(patch.hard) !== JSON.stringify(base.hard);
12
+ if (newEvidence.length === 0 && !weightsChanged && !hardChanged) return null;
13
+ let weights = {
14
+ ...base.weights
15
+ };
16
+ if (weightsChanged && newEvidence.length > 0 && patchWeights) {
17
+ weights = {
18
+ ...weights,
19
+ ...patchWeights
20
+ };
21
+ const sum = Object.values(weights).reduce((a, b)=>a + b, 0);
22
+ if (sum > 0) for (const k of Object.keys(weights))weights[k] = Math.round(weights[k] / sum * 100) / 100;
23
+ }
24
+ return {
25
+ weights,
26
+ evidence: [
27
+ ...base.evidence,
28
+ ...newEvidence
29
+ ],
30
+ hard: hardChanged ? {
31
+ ...base.hard,
32
+ ...patch.hard ?? {}
33
+ } : {
34
+ ...base.hard
35
+ }
36
+ };
37
+ }
38
+
39
+
40
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/memory-capture.ts
@@ -0,0 +1,55 @@
1
+ export function makeEventId(ev) {
2
+ return [
3
+ ev.wish_id,
4
+ ev.kind,
5
+ ev.ctx ?? '',
6
+ ev.attribution ?? ''
7
+ ].join('|');
8
+ }
9
+ export function appendEvent(events, ev) {
10
+ const full = {
11
+ schema: 'memory_utility_observation.v0',
12
+ event_id: makeEventId(ev),
13
+ ...ev
14
+ };
15
+ if (events.some((e)=>e.event_id === full.event_id)) return {
16
+ events,
17
+ appended: false
18
+ };
19
+ if (ev.kind === 'verified_outcome' && !ev.attribution) {
20
+ return appendEvent(events, {
21
+ ...ev,
22
+ kind: 'applied'
23
+ });
24
+ }
25
+ return {
26
+ events: [
27
+ ...events,
28
+ full
29
+ ],
30
+ appended: true
31
+ };
32
+ }
33
+ export function projectUtility(events) {
34
+ const out = {};
35
+ for (const ev of events){
36
+ const w = out[ev.wish_id] ??= {
37
+ wish_id: ev.wish_id,
38
+ status: 'unknown',
39
+ recalled: 0,
40
+ applied: 0,
41
+ verified: 0
42
+ };
43
+ if (ev.kind === 'recalled') w.recalled++;
44
+ if (ev.kind === 'applied') w.applied++;
45
+ if (ev.kind === 'verified_outcome') {
46
+ w.verified++;
47
+ if (ev.attribution) w.status = ev.attribution;
48
+ }
49
+ w.last_event_ts = ev.ts;
50
+ }
51
+ return out;
52
+ }
53
+
54
+
55
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/memory-utility.ts
@@ -50,7 +50,7 @@ const SCRIPT = [
50
50
  }
51
51
  }
52
52
  ];
53
- export function createMockLlm(flightPackPath) {
53
+ export function createMockLlm(flightPackPath, slotScript = []) {
54
54
  let stepIdx = 0;
55
55
  return {
56
56
  async extractFacts (history, _state) {
@@ -88,6 +88,11 @@ export function createMockLlm(flightPackPath) {
88
88
  spec.budgetCny = 9000;
89
89
  return spec;
90
90
  },
91
+ async extractSlots (history) {
92
+ const last = history[history.length - 1]?.text ?? '';
93
+ const hit = slotScript.find((s)=>last.includes(s.when));
94
+ return hit ? structuredClone(hit.extraction) : null;
95
+ },
91
96
  async polishQuestion (q) {
92
97
  return `【${q.key}】${q.text}\n(为什么问:${q.why})`;
93
98
  }
@@ -0,0 +1,165 @@
1
+ import { parseAbsoluteDate, ymd } from './time-anchor.js';
2
+ const WEEKDAY_ZH = {
3
+ 日: 0,
4
+ 一: 1,
5
+ 二: 2,
6
+ 三: 3,
7
+ 四: 4,
8
+ 五: 5,
9
+ 六: 6
10
+ };
11
+ function addDaysYmd(today, n) {
12
+ const [y, m, d] = today.split('-').map(Number);
13
+ return ymd(new Date(y, m - 1, d + n));
14
+ }
15
+ function splitPlusSuffix(expr) {
16
+ const m = expr.match(/^(.*?)\+(\d+)天?$/);
17
+ if (m && m[1]) return {
18
+ base: m[1],
19
+ plusDays: Number(m[2])
20
+ };
21
+ return {
22
+ base: expr,
23
+ plusDays: 0
24
+ };
25
+ }
26
+ function neighborDayOffset(expr) {
27
+ if (expr === '今天') return 0;
28
+ if (expr === '明天') return 1;
29
+ if (expr === '后天') return 2;
30
+ if (expr === '大后天') return 3;
31
+ return null;
32
+ }
33
+ function weekdayOffsetDays(expr, today) {
34
+ const m = expr.match(/^(本|下|下下)周([日一二三四五六])$/);
35
+ if (!m || !m[2]) return null;
36
+ const target = WEEKDAY_ZH[m[2]];
37
+ if (target === undefined) return null;
38
+ const [y, mm, d] = today.split('-').map(Number);
39
+ const todayMonOffset = (new Date(y, mm - 1, d).getDay() + 6) % 7;
40
+ const targetMonOffset = (target + 6) % 7;
41
+ const weekShift = m[1] === '本' ? 0 : m[1] === '下' ? 7 : 14;
42
+ return weekShift + targetMonOffset - todayMonOffset;
43
+ }
44
+ function monthFragmentDate(expr, today) {
45
+ if (![
46
+ '下个月初',
47
+ '下个月中旬',
48
+ '下个月下旬'
49
+ ].includes(expr)) return null;
50
+ const [y, m] = today.split('-').map(Number);
51
+ const day = expr === '下个月初' ? 1 : expr === '下个月中旬' ? 11 : 21;
52
+ return ymd(new Date(y, m, day));
53
+ }
54
+ export function resolveSlotDate(expr, anchor) {
55
+ const raw = expr.trim();
56
+ const year = Number(anchor.today.slice(0, 4));
57
+ const absolute = parseAbsoluteDate(raw, year);
58
+ if (absolute) return {
59
+ raw,
60
+ date: absolute,
61
+ kind: 'absolute'
62
+ };
63
+ const { base, plusDays } = splitPlusSuffix(raw);
64
+ const baseAbs = parseAbsoluteDate(base, year);
65
+ if (baseAbs) return {
66
+ raw,
67
+ date: addDaysYmd(baseAbs, plusDays),
68
+ kind: 'absolute'
69
+ };
70
+ const month = monthFragmentDate(base, anchor.today);
71
+ if (month) return {
72
+ raw,
73
+ date: addDaysYmd(month, plusDays),
74
+ kind: 'card'
75
+ };
76
+ const neighbor = neighborDayOffset(base);
77
+ if (neighbor !== null) return {
78
+ raw,
79
+ date: addDaysYmd(anchor.today, neighbor + plusDays),
80
+ kind: 'card'
81
+ };
82
+ const offset = weekdayOffsetDays(base, anchor.today);
83
+ if (offset !== null) return {
84
+ raw,
85
+ date: addDaysYmd(anchor.today, offset + plusDays),
86
+ kind: 'card'
87
+ };
88
+ return {
89
+ raw,
90
+ date: null,
91
+ kind: 'unresolved'
92
+ };
93
+ }
94
+ const DATE_FIELDS = {
95
+ requisition: [
96
+ 'start_date',
97
+ 'end_date'
98
+ ],
99
+ flight: [
100
+ 'departure_date',
101
+ 'return_date'
102
+ ],
103
+ hotel: [
104
+ 'check_in_date',
105
+ 'check_out_date'
106
+ ]
107
+ };
108
+ export function resolveSlots(ext, anchor) {
109
+ const out = {
110
+ unresolved: []
111
+ };
112
+ for (const domain of ext.domains){
113
+ const slots = ext.slots[domain];
114
+ if (!slots) continue;
115
+ for (const field of DATE_FIELDS[domain] ?? []){
116
+ const value = slots[field];
117
+ if (typeof value !== 'string') continue;
118
+ const res = resolveSlotDate(value, anchor);
119
+ const bucket = out[domain] ??= {};
120
+ bucket[field] = res;
121
+ if (res.kind === 'unresolved') out.unresolved.push({
122
+ field: `${domain}.${field}`,
123
+ raw: res.raw
124
+ });
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ export function specDateMismatches(spec, resolved) {
130
+ const mismatches = [];
131
+ const primary = [
132
+ {
133
+ field: 'requisition.start_date',
134
+ res: resolved.requisition?.start_date
135
+ },
136
+ {
137
+ field: 'flight.departure_date',
138
+ res: resolved.flight?.departure_date
139
+ },
140
+ {
141
+ field: 'hotel.check_in_date',
142
+ res: resolved.hotel?.check_in_date
143
+ }
144
+ ];
145
+ const dated = primary.filter((p)=>p.res?.date != null);
146
+ if (dated.length === 0) return mismatches;
147
+ const datedSegs = (spec.segments ?? []).filter((s)=>typeof s.date === 'string' && s.date);
148
+ if (datedSegs.length !== 1) return mismatches;
149
+ const seg = datedSegs[0];
150
+ const slotDates = new Set(dated.map((s)=>s.res.date));
151
+ if (!slotDates.has(seg.date)) {
152
+ mismatches.push({
153
+ specAt: `segments[${(spec.segments ?? []).indexOf(seg)}].date`,
154
+ field: dated.map((s)=>s.field).join('|'),
155
+ specDate: seg.date,
156
+ slotDate: [
157
+ ...slotDates
158
+ ].join('|')
159
+ });
160
+ }
161
+ return mismatches;
162
+ }
163
+
164
+
165
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/slot-spec.ts
@@ -0,0 +1,100 @@
1
+ const WEEKDAYS_ZH = [
2
+ '日',
3
+ '一',
4
+ '二',
5
+ '三',
6
+ '四',
7
+ '五',
8
+ '六'
9
+ ];
10
+ export function ymd(d) {
11
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
12
+ }
13
+ function addDays(d, n) {
14
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
15
+ }
16
+ function weekMonday(d) {
17
+ return addDays(d, -((d.getDay() + 6) % 7));
18
+ }
19
+ const SPRING_FESTIVAL = {
20
+ 2026: '2026-02-17',
21
+ 2027: '2027-02-06',
22
+ 2028: '2028-01-26',
23
+ 2029: '2029-02-13',
24
+ 2030: '2030-02-03',
25
+ 2031: '2031-01-23'
26
+ };
27
+ function tzLabelOf(d) {
28
+ const offsetMin = -d.getTimezoneOffset();
29
+ const sign = offsetMin >= 0 ? '+' : '-';
30
+ const abs = Math.abs(offsetMin);
31
+ const h = Math.floor(abs / 60);
32
+ const m = abs % 60;
33
+ return `UTC${sign}${m === 0 ? h : `${h}:${String(m).padStart(2, '0')}`}`;
34
+ }
35
+ function weekdayOf(d) {
36
+ return `周${WEEKDAYS_ZH[d.getDay()]}`;
37
+ }
38
+ function weekRow(label, monday) {
39
+ const days = Array.from({
40
+ length: 7
41
+ }, (_, i)=>{
42
+ const d = addDays(monday, i);
43
+ return `${weekdayOf(d)} ${ymd(d)}`;
44
+ });
45
+ return `${label}:${days.join('|')}`;
46
+ }
47
+ function nextOccurrence(todayYmd, year, monthDay) {
48
+ const thisYear = `${year}-${monthDay}`;
49
+ return thisYear >= todayYmd ? thisYear : `${year + 1}-${monthDay}`;
50
+ }
51
+ export function buildTimeAnchor(now = new Date()) {
52
+ const today = ymd(now);
53
+ const year = now.getFullYear();
54
+ const monday = weekMonday(now);
55
+ const tomorrow = addDays(now, 1);
56
+ const dayAfter = addDays(now, 2);
57
+ const inThreeDays = addDays(now, 3);
58
+ const nm = new Date(year, now.getMonth() + 1, 1);
59
+ const nmYear = nm.getFullYear();
60
+ const nmMonth = nm.getMonth();
61
+ const nmLastDay = new Date(nmYear, nmMonth + 1, 0).getDate();
62
+ const mm = String(nmMonth + 1).padStart(2, '0');
63
+ const nextQuarterMonth0 = Math.floor(now.getMonth() / 3) * 3 + 3;
64
+ const nq = new Date(nextQuarterMonth0 >= 12 ? year + 1 : year, nextQuarterMonth0 % 12, 1);
65
+ const festivals = [
66
+ `国庆 ${nextOccurrence(today, year, '10-01')}`,
67
+ `元旦 ${nextOccurrence(today, year, '01-01')}`
68
+ ];
69
+ const spring = Object.values(SPRING_FESTIVAL).filter((d)=>d >= today).sort()[0];
70
+ if (spring) festivals.push(`春节 ${spring}`);
71
+ const card = [
72
+ `今天 ${today} ${weekdayOf(now)}(时区 ${tzLabelOf(now)})`,
73
+ `明天 ${ymd(tomorrow)} ${weekdayOf(tomorrow)}|后天 ${ymd(dayAfter)} ${weekdayOf(dayAfter)}|大后天 ${ymd(inThreeDays)} ${weekdayOf(inThreeDays)}`,
74
+ weekRow('本周', monday),
75
+ weekRow('下周', addDays(monday, 7)),
76
+ weekRow('下下周', addDays(monday, 14)),
77
+ `下个月 ${nmYear}-${mm}:初 ${mm}-01~${mm}-10|中旬 ${mm}-11~${mm}-20|下旬 ${mm}-21~${mm}-${nmLastDay}`,
78
+ `下个季度初:${ymd(nq)} 起`,
79
+ `节日锚点:${festivals.join('|')}(「国庆前」= 该日之前,以此类推)`
80
+ ].join('\n');
81
+ return {
82
+ today,
83
+ todayWeekdayZh: weekdayOf(now),
84
+ tzLabel: tzLabelOf(now),
85
+ card
86
+ };
87
+ }
88
+ export function parseAbsoluteDate(expr, anchorYear) {
89
+ const t = expr.trim();
90
+ let m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
91
+ if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`;
92
+ m = t.match(/^(\d{1,2})[./](\d{1,2})(?:日|号)?$/);
93
+ if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`;
94
+ m = t.match(/^(\d{1,2})月(\d{1,2})[日号]?$/);
95
+ if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`;
96
+ return null;
97
+ }
98
+
99
+
100
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/time-anchor.ts
@@ -0,0 +1,12 @@
1
+ export function interpretArgs(args, stringKey) {
2
+ if (args.query && typeof args.query === 'object') return args.query;
3
+ const { query, ...rest } = args;
4
+ if (Object.keys(rest).length > 0) return rest;
5
+ if (typeof query === 'string' && stringKey) return {
6
+ [stringKey]: query
7
+ };
8
+ return {};
9
+ }
10
+
11
+
12
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/tool-packet.ts
@@ -0,0 +1,144 @@
1
+ import { parseAbsoluteDate } from './time-anchor.js';
2
+ const PRIMARY_DATE_FIELD = {
3
+ requisition: 'start_date',
4
+ flight: 'departure_date',
5
+ hotel: 'check_in_date'
6
+ };
7
+ export function flagExpiredSlots(ext, anchor) {
8
+ const anchorYear = Number(anchor.today.slice(0, 4));
9
+ const missing = [
10
+ ...ext.missing_slots
11
+ ];
12
+ for (const domain of ext.domains){
13
+ const slots = ext.slots[domain];
14
+ const field = PRIMARY_DATE_FIELD[domain];
15
+ const value = slots?.[field];
16
+ if (typeof value !== 'string') continue;
17
+ const resolved = parseAbsoluteDate(value, anchorYear);
18
+ if (resolved && resolved < anchor.today) {
19
+ const flag = `${domain}.${field} (date is expired)`;
20
+ if (!missing.includes(flag)) missing.push(flag);
21
+ }
22
+ }
23
+ return {
24
+ ...ext,
25
+ missing_slots: missing
26
+ };
27
+ }
28
+ export function buildSlotSystem(anchor) {
29
+ return `你是差旅需求的槽位抽取器。从用户原话抽取槽位,只输出 JSON:
30
+ {"schema_version":"travel_slot_extraction.v1","language":"zh|en|mixed","domains":["requisition"|"flight"|"hotel",...],"slots":{...},"missing_slots":[...]}
31
+
32
+ ## 时间锚点卡
33
+ ${anchor.card}
34
+ 相对日期(明天/下周X/下个月中旬/国庆前…)对应的绝对日期都查锚点卡,不要自己心算。
35
+
36
+ ## 铁律
37
+ 1. 时间表达逐字保留、不翻译:用户说「下周一」就写「下周一」,说「next Thursday」就写 "next Thursday"——绝不换算成绝对日期、绝不切换语言(换算归下游工具)。模糊时间词(过几天/近期/下周初/隔周/下个月)同样是合法槽位值,逐字保留,**不算缺失**。
38
+ 2. language 指**用户原话**的语言(与槽位值用什么语言无关):全中文 zh;全英文 en;中英混排 mixed。即使槽位值按规则 10 含中文城市名,只要原话全英文,language 仍为 en。
39
+ 3. 域判定——看用户的**动作动词**,一句话可落多个域:
40
+ - 安排行程/行程推荐/帮我安排 → requisition:{"mode":"create","destination","start_date","end_date"?,"trip_type"}
41
+ - 看/查/订机票、航班、票价 → flight:{"action":"search","origin"?,"destination","departure_date","return_date"?,"trip_type"}
42
+ - 看/订酒店、入住、订房 → hotel:{"action":"search","city","check_in_date","check_out_date"?};「待/住 N 天」只是时长语义,不单独构成 hotel 域
43
+ - 「出差/去 X」本身不构成 requisition;没有上述动词的单城出行意图按回程形态分流:
44
+ 「去 X,当天往返」→ 仅 requisition(round_trip);「去 X,周日回/X 号回」跨天回程 → 仅 flight(round_trip)。
45
+ 4. trip_type 必填(requisition 与 flight;hotel 不要):出现「往返/回程/当天往返/X 号回」→ "round_trip";否则显式 "one_way"。
46
+ 5. 停留天数:「出差/待/住 N 天」→ requisition 用 "duration_days":N;hotel 用 "check_out_date":"<入住表达>+N" 原样拼接。
47
+ 6. 回程机票(「住 N 天回 X」「待到周日回 X」):flight.origin=旅行城市, flight.destination=X, flight.departure_date="<入住/到达表达>+N" 或逐字回程表达,只有回程时 trip_type="one_way"。
48
+ 7. 价格**排序**偏好只在找低价时加(「哪天便宜/比较便宜/最便宜」→ flight "sort_name":"PRICE","sort_type":"ASC");单纯「看价格/多少钱」不加。
49
+ 8. 出发时段偏好(「早上七点后出发」)→ flight 加 "departure_time_pref":"07:00后"(HH:MM后 格式)。
50
+ 9. 多目的地:
51
+ - 国内多城链路(≥3 城):requisition.destination 与 flight.destination 都写**全程城市数组**(含首站),flight 不写 origin,departure_date=首段出发表达,加 "multi_destination":true;不为链路每段单独建 hotel/子段。
52
+ - 国际开口程(open-jaw):flight.origin=首站(单值), flight.destination=后续链路数组, 加 "multi_destination":true。
53
+ 10. 城市名:纯国内行程用中文标准名(北京/上海/广州/深圳…,即使原文是拼音);国际或跨国多城链路的城市名保留用户原文。
54
+ 11. 关键槽位缺失(如有机票意图但无目的地)→ missing_slots 加 "<domain>.<field>";不缺则空数组。
55
+ 12. 过期判定不归你:绝对日期是否已过由下游确定性层处理,你只管逐字抽取。
56
+
57
+ ## 示例(学习约定,勿照抄内容)
58
+ 用户:下周五入住上海,住三天回杭州
59
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["hotel","flight"],"slots":{"hotel":{"action":"search","city":"上海","check_in_date":"下周五","check_out_date":"下周五+3"},"flight":{"action":"search","origin":"上海","destination":"杭州","departure_date":"下周五+3","trip_type":"one_way"}},"missing_slots":[]}
60
+
61
+ 用户:next Monday 去上海,订酒店
62
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"mixed","domains":["hotel"],"slots":{"hotel":{"action":"search","city":"上海","check_in_date":"next Monday"}},"missing_slots":[]}
63
+
64
+ 用户:这阵子想去广州,帮我看下深圳到广州的机票
65
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["flight"],"slots":{"flight":{"action":"search","origin":"深圳","destination":"广州","departure_date":"这阵子","trip_type":"one_way"}},"missing_slots":[]}
66
+
67
+ 用户:8.20 日去杭州,当天往返
68
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["requisition"],"slots":{"requisition":{"mode":"create","destination":"杭州","start_date":"8.20","trip_type":"round_trip"}},"missing_slots":[]}
69
+
70
+ 用户:下周三去杭州,周六回
71
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["flight"],"slots":{"flight":{"action":"search","destination":"杭州","departure_date":"下周三","return_date":"周六","trip_type":"round_trip"}},"missing_slots":[]}
72
+
73
+ 用户:下周三去成都,待到周六,周六去西安待一天然后回杭州
74
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"zh","domains":["requisition","flight"],"slots":{"requisition":{"mode":"create","destination":["成都","西安","杭州"],"start_date":"下周三","trip_type":"round_trip"},"flight":{"action":"search","destination":["成都","西安","杭州"],"departure_date":"下周三","multi_destination":true,"trip_type":"round_trip"}},"missing_slots":[]}
75
+
76
+ 用户:open-jaw please: Shanghai to Tokyo next Monday, back from Osaka to Shanghai next Friday
77
+ 输出:{"schema_version":"travel_slot_extraction.v1","language":"en","domains":["flight"],"slots":{"flight":{"action":"search","origin":"Shanghai","destination":["Tokyo","Osaka","Shanghai"],"departure_date":"next Monday","return_date":"next Friday","multi_destination":true,"trip_type":"round_trip"}},"missing_slots":[]}
78
+
79
+ 只输出 JSON,不要多余文本。`;
80
+ }
81
+ export function detectLanguage(utterance) {
82
+ const hasCjk = /[一-鿿]/.test(utterance);
83
+ const hasLatin = /[A-Za-z]/.test(utterance);
84
+ if (hasCjk && hasLatin) return 'mixed';
85
+ if (hasLatin) return 'en';
86
+ return 'zh';
87
+ }
88
+ export function normalizeExtraction(obj, utterance) {
89
+ const domains = obj['domains'];
90
+ if (!Array.isArray(domains) || domains.length === 0) return null;
91
+ const valid = domains.filter((d)=>d === 'requisition' || d === 'flight' || d === 'hotel');
92
+ if (valid.length === 0) return null;
93
+ const lang = obj['language'];
94
+ const slots = obj['slots'] ?? {};
95
+ const missing = obj['missing_slots'];
96
+ return {
97
+ schema_version: 'travel_slot_extraction.v1',
98
+ language: utterance !== undefined ? detectLanguage(utterance) : lang === 'en' || lang === 'mixed' ? lang : 'zh',
99
+ domains: valid,
100
+ slots,
101
+ missing_slots: Array.isArray(missing) ? missing.map(String) : []
102
+ };
103
+ }
104
+ const sortedSet = (xs)=>[
105
+ ...xs
106
+ ].sort();
107
+ export function scoreExtraction(expected, actual) {
108
+ const diffs = [];
109
+ const warnings = [];
110
+ if (expected.language !== actual.language) {
111
+ diffs.push(`language: 期望 ${expected.language},实际 ${actual.language}`);
112
+ }
113
+ if (JSON.stringify(sortedSet(expected.domains)) !== JSON.stringify(sortedSet(actual.domains))) {
114
+ diffs.push(`domains: 期望 [${sortedSet(expected.domains)}],实际 [${sortedSet(actual.domains)}]`);
115
+ }
116
+ for (const domain of expected.domains){
117
+ const exp = expected.slots[domain];
118
+ const act = actual.slots[domain];
119
+ if (!exp) continue;
120
+ if (!act) {
121
+ diffs.push(`slots.${domain}: 实际缺失整个域`);
122
+ continue;
123
+ }
124
+ for (const [k, v] of Object.entries(exp)){
125
+ if (JSON.stringify(act[k]) !== JSON.stringify(v)) {
126
+ diffs.push(`slots.${domain}.${k}: 期望 ${JSON.stringify(v)},实际 ${JSON.stringify(act[k])}`);
127
+ }
128
+ }
129
+ for (const k of Object.keys(act)){
130
+ if (!(k in exp)) warnings.push(`slots.${domain}.${k}: 实际多出键 ${JSON.stringify(act[k])}`);
131
+ }
132
+ }
133
+ if (JSON.stringify(sortedSet(expected.missing_slots)) !== JSON.stringify(sortedSet(actual.missing_slots))) {
134
+ diffs.push(`missing_slots: 期望 [${expected.missing_slots}],实际 [${actual.missing_slots}]`);
135
+ }
136
+ return {
137
+ pass: diffs.length === 0,
138
+ diffs,
139
+ warnings
140
+ };
141
+ }
142
+
143
+
144
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/travel-slots.ts
@@ -0,0 +1,27 @@
1
+ export function scoreWishMatch(entry, ctx) {
2
+ const c = entry.conditions ?? {};
3
+ const hits = [];
4
+ if (typeof ctx.days === 'number' && typeof c.days === 'number' && ctx.days >= c.days) hits.push(`days≥${c.days}`);
5
+ if (typeof ctx.budgetCny === 'number' && typeof c.budget_cny === 'number' && ctx.budgetCny >= c.budget_cny) hits.push(`budget≥${c.budget_cny}`);
6
+ if (Array.isArray(c.best_months) && typeof ctx.month === 'number' && c.best_months.map(Number).includes(ctx.month)) hits.push(`month=${ctx.month}`);
7
+ if (hits.length === 0) return null;
8
+ return {
9
+ entry,
10
+ wishId: String(entry.wish_id ?? ''),
11
+ score: hits.length,
12
+ hits
13
+ };
14
+ }
15
+ export function pickNudgeWish(pool, ctx) {
16
+ const scored = pool.filter((e)=>!e.muted && typeof e.wish_id === 'string').map((e)=>scoreWishMatch(e, ctx)).filter((m)=>m !== null).sort((a, b)=>{
17
+ if (b.score !== a.score) return b.score - a.score;
18
+ const ta = String(a.entry.added_at ?? '');
19
+ const tb = String(b.entry.added_at ?? '');
20
+ if (ta !== tb) return ta < tb ? -1 : 1;
21
+ return a.wishId < b.wishId ? -1 : 1;
22
+ });
23
+ return scored[0] ?? null;
24
+ }
25
+
26
+
27
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/wish-pool.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danceiny/gotry",
3
- "version": "0.0.1-rc.7",
3
+ "version": "0.0.1-rc.8",
4
4
  "description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm 包入口 + vendored dsh runtime + 5 行 README 安装路径。",
5
5
  "type": "module",
6
6
  "main": "ts/src/index.ts",
@@ -37,7 +37,11 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@deepseek-ai/dsh": "^0.1.1-rc.1",
40
- "z3-solver": "^5.2.0"
40
+ "z3-solver": "^5.2.0",
41
+ "dsh-map-tools": "^0.4.4",
42
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
43
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
44
+ "dsh-calendar": "^0.3.2"
41
45
  },
42
46
  "repository": {
43
47
  "type": "git",