@curless/hotel-storefront 0.51.6 → 0.52.0
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/aggregate/hotels.d.ts +18 -1
- package/dist/aggregate/hotels.d.ts.map +1 -1
- package/dist/aggregate/hotels.js +38 -7
- package/dist/aggregate/hotels.js.map +1 -1
- package/dist/aggregate/make.d.ts.map +1 -1
- package/dist/aggregate/make.js +2 -0
- package/dist/aggregate/make.js.map +1 -1
- package/dist/backend.d.ts +26 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- 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/config.d.ts +27 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -1
- package/dist/money.d.ts +1 -0
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +1 -1
- package/dist/money.js.map +1 -1
- package/dist/reference.d.ts +34 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.js +117 -0
- package/dist/reference.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/dist/supplier.d.ts.map +1 -1
- package/dist/supplier.js +2 -1
- package/dist/supplier.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +7 -0
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +86 -4
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vocab.d.ts +1 -1
- package/dist/vocab.d.ts.map +1 -1
- package/dist/vocab.js +18 -3
- package/dist/vocab.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor }
|
|
|
7
7
|
import { namedExactly, roomNamesOf, roomTypeNamed, withTraditional } from './names.js';
|
|
8
8
|
import { overlapsOf } from './overlap.js';
|
|
9
9
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
|
|
10
|
+
import { withReference } from './reference.js';
|
|
10
11
|
import { regionOf } from './regions.js';
|
|
11
12
|
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
12
13
|
import { SAME_BOOKING_MS } from './same-booking.js';
|
|
@@ -1337,6 +1338,10 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1337
1338
|
stayDefault: STR,
|
|
1338
1339
|
/** Only where the client shows no cards: nothing was drawn - see `LIST_NOT_DRAWN`. */
|
|
1339
1340
|
notDrawn: STR,
|
|
1341
|
+
/** In a market: the groups that did not answer in time - none of their hotels is in this answer (`degradedOf`). */
|
|
1342
|
+
degraded: ARR,
|
|
1343
|
+
/** What `degraded` means, as the text says it: not that those groups have none here. */
|
|
1344
|
+
degradedSaid: STR,
|
|
1340
1345
|
...HOTEL_PAGE,
|
|
1341
1346
|
},
|
|
1342
1347
|
required: ['view'],
|
|
@@ -1535,6 +1540,9 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1535
1540
|
dropped: STRS,
|
|
1536
1541
|
/** For a name that fits more than one hotel, the ones it fits - each with \`for\`, the name. */
|
|
1537
1542
|
candidates: ARR,
|
|
1543
|
+
/** A \`city\` read in a market: the groups that did not answer in time (\`degradedOf\`). */
|
|
1544
|
+
degraded: ARR,
|
|
1545
|
+
degradedSaid: STR,
|
|
1538
1546
|
},
|
|
1539
1547
|
required: ['view'],
|
|
1540
1548
|
},
|
|
@@ -2833,6 +2841,30 @@ const remember = (deps, k, amount) => {
|
|
|
2833
2841
|
};
|
|
2834
2842
|
/** "a, b and c" - "a and b" - "a". */
|
|
2835
2843
|
const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}` : (xs[0] ?? '');
|
|
2844
|
+
/**
|
|
2845
|
+
* The groups of a market that did not answer a search, as the sentence the
|
|
2846
|
+
* model is told and the fields the card draws its own line from - the same
|
|
2847
|
+
* list in both (`HotelSearchResult.degraded`).
|
|
2848
|
+
*
|
|
2849
|
+
* ⚠️ "DID NOT ANSWER" IS NOT "HAS NONE". A list without Atour's hotels, from
|
|
2850
|
+
* an Atour that timed out, reads to a guest as a city where Atour has no
|
|
2851
|
+
* hotel. The sentence says the first and denies the second, in the text and in
|
|
2852
|
+
* `structuredContent` alike - a host with no card has only the words.
|
|
2853
|
+
*/
|
|
2854
|
+
const degradedOf = (deps, answer) => {
|
|
2855
|
+
const down = answer.degraded ?? [];
|
|
2856
|
+
if (down.length === 0)
|
|
2857
|
+
return { structured: {}, said: '' };
|
|
2858
|
+
const one = down.length === 1;
|
|
2859
|
+
const said = `${listAnd(down.map((d) => d.name))} did not answer in time, so none of ${one ? 'its' : 'their'} ${deps.vocab.nounPlural} is in this list - this does not mean ${one ? 'it has' : 'they have'} none here; asking again in a moment may bring them.`;
|
|
2860
|
+
return {
|
|
2861
|
+
structured: {
|
|
2862
|
+
degraded: down.map((d) => ({ supplier: d.supplier, name: d.name })),
|
|
2863
|
+
degradedSaid: said,
|
|
2864
|
+
},
|
|
2865
|
+
said,
|
|
2866
|
+
};
|
|
2867
|
+
};
|
|
2836
2868
|
/** The counts a call can carry, and what each may be. */
|
|
2837
2869
|
const COUNTS = ['guests', 'rooms', 'quantity'];
|
|
2838
2870
|
/** "2" is 2: a host that sends a count as a string has not asked for something else. */
|
|
@@ -3002,6 +3034,25 @@ const needsRoomIn = (t, lines, parts) => {
|
|
|
3002
3034
|
.join('\n'),
|
|
3003
3035
|
};
|
|
3004
3036
|
};
|
|
3037
|
+
/**
|
|
3038
|
+
* What a booking of these stays takes of the guest, in the words of the
|
|
3039
|
+
* sentence that asks for it: "name and email", and a phone number where a group
|
|
3040
|
+
* needs one - for all of them, or, in a market where only some of the trip's
|
|
3041
|
+
* groups do, for the stays with those.
|
|
3042
|
+
*/
|
|
3043
|
+
const guestDetailsFor = (deps, rooms) => {
|
|
3044
|
+
if (!deps.backend.suppliers)
|
|
3045
|
+
return (deps.backend.capabilities ?? FULL_CAPABILITIES).requiresPhone
|
|
3046
|
+
? 'name, email and phone number'
|
|
3047
|
+
: 'name and email';
|
|
3048
|
+
const groups = [...new Set(rooms.map((l) => deps.backend.supplierOf?.(l.hotelId)))];
|
|
3049
|
+
const needing = groups.filter((id) => !!holderOf(deps, id).caps.requiresPhone);
|
|
3050
|
+
if (needing.length === 0)
|
|
3051
|
+
return 'name and email';
|
|
3052
|
+
if (needing.length === groups.length)
|
|
3053
|
+
return 'name, email and phone number';
|
|
3054
|
+
return `name and email, and a phone number for the stays with ${listAnd(needing.map((id) => holderOf(deps, id).name))}`;
|
|
3055
|
+
};
|
|
3005
3056
|
/**
|
|
3006
3057
|
* The lines in one slot of the trip store, priced - the trip itself, or (with
|
|
3007
3058
|
* `store`) the picked room's slot. `key` stays the conversation's trip key: it
|
|
@@ -3040,8 +3091,14 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3040
3091
|
const overlapping = overlaps.length ? { overlaps } : {};
|
|
3041
3092
|
// ⚠️ WHAT COMES NEXT, IN WORDS (2026-11-02 audit, P2-4): the card has a
|
|
3042
3093
|
// Book button under the trip, and the words had nothing in its place.
|
|
3094
|
+
//
|
|
3095
|
+
// ⚠️ AND A PHONE NUMBER WHERE THE TRIP'S GROUP NEEDS ONE (the 0.52.0 gap): the
|
|
3096
|
+
// card's form asks for it, and this sentence - the whole of the card for a
|
|
3097
|
+
// client that draws none - said only "name and email", so the first booking
|
|
3098
|
+
// came back "needs a phone number". Whose need it is is the trip's groups',
|
|
3099
|
+
// as `requestBooking` judges it.
|
|
3043
3100
|
const nextStep = ctx?.noCards && parts.some((p) => p.lines.some((l) => l.kind === 'room'))
|
|
3044
|
-
? `Nothing in the trip is booked yet. To book it, ${deps.tools.requestBooking} takes the guest's own
|
|
3101
|
+
? `Nothing in the trip is booked yet. To book it, ${deps.tools.requestBooking} takes the guest's own ${guestDetailsFor(deps, lines.filter((l) => l.kind === 'room'))} - ask for them if the guest has not typed them in this conversation.`
|
|
3045
3102
|
: '';
|
|
3046
3103
|
if (parts.length === 1 && only) {
|
|
3047
3104
|
return {
|
|
@@ -3389,6 +3446,14 @@ export const handleTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3389
3446
|
// a card from before - its minor units and currency, for the card to write
|
|
3390
3447
|
// in its reader's language. The card does no sums: every figure is ours.
|
|
3391
3448
|
const amounts = amountsIn(named, written);
|
|
3449
|
+
// ⚠️ AND, WHERE THE DEPLOYMENT HAS REFERENCE RATES, A SMALL "≈" BESIDE EACH
|
|
3450
|
+
// AMOUNT IN ANOTHER CURRENCY (reference.ts): the hotel's own currency is what
|
|
3451
|
+
// is quoted and charged, and the figure is only ever beside it.
|
|
3452
|
+
if (deps.backend.reference)
|
|
3453
|
+
return {
|
|
3454
|
+
...answer,
|
|
3455
|
+
...withReference(deps.backend.reference, { structured: named, text: answer.text }, amounts, written),
|
|
3456
|
+
};
|
|
3392
3457
|
const structured = Object.keys(amounts).length ? { ...named, amounts } : named;
|
|
3393
3458
|
return { ...answer, structured };
|
|
3394
3459
|
};
|
|
@@ -4672,6 +4737,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4672
4737
|
: `${c.name} (${c.count})`)
|
|
4673
4738
|
.join(', ');
|
|
4674
4739
|
const n = deps.vocab.nounPlural;
|
|
4740
|
+
const degraded = degradedOf(deps, answer);
|
|
4675
4741
|
if (namedCountry) {
|
|
4676
4742
|
const [only] = countries;
|
|
4677
4743
|
return {
|
|
@@ -4683,8 +4749,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4683
4749
|
ask: { city: asked.city, ...stayArgs },
|
|
4684
4750
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
4685
4751
|
...(today ? { today } : {}),
|
|
4752
|
+
...degraded.structured,
|
|
4686
4753
|
},
|
|
4687
|
-
text: `${total} ${branded(deps.vocab, n)} in ${only?.name}, in ${only?.cities.length} cities to choose from: ${every(only?.cities ?? [])}. A city lists its ${n}; \`all\` lists every one in ${only?.name}. ${forPlans}`,
|
|
4754
|
+
text: `${total} ${branded(deps.vocab, n)} in ${only?.name}, in ${only?.cities.length} cities to choose from: ${every(only?.cities ?? [])}. A city lists its ${n}; \`all\` lists every one in ${only?.name}. ${degraded.said ? `${degraded.said} ` : ''}${forPlans}`,
|
|
4688
4755
|
};
|
|
4689
4756
|
}
|
|
4690
4757
|
const regions = [
|
|
@@ -4707,11 +4774,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4707
4774
|
ask: { ...stayArgs },
|
|
4708
4775
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
4709
4776
|
...(today ? { today } : {}),
|
|
4777
|
+
...degraded.structured,
|
|
4710
4778
|
},
|
|
4711
|
-
text: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} ${countries.length === 1 ? 'country' : 'countries'}, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${ctx.noCards ? everyWithCities(r.countries) : every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${forPlans}`,
|
|
4779
|
+
text: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} ${countries.length === 1 ? 'country' : 'countries'}, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${ctx.noCards ? everyWithCities(r.countries) : every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${degraded.said ? `${degraded.said} ` : ''}${forPlans}`,
|
|
4712
4780
|
};
|
|
4713
4781
|
}
|
|
4714
4782
|
if (hotels.length === 0) {
|
|
4783
|
+
const degraded = degradedOf(deps, answer);
|
|
4715
4784
|
const nearest = (answer.nearest ?? []).map((h) => ({
|
|
4716
4785
|
...h,
|
|
4717
4786
|
summary: hotelText('description', h.name, h.summary, DESCRIPTION_MAX).text,
|
|
@@ -4759,6 +4828,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4759
4828
|
...(nearest.length ? { nearest, overBudget: answer.overBudget } : {}),
|
|
4760
4829
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
4761
4830
|
...(widened?.found ? { widened } : {}),
|
|
4831
|
+
// Nothing found while a group did not answer is not "none here".
|
|
4832
|
+
...degraded.structured,
|
|
4762
4833
|
// Why it is empty, in the words the model is told, so the card says
|
|
4763
4834
|
// the same thing rather than guessing at it.
|
|
4764
4835
|
why,
|
|
@@ -4776,6 +4847,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4776
4847
|
text: [
|
|
4777
4848
|
widened?.found ? `Nothing matched ${widened.from}, so this looked in ${widened.to}.` : '',
|
|
4778
4849
|
why,
|
|
4850
|
+
degraded.said,
|
|
4779
4851
|
]
|
|
4780
4852
|
.filter(Boolean)
|
|
4781
4853
|
.join(' '),
|
|
@@ -4851,6 +4923,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4851
4923
|
priceNotes.push(notSold);
|
|
4852
4924
|
// Every one by name and city, and that there are no others - or, told
|
|
4853
4925
|
// only "2 hotels", the model asked for the list again to learn which.
|
|
4926
|
+
const degraded = degradedOf(deps, answer);
|
|
4854
4927
|
const inAll = homeList
|
|
4855
4928
|
? `This connector has ${total} ${branded(deps.vocab, deps.vocab.nounPlural)} in all, and this answer lists every one: ${hotels
|
|
4856
4929
|
.map((h) => `${hotelName(h.name)}${h.city ? ` in ${h.city}` : ''}`)
|
|
@@ -4903,6 +4976,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4903
4976
|
...(answer.overBudget ? { overBudget: answer.overBudget } : {}),
|
|
4904
4977
|
...(widened?.found ? { widened } : {}),
|
|
4905
4978
|
...(answer.currencies.length > 1 ? { currencies: answer.currencies } : {}),
|
|
4979
|
+
// A group that did not answer, in both: the card draws a line from it.
|
|
4980
|
+
...degraded.structured,
|
|
4906
4981
|
// In both, like a plan's: a host with no card may hand its model this
|
|
4907
4982
|
// rather than the text.
|
|
4908
4983
|
...(ctx.noCards ? { notDrawn: LIST_NOT_DRAWN } : {}),
|
|
@@ -4920,6 +4995,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4920
4995
|
: '',
|
|
4921
4996
|
...priceNotes,
|
|
4922
4997
|
widened?.found ? `Nothing matched ${widened.from}, so this is ${widened.to}.` : '',
|
|
4998
|
+
degraded.said,
|
|
4923
4999
|
ctx.noCards && hotels.length
|
|
4924
5000
|
? `\n${LIST_NOT_DRAWN}\n${hotels.map(listedInWords).join('\n')}\n`
|
|
4925
5001
|
: '',
|
|
@@ -5120,6 +5196,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5120
5196
|
/** For each name that fits more than one, which ones - see `namesLike`. */
|
|
5121
5197
|
const candidates = [];
|
|
5122
5198
|
let total = 0;
|
|
5199
|
+
let degradedFound = {};
|
|
5123
5200
|
if (named.length) {
|
|
5124
5201
|
for (const ref of named) {
|
|
5125
5202
|
const found = await deps.backend.getHotel(ref);
|
|
@@ -5139,7 +5216,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5139
5216
|
const found = await deps.backend.searchHotels({ city, limit: LOOK_UP_MAX }, who);
|
|
5140
5217
|
ids.push(...found.hotels.map((h) => h.id));
|
|
5141
5218
|
total = found.total;
|
|
5219
|
+
degradedFound = found;
|
|
5142
5220
|
}
|
|
5221
|
+
const degraded = degradedOf(deps, degradedFound);
|
|
5143
5222
|
const unique = [...new Set(ids)];
|
|
5144
5223
|
// A name that fits more than one is said with the ones it fits; the rest are not found.
|
|
5145
5224
|
const notFound = dropped.filter((d) => !candidates.some((c) => c.for === d));
|
|
@@ -5151,9 +5230,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5151
5230
|
...common(deps, key),
|
|
5152
5231
|
...(dropped.length ? { dropped } : {}),
|
|
5153
5232
|
...(candidates.length ? { candidates } : {}),
|
|
5233
|
+
...degraded.structured,
|
|
5154
5234
|
},
|
|
5155
5235
|
text: city
|
|
5156
|
-
? `No ${branded(deps.vocab, deps.vocab.nounPlural)} in "${city}". ${t.listHotels} with no place shows the places there are.`
|
|
5236
|
+
? `No ${branded(deps.vocab, deps.vocab.nounPlural)} in "${city}". ${degraded.said ? `${degraded.said} ` : ''}${t.listHotels} with no place shows the places there are.`
|
|
5157
5237
|
: named.length
|
|
5158
5238
|
? [
|
|
5159
5239
|
notFound.length
|
|
@@ -5222,9 +5302,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5222
5302
|
total,
|
|
5223
5303
|
...(dropped.length ? { dropped } : {}),
|
|
5224
5304
|
...(candidates.length ? { candidates } : {}),
|
|
5305
|
+
...degraded.structured,
|
|
5225
5306
|
},
|
|
5226
5307
|
text: [
|
|
5227
5308
|
`Read ${read.length}${total > read.length ? ` of ${total}` : ''} ${read.length === 1 ? deps.vocab.noun : deps.vocab.nounPlural}. Nothing was shown to the guest.`,
|
|
5309
|
+
degraded.said,
|
|
5228
5310
|
...blocks,
|
|
5229
5311
|
notFound.length ? `Not found: ${notFound.map((d) => `"${d}"`).join(', ')}.` : '',
|
|
5230
5312
|
...fitsMany,
|