@curless/hotel-storefront 0.21.1 → 0.22.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 +12 -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 +39 -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.map +1 -1
- package/dist/present.js +14 -0
- 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 +156 -2
- 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: {
|
|
@@ -1976,6 +2024,112 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
1976
2024
|
.join('\n'),
|
|
1977
2025
|
};
|
|
1978
2026
|
}
|
|
2027
|
+
if (name === t.lookUp) {
|
|
2028
|
+
const stay = asStay(args);
|
|
2029
|
+
const who = identity(deps, ctx, key);
|
|
2030
|
+
const roomsWanted = Math.max(1, Math.min(MAX_ROOMS_PER_LINE, Math.floor(Number(args.rooms) || 1)));
|
|
2031
|
+
const asked = args[deps.vocab.nounPlural];
|
|
2032
|
+
const named = (Array.isArray(asked) ? asked : [])
|
|
2033
|
+
.map((x) => str(x))
|
|
2034
|
+
.filter(Boolean)
|
|
2035
|
+
.slice(0, LOOK_UP_MAX);
|
|
2036
|
+
const city = str(args.city);
|
|
2037
|
+
const ids = [];
|
|
2038
|
+
const dropped = [];
|
|
2039
|
+
let total = 0;
|
|
2040
|
+
if (named.length) {
|
|
2041
|
+
for (const ref of named) {
|
|
2042
|
+
const found = await deps.backend.getHotel(ref);
|
|
2043
|
+
if (found)
|
|
2044
|
+
ids.push(found.hotel.id);
|
|
2045
|
+
else
|
|
2046
|
+
dropped.push(ref);
|
|
2047
|
+
}
|
|
2048
|
+
total = ids.length;
|
|
2049
|
+
}
|
|
2050
|
+
else if (city) {
|
|
2051
|
+
const found = await deps.backend.searchHotels({ city, limit: LOOK_UP_MAX }, who);
|
|
2052
|
+
ids.push(...found.hotels.map((h) => h.id));
|
|
2053
|
+
total = found.total;
|
|
2054
|
+
}
|
|
2055
|
+
const unique = [...new Set(ids)];
|
|
2056
|
+
if (unique.length === 0) {
|
|
2057
|
+
return {
|
|
2058
|
+
structured: {
|
|
2059
|
+
view: 'nothing-found',
|
|
2060
|
+
...common(deps, key),
|
|
2061
|
+
...(dropped.length ? { dropped } : {}),
|
|
2062
|
+
},
|
|
2063
|
+
text: city
|
|
2064
|
+
? `No ${deps.vocab.brand} ${deps.vocab.nounPlural} in "${city}". ${t.listHotels} with no place shows the places there are.`
|
|
2065
|
+
: named.length
|
|
2066
|
+
? `No ${deps.vocab.brand} ${deps.vocab.noun} answers to ${dropped.map((d) => `"${d}"`).join(', ')}.`
|
|
2067
|
+
: `Name a \`city\`, or up to ${LOOK_UP_MAX} ${deps.vocab.nounPlural} in \`${deps.vocab.nounPlural}\`.`,
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
const blocks = [];
|
|
2071
|
+
const read = [];
|
|
2072
|
+
for (const id of unique) {
|
|
2073
|
+
const detail = await deps.backend.getHotel(id, stay, who);
|
|
2074
|
+
if (!detail)
|
|
2075
|
+
continue;
|
|
2076
|
+
const h = detail.hotel;
|
|
2077
|
+
read.push({ id: h.id, name: h.name });
|
|
2078
|
+
const about = hotelText('description', h.name, [h.summary, h.amenities.length ? `Has: ${h.amenities.join(', ')}.` : '']
|
|
2079
|
+
.filter(Boolean)
|
|
2080
|
+
.join('\n'), DESCRIPTION_MAX, deps.vocab.demo);
|
|
2081
|
+
const extras = await deps.backend.extrasFor(h.id, stay);
|
|
2082
|
+
const rooms = (detail.rooms ?? []).filter((r) => r.roomsLeft >= roomsWanted);
|
|
2083
|
+
blocks.push([
|
|
2084
|
+
`### ${h.name} · id \`${h.id}\``,
|
|
2085
|
+
[
|
|
2086
|
+
[h.area, h.city, h.country].filter(Boolean).join(', '),
|
|
2087
|
+
h.brand,
|
|
2088
|
+
h.rating ? `${h.rating.value}/${h.rating.scaleMax} from ${h.rating.count} reviews` : '',
|
|
2089
|
+
]
|
|
2090
|
+
.filter(Boolean)
|
|
2091
|
+
.join(' · '),
|
|
2092
|
+
about.fenced,
|
|
2093
|
+
!stay
|
|
2094
|
+
? h.fromNightly
|
|
2095
|
+
? `From ${formatMoney(h.fromNightly)} a night; rooms are priced once there are dates.`
|
|
2096
|
+
: ''
|
|
2097
|
+
: rooms.length
|
|
2098
|
+
? [
|
|
2099
|
+
`Rooms for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}:`,
|
|
2100
|
+
...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'}`),
|
|
2101
|
+
].join('\n')
|
|
2102
|
+
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
2103
|
+
extras.length
|
|
2104
|
+
? `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('; ')}`
|
|
2105
|
+
: '',
|
|
2106
|
+
]
|
|
2107
|
+
.filter(Boolean)
|
|
2108
|
+
.join('\n'));
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
structured: {
|
|
2112
|
+
view: 'look-up',
|
|
2113
|
+
...common(deps, key),
|
|
2114
|
+
hotels: read,
|
|
2115
|
+
...(stay ? { stay } : {}),
|
|
2116
|
+
total,
|
|
2117
|
+
...(dropped.length ? { dropped } : {}),
|
|
2118
|
+
},
|
|
2119
|
+
text: [
|
|
2120
|
+
`Read ${read.length}${total > read.length ? ` of ${total}` : ''} ${read.length === 1 ? deps.vocab.noun : deps.vocab.nounPlural}. Nothing was shown to the guest.`,
|
|
2121
|
+
...blocks,
|
|
2122
|
+
dropped.length ? `Not found: ${dropped.map((d) => `"${d}"`).join(', ')}.` : '',
|
|
2123
|
+
total > read.length
|
|
2124
|
+
? `${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'}.`
|
|
2125
|
+
: '',
|
|
2126
|
+
`If the answer is a comparison, a recommendation or a plan, draw it now as ONE ${t.compare} or ${t.itinerary} call with these ids - not with ${t.listHotels}, ${t.getHotel} or ${t.listExtras}, which each draw another card.`,
|
|
2127
|
+
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2128
|
+
]
|
|
2129
|
+
.filter(Boolean)
|
|
2130
|
+
.join('\n\n'),
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
1979
2133
|
if (name === t.listExtras) {
|
|
1980
2134
|
// ⚠️ AN ADD-ON FOR A BOOKING ALREADY MADE (robin, 2026-09-28). Add-ons
|
|
1981
2135
|
// could only go in the trip before the request; once booked, an airport
|