@curless/hotel-storefront 0.47.4 → 0.47.5

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/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. "Day 1 — Arrive${places[0] ? ` in ${places[0]}` : ''}"`,
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: ARR,
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' },
@@ -2996,7 +3014,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
2996
3014
  })),
2997
3015
  },
2998
3016
  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('; ')}.`
3017
+ ? `${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
3018
  : ((await today)?.noRooms ?? `${detail.hotel.name} has no rooms by the hour today.`),
3001
3019
  };
3002
3020
  }
@@ -3085,12 +3103,37 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3085
3103
  text: dated?.noRooms ?? 'Those nights cannot be booked here.',
3086
3104
  };
3087
3105
  }
3088
- // ⚠️ BY THE OFFER'S WHOLE NAME, in any language it has: a guest who read
3106
+ // ⚠️ BY THE ID, THE OFFER'S WHOLE NAME, OR A ROOM TYPE'S BARE NAME - in
3107
+ // that order, and in any language the name has: a guest who read
3089
3108
  // 「豪华大床房 · 双早 · 不可取消」 on the card says that, and it is the same offer
3090
3109
  // as "Deluxe King Room · Breakfast for 2 · Non-refundable". Never a part of
3091
- // it, and never a room type's bare name when it has several rates.
3092
- const room = dated.rooms.find((r) => r.id === roomRef) ?? namedExactly(dated.rooms, roomRef);
3110
+ // a name. A type's bare name ("Deluxe King Room") used to be no room at all
3111
+ // when the type has several rates, and a model that planned a trip by the
3112
+ // type's name was answered "no such room" three times over (the owner,
3113
+ // 2026-10-26, Atour). It now takes the type's CHEAPEST rate that sleeps the
3114
+ // stay's guests and has rooms left - and the answer says which rate that
3115
+ // was and that there are others, so the guest is told what they got. Two
3116
+ // types answering to one name, or none, is still no room.
3117
+ const wanted = Math.max(1, clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE));
3118
+ let room = dated.rooms.find((r) => r.id === roomRef) ?? namedExactly(dated.rooms, roomRef);
3119
+ let took = '';
3120
+ const type = room ? undefined : roomTypeNamed(dated.rooms, roomRef);
3121
+ if (type) {
3122
+ const fits = type.rooms.filter((r) => r.sleeps >= stay.guests && r.roomsLeft >= wanted);
3123
+ const [first, ...rest] = fits;
3124
+ const cheapest = first
3125
+ ? rest.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a), first)
3126
+ : undefined;
3127
+ if (cheapest) {
3128
+ room = cheapest;
3129
+ const others = fits.filter((r) => r !== cheapest);
3130
+ took = `Took ${cheapest.name} (\`${cheapest.id}\`) - ${others.length
3131
+ ? `the lowest of ${type.name}'s ${fits.length} rates for these nights; the others: ${others.map((r) => `${r.name} (\`${r.id}\`)`).join(', ')}.`
3132
+ : `${type.name}'s only rate for these nights.`}`;
3133
+ }
3134
+ }
3093
3135
  if (!room) {
3136
+ const han = /\p{Script=Han}/u.test(roomRef);
3094
3137
  return {
3095
3138
  structured: {
3096
3139
  view: 'error',
@@ -3104,10 +3147,18 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3104
3147
  })),
3105
3148
  // The hotel and the nights, so the card can take the guest straight
3106
3149
  // back to its rooms rather than to the start (round-2 jury).
3107
- hotel: { id: detail.hotel.id, name: detail.hotel.name },
3150
+ hotel: {
3151
+ id: detail.hotel.id,
3152
+ name: detail.hotel.name,
3153
+ // The card says the hotel in the guest's language (「没能放进行程:…」).
3154
+ ...(detail.hotel.names ? { names: detail.hotel.names } : {}),
3155
+ },
3108
3156
  stay,
3109
3157
  },
3110
- text: `${detail.hotel.name} has no room type by that name for those nights. It has: ${dated.rooms.map((r) => (/\p{Script=Han}/u.test(roomRef) && r.names?.zh ? `${r.name} (${r.names.zh})` : r.name)).join(', ')}.`,
3158
+ // ⚠️ EVERY OFFER WITH ITS ID: the model that was told "no such room" by
3159
+ // a list of names it had just tried to match by name tried again by name.
3160
+ // The ids are what it can say, and it says them once.
3161
+ 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
3162
  };
3112
3163
  }
3113
3164
  deps.trips.set(store, {
@@ -3117,7 +3168,8 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3117
3168
  stay,
3118
3169
  rooms: clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE),
3119
3170
  });
3120
- return currentTrip(deps, key, identity(deps, ctx, key), store);
3171
+ const now = await currentTrip(deps, key, identity(deps, ctx, key), store);
3172
+ return took ? { ...now, text: `${took}\n${now.text}` } : now;
3121
3173
  };
3122
3174
  /**
3123
3175
  * Books the rooms in one slot of the trip store for a named guest - THE ONE
@@ -4844,6 +4896,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4844
4896
  const plan = await buildItinerary(deps.backend, days.map((d) => ({
4845
4897
  label: str(d.label),
4846
4898
  note: typeof d.note === 'string' ? d.note : undefined,
4899
+ why: typeof d.why === 'string' ? d.why : undefined,
4847
4900
  items: d.items ?? [],
4848
4901
  ...(Array.isArray(d.plan) ? { plan: d.plan } : {}),
4849
4902
  })), {
@@ -4866,15 +4919,26 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4866
4919
  // Each stay as the trip takes it, for a guest who asks in the chat to put
4867
4920
  // the plan in the trip: without the room, the model opened the hotel to
4868
4921
  // find one - another card - before it could add anything.
4922
+ //
4923
+ // ⚠️ BY ID, NEVER BY NAME. A model asked "put the stays on this plan in the
4924
+ // trip" added three rooms by the names it remembered and was answered "no
4925
+ // such room" three times, a whole error card each (the owner, 2026-10-26,
4926
+ // Atour). Each priced stay is listed with the hotel id, the room id and the
4927
+ // nights, which is exactly what add_to_trip takes.
4869
4928
  const stays = plan.days
4870
4929
  .flatMap((day) => day.items)
4871
4930
  .filter((x) => x.kind === 'hotel' && (x.night ?? 1) === 1 && x.checkIn && x.roomId)
4872
- .map((x) => `${x.name} (\`${x.id}\`) ${x.checkIn} to ${x.checkOut}, room \`${x.roomId}\`${x.booked ? ` - booked, ${x.booked}` : x.inTrip ? ' - in the trip' : ''}`);
4931
+ .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
4932
  return {
4874
4933
  structured: { view: 'itinerary', ...common(deps, key), ...plan, gap },
4875
4934
  text: [
4876
4935
  `**${plan.title}** · ${plan.days.length} ${plan.days.length === 1 ? 'day' : 'days'}. ${gap}`,
4877
- stays.length ? `Stays, priced on: ${stays.join('; ')}.` : '',
4936
+ ...(stays.length
4937
+ ? [
4938
+ `Priced stays (${plan.stay?.guests ?? 2} guests; ${t.addToTrip} takes each one's hotel id, room id and dates):`,
4939
+ ...stays,
4940
+ ]
4941
+ : []),
4878
4942
  ...plan.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
4879
4943
  ...fitsManyOf(deps, plan.dropped),
4880
4944
  deps.vocab.demo ? noteFor(deps.vocab) : '',