@curless/hotel-storefront 0.21.1 → 0.23.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/__tests__/card-contract.test.js +25 -7
- package/dist/__tests__/card-contract.test.js.map +1 -1
- package/dist/__tests__/present.test.js +25 -0
- package/dist/__tests__/present.test.js.map +1 -1
- package/dist/__tests__/server.test.js +7 -0
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +69 -0
- package/dist/__tests__/tools.test.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 +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/present.d.ts +5 -0
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +17 -1
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +8 -8
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +6 -5
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +39 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +190 -7
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -61,6 +61,8 @@ const cancels = (title, openWorld) => ({
|
|
|
61
61
|
openWorldHint: openWorld,
|
|
62
62
|
});
|
|
63
63
|
const DATE = (what) => ({ type: 'string', description: `${what}, as YYYY-MM-DD` });
|
|
64
|
+
/** How many ${nounPlural} one look-up reads - a city's worth, and what a comparison can hold and then some. */
|
|
65
|
+
export const LOOK_UP_MAX = 6;
|
|
64
66
|
export const buildTools = (n, v) => {
|
|
65
67
|
const open = !v.demo;
|
|
66
68
|
const stayProps = {
|
|
@@ -132,7 +134,7 @@ export const buildTools = (n, v) => {
|
|
|
132
134
|
},
|
|
133
135
|
{
|
|
134
136
|
name: n.getHotel,
|
|
135
|
-
description: `One ${v.noun} in full: description, amenities, address, check-in and check-out times. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, each with its nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rooms are left. \`rooms\` leaves out the types that have fewer than that many. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
|
|
137
|
+
description: `One ${v.noun} in full: description, amenities, address, check-in and check-out times. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, each with its nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rooms are left. \`rooms\` leaves out the types that have fewer than that many. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known. The ${v.noun} is drawn as a card for the guest to look at.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
|
|
136
138
|
annotations: reads(`View one ${v.brand} ${v.noun}`, open),
|
|
137
139
|
inputSchema: {
|
|
138
140
|
type: 'object',
|
|
@@ -150,9 +152,39 @@ export const buildTools = (n, v) => {
|
|
|
150
152
|
required: ['hotel'],
|
|
151
153
|
},
|
|
152
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: n.lookUp,
|
|
157
|
+
// ⚠️ THE ONE READING TOOL THAT DRAWS NOTHING. Every other one renders a
|
|
158
|
+
// card on every call, so a model reading up before it answered - list
|
|
159
|
+
// the city, open four hotels, then compare - put six cards in front of
|
|
160
|
+
// the guest for one question (robin, 2026-09-29, Opus in Claude.ai). The
|
|
161
|
+
// reference apps do the same split: search without a card, present with
|
|
162
|
+
// one.
|
|
163
|
+
description: `Reads up to ${LOOK_UP_MAX} ${v.nounPlural} - a \`city\`'s (or a country's), or the ones named in \`${v.nounPlural}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. The ids are the ones a comparison and a plan take.`,
|
|
164
|
+
annotations: reads(`Look up ${v.brand} ${v.nounPlural}`, open),
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
city: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: `A city or a country: its ${v.nounPlural}, up to ${LOOK_UP_MAX}`,
|
|
171
|
+
},
|
|
172
|
+
[v.nounPlural]: {
|
|
173
|
+
type: 'array',
|
|
174
|
+
items: { type: 'string' },
|
|
175
|
+
description: `Up to ${LOOK_UP_MAX} ${v.nounPlural} by name or id; these are read when both are given`,
|
|
176
|
+
},
|
|
177
|
+
...stayProps,
|
|
178
|
+
rooms: {
|
|
179
|
+
type: 'number',
|
|
180
|
+
description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
153
185
|
{
|
|
154
186
|
name: n.listExtras,
|
|
155
|
-
description: `What one ${v.noun} offers beside the room: airport transfers, dinners, guided experiences, spa and family add-ons, each with its price and whether it is charged per stay, per person or per night.`,
|
|
187
|
+
description: `What one ${v.noun} offers beside the room: airport transfers, dinners, guided experiences, spa and family add-ons, each with its price and whether it is charged per stay, per person or per night, drawn as a card for the guest to look at.`,
|
|
156
188
|
annotations: reads(`${v.brand} add-ons`, open),
|
|
157
189
|
inputSchema: {
|
|
158
190
|
type: 'object',
|
|
@@ -771,6 +803,22 @@ export const buildOutputSchemas = (n) => ({
|
|
|
771
803
|
},
|
|
772
804
|
required: ['view'],
|
|
773
805
|
},
|
|
806
|
+
// What was read, by id - the substance is the text, which is what the model
|
|
807
|
+
// plans from, and a second copy of it here would be read twice.
|
|
808
|
+
[n.lookUp]: {
|
|
809
|
+
type: 'object',
|
|
810
|
+
properties: {
|
|
811
|
+
view: { enum: ['look-up', 'nothing-found'] },
|
|
812
|
+
brand: STR,
|
|
813
|
+
toolNames: OBJ,
|
|
814
|
+
demo: BOOL,
|
|
815
|
+
hotels: ARR,
|
|
816
|
+
stay: OBJ,
|
|
817
|
+
total: { type: 'number' },
|
|
818
|
+
dropped: ARR,
|
|
819
|
+
},
|
|
820
|
+
required: ['view'],
|
|
821
|
+
},
|
|
774
822
|
[n.policies]: {
|
|
775
823
|
type: 'object',
|
|
776
824
|
properties: {
|
|
@@ -1445,11 +1493,40 @@ const suggestionsFor = async (deps, d) => {
|
|
|
1445
1493
|
});
|
|
1446
1494
|
}
|
|
1447
1495
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1496
|
+
// ⚠️ THE CARD PUTS THE STAYS IN ITSELF (robin, 2026-09-29: 「改成卡片直接加进行程
|
|
1497
|
+
// 吧」). This chip used to send "Put the stays on this plan in the trip" to the
|
|
1498
|
+
// chat, and the host held it under a caution banner - it cannot tell a
|
|
1499
|
+
// card's message from an injected one - for the guest to send on. Now it is
|
|
1500
|
+
// the card's own calls: each stay not yet in the trip or booked, on the room
|
|
1501
|
+
// the plan priced it on, for the plan's own nights. The trip is editable and
|
|
1502
|
+
// nothing is booked by it.
|
|
1503
|
+
if (view === 'itinerary') {
|
|
1504
|
+
const stays = (d.days ?? [])
|
|
1505
|
+
.flatMap((day) => day.items)
|
|
1506
|
+
.filter((x) => x.kind === 'hotel' &&
|
|
1507
|
+
(x.night ?? 1) === 1 &&
|
|
1508
|
+
x.roomId &&
|
|
1509
|
+
x.checkIn &&
|
|
1510
|
+
x.checkOut &&
|
|
1511
|
+
!x.inTrip &&
|
|
1512
|
+
!x.booked);
|
|
1513
|
+
if (stays.length)
|
|
1514
|
+
out.push({
|
|
1515
|
+
label: stays.length === 1
|
|
1516
|
+
? `Put ${stays[0]?.name} in the trip`
|
|
1517
|
+
: `Put these ${stays.length} stays in the trip`,
|
|
1518
|
+
asks: stays.map((x) => ({
|
|
1519
|
+
tool: 'addToTrip',
|
|
1520
|
+
args: {
|
|
1521
|
+
hotel: x.id,
|
|
1522
|
+
room: x.roomId,
|
|
1523
|
+
checkIn: x.checkIn,
|
|
1524
|
+
checkOut: x.checkOut,
|
|
1525
|
+
guests: stay?.guests ?? 2,
|
|
1526
|
+
},
|
|
1527
|
+
})),
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1453
1530
|
if (view === 'booking') {
|
|
1454
1531
|
const all = (Array.isArray(d.bookings) ? d.bookings : [d.booking]);
|
|
1455
1532
|
for (const b of all.slice(0, 2)) {
|
|
@@ -1976,6 +2053,112 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
1976
2053
|
.join('\n'),
|
|
1977
2054
|
};
|
|
1978
2055
|
}
|
|
2056
|
+
if (name === t.lookUp) {
|
|
2057
|
+
const stay = asStay(args);
|
|
2058
|
+
const who = identity(deps, ctx, key);
|
|
2059
|
+
const roomsWanted = Math.max(1, Math.min(MAX_ROOMS_PER_LINE, Math.floor(Number(args.rooms) || 1)));
|
|
2060
|
+
const asked = args[deps.vocab.nounPlural];
|
|
2061
|
+
const named = (Array.isArray(asked) ? asked : [])
|
|
2062
|
+
.map((x) => str(x))
|
|
2063
|
+
.filter(Boolean)
|
|
2064
|
+
.slice(0, LOOK_UP_MAX);
|
|
2065
|
+
const city = str(args.city);
|
|
2066
|
+
const ids = [];
|
|
2067
|
+
const dropped = [];
|
|
2068
|
+
let total = 0;
|
|
2069
|
+
if (named.length) {
|
|
2070
|
+
for (const ref of named) {
|
|
2071
|
+
const found = await deps.backend.getHotel(ref);
|
|
2072
|
+
if (found)
|
|
2073
|
+
ids.push(found.hotel.id);
|
|
2074
|
+
else
|
|
2075
|
+
dropped.push(ref);
|
|
2076
|
+
}
|
|
2077
|
+
total = ids.length;
|
|
2078
|
+
}
|
|
2079
|
+
else if (city) {
|
|
2080
|
+
const found = await deps.backend.searchHotels({ city, limit: LOOK_UP_MAX }, who);
|
|
2081
|
+
ids.push(...found.hotels.map((h) => h.id));
|
|
2082
|
+
total = found.total;
|
|
2083
|
+
}
|
|
2084
|
+
const unique = [...new Set(ids)];
|
|
2085
|
+
if (unique.length === 0) {
|
|
2086
|
+
return {
|
|
2087
|
+
structured: {
|
|
2088
|
+
view: 'nothing-found',
|
|
2089
|
+
...common(deps, key),
|
|
2090
|
+
...(dropped.length ? { dropped } : {}),
|
|
2091
|
+
},
|
|
2092
|
+
text: city
|
|
2093
|
+
? `No ${deps.vocab.brand} ${deps.vocab.nounPlural} in "${city}". ${t.listHotels} with no place shows the places there are.`
|
|
2094
|
+
: named.length
|
|
2095
|
+
? `No ${deps.vocab.brand} ${deps.vocab.noun} answers to ${dropped.map((d) => `"${d}"`).join(', ')}.`
|
|
2096
|
+
: `Name a \`city\`, or up to ${LOOK_UP_MAX} ${deps.vocab.nounPlural} in \`${deps.vocab.nounPlural}\`.`,
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
const blocks = [];
|
|
2100
|
+
const read = [];
|
|
2101
|
+
for (const id of unique) {
|
|
2102
|
+
const detail = await deps.backend.getHotel(id, stay, who);
|
|
2103
|
+
if (!detail)
|
|
2104
|
+
continue;
|
|
2105
|
+
const h = detail.hotel;
|
|
2106
|
+
read.push({ id: h.id, name: h.name });
|
|
2107
|
+
const about = hotelText('description', h.name, [h.summary, h.amenities.length ? `Has: ${h.amenities.join(', ')}.` : '']
|
|
2108
|
+
.filter(Boolean)
|
|
2109
|
+
.join('\n'), DESCRIPTION_MAX, deps.vocab.demo);
|
|
2110
|
+
const extras = await deps.backend.extrasFor(h.id, stay);
|
|
2111
|
+
const rooms = (detail.rooms ?? []).filter((r) => r.roomsLeft >= roomsWanted);
|
|
2112
|
+
blocks.push([
|
|
2113
|
+
`### ${h.name} · id \`${h.id}\``,
|
|
2114
|
+
[
|
|
2115
|
+
[h.area, h.city, h.country].filter(Boolean).join(', '),
|
|
2116
|
+
h.brand,
|
|
2117
|
+
h.rating ? `${h.rating.value}/${h.rating.scaleMax} from ${h.rating.count} reviews` : '',
|
|
2118
|
+
]
|
|
2119
|
+
.filter(Boolean)
|
|
2120
|
+
.join(' · '),
|
|
2121
|
+
about.fenced,
|
|
2122
|
+
!stay
|
|
2123
|
+
? h.fromNightly
|
|
2124
|
+
? `From ${formatMoney(h.fromNightly)} a night; rooms are priced once there are dates.`
|
|
2125
|
+
: ''
|
|
2126
|
+
: rooms.length
|
|
2127
|
+
? [
|
|
2128
|
+
`Rooms for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}:`,
|
|
2129
|
+
...rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${r.freeCancellationUntil ? `, free to cancel until ${r.freeCancellationUntil.slice(0, 10)}` : r.refundable ? '' : ', non-refundable'}`),
|
|
2130
|
+
].join('\n')
|
|
2131
|
+
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
2132
|
+
extras.length
|
|
2133
|
+
? `Add-ons: ${extras.map((e) => `${e.name} (id \`${e.id}\`, ${formatMoney(e.price)} per ${e.per}${e.when && e.when !== 'any' ? `, on ${e.when}` : ''})`).join('; ')}`
|
|
2134
|
+
: '',
|
|
2135
|
+
]
|
|
2136
|
+
.filter(Boolean)
|
|
2137
|
+
.join('\n'));
|
|
2138
|
+
}
|
|
2139
|
+
return {
|
|
2140
|
+
structured: {
|
|
2141
|
+
view: 'look-up',
|
|
2142
|
+
...common(deps, key),
|
|
2143
|
+
hotels: read,
|
|
2144
|
+
...(stay ? { stay } : {}),
|
|
2145
|
+
total,
|
|
2146
|
+
...(dropped.length ? { dropped } : {}),
|
|
2147
|
+
},
|
|
2148
|
+
text: [
|
|
2149
|
+
`Read ${read.length}${total > read.length ? ` of ${total}` : ''} ${read.length === 1 ? deps.vocab.noun : deps.vocab.nounPlural}. Nothing was shown to the guest.`,
|
|
2150
|
+
...blocks,
|
|
2151
|
+
dropped.length ? `Not found: ${dropped.map((d) => `"${d}"`).join(', ')}.` : '',
|
|
2152
|
+
total > read.length
|
|
2153
|
+
? `${total - read.length} more ${total - read.length === 1 ? 'is' : 'are'} there; name ${total - read.length === 1 ? 'it' : 'them'} in \`${deps.vocab.nounPlural}\` to read ${total - read.length === 1 ? 'it' : 'them'}.`
|
|
2154
|
+
: '',
|
|
2155
|
+
`Draw the answer now as ONE card with these ids: a plan or days the guest asked for (行程 - recommended or arranged) is ${t.itinerary}; which ${deps.vocab.noun} to stay at is ${t.compare}. Not ${t.listHotels}, ${t.getHotel} or ${t.listExtras}, which each draw another card.`,
|
|
2156
|
+
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2157
|
+
]
|
|
2158
|
+
.filter(Boolean)
|
|
2159
|
+
.join('\n\n'),
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
1979
2162
|
if (name === t.listExtras) {
|
|
1980
2163
|
// ⚠️ AN ADD-ON FOR A BOOKING ALREADY MADE (robin, 2026-09-28). Add-ons
|
|
1981
2164
|
// could only go in the trip before the request; once booked, an airport
|