@agent-cards/commerce-sdk 0.8.0 → 0.10.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 +117 -8
- package/dist/ack.d.ts +44 -2
- package/dist/ack.js +219 -10
- package/dist/adapters.d.ts +3 -3
- package/dist/adapters.js +2 -1
- package/dist/buy-tool.d.ts +190 -13
- package/dist/buy-tool.js +544 -91
- package/dist/client.d.ts +71 -3
- package/dist/client.js +79 -1
- package/dist/definition.d.ts +14 -6
- package/dist/definition.js +31 -12
- package/dist/handler.d.ts +6 -0
- package/dist/handler.js +4 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/listen.d.ts +2 -0
- package/dist/listen.js +4 -1
- package/dist/messages.d.ts +33 -2
- package/dist/messages.js +82 -5
- package/dist/model-view.d.ts +48 -3
- package/dist/model-view.js +40 -3
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AgentcardListener, type ListenOptions } from './listen.js';
|
|
2
|
+
import type { ShopperMessage } from './shopper-message.js';
|
|
2
3
|
/** The fetch the client calls: Node 18 and later have one built in. */
|
|
3
4
|
export type FetchLike = (url: string, init: {
|
|
4
5
|
method: string;
|
|
@@ -94,10 +95,14 @@ export interface BuyRequest {
|
|
|
94
95
|
new_order?: true;
|
|
95
96
|
/**
|
|
96
97
|
* The product the shopper picked from the last search's `catalog.items`, by its `id` or its `ref`:
|
|
97
|
-
* Agentcard adds exactly that item, with no new search.
|
|
98
|
+
* Agentcard adds exactly that item, with no new search. Or the store the shopper picked from the last
|
|
99
|
+
* answer's `stores.items`, by its `id`: Agentcard selects exactly that store before the turn runs.
|
|
100
|
+
* Goes with an `ask`, never with `confirm` or `new_order`.
|
|
98
101
|
*/
|
|
99
102
|
pick?: {
|
|
100
103
|
product_id: string;
|
|
104
|
+
} | {
|
|
105
|
+
store_id: string;
|
|
101
106
|
};
|
|
102
107
|
payment_source?: 'vault';
|
|
103
108
|
delivery_address?: BuyDeliveryAddress;
|
|
@@ -121,14 +126,28 @@ export interface BuyCartLine {
|
|
|
121
126
|
* A cart as Agentcard answers it. `totalCents` is the all-in figure the shopper sees; `hash` is what a confirm names.
|
|
122
127
|
* Every amount is in the minor unit of `currency`, the store's ISO 4217 code.
|
|
123
128
|
*/
|
|
129
|
+
/** One of the store's own fee or tax lines, as the store labels it: `Delivery Fee`, `Service Fee`, `Estimated Tax`. */
|
|
130
|
+
export interface BuyCartFeeLine {
|
|
131
|
+
label: string;
|
|
132
|
+
amountCents: number;
|
|
133
|
+
}
|
|
124
134
|
export interface BuyCart {
|
|
125
135
|
merchant: string;
|
|
126
136
|
merchant_name: string;
|
|
127
137
|
store: BuyStore | null;
|
|
128
138
|
items: BuyCartLine[];
|
|
139
|
+
/** The items alone, or null from an older API. */
|
|
140
|
+
subtotalCents: number | null;
|
|
129
141
|
serviceFeesCents: number;
|
|
142
|
+
/** The store's own fee and tax lines inside `serviceFeesCents`, or null when the store itemized none. */
|
|
143
|
+
merchantFeeLines: BuyCartFeeLine[] | null;
|
|
130
144
|
tipCents: number;
|
|
131
145
|
totalCents: number;
|
|
146
|
+
/**
|
|
147
|
+
* True when the store has not priced the cart's fees and tax yet, so `totalCents` covers the items and
|
|
148
|
+
* known fees only: show it as an estimate. DoorDash prices a cart once the shopper's own account holds it.
|
|
149
|
+
*/
|
|
150
|
+
totalIsEstimate: boolean;
|
|
132
151
|
/** The ISO 4217 code the store prices the cart in, or null when Agentcard sent none. */
|
|
133
152
|
currency: string | null;
|
|
134
153
|
approvedCeilingCents: number | null;
|
|
@@ -177,6 +196,32 @@ export interface BuyPick {
|
|
|
177
196
|
ref: string | null;
|
|
178
197
|
in_cart: boolean;
|
|
179
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* One store a delivery store search found that the answer's reply names. `id` holds no link and is what
|
|
201
|
+
* `pick.store_id` takes; `store_page_url` is the store's page at the merchant. `eta_minutes` is the
|
|
202
|
+
* merchant's delivery estimate, or null when it showed none. `image_url` is the picture the store's
|
|
203
|
+
* card shows at the merchant, or null when it shows none.
|
|
204
|
+
*/
|
|
205
|
+
export interface BuyStoreChoice {
|
|
206
|
+
id: string;
|
|
207
|
+
name: string;
|
|
208
|
+
store_page_url: string | null;
|
|
209
|
+
eta_minutes: number | null;
|
|
210
|
+
image_url: string | null;
|
|
211
|
+
}
|
|
212
|
+
/** The stores an answer's reply presents to choose from, at most three, in the order the reply names them. */
|
|
213
|
+
export interface BuyStores {
|
|
214
|
+
merchant: string;
|
|
215
|
+
merchant_name: string;
|
|
216
|
+
items: BuyStoreChoice[];
|
|
217
|
+
as_of: string | null;
|
|
218
|
+
}
|
|
219
|
+
/** The store a store pick named, and whether Agentcard selected it before the turn ran. */
|
|
220
|
+
export interface BuyStorePick {
|
|
221
|
+
store_id: string;
|
|
222
|
+
name: string | null;
|
|
223
|
+
selected: boolean;
|
|
224
|
+
}
|
|
180
225
|
export interface BuyPaymentSource {
|
|
181
226
|
source: string;
|
|
182
227
|
brand: string | null;
|
|
@@ -185,11 +230,12 @@ export interface BuyPaymentSource {
|
|
|
185
230
|
/**
|
|
186
231
|
* The one thing the shopper does next, as Agentcard names it, in the order a purchase asks for them:
|
|
187
232
|
* `shipping_details` (a shipping address and phone), `guest_details` (a name, email and phone for a
|
|
188
|
-
* booking), `
|
|
233
|
+
* booking), `store_login` (a login to their own account at a store that checks out only on it, with
|
|
234
|
+
* the `store_login` link), `add_card` (a card in their Vault), `confirm` (their yes to the cart), `verification_code`
|
|
189
235
|
* (the code a booking texted them), then `approve` (the approval link). A value this version does
|
|
190
236
|
* not know reads as null.
|
|
191
237
|
*/
|
|
192
|
-
export type BuyNextAction = 'shipping_details' | 'guest_details' | 'add_card' | 'confirm' | 'verification_code' | 'approve';
|
|
238
|
+
export type BuyNextAction = 'shipping_details' | 'guest_details' | 'store_login' | 'add_card' | 'confirm' | 'verification_code' | 'approve';
|
|
193
239
|
/** One turn's answer, every field read for its type; a field an older API does not send reads as null. */
|
|
194
240
|
export interface BuyTurn {
|
|
195
241
|
conversation_id: string;
|
|
@@ -223,6 +269,10 @@ export interface BuyTurn {
|
|
|
223
269
|
store_login_merchant_name: string | null;
|
|
224
270
|
/** The product this turn's `pick` named, and whether the cart holds it; null for a turn without one. */
|
|
225
271
|
pick: BuyPick | null;
|
|
272
|
+
/** The stores this answer's reply presents to choose from; null when it presents none, and on an older API. */
|
|
273
|
+
stores: BuyStores | null;
|
|
274
|
+
/** The store this turn's `pick.store_id` named, or an answer such as `the second one` picked; null without one. */
|
|
275
|
+
store_pick: BuyStorePick | null;
|
|
226
276
|
/** The answer as Agentcard sent it. */
|
|
227
277
|
raw: Record<string, unknown>;
|
|
228
278
|
}
|
|
@@ -301,6 +351,9 @@ export declare class AgentcardClient {
|
|
|
301
351
|
private token;
|
|
302
352
|
private inflight;
|
|
303
353
|
private learnedSandbox;
|
|
354
|
+
/** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
|
|
355
|
+
private readonly inbox;
|
|
356
|
+
private readonly deliveredIds;
|
|
304
357
|
constructor(options: AgentcardClientOptions);
|
|
305
358
|
/** True when the client buys for many shoppers with a company's credentials; false under one shopper's own token. */
|
|
306
359
|
get company(): boolean;
|
|
@@ -333,6 +386,21 @@ export declare class AgentcardClient {
|
|
|
333
386
|
* webhook endpoint: `listenToAgentcard(this, opts)`. Takes your company credentials.
|
|
334
387
|
*/
|
|
335
388
|
listen(opts: ListenOptions): AgentcardListener;
|
|
389
|
+
/**
|
|
390
|
+
* Notes that `message` reached the chat of the shopper you call `externalUserId`, so that shopper's
|
|
391
|
+
* next `buy.turn` hands it to your model (`turn.messages`), until a history the model ran with holds it. `listen` and `agentcardWebhook({ agentcard })`
|
|
392
|
+
* call it once `onMessage` returns, so call it yourself only for a message you delivered another way.
|
|
393
|
+
* A message is kept once per event id. The messages live in this process's memory, at most 20 per
|
|
394
|
+
* shopper: the listener and the buy tool share one client in one process.
|
|
395
|
+
*/
|
|
396
|
+
delivered(externalUserId: string, message: ShopperMessage): void;
|
|
397
|
+
/**
|
|
398
|
+
* The messages kept for this shopper, oldest first: each one that reached their chat and that no
|
|
399
|
+
* history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
|
|
400
|
+
*/
|
|
401
|
+
keptMessages(externalUserId: string): ShopperMessage[];
|
|
402
|
+
/** Forgets these messages for this shopper: `turn.messages` calls it once the history it reads holds them. */
|
|
403
|
+
forgetMessages(externalUserId: string, messages: readonly ShopperMessage[]): void;
|
|
336
404
|
/** A company names the shopper exactly once; a shopper's own token already names them. */
|
|
337
405
|
private checkShopper;
|
|
338
406
|
private call;
|
package/dist/client.js
CHANGED
|
@@ -18,7 +18,13 @@ export const BUY_TURN_TIMEOUT_MS = 130_000;
|
|
|
18
18
|
const CALL_TIMEOUT_MS = 30_000;
|
|
19
19
|
/** A token is renewed this long before it expires, so it never expires mid-call. */
|
|
20
20
|
const RENEW_EARLY_MS = 60_000;
|
|
21
|
-
|
|
21
|
+
/** The messages a shopper's chat got that the client keeps for that shopper's next turn, the oldest dropped first. */
|
|
22
|
+
const DELIVERED_PER_SHOPPER = 20;
|
|
23
|
+
/** The shoppers the client keeps such messages for, the one written to longest ago dropped first. */
|
|
24
|
+
const DELIVERED_SHOPPERS = 10_000;
|
|
25
|
+
/** The event ids the client remembers taking in, so a message delivered twice reaches the model once. */
|
|
26
|
+
const DELIVERED_IDS_KEPT = 10_000;
|
|
27
|
+
const NEXT_ACTIONS = new Set(['shipping_details', 'guest_details', 'store_login', 'add_card', 'confirm', 'verification_code', 'approve']);
|
|
22
28
|
/**
|
|
23
29
|
* A refusal, or a failure to reach Agentcard. `code` is the API's own
|
|
24
30
|
* (`user_not_connected`, `external_user_id_ambiguous`, `turn_in_progress`,
|
|
@@ -75,6 +81,12 @@ function storeOf(v) {
|
|
|
75
81
|
return null;
|
|
76
82
|
return { id: v.id, ...(str(v.name) ? { name: v.name } : {}) };
|
|
77
83
|
}
|
|
84
|
+
function feeLinesOf(v) {
|
|
85
|
+
if (!Array.isArray(v))
|
|
86
|
+
return null;
|
|
87
|
+
const lines = v.filter(isRecord).flatMap((f) => (str(f.label) && num(f.amountCents) !== null ? [{ label: f.label, amountCents: f.amountCents }] : []));
|
|
88
|
+
return lines.length ? lines : null;
|
|
89
|
+
}
|
|
78
90
|
function cartOf(v) {
|
|
79
91
|
if (!isRecord(v) || !str(v.hash) || !str(v.merchant))
|
|
80
92
|
return null;
|
|
@@ -89,9 +101,12 @@ function cartOf(v) {
|
|
|
89
101
|
product_id: str(i.product_id),
|
|
90
102
|
url: pageUrl(i.url),
|
|
91
103
|
})) : [],
|
|
104
|
+
subtotalCents: num(v.subtotalCents),
|
|
92
105
|
serviceFeesCents: num(v.serviceFeesCents) ?? 0,
|
|
106
|
+
merchantFeeLines: feeLinesOf(v.merchantFeeLines),
|
|
93
107
|
tipCents: num(v.tipCents) ?? 0,
|
|
94
108
|
totalCents: num(v.totalCents) ?? 0,
|
|
109
|
+
totalIsEstimate: v.totalIsEstimate === true,
|
|
95
110
|
currency: str(v.currency),
|
|
96
111
|
approvedCeilingCents: num(v.approvedCeilingCents),
|
|
97
112
|
hash: v.hash,
|
|
@@ -132,6 +147,18 @@ function catalogOf(v) {
|
|
|
132
147
|
}) : [];
|
|
133
148
|
return { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items, presented: v.presented === true };
|
|
134
149
|
}
|
|
150
|
+
/** The stores an answer presents: each one with an id and a name, and no block when none has both. */
|
|
151
|
+
function storesOf(v) {
|
|
152
|
+
if (!isRecord(v) || !Array.isArray(v.items))
|
|
153
|
+
return null;
|
|
154
|
+
const items = v.items.filter(isRecord).flatMap((i) => {
|
|
155
|
+
const id = str(i.id);
|
|
156
|
+
const name = str(i.name);
|
|
157
|
+
const eta = num(i.eta_minutes);
|
|
158
|
+
return id && name ? [{ id, name, store_page_url: pageUrl(i.store_page_url), eta_minutes: eta, image_url: str(i.image_url) }] : [];
|
|
159
|
+
});
|
|
160
|
+
return items.length ? { merchant: str(v.merchant) ?? '', merchant_name: str(v.merchant_name) ?? str(v.merchant) ?? '', items, as_of: str(v.as_of) } : null;
|
|
161
|
+
}
|
|
135
162
|
function paymentSourceOf(v) {
|
|
136
163
|
return isRecord(v) && str(v.source) ? { source: v.source, brand: str(v.brand), last4: str(v.last4) } : null;
|
|
137
164
|
}
|
|
@@ -165,6 +192,8 @@ export function turnOf(body) {
|
|
|
165
192
|
store_login_merchant_name: str(body.store_login_merchant_name),
|
|
166
193
|
next_action: typeof body.next_action === 'string' && NEXT_ACTIONS.has(body.next_action) ? body.next_action : null,
|
|
167
194
|
pick: isRecord(body.pick) && str(body.pick.product_id) ? { product_id: body.pick.product_id, ref: str(body.pick.ref), in_cart: body.pick.in_cart === true } : null,
|
|
195
|
+
stores: storesOf(body.stores),
|
|
196
|
+
store_pick: isRecord(body.store_pick) && str(body.store_pick.store_id) ? { store_id: body.store_pick.store_id, name: str(body.store_pick.name), selected: body.store_pick.selected === true } : null,
|
|
168
197
|
raw: body,
|
|
169
198
|
};
|
|
170
199
|
}
|
|
@@ -366,6 +395,9 @@ export class AgentcardClient {
|
|
|
366
395
|
token = null;
|
|
367
396
|
inflight = null;
|
|
368
397
|
learnedSandbox = null;
|
|
398
|
+
/** Each shopper's messages that no history the model ran with holds yet, under your own id for them. */
|
|
399
|
+
inbox = new Map();
|
|
400
|
+
deliveredIds = new Set();
|
|
369
401
|
constructor(options) {
|
|
370
402
|
const o = (options ?? {});
|
|
371
403
|
const hasCredentials = o.clientId !== undefined || o.clientSecret !== undefined;
|
|
@@ -456,6 +488,52 @@ export class AgentcardClient {
|
|
|
456
488
|
listen(opts) {
|
|
457
489
|
return listenToAgentcard(this, opts);
|
|
458
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Notes that `message` reached the chat of the shopper you call `externalUserId`, so that shopper's
|
|
493
|
+
* next `buy.turn` hands it to your model (`turn.messages`), until a history the model ran with holds it. `listen` and `agentcardWebhook({ agentcard })`
|
|
494
|
+
* call it once `onMessage` returns, so call it yourself only for a message you delivered another way.
|
|
495
|
+
* A message is kept once per event id. The messages live in this process's memory, at most 20 per
|
|
496
|
+
* shopper: the listener and the buy tool share one client in one process.
|
|
497
|
+
*/
|
|
498
|
+
delivered(externalUserId, message) {
|
|
499
|
+
if (!str(externalUserId))
|
|
500
|
+
throw new TypeError('Pass externalUserId: your own id for the shopper whose chat got the message.');
|
|
501
|
+
if (!isRecord(message) || typeof message.text !== 'string')
|
|
502
|
+
throw new TypeError('Pass the message onMessage got, with its text.');
|
|
503
|
+
if (message.text.trim() === '')
|
|
504
|
+
return;
|
|
505
|
+
if (str(message.eventId)) {
|
|
506
|
+
if (this.deliveredIds.has(message.eventId))
|
|
507
|
+
return;
|
|
508
|
+
this.deliveredIds.add(message.eventId);
|
|
509
|
+
if (this.deliveredIds.size > DELIVERED_IDS_KEPT)
|
|
510
|
+
this.deliveredIds.delete(this.deliveredIds.values().next().value);
|
|
511
|
+
}
|
|
512
|
+
const kept = this.inbox.get(externalUserId) ?? [];
|
|
513
|
+
// The shopper written to last moves to the end, so the one dropped first is the one written to longest ago.
|
|
514
|
+
this.inbox.delete(externalUserId);
|
|
515
|
+
this.inbox.set(externalUserId, [...kept, message].slice(-DELIVERED_PER_SHOPPER));
|
|
516
|
+
if (this.inbox.size > DELIVERED_SHOPPERS)
|
|
517
|
+
this.inbox.delete(this.inbox.keys().next().value);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* The messages kept for this shopper, oldest first: each one that reached their chat and that no
|
|
521
|
+
* history `turn.messages` read has held yet. `buy.turn` reads them when it opens.
|
|
522
|
+
*/
|
|
523
|
+
keptMessages(externalUserId) {
|
|
524
|
+
return [...(this.inbox.get(externalUserId) ?? [])];
|
|
525
|
+
}
|
|
526
|
+
/** Forgets these messages for this shopper: `turn.messages` calls it once the history it reads holds them. */
|
|
527
|
+
forgetMessages(externalUserId, messages) {
|
|
528
|
+
const kept = this.inbox.get(externalUserId);
|
|
529
|
+
if (!kept)
|
|
530
|
+
return;
|
|
531
|
+
const left = kept.filter((m) => !messages.includes(m));
|
|
532
|
+
if (left.length > 0)
|
|
533
|
+
this.inbox.set(externalUserId, left);
|
|
534
|
+
else
|
|
535
|
+
this.inbox.delete(externalUserId);
|
|
536
|
+
}
|
|
459
537
|
/** A company names the shopper exactly once; a shopper's own token already names them. */
|
|
460
538
|
checkShopper(who) {
|
|
461
539
|
const named = [who.externalUserId, who.userId].filter((v) => v !== undefined);
|
package/dist/definition.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The buy tool as a model sees it: one name, one description, and a strict
|
|
3
|
-
* JSON Schema with
|
|
4
|
-
* picks a product they were shown. The delivery address and the
|
|
3
|
+
* JSON Schema with five required fields, `product_id` null unless the shopper
|
|
4
|
+
* picks a product they were shown and `store_id` null unless they pick a store. The delivery address and the
|
|
5
5
|
* shopper's id are construction options, never model inputs, so a model can
|
|
6
6
|
* neither change where an order goes nor buy for someone else.
|
|
7
7
|
*/
|
|
@@ -9,8 +9,9 @@ export declare const BUY_TOOL_NAME = "buy";
|
|
|
9
9
|
/** What the model reads before it calls the tool. Every sentence is a rule the model follows. */
|
|
10
10
|
export declare const BUY_TOOL_DESCRIPTION: string;
|
|
11
11
|
/**
|
|
12
|
-
* What the model sends: `ask`, `confirm` and `new_order` every time,
|
|
13
|
-
* shopper picks a product from the last result
|
|
12
|
+
* What the model sends: `ask`, `confirm` and `new_order` every time, `product_id`, null unless the
|
|
13
|
+
* shopper picks a product from the last result, and `store_id`, null unless they pick a store from it.
|
|
14
|
+
* Your own code may leave `product_id` and `store_id` out.
|
|
14
15
|
*/
|
|
15
16
|
export interface BuyToolInput {
|
|
16
17
|
ask: string;
|
|
@@ -18,6 +19,8 @@ export interface BuyToolInput {
|
|
|
18
19
|
new_order: boolean;
|
|
19
20
|
/** The `product_id` of the product the shopper picked from the last result's `products`, or null. */
|
|
20
21
|
product_id?: string | null;
|
|
22
|
+
/** The `store_id` of the store the shopper picked from the last result's `stores`, or null. */
|
|
23
|
+
store_id?: string | null;
|
|
21
24
|
}
|
|
22
25
|
/** A type, not an interface, so it fits the SDKs' own input schema types, which take any key. */
|
|
23
26
|
export type BuyToolInputSchema = {
|
|
@@ -39,6 +42,10 @@ export type BuyToolInputSchema = {
|
|
|
39
42
|
type: ['string', 'null'];
|
|
40
43
|
description: string;
|
|
41
44
|
};
|
|
45
|
+
store_id: {
|
|
46
|
+
type: ['string', 'null'];
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
42
49
|
};
|
|
43
50
|
required: Array<keyof BuyToolInput>;
|
|
44
51
|
additionalProperties: false;
|
|
@@ -52,8 +59,9 @@ export interface BuyToolDefinition {
|
|
|
52
59
|
export declare function buyToolDefinition(): BuyToolDefinition;
|
|
53
60
|
/**
|
|
54
61
|
* The model's input, checked against the schema: an object, or its JSON text,
|
|
55
|
-
* with `ask` a string, `confirm` and `new_order` booleans, `product_id`
|
|
56
|
-
* string, null or left out,
|
|
62
|
+
* with `ask` a string, `confirm` and `new_order` booleans, `product_id` and
|
|
63
|
+
* `store_id` each a string, null or left out, at most one of them set, and
|
|
64
|
+
* nothing else. Throws a TypeError whose message
|
|
57
65
|
* tells the model what to send.
|
|
58
66
|
*/
|
|
59
67
|
export declare function parseBuyToolInput(value: unknown): BuyToolInput;
|
package/dist/definition.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The buy tool as a model sees it: one name, one description, and a strict
|
|
3
|
-
* JSON Schema with
|
|
4
|
-
* picks a product they were shown. The delivery address and the
|
|
3
|
+
* JSON Schema with five required fields, `product_id` null unless the shopper
|
|
4
|
+
* picks a product they were shown and `store_id` null unless they pick a store. The delivery address and the
|
|
5
5
|
* shopper's id are construction options, never model inputs, so a model can
|
|
6
6
|
* neither change where an order goes nor buy for someone else.
|
|
7
7
|
*/
|
|
@@ -10,21 +10,27 @@ export const BUY_TOOL_NAME = 'buy';
|
|
|
10
10
|
export const BUY_TOOL_DESCRIPTION = [
|
|
11
11
|
'Buy what this shopper asks for at real stores.',
|
|
12
12
|
"Put the shopper's own words in `ask`.",
|
|
13
|
-
'Tell the shopper what `reply` says, in your own voice.',
|
|
13
|
+
'Tell the shopper what `reply` says, in your own voice, except the products and stores it lists that reach the shopper as cards.',
|
|
14
14
|
"When `shopper_already_told_looking` is true, the shopper was already told you are working on their request, so never acknowledge it again: open with the answer or the question.",
|
|
15
15
|
'Show the shopper the cart before you confirm it.',
|
|
16
16
|
'Ask the shopper for exactly one thing per message: the step `next_action` names, even when `reply` lists more. Never mention a later step, and never ask for the next one until the shopper has done this one.',
|
|
17
|
-
'
|
|
17
|
+
'When `next_action` is null and `reply` is about something other than a cart, ask for no checkout step: a cart from an earlier request gets one short line at most, after the answer to what the shopper just asked, and never a question about confirming or placing it.',
|
|
18
|
+
'Set `confirm` to true only when the shopper just said yes to the cart in your last result, asks to check out or pay for a cart that waits, says they added the card for a cart that waits, says they approved, or asks for the approval link again.',
|
|
19
|
+
'When the shopper asks to check out, pay or place the order, call this tool: Agentcard knows whether their card is saved, so never ask the shopper whether they added it.',
|
|
18
20
|
'Never write a link.',
|
|
19
21
|
'Each link reaches the shopper on its own.',
|
|
20
22
|
'When `link_sent` is true, tell the shopper a link is waiting for them.',
|
|
21
|
-
'Product links reach the shopper on their own,
|
|
23
|
+
'Product links reach the shopper on their own, as cards that show each product\'s name, price and picture. When `product_links_sent` is above zero, the shopper already sees those products, so never list them again, by name, price or number, even when `reply` lists them: write one short line, such as which one they want, plus only what the cards do not show, such as why one is the pick. When the shopper asks for links, tell them the links are there; never write or invent a URL.',
|
|
22
24
|
'A `placing` status means the order is going through, so never ask for another approval.',
|
|
23
25
|
'Tell the shopper they need not reply only when `shopper_need_not_reply` is true.',
|
|
24
26
|
'After `needs_card` with a cart, Agentcard keeps the cart, so set `confirm` to true once the shopper says they added the card.',
|
|
25
27
|
"After `needs_card` with no cart, call this tool again with the shopper's request once they say they added a card.",
|
|
26
28
|
'Set `new_order` to true only when the shopper drops the cart.',
|
|
29
|
+
"Never tell the shopper that you or Agentcard can't see their name, phone number or address: Agentcard may use the ones saved on their Agentcard account for an order, and `reply` says when it does.",
|
|
27
30
|
"When the shopper picks one of the `products` in your last result, by its name, its place in the list or a tap that names it, set `product_id` to that product's `product_id`, so Agentcard adds exactly that product; otherwise set it to null.",
|
|
31
|
+
"When the shopper picks one of the `stores` in your last result, by its name, its place in the list or a tap that names it, set `store_id` to that store's `store_id`, so Agentcard orders from exactly that store; otherwise set it to null. Never set both `product_id` and `store_id`.",
|
|
32
|
+
'Store links reach the shopper on their own, as cards that show each store\'s name and delivery time. When `store_links_sent` is above zero, never list those stores again: ask which one in one short line.',
|
|
33
|
+
'When `store_links_sent` is 0 and you offer the `stores`, name each one by its `name`, never only "their links are there".',
|
|
28
34
|
'When `pick.in_cart` is false, the product the shopper picked is not in the cart: tell the shopper what `reply` says, and never ask them to confirm the cart.',
|
|
29
35
|
].join(' ');
|
|
30
36
|
/** A fresh copy of the tool's definition, so a caller that edits one never changes another. */
|
|
@@ -38,24 +44,29 @@ export function buyToolDefinition() {
|
|
|
38
44
|
ask: { type: 'string', description: 'What the shopper said, in their own words.' },
|
|
39
45
|
confirm: {
|
|
40
46
|
type: 'boolean',
|
|
41
|
-
description: 'True only when the shopper just said yes to the cart in your last result, says they added the card for a cart that waits, says they approved, or asks for the approval link again. False otherwise.',
|
|
47
|
+
description: 'True only when the shopper just said yes to the cart in your last result, asks to check out or pay for a cart that waits, says they added the card for a cart that waits, says they approved, or asks for the approval link again. False otherwise.',
|
|
42
48
|
},
|
|
43
49
|
new_order: { type: 'boolean', description: 'True only when the shopper drops the cart to start a new order. False otherwise.' },
|
|
44
50
|
product_id: {
|
|
45
51
|
type: ['string', 'null'],
|
|
46
52
|
description: "The `product_id` of the product the shopper picked from the `products` in your last result. Null when they picked none.",
|
|
47
53
|
},
|
|
54
|
+
store_id: {
|
|
55
|
+
type: ['string', 'null'],
|
|
56
|
+
description: 'The `store_id` of the store the shopper picked from the `stores` in your last result. Null when they picked none.',
|
|
57
|
+
},
|
|
48
58
|
},
|
|
49
|
-
required: ['ask', 'confirm', 'new_order', 'product_id'],
|
|
59
|
+
required: ['ask', 'confirm', 'new_order', 'product_id', 'store_id'],
|
|
50
60
|
additionalProperties: false,
|
|
51
61
|
},
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
|
-
const FIELDS = new Set(['ask', 'confirm', 'new_order', 'product_id']);
|
|
64
|
+
const FIELDS = new Set(['ask', 'confirm', 'new_order', 'product_id', 'store_id']);
|
|
55
65
|
/**
|
|
56
66
|
* The model's input, checked against the schema: an object, or its JSON text,
|
|
57
|
-
* with `ask` a string, `confirm` and `new_order` booleans, `product_id`
|
|
58
|
-
* string, null or left out,
|
|
67
|
+
* with `ask` a string, `confirm` and `new_order` booleans, `product_id` and
|
|
68
|
+
* `store_id` each a string, null or left out, at most one of them set, and
|
|
69
|
+
* nothing else. Throws a TypeError whose message
|
|
59
70
|
* tells the model what to send.
|
|
60
71
|
*/
|
|
61
72
|
export function parseBuyToolInput(value) {
|
|
@@ -74,7 +85,7 @@ export function parseBuyToolInput(value) {
|
|
|
74
85
|
const fields = input;
|
|
75
86
|
const extra = Object.keys(fields).find((key) => !FIELDS.has(key));
|
|
76
87
|
if (extra !== undefined)
|
|
77
|
-
throw new TypeError(`The buy tool takes only ask, confirm, new_order and
|
|
88
|
+
throw new TypeError(`The buy tool takes only ask, confirm, new_order, product_id and store_id, and ${extra} is not one of them.`);
|
|
78
89
|
if (typeof fields.ask !== 'string')
|
|
79
90
|
throw new TypeError("Set ask to the shopper's words, as a string.");
|
|
80
91
|
if (typeof fields.confirm !== 'boolean')
|
|
@@ -85,7 +96,15 @@ export function parseBuyToolInput(value) {
|
|
|
85
96
|
if (productId !== undefined && productId !== null && typeof productId !== 'string') {
|
|
86
97
|
throw new TypeError("Set product_id to the picked product's product_id, as a string, or to null.");
|
|
87
98
|
}
|
|
88
|
-
|
|
99
|
+
const storeId = fields.store_id;
|
|
100
|
+
if (storeId !== undefined && storeId !== null && typeof storeId !== 'string') {
|
|
101
|
+
throw new TypeError("Set store_id to the picked store's store_id, as a string, or to null.");
|
|
102
|
+
}
|
|
103
|
+
const product = typeof productId === 'string' && productId.trim() !== '' ? productId.trim() : null;
|
|
104
|
+
const store = typeof storeId === 'string' && storeId.trim() !== '' ? storeId.trim() : null;
|
|
105
|
+
if (product && store)
|
|
106
|
+
throw new TypeError('Set product_id or store_id for the one thing the shopper picked, and the other to null.');
|
|
107
|
+
return { ask: fields.ask, confirm: fields.confirm, new_order: fields.new_order, product_id: product, store_id: store };
|
|
89
108
|
}
|
|
90
109
|
/** A fresh copy of the tool in OpenAI's function-calling format: plain JSON, so an SDK that sends it as is sends exactly this. */
|
|
91
110
|
export function openAIBuyTool() {
|
package/dist/handler.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type AgentcardEvent, type VerifyOptions } from './verify.js';
|
|
9
9
|
import { type PurchaseMessage } from './message.js';
|
|
10
|
+
import type { AgentcardClient } from './client.js';
|
|
10
11
|
import { type ShopperMessage } from './shopper-message.js';
|
|
11
12
|
/** The parts of a Node or Express request the handler reads. */
|
|
12
13
|
export interface WebhookRequest {
|
|
@@ -80,6 +81,11 @@ export interface AgentcardWebhookOptions extends PurchaseAgentWebhookOptions {
|
|
|
80
81
|
* such as `vault.card_stored`, and a shopper message for a shopper you gave Agentcard no id for.
|
|
81
82
|
*/
|
|
82
83
|
onEvent?: (event: AgentcardEvent) => void | Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* The client your buy tools share. Once `onMessage` returns, the client keeps the message for that
|
|
86
|
+
* shopper's next `buy.turn`, which hands it to your model, as `agentcard.listen` does.
|
|
87
|
+
*/
|
|
88
|
+
agentcard?: AgentcardClient;
|
|
83
89
|
}
|
|
84
90
|
/** The handler `agentcardWebhook` returns: a Node or Express `(req, res)` handler, with `fetch` for a web `Request`. */
|
|
85
91
|
export interface AgentcardWebhookHandler {
|
package/dist/handler.js
CHANGED
|
@@ -284,6 +284,9 @@ export function agentcardWebhook(opts) {
|
|
|
284
284
|
if (typeof opts.onMessage !== 'function') {
|
|
285
285
|
throw new TypeError('Pass onMessage, the function that receives each message for a shopper.');
|
|
286
286
|
}
|
|
287
|
+
if (opts.agentcard !== undefined && typeof opts.agentcard?.delivered !== 'function') {
|
|
288
|
+
throw new TypeError('Pass agentcard as the AgentcardClient your buy tools share, or leave it out.');
|
|
289
|
+
}
|
|
287
290
|
const secret = opts.secret;
|
|
288
291
|
const verifyOpts = { toleranceSeconds: toleranceOf(opts) };
|
|
289
292
|
const report = reporter(opts.onError);
|
|
@@ -316,6 +319,7 @@ export function agentcardWebhook(opts) {
|
|
|
316
319
|
try {
|
|
317
320
|
if (message?.externalUserId) {
|
|
318
321
|
await opts.onMessage(message.externalUserId, message, event);
|
|
322
|
+
opts.agentcard?.delivered(message.externalUserId, message);
|
|
319
323
|
}
|
|
320
324
|
else if (opts.onEvent) {
|
|
321
325
|
await opts.onEvent(event);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,12 @@ export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatur
|
|
|
2
2
|
export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
|
|
3
3
|
export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
|
|
4
4
|
export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
|
|
5
|
-
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, } from './client.js';
|
|
5
|
+
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartLine, type BuyCartFeeLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, } from './client.js';
|
|
6
6
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
|
|
7
|
-
export { toMessages, messageStream, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
8
|
-
export { buyTool, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, } from './buy-tool.js';
|
|
7
|
+
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
8
|
+
export { buyTool, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, } from './buy-tool.js';
|
|
9
9
|
export { instantAck, ackFor, type AckForOptions } from './ack.js';
|
|
10
|
-
export { forModel, forModelError, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, } from './model-view.js';
|
|
10
|
+
export { forModel, forModelError, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, } from './model-view.js';
|
|
11
11
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
|
12
12
|
export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
|
|
13
13
|
export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, } from './shopper-mes
|
|
|
4
4
|
export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
|
|
5
5
|
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
|
|
6
6
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
|
|
7
|
-
export { toMessages, messageStream, } from './messages.js';
|
|
7
|
+
export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
8
8
|
export { buyTool, } from './buy-tool.js';
|
|
9
9
|
export { instantAck, ackFor } from './ack.js';
|
|
10
10
|
export { forModel, forModelError, stripLinks, } from './model-view.js';
|
package/dist/listen.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface ListenOptions {
|
|
|
47
47
|
* Receives each event for one of your shoppers, under your own id for them, with the message
|
|
48
48
|
* ready to post into that shopper's chat: the same as `agentcardWebhook`'s `onMessage`. A message
|
|
49
49
|
* whose link or window closed before it arrived reaches no shopper: its event goes to `onEvent`.
|
|
50
|
+
* Once it returns, the client keeps the message for that shopper's next `buy.turn`, which hands it
|
|
51
|
+
* to your model, so the model knows what the shopper read between turns.
|
|
50
52
|
*/
|
|
51
53
|
onMessage?: (externalUserId: string, message: ShopperMessage, event: AgentcardEvent<AgentcardEventType>) => void | Promise<void>;
|
|
52
54
|
/** Receives every event `onMessage` does not take: every event, when you pass no `onMessage`. */
|
package/dist/listen.js
CHANGED
|
@@ -198,8 +198,11 @@ export function listenToAgentcard(agentcard, opts) {
|
|
|
198
198
|
const forShopper = message && message.externalUserId !== null && !isStale(message) ? message : null;
|
|
199
199
|
for (let attempt = 1;; attempt++) {
|
|
200
200
|
try {
|
|
201
|
-
if (opts.onMessage && forShopper?.externalUserId)
|
|
201
|
+
if (opts.onMessage && forShopper?.externalUserId) {
|
|
202
202
|
await opts.onMessage(forShopper.externalUserId, forShopper, event);
|
|
203
|
+
// The shopper's chat holds the message now, so their next `buy.turn` tells the model.
|
|
204
|
+
agentcard.delivered(forShopper.externalUserId, forShopper);
|
|
205
|
+
}
|
|
203
206
|
else if (opts.onEvent)
|
|
204
207
|
await opts.onEvent(event);
|
|
205
208
|
return finish(event.id);
|
package/dist/messages.d.ts
CHANGED
|
@@ -14,6 +14,23 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export declare function toMessages(text: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* How long someone texting takes to type `message`, in milliseconds: 300, plus 12 a character, at most
|
|
19
|
+
* 1,500. `messageStream` and `buy.turn` wait this long between two messages by default, so a model that
|
|
20
|
+
* writes three paragraphs in 100 ms still delivers them one at a time.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* typingDelay('which one do you want?'); // 564
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function typingDelay(message: string): number;
|
|
27
|
+
/** How messages are spaced: `true` by `typingDelay`, `false` not at all, or a function that returns the pause for a message. */
|
|
28
|
+
export type MessagePace = boolean | ((message: string) => number);
|
|
29
|
+
/** The pause for each message `pace` asks for, or null for none. Throws a TypeError for a pace that cannot work. */
|
|
30
|
+
export declare function paceOf(pace: unknown): ((message: string) => number) | null;
|
|
31
|
+
/** A typing hook that never throws: one that fails costs nothing but the indicator. */
|
|
32
|
+
export declare function typingHook(onTyping: unknown): (typing: boolean) => void;
|
|
33
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
17
34
|
/** What `messageStream` hands back: push each piece of text as it arrives, then end once it stops. */
|
|
18
35
|
export interface MessageStream {
|
|
19
36
|
/** Adds the next piece of text. Each message the piece completes goes to `onMessage` in order, after the one before it settled. */
|
|
@@ -31,10 +48,24 @@ export interface MessageStreamOptions {
|
|
|
31
48
|
* After a send fails, no later message is sent, and `end` rejects with the failure.
|
|
32
49
|
*/
|
|
33
50
|
onMessage: (message: string) => void | Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Spaces the messages like someone texting. Each message after the first waits until `typingDelay`
|
|
53
|
+
* of it has passed since the one before it went out, so a model that writes three paragraphs in
|
|
54
|
+
* 100 ms still delivers them one at a time. Time the model spent writing counts toward the pause, so
|
|
55
|
+
* a slow model gets no added wait. Default `true`. `false` hands each message over the moment it is
|
|
56
|
+
* written, and a function returns the pause in milliseconds for a message.
|
|
57
|
+
*/
|
|
58
|
+
pace?: MessagePace;
|
|
59
|
+
/**
|
|
60
|
+
* Hears `true` when the pause before a message starts and `false` the moment that message goes out,
|
|
61
|
+
* so your app can show a typing indicator between messages. One that throws costs nothing.
|
|
62
|
+
*/
|
|
63
|
+
onTyping?: (typing: boolean) => void;
|
|
34
64
|
}
|
|
35
65
|
/**
|
|
36
|
-
* Cuts streamed text into the messages `toMessages` finds, as it arrives. Each paragraph
|
|
37
|
-
*
|
|
66
|
+
* Cuts streamed text into the messages `toMessages` finds, as it arrives. Each paragraph is ready the
|
|
67
|
+
* moment the blank line that ends it arrives, whatever the pieces split it into, and goes to
|
|
68
|
+
* `onMessage` then, or, after the first, once its typing pause since the last one passed (`pace`). A
|
|
38
69
|
* paragraph inside a list waits for the first characters of the line after the blank, which say
|
|
39
70
|
* whether the list goes on. `end` hands over the last message and waits for every send.
|
|
40
71
|
*
|