@danceiny/gotry 0.0.1-rc.5 → 0.0.1-rc.7
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 +1 -1
- package/bin/gotry-inner.js +73 -32
- package/cordis.gotry-patch.yml +2 -2
- package/data/flights_2026.json +215 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +64 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/yunnan-pack.json +201 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/hbcli.js +138 -0
- package/dist/capabilities/incident-log.js +99 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +18 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +85 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +79 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +44 -0
- package/dist/scripts/skeleton-check.js +35 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/smoke.js +90 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +154 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +760 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +296 -0
- package/dist/src/mock-llm.js +98 -0
- package/dist/src/model.js +134 -0
- package/dist/src/unified.js +536 -0
- package/package.json +3 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { init } from 'z3-solver';
|
|
2
|
+
import { hhmmToMin, minToHhmm } from './model.js';
|
|
3
|
+
let z3Promise = null;
|
|
4
|
+
async function getZ3() {
|
|
5
|
+
if (!z3Promise) z3Promise = (async ()=>(await init()).Context('main'))();
|
|
6
|
+
return z3Promise;
|
|
7
|
+
}
|
|
8
|
+
export function evaluateLeg(leg, svc) {
|
|
9
|
+
const wake = svc.depMin - leg.bufferMin - leg.originTransferMin;
|
|
10
|
+
const wakeDisplay = wake < 0 ? `${minToHhmm(wake + 1440)}(前一日)` : minToHhmm(wake);
|
|
11
|
+
const arriveStay = svc.arrMin + leg.destTransferMin;
|
|
12
|
+
const d2d = arriveStay - wake;
|
|
13
|
+
let energy;
|
|
14
|
+
if (leg.redEye && (leg.redEyeDurationMin ?? 0) > 0) {
|
|
15
|
+
const sleepH = ((leg.redEyeDurationMin ?? 0) - 60) / 60;
|
|
16
|
+
energy = Math.max(30, Math.min(75, 30 + 8 * sleepH));
|
|
17
|
+
} else {
|
|
18
|
+
energy = 100 - 2 * 8;
|
|
19
|
+
if (wake < 5 * 60) energy -= 30;
|
|
20
|
+
else if (wake < 6 * 60) energy -= 25;
|
|
21
|
+
if (arriveStay > 21 * 60) energy -= 10;
|
|
22
|
+
if (d2d > 6 * 60) energy -= 10;
|
|
23
|
+
energy = Math.max(0, energy);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
service: svc.id,
|
|
27
|
+
dep: minToHhmm(svc.depMin),
|
|
28
|
+
wake: wakeDisplay,
|
|
29
|
+
wakeMin: wake,
|
|
30
|
+
arrive_stay: minToHhmm(arriveStay),
|
|
31
|
+
door_to_door: `${Math.floor(d2d / 60)}h${String(d2d % 60).padStart(2, '0')}m`,
|
|
32
|
+
energy_pct: Math.round(energy),
|
|
33
|
+
price_cny: svc.priceCny
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function exactlyOne(z3, sels) {
|
|
37
|
+
const { If, Sum, Int } = z3;
|
|
38
|
+
return Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
|
|
39
|
+
}
|
|
40
|
+
export async function solveJourney(req) {
|
|
41
|
+
const z3 = await getZ3();
|
|
42
|
+
const { Bool, If, Sum, Int, Solver } = z3;
|
|
43
|
+
const wakeFloor = req.wakeFloorMin ?? hhmmToMin('06:00');
|
|
44
|
+
const allSels = {};
|
|
45
|
+
const assertions = {};
|
|
46
|
+
const legSpecs = req.legs;
|
|
47
|
+
for (const leg of legSpecs){
|
|
48
|
+
const sels = leg.services.map((_, i)=>Bool.const(`${leg.id}_s${i}`));
|
|
49
|
+
allSels[leg.id] = sels;
|
|
50
|
+
const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(leg.services[i][attr]), Int.val(0))));
|
|
51
|
+
const dep = pick('depMin'), arr = pick('arrMin'), price = pick('priceCny');
|
|
52
|
+
const wake = dep.sub(Int.val(leg.bufferMin + leg.originTransferMin));
|
|
53
|
+
const arriveStay = arr.add(Int.val(leg.destTransferMin));
|
|
54
|
+
if (leg.arriveByMin !== undefined) assertions[`${leg.id}:arrive_by`] = arriveStay.le(Int.val(leg.arriveByMin));
|
|
55
|
+
if (leg.departAfterMin !== undefined) assertions[`${leg.id}:depart_after`] = dep.ge(Int.val(leg.departAfterMin));
|
|
56
|
+
if (!leg.redEye) assertions[`${leg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
|
|
57
|
+
}
|
|
58
|
+
const total = Sum(...legSpecs.flatMap((leg)=>allSels[leg.id].map((s, i)=>If(s, Int.val(leg.services[i].priceCny), Int.val(0)))));
|
|
59
|
+
if (req.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(req.budgetCny));
|
|
60
|
+
const coreOf = (s)=>{
|
|
61
|
+
const v = s.unsatCore();
|
|
62
|
+
const out = [];
|
|
63
|
+
for(let i = 0; i < v.length(); i++){
|
|
64
|
+
out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
};
|
|
68
|
+
const extract = async (model)=>{
|
|
69
|
+
const chosen = new Map();
|
|
70
|
+
for (const leg of legSpecs){
|
|
71
|
+
for(let i = 0; i < allSels[leg.id].length; i++){
|
|
72
|
+
const v = await maybeAwait(model.eval(allSels[leg.id][i], true));
|
|
73
|
+
if (String(v) === 'true') chosen.set(leg.id, leg.services[i]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return chosen;
|
|
77
|
+
};
|
|
78
|
+
const s = new Solver();
|
|
79
|
+
for (const sel of Object.values(allSels))s.add(exactlyOne(z3, sel));
|
|
80
|
+
for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
|
|
81
|
+
if (String(await s.check()) !== 'unsat') {
|
|
82
|
+
const chosen = await extract(s.model());
|
|
83
|
+
const legReports = legSpecs.map((leg)=>evaluateLeg(leg, chosen.get(leg.id)));
|
|
84
|
+
const money = legReports.reduce((a, r)=>a + r.price_cny, 0);
|
|
85
|
+
const redFlags = [
|
|
86
|
+
...legSpecs.map((leg, i)=>leg.redEye && legReports[i].energy_pct < 50 ? `${leg.id} 落地精力仅 ${legReports[i].energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)` : '').filter(Boolean),
|
|
87
|
+
...legSpecs.map((leg, i)=>!leg.redEye && legReports[i].wakeMin >= 0 && legReports[i].wakeMin < 6 * 60 ? `${leg.id} 起床 ${legReports[i].wake}(早于 6:00,生物钟代价)` : '').filter(Boolean)
|
|
88
|
+
];
|
|
89
|
+
return {
|
|
90
|
+
feasible: true,
|
|
91
|
+
money_cny: money,
|
|
92
|
+
legs: legReports,
|
|
93
|
+
red_flags: redFlags
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const core = coreOf(s).sort();
|
|
97
|
+
const suggestions = [];
|
|
98
|
+
for (const name of core){
|
|
99
|
+
const s2 = new Solver();
|
|
100
|
+
for (const sel of Object.values(allSels))s2.add(exactlyOne(z3, sel));
|
|
101
|
+
for (const [n2, expr] of Object.entries(assertions)){
|
|
102
|
+
if (n2 !== name) s2.addAndTrack(expr, n2);
|
|
103
|
+
}
|
|
104
|
+
if (String(await s2.check()) !== 'unsat') {
|
|
105
|
+
const chosen = await extract(s2.model());
|
|
106
|
+
const rep = legSpecs.map((leg)=>evaluateLeg(leg, chosen.get(leg.id)));
|
|
107
|
+
suggestions.push({
|
|
108
|
+
relax: name,
|
|
109
|
+
money_cny: rep.reduce((a, r)=>a + r.price_cny, 0),
|
|
110
|
+
legs: rep.map((r, i)=>({
|
|
111
|
+
...r,
|
|
112
|
+
leg: legSpecs[i].id
|
|
113
|
+
}))
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
feasible: false,
|
|
119
|
+
unsat_core: core,
|
|
120
|
+
suggestions
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function maybeAwait(v) {
|
|
124
|
+
return v instanceof Promise ? await v : v;
|
|
125
|
+
}
|
|
126
|
+
export function parseFlightPackLegs(pack) {
|
|
127
|
+
const legs = pack['legs'].map((l)=>({
|
|
128
|
+
id: String(l['id']),
|
|
129
|
+
note: l['note'] ? String(l['note']) : undefined,
|
|
130
|
+
services: l['services'].map((sv)=>({
|
|
131
|
+
id: String(sv['id']),
|
|
132
|
+
depMin: hhmmToMin(String(sv['dep'])),
|
|
133
|
+
arrMin: hhmmToMin(String(sv['arr'])),
|
|
134
|
+
priceCny: Number(sv['price_cny'])
|
|
135
|
+
})),
|
|
136
|
+
bufferMin: Number(l['buffer_min']),
|
|
137
|
+
originTransferMin: Number(l['origin_transfer_min']),
|
|
138
|
+
destTransferMin: Number(l['dest_transfer_min']),
|
|
139
|
+
arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined,
|
|
140
|
+
redEye: Boolean(l['red_eye']),
|
|
141
|
+
redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0)
|
|
142
|
+
}));
|
|
143
|
+
return legs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/journey.ts
|
package/dist/src/loop.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { anythingSearch } from '../capabilities/anything.js';
|
|
2
|
+
export function newState(year = 2026) {
|
|
3
|
+
return {
|
|
4
|
+
calendar: {
|
|
5
|
+
year,
|
|
6
|
+
assertedWeekdays: {}
|
|
7
|
+
},
|
|
8
|
+
profile: {},
|
|
9
|
+
gates: [],
|
|
10
|
+
wishes: []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function interviewNext(state) {
|
|
14
|
+
const p = state.profile;
|
|
15
|
+
const questions = [];
|
|
16
|
+
const ww = p.workWindow;
|
|
17
|
+
if (!p.workWindow && !ww?.vacation) {
|
|
18
|
+
questions.push({
|
|
19
|
+
key: 'workWindow',
|
|
20
|
+
text: '你这两周要远程办公——工作时间是几点到几点、按哪个时区?这决定每天的可玩时段。',
|
|
21
|
+
why: '工作窗口直接决定每日节奏(普吉 13:00-22:00 与 9:00-18:00 是两种人生)'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (!p.bookedResources) {
|
|
25
|
+
questions.push({
|
|
26
|
+
key: 'bookedResources',
|
|
27
|
+
text: '已经有订好的航班或酒店吗?(哪怕只是意向)——已订资源是硬锚点,先告诉我再规划。',
|
|
28
|
+
why: '已订资源决定哪些段不可动,Kimi 式规划会绕开它们重排'
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!p.budgetTier) {
|
|
32
|
+
questions.push({
|
|
33
|
+
key: 'budgetTier',
|
|
34
|
+
text: '预算档位:经济(¥12.6k 级)/ 舒适(¥16.3k 级)/ 便利优先?',
|
|
35
|
+
why: '分层预算决定班次与住宿的选型口径',
|
|
36
|
+
options: [
|
|
37
|
+
{
|
|
38
|
+
label: '经济',
|
|
39
|
+
tradeOff: '省钱,接驳多走路'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: '舒适',
|
|
43
|
+
tradeOff: 'workation 办公质量优先'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: '便利优先',
|
|
47
|
+
tradeOff: '时间最省'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
questions,
|
|
54
|
+
missing: questions.map((q)=>q.key)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function validateSpec(spec) {
|
|
58
|
+
if (!Array.isArray(spec.segments) || spec.segments.length === 0) return '没有段(segments)';
|
|
59
|
+
for (const seg of spec.segments){
|
|
60
|
+
if (!seg.id) return '存在缺少 id 的段';
|
|
61
|
+
if (!Array.isArray(seg.options) || seg.options.length === 0) return `段 ${seg.id} 没有可选方案`;
|
|
62
|
+
for (const opt of seg.options){
|
|
63
|
+
const svcs = opt.move?.services;
|
|
64
|
+
if (!Array.isArray(svcs) || svcs.length === 0) return `段 ${seg.id} 的方案 ${opt.id} 缺少班次(services)`;
|
|
65
|
+
for (const s of svcs){
|
|
66
|
+
if (typeof s.depMin !== 'number' || typeof s.arrMin !== 'number') {
|
|
67
|
+
return `段 ${seg.id}/${opt.id} 的班次 ${s.id ?? '?'} 缺少时刻(depMin/arrMin)`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
export function isComplex(state) {
|
|
75
|
+
const p = state.profile;
|
|
76
|
+
return Boolean(p.workWindow && p.bookedResources) && (state.gates.length > 0 || Boolean(state.spec));
|
|
77
|
+
}
|
|
78
|
+
export async function requestDeepPlanning(state) {
|
|
79
|
+
const ticket = {
|
|
80
|
+
id: `dp-${Date.now().toString(36)}`,
|
|
81
|
+
objective: '生成已验证的行程方案:全成本核算 + 动机/约束匹配 + 不失望四条',
|
|
82
|
+
requestedAt: new Date().toISOString(),
|
|
83
|
+
etaLabel: '约 1 小时(mock 期:秒级)'
|
|
84
|
+
};
|
|
85
|
+
state.gates.push({
|
|
86
|
+
id: `async-${ticket.id}`,
|
|
87
|
+
question: `深度规划已启动(${ticket.etaLabel})`,
|
|
88
|
+
options: []
|
|
89
|
+
});
|
|
90
|
+
const reply = '这趟行程跨度大(两周 workation + 三城 + 红眼返程),我切到**深度规划模式**:后台做多轮校验' + '(锚点/工作窗口/全成本/负例排查),做好后通知你。**先留一道选择题给你**,回来直接定:' + '\n1. 预算档位?(经济/舒适/便利优先)';
|
|
91
|
+
return {
|
|
92
|
+
reply,
|
|
93
|
+
ticket
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function collectDeepPlanning(state, ticket, solve) {
|
|
97
|
+
const spec = state.spec;
|
|
98
|
+
if (!spec) return {
|
|
99
|
+
reply: '(内部状态缺失 spec——深度规划未就绪,这是不应发生的路径)',
|
|
100
|
+
state
|
|
101
|
+
};
|
|
102
|
+
state.solve = await solve(spec);
|
|
103
|
+
state.gates = state.gates.filter((g)=>!g.id.startsWith('async-'));
|
|
104
|
+
const checks = {
|
|
105
|
+
'1_承诺时间后必有明确产物': Boolean(state.solve.legs?.length || state.solve.verdicts?.length),
|
|
106
|
+
'2_产物通过自检清单': state.solve.feasible ? state.solve.legs?.every((l)=>l['energy_pct'] !== undefined) ?? false : Boolean(state.solve.unsat_core?.length),
|
|
107
|
+
'3_待决问题全部是简单选择题': state.gates.every((g)=>g.id === 'budget' || g.options.length >= 2),
|
|
108
|
+
'4_做不到的诚实说': state.solve.feasible || Boolean(state.solve.suggestions?.length)
|
|
109
|
+
};
|
|
110
|
+
const head = `# 回访交付:${ticket.objective}\n(工单 ${ticket.id},不失望四条:${Object.values(checks).every(Boolean) ? '4/4 ✅' : '有未达项 ❌'})`;
|
|
111
|
+
return {
|
|
112
|
+
reply: `${head}\n\n${renderSolve(state)}`,
|
|
113
|
+
state
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function renderSolve(state) {
|
|
117
|
+
const s = state.solve;
|
|
118
|
+
if (!s) return '(无求解结果)';
|
|
119
|
+
const lines = [];
|
|
120
|
+
if (s.feasible) {
|
|
121
|
+
lines.push(`**方案可行,机票合计 ¥${s.money_cny}**`);
|
|
122
|
+
for (const lg of s.legs ?? []){
|
|
123
|
+
const l = lg;
|
|
124
|
+
lines.push(`- ${l['leg']} ${l['service']}:${l['dep']} 起飞,${l['wake']} 出发,${l['arrive_stay']} 到,` + `门到门 ${l['door_to_door']},落地精力 ${l['energy_pct']}%,¥${l['price_cny']}`);
|
|
125
|
+
}
|
|
126
|
+
for (const e of s.work_window_exclusions ?? []){
|
|
127
|
+
lines.push(`- 已排除 ${e.option}(工作窗口):${e.reason}`);
|
|
128
|
+
}
|
|
129
|
+
for (const n of s.skeleton_notes ?? [])lines.push(`- ${n}`);
|
|
130
|
+
for (const f of s.red_flags ?? [])lines.push(`- ⚠️ ${f}`);
|
|
131
|
+
} else {
|
|
132
|
+
lines.push(`**当前约束下不可行——冲突:${(s.unsat_core ?? []).join('、')}`);
|
|
133
|
+
for (const sg of s.suggestions ?? [])lines.push(`- 放宽「${sg.relax}」可解(约 ¥${sg.money_cny})`);
|
|
134
|
+
}
|
|
135
|
+
if (state.gates.length) {
|
|
136
|
+
lines.push('', '**待你决定(选择题)**');
|
|
137
|
+
for (const g of state.gates){
|
|
138
|
+
lines.push(`- ${g.question} → ${g.options.map((o)=>o.label + (o.tradeOff ? `(${o.tradeOff})` : '')).join(' / ')}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
export async function runTurn(state, userMsg, llm, history = [], solve = null) {
|
|
144
|
+
const facts = await llm.extractFacts([
|
|
145
|
+
...history,
|
|
146
|
+
{
|
|
147
|
+
role: 'user',
|
|
148
|
+
text: userMsg
|
|
149
|
+
}
|
|
150
|
+
], state);
|
|
151
|
+
const conflicts = [];
|
|
152
|
+
if (facts.calendar?.assertedWeekdays) {
|
|
153
|
+
for (const [date, wd] of Object.entries(facts.calendar.assertedWeekdays)){
|
|
154
|
+
const prev = state.calendar.assertedWeekdays[date];
|
|
155
|
+
if (prev && prev !== wd) {
|
|
156
|
+
conflicts.push(`${date}:已有断言 ${prev},新说法 ${wd}——以先断言为准,如需改请明确说`);
|
|
157
|
+
} else {
|
|
158
|
+
state.calendar.assertedWeekdays[date] = wd;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
Object.assign(state.profile, facts.profile ?? {});
|
|
163
|
+
const { questions } = interviewNext(state);
|
|
164
|
+
const blocking = questions.filter((q)=>q.key !== 'budgetTier');
|
|
165
|
+
const budgetQ = questions.find((q)=>q.key === 'budgetTier');
|
|
166
|
+
if (budgetQ && !state.gates.some((g)=>g.id === 'budget')) {
|
|
167
|
+
state.gates.push({
|
|
168
|
+
id: 'budget',
|
|
169
|
+
question: budgetQ.text,
|
|
170
|
+
options: budgetQ.options ?? [
|
|
171
|
+
{
|
|
172
|
+
label: '经济'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: '舒适'
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
label: '便利优先'
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const parts = [];
|
|
184
|
+
if (conflicts.length) parts.push(`⚠️ 日历冲突:\n${conflicts.map((c)=>`- ${c}`).join('\n')}`);
|
|
185
|
+
const poiProbe = probePoi(userMsg);
|
|
186
|
+
if (poiProbe) {
|
|
187
|
+
const ar = await anythingSearch({
|
|
188
|
+
keyword: poiProbe
|
|
189
|
+
});
|
|
190
|
+
if (ar.verdict === 'hit' && ar.hits && ar.hits.length > 0) {
|
|
191
|
+
const top = ar.hits.slice(0, 5);
|
|
192
|
+
const lines = top.map((h, i)=>{
|
|
193
|
+
const latlng = h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : '';
|
|
194
|
+
return ` ${i + 1}. [${h.type}] ${h.name}${latlng}`;
|
|
195
|
+
}).join('\n');
|
|
196
|
+
parts.push(`**${poiProbe} → hit (${ar.hits.length} 候选项)**\n${lines}\n${ar.evidence}`);
|
|
197
|
+
} else if (ar.verdict === 'miss') {
|
|
198
|
+
parts.push(`**${poiProbe} → miss** (酒店-be Anything 一切正常但无候选)\n${ar.evidence}`);
|
|
199
|
+
} else {
|
|
200
|
+
parts.push(`**${poiProbe} → unavailable** (${ar.error ?? 'hbcli 不可达'})\n${ar.evidence}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const q of blocking)parts.push(await llm.polishQuestion(q));
|
|
204
|
+
if (blocking.length === 0 && solve) {
|
|
205
|
+
const spec = await llm.extractSpec([
|
|
206
|
+
...history,
|
|
207
|
+
{
|
|
208
|
+
role: 'user',
|
|
209
|
+
text: userMsg
|
|
210
|
+
}
|
|
211
|
+
], state);
|
|
212
|
+
if (spec && spec.note === 'erhai-candidates') {
|
|
213
|
+
const { readFile } = await import('node:fs/promises');
|
|
214
|
+
const { join } = await import('node:path');
|
|
215
|
+
try {
|
|
216
|
+
const raw = JSON.parse(await readFile(join(import.meta.dirname, '..', '..', 'data', 'golden_erhai.json'), 'utf-8'));
|
|
217
|
+
const { parseCandidate, parseRequest } = await import('./model.js');
|
|
218
|
+
const { segmentsFromCandidate, solveChoiceSegment } = await import('./unified.js');
|
|
219
|
+
const req = parseRequest(raw['request']);
|
|
220
|
+
const cands = raw['candidates'].map(parseCandidate);
|
|
221
|
+
const erhaiSpec = segmentsFromCandidate(req, cands);
|
|
222
|
+
const result = solveChoiceSegment(erhaiSpec, req);
|
|
223
|
+
state.solve = result;
|
|
224
|
+
parts.push(result.answer_md ?? '(候选求解完成)');
|
|
225
|
+
} catch {
|
|
226
|
+
parts.push('(洱海候选求解暂不可用——退回访谈)');
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const invalid = spec ? validateSpec(spec) : '翻译器未产出 spec';
|
|
230
|
+
if (spec && !invalid) {
|
|
231
|
+
state.spec = spec;
|
|
232
|
+
state.solve = await solve(spec);
|
|
233
|
+
parts.push(llm.render ? await llm.render(state) : renderSolve(state));
|
|
234
|
+
} else if (blocking.length === 0 && invalid) {
|
|
235
|
+
parts.push(`(行程骨架还不完整:${invalid}——请补充对应信息,我不猜)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if (blocking.length === 0) {
|
|
239
|
+
parts.push('(约束齐备——进入规划,S3 接线后此处产出方案与选择题)');
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
reply: parts.join('\n\n'),
|
|
243
|
+
state
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
import { join } from 'node:path';
|
|
247
|
+
import { mkdir } from 'node:fs/promises';
|
|
248
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
249
|
+
const ASYNC_DIR = 'gotry-state/async';
|
|
250
|
+
async function asyncPath(name) {
|
|
251
|
+
await mkdir(ASYNC_DIR, {
|
|
252
|
+
recursive: true
|
|
253
|
+
});
|
|
254
|
+
return join(ASYNC_DIR, name);
|
|
255
|
+
}
|
|
256
|
+
export async function persistAsyncTicket(ticket, state) {
|
|
257
|
+
const p = await asyncPath(`${ticket.id}.json`);
|
|
258
|
+
await writeFile(p, JSON.stringify({
|
|
259
|
+
ticket,
|
|
260
|
+
state
|
|
261
|
+
}, null, 2), 'utf-8');
|
|
262
|
+
return p;
|
|
263
|
+
}
|
|
264
|
+
export async function loadAsyncTicket(ticketId) {
|
|
265
|
+
try {
|
|
266
|
+
const p = await asyncPath(`${ticketId}.json`);
|
|
267
|
+
return JSON.parse(await readFile(p, 'utf-8'));
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export async function settleAsyncTicket(ticketId, reply) {
|
|
273
|
+
const p = await asyncPath(`${ticketId}.deliverable.md`);
|
|
274
|
+
await writeFile(p, reply, 'utf-8');
|
|
275
|
+
return p;
|
|
276
|
+
}
|
|
277
|
+
export function probePoi(msg) {
|
|
278
|
+
const trimmed = msg.trim();
|
|
279
|
+
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
|
+
}
|
|
285
|
+
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);
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/loop.ts
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { parseFlightPackToSpec } from './unified.js';
|
|
2
|
+
const SCRIPT = [
|
|
3
|
+
{
|
|
4
|
+
when: '请给我做机票和酒店的行程规划和推荐',
|
|
5
|
+
calendar: {
|
|
6
|
+
year: 2026,
|
|
7
|
+
assertedWeekdays: {
|
|
8
|
+
'2026-07-17': 'fri',
|
|
9
|
+
'2026-07-18': 'sat',
|
|
10
|
+
'2026-07-31': 'fri',
|
|
11
|
+
'2026-08-01': 'sat',
|
|
12
|
+
'2026-08-09': 'sun',
|
|
13
|
+
'2026-08-10': 'mon'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
profile: {
|
|
17
|
+
companions: [
|
|
18
|
+
'girlfriend(先在普吉)'
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
when: '工作时间',
|
|
24
|
+
profile: {
|
|
25
|
+
workWindow: {
|
|
26
|
+
homeTzOffsetMin: 240,
|
|
27
|
+
startMin: 600,
|
|
28
|
+
endMin: 1140,
|
|
29
|
+
workdays: [
|
|
30
|
+
0,
|
|
31
|
+
1,
|
|
32
|
+
2,
|
|
33
|
+
3,
|
|
34
|
+
4
|
|
35
|
+
],
|
|
36
|
+
evidence: '用户原话:我的工作时间是UTC+4的早上10点到下午7点'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
when: '订了酒店',
|
|
42
|
+
profile: {
|
|
43
|
+
bookedResources: [
|
|
44
|
+
{
|
|
45
|
+
kind: 'hotel',
|
|
46
|
+
ref: 'The Title East Wing Rawai(7.18-23, 5 晚)',
|
|
47
|
+
window: '2026-07-18~07-23'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
export function createMockLlm(flightPackPath) {
|
|
54
|
+
let stepIdx = 0;
|
|
55
|
+
return {
|
|
56
|
+
async extractFacts (history, _state) {
|
|
57
|
+
const last = history[history.length - 1]?.text ?? '';
|
|
58
|
+
const step = SCRIPT[stepIdx];
|
|
59
|
+
if (step && last.includes(step.when)) {
|
|
60
|
+
stepIdx++;
|
|
61
|
+
return {
|
|
62
|
+
calendar: step.calendar,
|
|
63
|
+
profile: step.profile,
|
|
64
|
+
assumptions: Object.keys({
|
|
65
|
+
...step.profile
|
|
66
|
+
}).map((f)=>({
|
|
67
|
+
field: f,
|
|
68
|
+
source: 'user-verbatim'
|
|
69
|
+
}))
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
assumptions: []
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
async extractSpec (history, state) {
|
|
77
|
+
if (!state.profile.workWindow || !state.profile.bookedResources) return null;
|
|
78
|
+
if (!flightPackPath) return null;
|
|
79
|
+
const { readFile } = await import('node:fs/promises');
|
|
80
|
+
const pack = JSON.parse(await readFile(flightPackPath, 'utf-8'));
|
|
81
|
+
const spec = parseFlightPackToSpec(pack);
|
|
82
|
+
spec.workWindow = {
|
|
83
|
+
homeTzOffsetMin: state.profile.workWindow.homeTzOffsetMin,
|
|
84
|
+
startMin: state.profile.workWindow.startMin,
|
|
85
|
+
endMin: state.profile.workWindow.endMin,
|
|
86
|
+
workdays: state.profile.workWindow.workdays
|
|
87
|
+
};
|
|
88
|
+
spec.budgetCny = 9000;
|
|
89
|
+
return spec;
|
|
90
|
+
},
|
|
91
|
+
async polishQuestion (q) {
|
|
92
|
+
return `【${q.key}】${q.text}\n(为什么问:${q.why})`;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/mock-llm.ts
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export function hhmmToMin(s) {
|
|
2
|
+
const [h, m] = s.split(':');
|
|
3
|
+
return Number(h) * 60 + Number(m);
|
|
4
|
+
}
|
|
5
|
+
export function minToHhmm(m) {
|
|
6
|
+
return `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
|
|
7
|
+
}
|
|
8
|
+
export function requiredUsableHours(mot) {
|
|
9
|
+
return mot.baseUsableHours + mot.escapeHoursPerWeight * (mot.weights['escape_rest'] ?? 0);
|
|
10
|
+
}
|
|
11
|
+
export const WAKE_PENALTY_BEFORE_5 = 30;
|
|
12
|
+
export const WAKE_PENALTY_BEFORE_6 = 25;
|
|
13
|
+
export const WAKE_PENALTY_BEFORE_630 = 15;
|
|
14
|
+
export const TRANSFER_PENALTY = 8;
|
|
15
|
+
export const LATE_ARRIVAL_PENALTY = 10;
|
|
16
|
+
export const LONG_D2D_PENALTY = 10;
|
|
17
|
+
export const DAY_END_MIN = 21 * 60;
|
|
18
|
+
export const DAY2_START_MIN = 9 * 60;
|
|
19
|
+
export const DAY2_QUALITY = 0.9;
|
|
20
|
+
export const LATEST_ARRIVE_STAY_MIN = 18 * 60;
|
|
21
|
+
export function parseMotivation(d) {
|
|
22
|
+
const hard = d['hard'] ?? {};
|
|
23
|
+
const rawWeights = d['weights'] ?? d;
|
|
24
|
+
const weights = {};
|
|
25
|
+
for (const [k, v] of Object.entries(rawWeights)){
|
|
26
|
+
if (typeof v === 'number') weights[k] = v;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
weights,
|
|
30
|
+
wakeFloorMin: hhmmToMin(String(hard['wake_not_before'] ?? '06:30')),
|
|
31
|
+
minArrivalEnergyPct: Number(hard['min_arrival_energy_pct'] ?? 40),
|
|
32
|
+
baseUsableHours: 4.0,
|
|
33
|
+
escapeHoursPerWeight: 2.0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function parseRequest(d) {
|
|
37
|
+
const home = (d['home'] ?? {})['hubs'] ?? {};
|
|
38
|
+
const homeHubAccess = {};
|
|
39
|
+
for (const [hub, acc] of Object.entries(home)){
|
|
40
|
+
homeHubAccess[hub] = {
|
|
41
|
+
hub,
|
|
42
|
+
toHubMin: Number(acc['to_hub_min'])
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
note: String(d['note'] ?? ''),
|
|
47
|
+
motivation: parseMotivation(d['motivation']),
|
|
48
|
+
windowDays: Number(d['window_days']),
|
|
49
|
+
budgetCny: Number(d['budget_cny']),
|
|
50
|
+
homeHubAccess
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function parseService(d) {
|
|
54
|
+
return {
|
|
55
|
+
id: String(d['id']),
|
|
56
|
+
depMin: hhmmToMin(String(d['dep'])),
|
|
57
|
+
arrMin: hhmmToMin(String(d['arr'])),
|
|
58
|
+
priceCny: Number(d['price_cny'])
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseTransfer(d) {
|
|
62
|
+
return {
|
|
63
|
+
mode: String(d['mode']),
|
|
64
|
+
minutes: Number(d['min']),
|
|
65
|
+
priceCny: Number(d['price_cny'])
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function parseCandidate(d) {
|
|
69
|
+
return {
|
|
70
|
+
id: String(d['id']),
|
|
71
|
+
name: String(d['name']),
|
|
72
|
+
hub: String(d['hub']),
|
|
73
|
+
bufferOutMin: Number(d['buffer_out_min'] ?? 60),
|
|
74
|
+
bufferRetMin: Number(d['buffer_ret_min'] ?? 60),
|
|
75
|
+
servicesOut: d['services_out'].map(parseService),
|
|
76
|
+
servicesRet: d['services_ret'].map(parseService),
|
|
77
|
+
destTransfers: d['dest_transfers'].map(parseTransfer),
|
|
78
|
+
stayCnyPerNight: Number(d['stay_cny_per_night']),
|
|
79
|
+
localDailyCny: Number(d['local_daily_cny']),
|
|
80
|
+
minDaysForPurpose: Number(d['min_days_for_purpose']),
|
|
81
|
+
imageryMatch: Number(d['imagery_match']),
|
|
82
|
+
bestMonths: (d['best_months'] ?? []).map(Number)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function evaluateChoice(cand, req, ch) {
|
|
86
|
+
const access = req.homeHubAccess[cand.hub];
|
|
87
|
+
const wake = ch.outService.depMin - cand.bufferOutMin - access.toHubMin;
|
|
88
|
+
const arriveStay = ch.outService.arrMin + ch.outTransfer.minutes;
|
|
89
|
+
const d2dOut = arriveStay - wake;
|
|
90
|
+
let energy = 100;
|
|
91
|
+
if (wake < 5 * 60) energy -= WAKE_PENALTY_BEFORE_5;
|
|
92
|
+
else if (wake < 6 * 60) energy -= WAKE_PENALTY_BEFORE_6;
|
|
93
|
+
else if (wake < hhmmToMin('06:30')) energy -= WAKE_PENALTY_BEFORE_630;
|
|
94
|
+
energy -= 2 * TRANSFER_PENALTY;
|
|
95
|
+
if (arriveStay > 21 * 60) energy -= LATE_ARRIVAL_PENALTY;
|
|
96
|
+
if (d2dOut > 6 * 60) energy -= LONG_D2D_PENALTY;
|
|
97
|
+
energy = Math.max(0, energy);
|
|
98
|
+
const day1Raw = Math.max(0, DAY_END_MIN - arriveStay) / 60;
|
|
99
|
+
const day1 = day1Raw * (0.5 + energy / 200);
|
|
100
|
+
const leaveStayRet = ch.retService.depMin - cand.bufferRetMin - ch.retTransfer.minutes;
|
|
101
|
+
const day2 = Math.max(0, leaveStayRet - DAY2_START_MIN) / 60 * DAY2_QUALITY;
|
|
102
|
+
const midDays = Math.max(0, ch.days - 2);
|
|
103
|
+
const usable = day1 + day2 + midDays * 8.0 * DAY2_QUALITY;
|
|
104
|
+
const money = ch.outService.priceCny + ch.retService.priceCny + ch.outTransfer.priceCny + ch.retTransfer.priceCny + cand.stayCnyPerNight * (ch.days - 1) + cand.localDailyCny * ch.days;
|
|
105
|
+
return {
|
|
106
|
+
moneyCny: money,
|
|
107
|
+
wakeMin: wake,
|
|
108
|
+
arriveStayMin: arriveStay,
|
|
109
|
+
doorToDoorOutMin: d2dOut,
|
|
110
|
+
energyArrivalPct: energy,
|
|
111
|
+
usableHours: usable,
|
|
112
|
+
usableDay1Hours: day1,
|
|
113
|
+
usableDay2Hours: day2,
|
|
114
|
+
departHomeRetMin: leaveStayRet,
|
|
115
|
+
arriveHomeRetMin: ch.retService.arrMin + access.toHubMin
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function trueCostToDict(t) {
|
|
119
|
+
return {
|
|
120
|
+
money_cny: t.moneyCny,
|
|
121
|
+
wake: minToHhmm(t.wakeMin),
|
|
122
|
+
arrive_stay: minToHhmm(t.arriveStayMin),
|
|
123
|
+
door_to_door_out: `${Math.floor(t.doorToDoorOutMin / 60)}h${String(t.doorToDoorOutMin % 60).padStart(2, '0')}m`,
|
|
124
|
+
energy_arrival_pct: t.energyArrivalPct,
|
|
125
|
+
usable_hours: Math.round(t.usableHours * 10) / 10,
|
|
126
|
+
usable_day1_hours: Math.round(t.usableDay1Hours * 10) / 10,
|
|
127
|
+
usable_day2_hours: Math.round(t.usableDay2Hours * 10) / 10,
|
|
128
|
+
leave_stay_return: minToHhmm(t.departHomeRetMin),
|
|
129
|
+
arrive_home_return: minToHhmm(Math.min(1440, t.arriveHomeRetMin))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/model.ts
|