@curless/hotel-storefront 0.47.4 → 0.47.6
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/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/names.d.ts +27 -2
- package/dist/names.d.ts.map +1 -1
- package/dist/names.js +32 -2
- package/dist/names.js.map +1 -1
- package/dist/present.d.ts +10 -0
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +3 -0
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +30 -7
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +5 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +132 -14
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -4,7 +4,7 @@ import { FACILITIES } from './facilities.js';
|
|
|
4
4
|
import { DESCRIPTION_MAX, POLICY_MAX, guestText, hotelText } from './fence.js';
|
|
5
5
|
import { clockOf, minutesOf, startProblem, stayWords, windowWords } from './hourly.js';
|
|
6
6
|
import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor } from './money.js';
|
|
7
|
-
import { namedExactly, roomNamesOf, withTraditional } from './names.js';
|
|
7
|
+
import { namedExactly, roomNamesOf, roomTypeNamed, withTraditional } from './names.js';
|
|
8
8
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
|
|
9
9
|
import { regionOf } from './regions.js';
|
|
10
10
|
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
@@ -747,12 +747,16 @@ const allTools = (n, v) => {
|
|
|
747
747
|
properties: {
|
|
748
748
|
label: {
|
|
749
749
|
type: 'string',
|
|
750
|
-
description: `e.g. "
|
|
750
|
+
description: `e.g. "Arrive${places[0] ? ` in ${places[0]}` : ''}" - the card prints each day's date beside it, so leave the date out`,
|
|
751
751
|
},
|
|
752
752
|
note: {
|
|
753
753
|
type: 'string',
|
|
754
754
|
description: 'One or two lines on the day, in your own words',
|
|
755
755
|
},
|
|
756
|
+
why: {
|
|
757
|
+
type: 'string',
|
|
758
|
+
description: `Why the ${v.noun} named on this day suits the days of that stay - near what, good for what - one sentence, in your own words, with no lead-in such as "My pick:" - the card has its own`,
|
|
759
|
+
},
|
|
756
760
|
items: {
|
|
757
761
|
type: 'array',
|
|
758
762
|
items: { type: 'string' },
|
|
@@ -1321,7 +1325,21 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1321
1325
|
demoNote: STR,
|
|
1322
1326
|
title: STR,
|
|
1323
1327
|
stay: OBJ,
|
|
1324
|
-
days:
|
|
1328
|
+
days: {
|
|
1329
|
+
type: 'array',
|
|
1330
|
+
items: {
|
|
1331
|
+
type: 'object',
|
|
1332
|
+
properties: {
|
|
1333
|
+
label: STR,
|
|
1334
|
+
note: STR,
|
|
1335
|
+
/** Why the hotel this day names suits the days of that stay - the model's sentence. */
|
|
1336
|
+
why: STR,
|
|
1337
|
+
date: STR,
|
|
1338
|
+
plan: ARR,
|
|
1339
|
+
items: ARR,
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1325
1343
|
/** How many of the planned items are actually in the trip. Computed, never written. */
|
|
1326
1344
|
inTrip: { type: 'number' },
|
|
1327
1345
|
planned: { type: 'number' },
|
|
@@ -2925,6 +2943,58 @@ export const handleTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2925
2943
|
const fitsManyOf = (deps, dropped) => dropped.flatMap((d) => d.candidates?.length
|
|
2926
2944
|
? [`"${d.name}" ${namesMoreThanOne(d.candidates, branded(deps.vocab, deps.vocab.noun))}`]
|
|
2927
2945
|
: []);
|
|
2946
|
+
/** A stay this long, at one hotel, is worth asking whether the days stay in one area. */
|
|
2947
|
+
const LONG_STAY_NIGHTS = 4;
|
|
2948
|
+
/** The other areas a long stay's answer names - one hotel each. */
|
|
2949
|
+
const OTHER_AREAS_MAX = 4;
|
|
2950
|
+
/**
|
|
2951
|
+
* For each priced stay of four nights or more, the other areas of its city that
|
|
2952
|
+
* have a hotel - for the model, in the tool's text, never on the card or in the
|
|
2953
|
+
* schema.
|
|
2954
|
+
*
|
|
2955
|
+
* ⚠️ A WEEK AT ONE HOTEL WAS THE PLAN, AND THE MODEL NEVER SAW WHAT ELSE THE
|
|
2956
|
+
* CITY HAS (the owner, 2026-10-26, on the live Atour connector: both Beijing
|
|
2957
|
+
* weeks, all six nights at one hotel). The instructions say a long stay in a big
|
|
2958
|
+
* city usually spans areas; this is the answer saying it at the moment the plan
|
|
2959
|
+
* has just been drawn that way, with the hotels of the other areas by id, so the
|
|
2960
|
+
* model can plan the stay in parts - or offer the move - without a second
|
|
2961
|
+
* look-up. Nothing where the city has no other area, or the backend cannot say:
|
|
2962
|
+
* a nudge that guesses is worse than none.
|
|
2963
|
+
*/
|
|
2964
|
+
const longStayNudges = async (deps, stays) => {
|
|
2965
|
+
// The areas the plan already uses are not "other" for any of its stays.
|
|
2966
|
+
const used = new Set(stays.map((x) => x.area?.toLowerCase()).filter(Boolean));
|
|
2967
|
+
const out = [];
|
|
2968
|
+
for (const x of stays) {
|
|
2969
|
+
if ((x.nights ?? 0) < LONG_STAY_NIGHTS || !x.area || !x.city)
|
|
2970
|
+
continue;
|
|
2971
|
+
let hotels;
|
|
2972
|
+
try {
|
|
2973
|
+
hotels = (await deps.backend.searchHotels({ city: x.city, limit: MAX_PAGE })).hotels;
|
|
2974
|
+
}
|
|
2975
|
+
catch {
|
|
2976
|
+
// The plan is already drawn; a city that cannot be read just gets no nudge.
|
|
2977
|
+
continue;
|
|
2978
|
+
}
|
|
2979
|
+
const byArea = new Map();
|
|
2980
|
+
for (const h of hotels) {
|
|
2981
|
+
const area = h.area?.trim();
|
|
2982
|
+
if (!area || h.city !== x.city || h.id === x.id)
|
|
2983
|
+
continue;
|
|
2984
|
+
const lower = area.toLowerCase();
|
|
2985
|
+
if (used.has(lower) || byArea.has(lower))
|
|
2986
|
+
continue;
|
|
2987
|
+
byArea.set(lower, h);
|
|
2988
|
+
}
|
|
2989
|
+
if (byArea.size === 0)
|
|
2990
|
+
continue;
|
|
2991
|
+
const others = [...byArea.values()]
|
|
2992
|
+
.slice(0, OTHER_AREAS_MAX)
|
|
2993
|
+
.map((h) => `${h.area} - ${h.name} (\`${h.id}\`)`);
|
|
2994
|
+
out.push(`${stays.length === 1 ? `All ${x.nights} nights are` : `${x.nights} nights are`} at ${x.name} (${x.area}). ${x.city} also has: ${others.join('; ')}. In your reply, name the planned days that are far from ${x.area} and offer moving those nights to the hotel here nearest them, with the reason in one sentence - or say why every day suits ${x.area}. The plan on screen stays as it is; draw it again only if the guest asks.`);
|
|
2995
|
+
}
|
|
2996
|
+
return out;
|
|
2997
|
+
};
|
|
2928
2998
|
/** The hotel a call named - or the answer that says there is none, or several by that name. */
|
|
2929
2999
|
const hotelNamed = async (deps, key, wanted) => {
|
|
2930
3000
|
const detail = await deps.backend.getHotel(wanted);
|
|
@@ -2996,7 +3066,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
2996
3066
|
})),
|
|
2997
3067
|
},
|
|
2998
3068
|
text: sold.length
|
|
2999
|
-
? `${detail.hotel.name} has no room by the hour by that name today. It has: ${sold.map((r) => `${r.name} (\`${r.id}\`, ${windowWords(r.hourly)})`).join('; ')}.`
|
|
3069
|
+
? `${detail.hotel.name} has no room by the hour by that name today. It has: ${sold.map((r) => `${r.name} (\`${r.id}\`, ${windowWords(r.hourly)})`).join('; ')}. Call again with the id.`
|
|
3000
3070
|
: ((await today)?.noRooms ?? `${detail.hotel.name} has no rooms by the hour today.`),
|
|
3001
3071
|
};
|
|
3002
3072
|
}
|
|
@@ -3085,12 +3155,37 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3085
3155
|
text: dated?.noRooms ?? 'Those nights cannot be booked here.',
|
|
3086
3156
|
};
|
|
3087
3157
|
}
|
|
3088
|
-
// ⚠️ BY THE OFFER'S WHOLE NAME,
|
|
3158
|
+
// ⚠️ BY THE ID, THE OFFER'S WHOLE NAME, OR A ROOM TYPE'S BARE NAME - in
|
|
3159
|
+
// that order, and in any language the name has: a guest who read
|
|
3089
3160
|
// 「豪华大床房 · 双早 · 不可取消」 on the card says that, and it is the same offer
|
|
3090
3161
|
// as "Deluxe King Room · Breakfast for 2 · Non-refundable". Never a part of
|
|
3091
|
-
//
|
|
3092
|
-
|
|
3162
|
+
// a name. A type's bare name ("Deluxe King Room") used to be no room at all
|
|
3163
|
+
// when the type has several rates, and a model that planned a trip by the
|
|
3164
|
+
// type's name was answered "no such room" three times over (the owner,
|
|
3165
|
+
// 2026-10-26, Atour). It now takes the type's CHEAPEST rate that sleeps the
|
|
3166
|
+
// stay's guests and has rooms left - and the answer says which rate that
|
|
3167
|
+
// was and that there are others, so the guest is told what they got. Two
|
|
3168
|
+
// types answering to one name, or none, is still no room.
|
|
3169
|
+
const wanted = Math.max(1, clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE));
|
|
3170
|
+
let room = dated.rooms.find((r) => r.id === roomRef) ?? namedExactly(dated.rooms, roomRef);
|
|
3171
|
+
let took = '';
|
|
3172
|
+
const type = room ? undefined : roomTypeNamed(dated.rooms, roomRef);
|
|
3173
|
+
if (type) {
|
|
3174
|
+
const fits = type.rooms.filter((r) => r.sleeps >= stay.guests && r.roomsLeft >= wanted);
|
|
3175
|
+
const [first, ...rest] = fits;
|
|
3176
|
+
const cheapest = first
|
|
3177
|
+
? rest.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a), first)
|
|
3178
|
+
: undefined;
|
|
3179
|
+
if (cheapest) {
|
|
3180
|
+
room = cheapest;
|
|
3181
|
+
const others = fits.filter((r) => r !== cheapest);
|
|
3182
|
+
took = `Took ${cheapest.name} (\`${cheapest.id}\`) - ${others.length
|
|
3183
|
+
? `the lowest of ${type.name}'s ${fits.length} rates for these nights; the others: ${others.map((r) => `${r.name} (\`${r.id}\`)`).join(', ')}.`
|
|
3184
|
+
: `${type.name}'s only rate for these nights.`}`;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3093
3187
|
if (!room) {
|
|
3188
|
+
const han = /\p{Script=Han}/u.test(roomRef);
|
|
3094
3189
|
return {
|
|
3095
3190
|
structured: {
|
|
3096
3191
|
view: 'error',
|
|
@@ -3104,10 +3199,18 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3104
3199
|
})),
|
|
3105
3200
|
// The hotel and the nights, so the card can take the guest straight
|
|
3106
3201
|
// back to its rooms rather than to the start (round-2 jury).
|
|
3107
|
-
hotel: {
|
|
3202
|
+
hotel: {
|
|
3203
|
+
id: detail.hotel.id,
|
|
3204
|
+
name: detail.hotel.name,
|
|
3205
|
+
// The card says the hotel in the guest's language (「没能放进行程:…」).
|
|
3206
|
+
...(detail.hotel.names ? { names: detail.hotel.names } : {}),
|
|
3207
|
+
},
|
|
3108
3208
|
stay,
|
|
3109
3209
|
},
|
|
3110
|
-
|
|
3210
|
+
// ⚠️ EVERY OFFER WITH ITS ID: the model that was told "no such room" by
|
|
3211
|
+
// a list of names it had just tried to match by name tried again by name.
|
|
3212
|
+
// The ids are what it can say, and it says them once.
|
|
3213
|
+
text: `${type ? `${detail.hotel.name} has no ${type.name} that sleeps ${stay.guests} and has ${wanted === 1 ? 'a room' : `${wanted} rooms`} left for those nights.` : `${detail.hotel.name} has no room type by that name for those nights.`} It has: ${dated.rooms.map((r) => `${r.name} (${han && r.names?.zh ? `${r.names.zh}, ` : ''}\`${r.id}\`)`).join(', ')}. Call again with the id.`,
|
|
3111
3214
|
};
|
|
3112
3215
|
}
|
|
3113
3216
|
deps.trips.set(store, {
|
|
@@ -3117,7 +3220,8 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3117
3220
|
stay,
|
|
3118
3221
|
rooms: clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE),
|
|
3119
3222
|
});
|
|
3120
|
-
|
|
3223
|
+
const now = await currentTrip(deps, key, identity(deps, ctx, key), store);
|
|
3224
|
+
return took ? { ...now, text: `${took}\n${now.text}` } : now;
|
|
3121
3225
|
};
|
|
3122
3226
|
/**
|
|
3123
3227
|
* Books the rooms in one slot of the trip store for a named guest - THE ONE
|
|
@@ -4844,6 +4948,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4844
4948
|
const plan = await buildItinerary(deps.backend, days.map((d) => ({
|
|
4845
4949
|
label: str(d.label),
|
|
4846
4950
|
note: typeof d.note === 'string' ? d.note : undefined,
|
|
4951
|
+
why: typeof d.why === 'string' ? d.why : undefined,
|
|
4847
4952
|
items: d.items ?? [],
|
|
4848
4953
|
...(Array.isArray(d.plan) ? { plan: d.plan } : {}),
|
|
4849
4954
|
})), {
|
|
@@ -4866,15 +4971,28 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4866
4971
|
// Each stay as the trip takes it, for a guest who asks in the chat to put
|
|
4867
4972
|
// the plan in the trip: without the room, the model opened the hotel to
|
|
4868
4973
|
// find one - another card - before it could add anything.
|
|
4869
|
-
|
|
4974
|
+
//
|
|
4975
|
+
// ⚠️ BY ID, NEVER BY NAME. A model asked "put the stays on this plan in the
|
|
4976
|
+
// trip" added three rooms by the names it remembered and was answered "no
|
|
4977
|
+
// such room" three times, a whole error card each (the owner, 2026-10-26,
|
|
4978
|
+
// Atour). Each priced stay is listed with the hotel id, the room id and the
|
|
4979
|
+
// nights, which is exactly what add_to_trip takes.
|
|
4980
|
+
const stayItems = plan.days
|
|
4870
4981
|
.flatMap((day) => day.items)
|
|
4871
|
-
.filter((x) => x.kind === 'hotel' && (x.night ?? 1) === 1 && x.checkIn && x.roomId)
|
|
4872
|
-
|
|
4982
|
+
.filter((x) => x.kind === 'hotel' && (x.night ?? 1) === 1 && x.checkIn && x.roomId);
|
|
4983
|
+
const stays = stayItems.map((x, i) => `Stay ${i + 1}: ${x.name} (\`${x.id}\`), room \`${x.roomId}\`${x.roomName ? ` (${x.roomName})` : ''}, ${x.checkIn} to ${x.checkOut}${x.nights ? ` (${x.nights} ${x.nights === 1 ? 'night' : 'nights'})` : ''}${x.booked ? ` - booked, ${x.booked}` : x.inTrip ? ' - in the trip' : ''}`);
|
|
4873
4984
|
return {
|
|
4874
4985
|
structured: { view: 'itinerary', ...common(deps, key), ...plan, gap },
|
|
4875
4986
|
text: [
|
|
4876
4987
|
`**${plan.title}** · ${plan.days.length} ${plan.days.length === 1 ? 'day' : 'days'}. ${gap}`,
|
|
4877
|
-
stays.length
|
|
4988
|
+
...(stays.length
|
|
4989
|
+
? [
|
|
4990
|
+
`Priced stays (${plan.stay?.guests ?? 2} guests; ${t.addToTrip} takes each one's hotel id, room id and dates):`,
|
|
4991
|
+
...stays,
|
|
4992
|
+
// ⚠️ IN THE TEXT ONLY - not in `structured`, not in the schema.
|
|
4993
|
+
...(await longStayNudges(deps, stayItems)),
|
|
4994
|
+
]
|
|
4995
|
+
: []),
|
|
4878
4996
|
...plan.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
|
|
4879
4997
|
...fitsManyOf(deps, plan.dropped),
|
|
4880
4998
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|