@voyant-travel/max-sdk 0.1.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/README.md +131 -0
- package/dist/cards.d.ts +1141 -0
- package/dist/cards.d.ts.map +1 -0
- package/dist/cards.js +462 -0
- package/dist/define-tool.d.ts +32 -0
- package/dist/define-tool.d.ts.map +1 -0
- package/dist/define-tool.js +37 -0
- package/dist/handler.d.ts +32 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +108 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/manifest.d.ts +25 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +40 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../src/cards.ts"],"names":[],"mappings":"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"}
|
package/dist/cards.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* `@repo/agent-cards` — the shared contract for the structured "cards" the Max
|
|
4
|
+
* agent embed renders as rich widgets.
|
|
5
|
+
*
|
|
6
|
+
* A card is an additive, display-ready payload attached to a tool result under
|
|
7
|
+
* the `card` key. The backend (`@repo/agent-core`'s `buildCardForTool`) maps raw
|
|
8
|
+
* upstream JSON into a card; the agent-app frontend renders it via a widget
|
|
9
|
+
* registry. Everything here is pure zod + TS — no env, no upstream coupling — so
|
|
10
|
+
* both sides import the same source of truth.
|
|
11
|
+
*
|
|
12
|
+
* Two tiers:
|
|
13
|
+
* - Bespoke kinds (`booking`, `customer`, `itinerary`, …) for the common
|
|
14
|
+
* operations, hand-tuned in the UI.
|
|
15
|
+
* - A `dynamic` kind = a list of LLM-safe presentation `Block`s, emitted by the
|
|
16
|
+
* `present_view` tool, for the long tail of ad-hoc views.
|
|
17
|
+
*
|
|
18
|
+
* Cards carry display-ready strings (the backend pre-formats money/dates with the
|
|
19
|
+
* operator's locale) plus ids for actions — the frontend stays presentational.
|
|
20
|
+
*/
|
|
21
|
+
/** Semantic colour for badges/statuses. The UI maps these to design tokens. */
|
|
22
|
+
export const ToneSchema = z.enum(["default", "success", "warning", "danger", "info", "brand"]);
|
|
23
|
+
export const BadgeSchema = z.object({
|
|
24
|
+
label: z.string().min(1).max(80),
|
|
25
|
+
tone: ToneSchema.optional(),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* What an actionable element does when clicked.
|
|
29
|
+
* - `open` → open a URL in a new tab (deep links into admin / storefront).
|
|
30
|
+
* - `prompt` → send a natural-language message to Max, so the model performs
|
|
31
|
+
* the next tool call and the existing approval card confirms any
|
|
32
|
+
* write. This powers "Publish", "Send offer", "Rebook", etc.
|
|
33
|
+
* without any new endpoints.
|
|
34
|
+
*/
|
|
35
|
+
export const CardActionSchema = z.discriminatedUnion("kind", [
|
|
36
|
+
z.object({
|
|
37
|
+
kind: z.literal("open"),
|
|
38
|
+
label: z.string().min(1).max(60).optional(),
|
|
39
|
+
url: z.string().min(1).max(2000),
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
kind: z.literal("prompt"),
|
|
43
|
+
label: z.string().min(1).max(60),
|
|
44
|
+
prompt: z.string().min(1).max(2000),
|
|
45
|
+
tone: ToneSchema.optional(),
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
const KeyValueSchema = z.object({
|
|
49
|
+
label: z.string().min(1).max(120),
|
|
50
|
+
value: z.string().min(1).max(600),
|
|
51
|
+
});
|
|
52
|
+
const TimelineItemSchema = z.object({
|
|
53
|
+
title: z.string().min(1).max(240),
|
|
54
|
+
date: z.string().max(80).optional(),
|
|
55
|
+
amount: z.string().max(80).optional(),
|
|
56
|
+
status: BadgeSchema.optional(),
|
|
57
|
+
description: z.string().max(600).optional(),
|
|
58
|
+
action: CardActionSchema.optional(),
|
|
59
|
+
});
|
|
60
|
+
const StatTileSchema = z.object({
|
|
61
|
+
label: z.string().min(1).max(80),
|
|
62
|
+
value: z.string().min(1).max(120),
|
|
63
|
+
hint: z.string().max(120).optional(),
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Bespoke card kinds
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
const CustomerCard = z.object({
|
|
69
|
+
kind: z.literal("customer"),
|
|
70
|
+
name: z.string().min(1).max(200),
|
|
71
|
+
email: z.string().max(320).optional(),
|
|
72
|
+
phone: z.string().max(80).optional(),
|
|
73
|
+
avatarUrl: z.string().max(2000).optional(),
|
|
74
|
+
jobTitle: z.string().max(160).optional(),
|
|
75
|
+
status: BadgeSchema.optional(),
|
|
76
|
+
segment: z.string().max(80).optional(),
|
|
77
|
+
lifetimeValue: z.string().max(80).optional(),
|
|
78
|
+
stats: z.array(StatTileSchema).max(6).optional(),
|
|
79
|
+
timeline: z.array(TimelineItemSchema).max(20).optional(),
|
|
80
|
+
note: z.string().max(600).optional(),
|
|
81
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
82
|
+
});
|
|
83
|
+
const BookingCard = z.object({
|
|
84
|
+
kind: z.literal("booking"),
|
|
85
|
+
title: z.string().min(1).max(240),
|
|
86
|
+
reference: z.string().max(120).optional(),
|
|
87
|
+
customer: z.string().max(200).optional(),
|
|
88
|
+
status: BadgeSchema.optional(),
|
|
89
|
+
dateRange: z.string().max(120).optional(),
|
|
90
|
+
travelers: z.string().max(80).optional(),
|
|
91
|
+
total: z.string().max(80).optional(),
|
|
92
|
+
rows: z.array(KeyValueSchema).max(12).optional(),
|
|
93
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
94
|
+
});
|
|
95
|
+
const BookingListItem = z.object({
|
|
96
|
+
title: z.string().min(1).max(240),
|
|
97
|
+
subtitle: z.string().max(240).optional(),
|
|
98
|
+
date: z.string().max(80).optional(),
|
|
99
|
+
amount: z.string().max(80).optional(),
|
|
100
|
+
status: BadgeSchema.optional(),
|
|
101
|
+
action: CardActionSchema.optional(),
|
|
102
|
+
});
|
|
103
|
+
const BookingListCard = z.object({
|
|
104
|
+
kind: z.literal("bookingList"),
|
|
105
|
+
title: z.string().max(160).optional(),
|
|
106
|
+
total: z.number().int().nonnegative().optional(),
|
|
107
|
+
items: z.array(BookingListItem).max(50),
|
|
108
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
109
|
+
});
|
|
110
|
+
const PeopleListItem = z.object({
|
|
111
|
+
name: z.string().min(1).max(200),
|
|
112
|
+
subtitle: z.string().max(200).optional(),
|
|
113
|
+
email: z.string().max(320).optional(),
|
|
114
|
+
phone: z.string().max(80).optional(),
|
|
115
|
+
action: CardActionSchema.optional(),
|
|
116
|
+
});
|
|
117
|
+
const PeopleListCard = z.object({
|
|
118
|
+
kind: z.literal("peopleList"),
|
|
119
|
+
title: z.string().max(160).optional(),
|
|
120
|
+
total: z.number().int().nonnegative().optional(),
|
|
121
|
+
items: z.array(PeopleListItem).max(50),
|
|
122
|
+
});
|
|
123
|
+
const ProductCard = z.object({
|
|
124
|
+
kind: z.literal("product"),
|
|
125
|
+
title: z.string().min(1).max(240),
|
|
126
|
+
imageUrl: z.string().max(2000).optional(),
|
|
127
|
+
priceDisplay: z.string().max(80).optional(),
|
|
128
|
+
location: z.string().max(160).optional(),
|
|
129
|
+
summary: z.string().max(800).optional(),
|
|
130
|
+
badges: z.array(BadgeSchema).max(6).optional(),
|
|
131
|
+
rows: z.array(KeyValueSchema).max(10).optional(),
|
|
132
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
133
|
+
});
|
|
134
|
+
const ProductListItem = z.object({
|
|
135
|
+
title: z.string().min(1).max(240),
|
|
136
|
+
imageUrl: z.string().max(2000).optional(),
|
|
137
|
+
priceDisplay: z.string().max(80).optional(),
|
|
138
|
+
subtitle: z.string().max(200).optional(),
|
|
139
|
+
action: CardActionSchema.optional(),
|
|
140
|
+
});
|
|
141
|
+
const ProductListCard = z.object({
|
|
142
|
+
kind: z.literal("productList"),
|
|
143
|
+
title: z.string().max(160).optional(),
|
|
144
|
+
total: z.number().int().nonnegative().optional(),
|
|
145
|
+
items: z.array(ProductListItem).max(50),
|
|
146
|
+
});
|
|
147
|
+
const ItineraryDay = z.object({
|
|
148
|
+
number: z.number().int().min(0).max(366).optional(),
|
|
149
|
+
title: z.string().min(1).max(240),
|
|
150
|
+
description: z.string().max(2000).optional(),
|
|
151
|
+
imageUrl: z.string().max(2000).optional(),
|
|
152
|
+
});
|
|
153
|
+
const ItineraryCard = z.object({
|
|
154
|
+
kind: z.literal("itinerary"),
|
|
155
|
+
title: z.string().min(1).max(240),
|
|
156
|
+
dateRange: z.string().max(120).optional(),
|
|
157
|
+
heroImageUrl: z.string().max(2000).optional(),
|
|
158
|
+
status: BadgeSchema.optional(),
|
|
159
|
+
summary: z.string().max(2000).optional(),
|
|
160
|
+
facts: z.array(KeyValueSchema).max(12).optional(),
|
|
161
|
+
days: z.array(ItineraryDay).max(60),
|
|
162
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
163
|
+
});
|
|
164
|
+
const OfferCard = z.object({
|
|
165
|
+
kind: z.literal("offer"),
|
|
166
|
+
title: z.string().min(1).max(240),
|
|
167
|
+
imageUrl: z.string().max(2000).optional(),
|
|
168
|
+
priceDisplay: z.string().max(80).optional(),
|
|
169
|
+
summary: z.string().max(800).optional(),
|
|
170
|
+
link: z
|
|
171
|
+
.object({
|
|
172
|
+
label: z.string().min(1).max(160),
|
|
173
|
+
url: z.string().min(1).max(2000),
|
|
174
|
+
})
|
|
175
|
+
.optional(),
|
|
176
|
+
booked: z
|
|
177
|
+
.object({
|
|
178
|
+
label: z.string().min(1).max(200),
|
|
179
|
+
total: z.string().max(80).optional(),
|
|
180
|
+
})
|
|
181
|
+
.optional(),
|
|
182
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
183
|
+
});
|
|
184
|
+
const DepartureListItem = z.object({
|
|
185
|
+
date: z.string().min(1).max(120),
|
|
186
|
+
priceDisplay: z.string().max(80).optional(),
|
|
187
|
+
seats: z.string().max(80).optional(),
|
|
188
|
+
status: BadgeSchema.optional(),
|
|
189
|
+
action: CardActionSchema.optional(),
|
|
190
|
+
});
|
|
191
|
+
const DepartureListCard = z.object({
|
|
192
|
+
kind: z.literal("departureList"),
|
|
193
|
+
productTitle: z.string().max(240).optional(),
|
|
194
|
+
items: z.array(DepartureListItem).max(60),
|
|
195
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
196
|
+
});
|
|
197
|
+
const InvoiceCard = z.object({
|
|
198
|
+
kind: z.literal("invoice"),
|
|
199
|
+
number: z.string().min(1).max(120),
|
|
200
|
+
docType: z.string().max(60).optional(),
|
|
201
|
+
status: BadgeSchema.optional(),
|
|
202
|
+
total: z.string().max(80).optional(),
|
|
203
|
+
customer: z.string().max(200).optional(),
|
|
204
|
+
issuedDate: z.string().max(80).optional(),
|
|
205
|
+
dueDate: z.string().max(80).optional(),
|
|
206
|
+
rows: z.array(KeyValueSchema).max(12).optional(),
|
|
207
|
+
actions: z.array(CardActionSchema).max(4).optional(),
|
|
208
|
+
});
|
|
209
|
+
const InvoiceListItem = z.object({
|
|
210
|
+
number: z.string().min(1).max(120),
|
|
211
|
+
subtitle: z.string().max(200).optional(),
|
|
212
|
+
amount: z.string().max(80).optional(),
|
|
213
|
+
status: BadgeSchema.optional(),
|
|
214
|
+
date: z.string().max(80).optional(),
|
|
215
|
+
action: CardActionSchema.optional(),
|
|
216
|
+
});
|
|
217
|
+
const InvoiceListCard = z.object({
|
|
218
|
+
kind: z.literal("invoiceList"),
|
|
219
|
+
title: z.string().max(160).optional(),
|
|
220
|
+
total: z.number().int().nonnegative().optional(),
|
|
221
|
+
items: z.array(InvoiceListItem).max(50),
|
|
222
|
+
});
|
|
223
|
+
const ImageGridImage = z.object({
|
|
224
|
+
url: z.string().min(1).max(2000),
|
|
225
|
+
thumbUrl: z.string().max(2000).optional(),
|
|
226
|
+
alt: z.string().max(400).optional(),
|
|
227
|
+
credit: z.string().max(240).optional(),
|
|
228
|
+
link: z.string().max(2000).optional(),
|
|
229
|
+
});
|
|
230
|
+
const ImageGridCard = z.object({
|
|
231
|
+
kind: z.literal("imageGrid"),
|
|
232
|
+
title: z.string().max(160).optional(),
|
|
233
|
+
images: z.array(ImageGridImage).min(1).max(24),
|
|
234
|
+
note: z.string().max(400).optional(),
|
|
235
|
+
});
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Geo + weather + maps
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
const LatLngSchema = z.object({
|
|
240
|
+
lat: z.number().min(-90).max(90),
|
|
241
|
+
lng: z.number().min(-180).max(180),
|
|
242
|
+
});
|
|
243
|
+
/** Coarse condition buckets the weather widget maps to lucide icons (no fetch). */
|
|
244
|
+
export const WeatherIconSchema = z.enum([
|
|
245
|
+
"clear",
|
|
246
|
+
"partly-cloudy",
|
|
247
|
+
"cloudy",
|
|
248
|
+
"rain",
|
|
249
|
+
"drizzle",
|
|
250
|
+
"thunderstorm",
|
|
251
|
+
"snow",
|
|
252
|
+
"mist",
|
|
253
|
+
"wind",
|
|
254
|
+
]);
|
|
255
|
+
const WeatherHour = z.object({
|
|
256
|
+
time: z.string().min(1).max(40),
|
|
257
|
+
tempDisplay: z.string().min(1).max(20),
|
|
258
|
+
icon: WeatherIconSchema.optional(),
|
|
259
|
+
popDisplay: z.string().max(20).optional(),
|
|
260
|
+
});
|
|
261
|
+
const WeatherDay = z.object({
|
|
262
|
+
day: z.string().min(1).max(40),
|
|
263
|
+
hiDisplay: z.string().min(1).max(20),
|
|
264
|
+
loDisplay: z.string().max(20).optional(),
|
|
265
|
+
icon: WeatherIconSchema.optional(),
|
|
266
|
+
popDisplay: z.string().max(20).optional(),
|
|
267
|
+
});
|
|
268
|
+
const WeatherCard = z.object({
|
|
269
|
+
kind: z.literal("weather"),
|
|
270
|
+
location: z.string().min(1).max(160),
|
|
271
|
+
current: z.object({
|
|
272
|
+
tempDisplay: z.string().min(1).max(20),
|
|
273
|
+
feelsLikeDisplay: z.string().max(20).optional(),
|
|
274
|
+
description: z.string().max(120).optional(),
|
|
275
|
+
icon: WeatherIconSchema.optional(),
|
|
276
|
+
humidityDisplay: z.string().max(20).optional(),
|
|
277
|
+
windDisplay: z.string().max(40).optional(),
|
|
278
|
+
}),
|
|
279
|
+
hourly: z.array(WeatherHour).max(24).optional(),
|
|
280
|
+
daily: z.array(WeatherDay).max(8).optional(),
|
|
281
|
+
alerts: z
|
|
282
|
+
.array(z.object({
|
|
283
|
+
event: z.string().min(1).max(160),
|
|
284
|
+
detail: z.string().max(400).optional(),
|
|
285
|
+
}))
|
|
286
|
+
.max(4)
|
|
287
|
+
.optional(),
|
|
288
|
+
localTime: z.string().max(60).optional(),
|
|
289
|
+
});
|
|
290
|
+
const AirQualityCard = z.object({
|
|
291
|
+
kind: z.literal("airQuality"),
|
|
292
|
+
location: z.string().min(1).max(160),
|
|
293
|
+
aqi: z
|
|
294
|
+
.object({
|
|
295
|
+
label: z.string().min(1).max(80),
|
|
296
|
+
value: z.string().max(20).optional(),
|
|
297
|
+
color: z.string().max(9).optional(),
|
|
298
|
+
dominant: z.string().max(40).optional(),
|
|
299
|
+
})
|
|
300
|
+
.optional(),
|
|
301
|
+
pollen: z
|
|
302
|
+
.array(z.object({
|
|
303
|
+
type: z.string().min(1).max(60),
|
|
304
|
+
level: z.string().max(40).optional(),
|
|
305
|
+
}))
|
|
306
|
+
.max(6)
|
|
307
|
+
.optional(),
|
|
308
|
+
coord: LatLngSchema.optional(),
|
|
309
|
+
});
|
|
310
|
+
const AddressCheckCard = z.object({
|
|
311
|
+
kind: z.literal("addressCheck"),
|
|
312
|
+
formattedAddress: z.string().max(400).optional(),
|
|
313
|
+
status: BadgeSchema,
|
|
314
|
+
unconfirmed: z.array(z.string().min(1).max(120)).max(12).optional(),
|
|
315
|
+
});
|
|
316
|
+
const MapMarker = z.object({
|
|
317
|
+
lat: z.number().min(-90).max(90),
|
|
318
|
+
lng: z.number().min(-180).max(180),
|
|
319
|
+
label: z.string().max(40).optional(),
|
|
320
|
+
title: z.string().max(160).optional(),
|
|
321
|
+
});
|
|
322
|
+
const ItineraryStop = z.object({
|
|
323
|
+
time: z.string().max(40).optional(),
|
|
324
|
+
title: z.string().min(1).max(160),
|
|
325
|
+
description: z.string().max(800).optional(),
|
|
326
|
+
location: z.string().max(160).optional(),
|
|
327
|
+
link: z
|
|
328
|
+
.object({
|
|
329
|
+
label: z.string().min(1).max(120),
|
|
330
|
+
url: z.string().min(1).max(2000),
|
|
331
|
+
})
|
|
332
|
+
.optional(),
|
|
333
|
+
/** Map-marker number for this stop, when it has a resolved location. */
|
|
334
|
+
marker: z.number().int().min(1).max(30).optional(),
|
|
335
|
+
});
|
|
336
|
+
/** A composed day / multi-stop itinerary plan with a map (`itinerary_plan`). */
|
|
337
|
+
const ItineraryPlanCard = z.object({
|
|
338
|
+
kind: z.literal("itineraryPlan"),
|
|
339
|
+
title: z.string().min(1).max(200),
|
|
340
|
+
subtitle: z.string().max(600).optional(),
|
|
341
|
+
stops: z.array(ItineraryStop).min(1).max(20),
|
|
342
|
+
map: z
|
|
343
|
+
.object({
|
|
344
|
+
center: LatLngSchema,
|
|
345
|
+
markers: z.array(MapMarker).max(20),
|
|
346
|
+
})
|
|
347
|
+
.optional(),
|
|
348
|
+
});
|
|
349
|
+
const MapCard = z.object({
|
|
350
|
+
kind: z.literal("map"),
|
|
351
|
+
center: LatLngSchema,
|
|
352
|
+
zoom: z.number().int().min(1).max(20).optional(),
|
|
353
|
+
markers: z.array(MapMarker).max(25).optional(),
|
|
354
|
+
label: z.string().max(160).optional(),
|
|
355
|
+
});
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Dynamic blocks — the long-tail composer (`present_view`)
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Limits are deliberately tight: a `present_view` result is echoed straight to
|
|
360
|
+
// the model on the next step (it bypasses the upstream tool-result size cap
|
|
361
|
+
// because the card must reach the UI intact), so bounding the worst case here
|
|
362
|
+
// keeps the next request from blowing context / body limits.
|
|
363
|
+
export const BlockSchema = z.discriminatedUnion("type", [
|
|
364
|
+
z.object({
|
|
365
|
+
type: z.literal("heading"),
|
|
366
|
+
text: z.string().min(1).max(160),
|
|
367
|
+
level: z.number().int().min(1).max(3).optional(),
|
|
368
|
+
}),
|
|
369
|
+
z.object({
|
|
370
|
+
type: z.literal("text"),
|
|
371
|
+
text: z.string().min(1).max(1000),
|
|
372
|
+
muted: z.boolean().optional(),
|
|
373
|
+
}),
|
|
374
|
+
z.object({
|
|
375
|
+
type: z.literal("statTiles"),
|
|
376
|
+
tiles: z.array(StatTileSchema).min(1).max(6),
|
|
377
|
+
}),
|
|
378
|
+
z.object({
|
|
379
|
+
type: z.literal("keyValues"),
|
|
380
|
+
items: z.array(KeyValueSchema).min(1).max(16),
|
|
381
|
+
}),
|
|
382
|
+
z.object({
|
|
383
|
+
type: z.literal("badges"),
|
|
384
|
+
items: z.array(BadgeSchema).min(1).max(10),
|
|
385
|
+
}),
|
|
386
|
+
z.object({
|
|
387
|
+
type: z.literal("image"),
|
|
388
|
+
url: z.string().min(1).max(2000),
|
|
389
|
+
alt: z.string().max(240).optional(),
|
|
390
|
+
caption: z.string().max(240).optional(),
|
|
391
|
+
}),
|
|
392
|
+
z.object({
|
|
393
|
+
type: z.literal("timeline"),
|
|
394
|
+
items: z.array(TimelineItemSchema).min(1).max(16),
|
|
395
|
+
}),
|
|
396
|
+
z.object({
|
|
397
|
+
type: z.literal("table"),
|
|
398
|
+
columns: z.array(z.string().max(80)).min(1).max(6),
|
|
399
|
+
rows: z.array(z.array(z.string().max(160)).max(6)).max(24),
|
|
400
|
+
}),
|
|
401
|
+
z.object({
|
|
402
|
+
type: z.literal("list"),
|
|
403
|
+
ordered: z.boolean().optional(),
|
|
404
|
+
items: z.array(z.string().min(1).max(300)).min(1).max(30),
|
|
405
|
+
}),
|
|
406
|
+
z.object({ type: z.literal("divider") }),
|
|
407
|
+
z.object({
|
|
408
|
+
type: z.literal("button"),
|
|
409
|
+
action: CardActionSchema,
|
|
410
|
+
}),
|
|
411
|
+
]);
|
|
412
|
+
export const DynamicCardSchema = z.object({
|
|
413
|
+
kind: z.literal("dynamic"),
|
|
414
|
+
title: z.string().max(200).optional(),
|
|
415
|
+
blocks: z.array(BlockSchema).min(1).max(24),
|
|
416
|
+
});
|
|
417
|
+
/** Input schema for the `present_view` tool (the dynamic card without `kind`). */
|
|
418
|
+
export const PresentViewInputSchema = z.object({
|
|
419
|
+
title: z.string().max(200).optional(),
|
|
420
|
+
blocks: z.array(BlockSchema).min(1).max(24),
|
|
421
|
+
});
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Union + helpers
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
export const AgentCardSchema = z.discriminatedUnion("kind", [
|
|
426
|
+
CustomerCard,
|
|
427
|
+
BookingCard,
|
|
428
|
+
BookingListCard,
|
|
429
|
+
PeopleListCard,
|
|
430
|
+
ProductCard,
|
|
431
|
+
ProductListCard,
|
|
432
|
+
ItineraryCard,
|
|
433
|
+
OfferCard,
|
|
434
|
+
DepartureListCard,
|
|
435
|
+
InvoiceCard,
|
|
436
|
+
InvoiceListCard,
|
|
437
|
+
ImageGridCard,
|
|
438
|
+
WeatherCard,
|
|
439
|
+
AirQualityCard,
|
|
440
|
+
MapCard,
|
|
441
|
+
ItineraryPlanCard,
|
|
442
|
+
AddressCheckCard,
|
|
443
|
+
DynamicCardSchema,
|
|
444
|
+
]);
|
|
445
|
+
/** Validate a standalone card object. Returns null when it doesn't match. */
|
|
446
|
+
export function parseCard(value) {
|
|
447
|
+
const result = AgentCardSchema.safeParse(value);
|
|
448
|
+
return result.success ? result.data : null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Pull a card off a tool output (`{ ...result, card }`) and validate it.
|
|
452
|
+
* The frontend calls this on `part.output`; returns null for outputs with no
|
|
453
|
+
* (or a malformed) card so the caller falls back to the plain status row.
|
|
454
|
+
*/
|
|
455
|
+
export function extractCard(toolOutput) {
|
|
456
|
+
if (!toolOutput || typeof toolOutput !== "object")
|
|
457
|
+
return null;
|
|
458
|
+
const card = toolOutput.card;
|
|
459
|
+
if (card === undefined)
|
|
460
|
+
return null;
|
|
461
|
+
return parseCard(card);
|
|
462
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { MaxTool, ToolConfig, ToolFileResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Define a custom Max tool: a name + description the model reasons over, a Zod
|
|
5
|
+
* schema for its arguments, and a handler that does the work. The schema is the
|
|
6
|
+
* single source of truth — it generates the JSON Schema in the manifest and
|
|
7
|
+
* validates incoming args at call time.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { defineTool } from "@voyant-travel/max-sdk"
|
|
12
|
+
* import { z } from "zod"
|
|
13
|
+
*
|
|
14
|
+
* export const lookupBooking = defineTool({
|
|
15
|
+
* name: "acme_lookup_booking",
|
|
16
|
+
* description: "Look up a booking by its reference.",
|
|
17
|
+
* tier: "read",
|
|
18
|
+
* input: z.object({ reference: z.string().describe("Booking reference, e.g. AC-1234") }),
|
|
19
|
+
* handler: async ({ reference }, ctx) => {
|
|
20
|
+
* const booking = await db.bookings.find(reference, ctx.organizationId)
|
|
21
|
+
* return booking ?? { notFound: true }
|
|
22
|
+
* },
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function defineTool<TSchema extends z.ZodType>(config: ToolConfig<TSchema>): MaxTool<TSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Build a file result to return from a handler whose tool sets
|
|
29
|
+
* `outputKind: "file"`. Voyant renders it as a downloadable attachment.
|
|
30
|
+
*/
|
|
31
|
+
export declare function file(input: Omit<ToolFileResult, "type">): ToolFileResult;
|
|
32
|
+
//# sourceMappingURL=define-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-tool.d.ts","sourceRoot":"","sources":["../src/define-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAIrE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,EAClD,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,GAC1B,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,cAAc,CAExE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const TOOL_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
2
|
+
/**
|
|
3
|
+
* Define a custom Max tool: a name + description the model reasons over, a Zod
|
|
4
|
+
* schema for its arguments, and a handler that does the work. The schema is the
|
|
5
|
+
* single source of truth — it generates the JSON Schema in the manifest and
|
|
6
|
+
* validates incoming args at call time.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineTool } from "@voyant-travel/max-sdk"
|
|
11
|
+
* import { z } from "zod"
|
|
12
|
+
*
|
|
13
|
+
* export const lookupBooking = defineTool({
|
|
14
|
+
* name: "acme_lookup_booking",
|
|
15
|
+
* description: "Look up a booking by its reference.",
|
|
16
|
+
* tier: "read",
|
|
17
|
+
* input: z.object({ reference: z.string().describe("Booking reference, e.g. AC-1234") }),
|
|
18
|
+
* handler: async ({ reference }, ctx) => {
|
|
19
|
+
* const booking = await db.bookings.find(reference, ctx.organizationId)
|
|
20
|
+
* return booking ?? { notFound: true }
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function defineTool(config) {
|
|
26
|
+
if (!TOOL_NAME_RE.test(config.name)) {
|
|
27
|
+
throw new Error(`Invalid Max tool name "${config.name}": use only letters, digits, "_" or "-".`);
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a file result to return from a handler whose tool sets
|
|
33
|
+
* `outputKind: "file"`. Voyant renders it as a downloadable attachment.
|
|
34
|
+
*/
|
|
35
|
+
export function file(input) {
|
|
36
|
+
return { type: "file", ...input };
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MaxTool } from "./types.js";
|
|
2
|
+
export type CreateHandlerOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Shared secret Voyant presents (matched against a `Bearer <token>`
|
|
5
|
+
* Authorization header). The simplest way to authenticate calls.
|
|
6
|
+
*/
|
|
7
|
+
authToken?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Full control over auth. Return `false` (or throw) to reject the request
|
|
10
|
+
* with 401. Runs after the `authToken` check when both are set.
|
|
11
|
+
*/
|
|
12
|
+
verifyRequest?: (request: Request) => boolean | Promise<boolean>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build a framework-agnostic request handler for your tools. It speaks the Web
|
|
16
|
+
* Fetch `Request`/`Response` API, so it drops into Cloudflare Workers, Hono,
|
|
17
|
+
* Next.js route handlers, Deno, Bun, or anything with an adapter.
|
|
18
|
+
*
|
|
19
|
+
* Mount it so it receives `POST /v1/max/tools/:name/call`. It authenticates the
|
|
20
|
+
* request, validates the args against the tool's schema, runs the handler, and
|
|
21
|
+
* returns the result as JSON.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const handler = createMaxToolsHandler([lookupBooking, createQuote], {
|
|
26
|
+
* authToken: process.env.MAX_TOOLS_SECRET,
|
|
27
|
+
* })
|
|
28
|
+
* export default { fetch: handler } // Cloudflare Worker
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function createMaxToolsHandler(tools: readonly MaxTool[], options?: CreateHandlerOptions): (request: Request) => Promise<Response>;
|
|
32
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmB,MAAM,YAAY,CAAA;AAE1D,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACjE,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,GAAE,oBAAyB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA8DzC"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a framework-agnostic request handler for your tools. It speaks the Web
|
|
3
|
+
* Fetch `Request`/`Response` API, so it drops into Cloudflare Workers, Hono,
|
|
4
|
+
* Next.js route handlers, Deno, Bun, or anything with an adapter.
|
|
5
|
+
*
|
|
6
|
+
* Mount it so it receives `POST /v1/max/tools/:name/call`. It authenticates the
|
|
7
|
+
* request, validates the args against the tool's schema, runs the handler, and
|
|
8
|
+
* returns the result as JSON.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const handler = createMaxToolsHandler([lookupBooking, createQuote], {
|
|
13
|
+
* authToken: process.env.MAX_TOOLS_SECRET,
|
|
14
|
+
* })
|
|
15
|
+
* export default { fetch: handler } // Cloudflare Worker
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function createMaxToolsHandler(tools, options = {}) {
|
|
19
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
20
|
+
return async (request) => {
|
|
21
|
+
if (request.method !== "POST") {
|
|
22
|
+
return json({ error: "method_not_allowed" }, 405);
|
|
23
|
+
}
|
|
24
|
+
if (options.authToken) {
|
|
25
|
+
const auth = request.headers.get("authorization") ?? "";
|
|
26
|
+
if (auth !== `Bearer ${options.authToken}`) {
|
|
27
|
+
return json({ error: "unauthorized" }, 401);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (options.verifyRequest) {
|
|
31
|
+
let ok = false;
|
|
32
|
+
try {
|
|
33
|
+
ok = await options.verifyRequest(request);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
ok = false;
|
|
37
|
+
}
|
|
38
|
+
if (!ok)
|
|
39
|
+
return json({ error: "unauthorized" }, 401);
|
|
40
|
+
}
|
|
41
|
+
let body;
|
|
42
|
+
try {
|
|
43
|
+
body = await request.json();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return json({ error: "invalid_json" }, 400);
|
|
47
|
+
}
|
|
48
|
+
const toolName = toolNameFromPath(request.url) ?? readString(body, "toolName");
|
|
49
|
+
if (!toolName)
|
|
50
|
+
return json({ error: "missing_tool_name" }, 400);
|
|
51
|
+
const tool = byName.get(toolName);
|
|
52
|
+
if (!tool)
|
|
53
|
+
return json({ error: "unknown_tool", toolName }, 404);
|
|
54
|
+
const parsed = tool.input.safeParse(readArgs(body));
|
|
55
|
+
if (!parsed.success) {
|
|
56
|
+
return json({ error: "invalid_args", issues: parsed.error.issues }, 422);
|
|
57
|
+
}
|
|
58
|
+
const ctx = {
|
|
59
|
+
toolName,
|
|
60
|
+
operatorId: readContext(body, "operatorId"),
|
|
61
|
+
organizationId: readContext(body, "organizationId"),
|
|
62
|
+
userId: readContext(body, "userId"),
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const result = await tool.handler(parsed.data, ctx);
|
|
66
|
+
return json(result ?? {}, 200);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return json({
|
|
70
|
+
error: "tool_failed",
|
|
71
|
+
message: error instanceof Error ? error.message : String(error),
|
|
72
|
+
}, 500);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function toolNameFromPath(url) {
|
|
77
|
+
try {
|
|
78
|
+
const match = new URL(url).pathname.match(/\/tools\/([^/]+)\/call\/?$/);
|
|
79
|
+
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
function readString(body, key) {
|
|
89
|
+
if (!isRecord(body))
|
|
90
|
+
return null;
|
|
91
|
+
const value = body[key];
|
|
92
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
93
|
+
}
|
|
94
|
+
function readArgs(body) {
|
|
95
|
+
return isRecord(body) ? (body.args ?? {}) : {};
|
|
96
|
+
}
|
|
97
|
+
function readContext(body, key) {
|
|
98
|
+
if (!isRecord(body) || !isRecord(body.context))
|
|
99
|
+
return "";
|
|
100
|
+
const value = body.context[key];
|
|
101
|
+
return typeof value === "string" ? value : "";
|
|
102
|
+
}
|
|
103
|
+
function json(data, status) {
|
|
104
|
+
return new Response(JSON.stringify(data), {
|
|
105
|
+
status,
|
|
106
|
+
headers: { "content-type": "application/json" },
|
|
107
|
+
});
|
|
108
|
+
}
|