@agent-cards/commerce-sdk 0.15.0 → 0.17.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 +56 -9
- package/dist/ack.d.ts +40 -20
- package/dist/ack.js +93 -5
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +2 -0
- package/dist/buy-tool.d.ts +61 -15
- package/dist/buy-tool.js +491 -41
- package/dist/client.d.ts +38 -0
- package/dist/client.js +15 -1
- package/dist/definition.js +3 -0
- package/dist/direct.d.ts +16 -3
- package/dist/direct.js +34 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/messages.d.ts +27 -6
- package/dist/messages.js +72 -12
- package/dist/model-view.d.ts +51 -4
- package/dist/model-view.js +392 -42
- package/dist/order.d.ts +119 -0
- package/dist/order.js +155 -0
- package/dist/sentences.d.ts +16 -0
- package/dist/sentences.js +57 -0
- package/dist/shopper-message.d.ts +10 -0
- package/dist/shopper-message.js +51 -11
- package/dist/texting.d.ts +83 -0
- package/dist/texting.js +352 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -127,6 +127,12 @@ export interface BuyStreamHandlers {
|
|
|
127
127
|
onCatalog?: (update: BuyCatalogUpdate) => void | Promise<void>;
|
|
128
128
|
/** Each message Agentcard writes, the moment it is written. */
|
|
129
129
|
onSegment?: (update: BuySegmentUpdate) => void | Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* The line that says what Agentcard is looking for, such as `one sec, checking target for la croix`, written while the
|
|
132
|
+
* search runs on an ask for something new. It comes before the turn's cards and messages, or not at all, and the
|
|
133
|
+
* answer's `messages` never hold it.
|
|
134
|
+
*/
|
|
135
|
+
onAck?: (update: BuySegmentUpdate) => void | Promise<void>;
|
|
130
136
|
/** The carts, each time the turn builds or changes one. */
|
|
131
137
|
onCart?: (update: BuyCartUpdate) => void | Promise<void>;
|
|
132
138
|
/** Each piece of a message as Agentcard writes it. Passing it asks Agentcard for the pieces (`?stream=deltas`). */
|
|
@@ -234,6 +240,8 @@ export interface BuyRequest {
|
|
|
234
240
|
delivery_address?: BuyDeliveryAddress;
|
|
235
241
|
external_user_id?: string;
|
|
236
242
|
user_id?: string;
|
|
243
|
+
/** `false` asks for no line that says what Agentcard is looking for (`onAck`): the shopper already read one. */
|
|
244
|
+
ack?: false;
|
|
237
245
|
}
|
|
238
246
|
export type BuyStatus = 'needs_input' | 'needs_approval' | 'order_placed' | 'partially_placed' | 'declined';
|
|
239
247
|
export interface BuyStore {
|
|
@@ -373,6 +381,32 @@ export interface BuyStorePick {
|
|
|
373
381
|
name: string | null;
|
|
374
382
|
selected: boolean;
|
|
375
383
|
}
|
|
384
|
+
/** One value the shopper picks from. `id` is the option id the cart takes, null when the shopper's words answer it. */
|
|
385
|
+
export interface BuyChoiceOption {
|
|
386
|
+
id: string | null;
|
|
387
|
+
label: string;
|
|
388
|
+
/** What choosing it adds to the price, in cents: 0 when free, null when the store does not say. */
|
|
389
|
+
price_delta_cents: number | null;
|
|
390
|
+
}
|
|
391
|
+
/** One group of values, such as a sandwich's bread or a shirt's size. `min` and `max` count the values picked; `max` null for no limit. */
|
|
392
|
+
export interface BuyChoiceGroup {
|
|
393
|
+
name: string;
|
|
394
|
+
required: boolean;
|
|
395
|
+
min: number;
|
|
396
|
+
max: number | null;
|
|
397
|
+
options: BuyChoiceOption[];
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The choice an answer's reply asks the shopper to make, as data: the question, the item it is about, where the values come
|
|
401
|
+
* from (`menu_item`: a restaurant item's options, `store_checkout`: a store's own size or colour question, `catalog_variant`:
|
|
402
|
+
* a product's sizes or colours in stock), and every group with its values.
|
|
403
|
+
*/
|
|
404
|
+
export interface BuyChoice {
|
|
405
|
+
question: string | null;
|
|
406
|
+
item: string | null;
|
|
407
|
+
source: string;
|
|
408
|
+
groups: BuyChoiceGroup[];
|
|
409
|
+
}
|
|
376
410
|
export interface BuyPaymentSource {
|
|
377
411
|
source: string;
|
|
378
412
|
brand: string | null;
|
|
@@ -435,6 +469,8 @@ export interface BuyTurn {
|
|
|
435
469
|
store_pick: BuyStorePick | null;
|
|
436
470
|
/** The cart the store's checkout is still building after this answer; null when none is, and on an older API. */
|
|
437
471
|
cart_building: BuyCartBuilding | null;
|
|
472
|
+
/** The choice the reply asks the shopper to make, each group with every value; null when it asks none, and on an older API. */
|
|
473
|
+
choice: BuyChoice | null;
|
|
438
474
|
/** The answer as Agentcard sent it. */
|
|
439
475
|
raw: Record<string, unknown>;
|
|
440
476
|
}
|
|
@@ -506,6 +542,8 @@ export declare class AgentcardApiError extends Error {
|
|
|
506
542
|
}
|
|
507
543
|
/** A turn as Agentcard answers it. */
|
|
508
544
|
export declare function turnOf(body: Record<string, unknown>): BuyTurn;
|
|
545
|
+
/** An answer's `choice`, its groups with at least one value each, or null when it carries none. */
|
|
546
|
+
export declare function choiceOf(value: unknown): BuyChoice | null;
|
|
509
547
|
/** A cart being built, as an answer, a refusal or the conversation read names it. */
|
|
510
548
|
export declare function cartBuildingOf(value: unknown): BuyCartBuilding | null;
|
|
511
549
|
/** The client every shopper's buy tool shares. Build one per set of credentials and keep it. */
|
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { isRecord } from './verify.js';
|
|
12
12
|
import { listenToAgentcard } from './listen.js';
|
|
13
13
|
/** The handlers that ask Agentcard to stream a turn. */
|
|
14
|
-
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onCart', 'onDelta', 'onAvailability'];
|
|
14
|
+
const STREAM_HANDLERS = ['onProgress', 'onStatus', 'onFound', 'onCatalog', 'onSegment', 'onAck', 'onCart', 'onDelta', 'onAvailability'];
|
|
15
15
|
const PROGRESS_STAGES = new Set(['searching', 'building_cart', 'checking_out', 'working']);
|
|
16
16
|
export const DEFAULT_BASE_URL = 'https://api.agentcard.sh';
|
|
17
17
|
/** The wait for one purchase turn: past Agentcard's own 110-second budget, so a slow turn answers `turn_timeout` rather than aborting here. */
|
|
@@ -299,9 +299,20 @@ export function turnOf(body) {
|
|
|
299
299
|
stores: storesOf(body.stores),
|
|
300
300
|
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,
|
|
301
301
|
cart_building: cartBuildingOf(body.cart_building),
|
|
302
|
+
choice: choiceOf(body.choice),
|
|
302
303
|
raw: body,
|
|
303
304
|
};
|
|
304
305
|
}
|
|
306
|
+
/** An answer's `choice`, its groups with at least one value each, or null when it carries none. */
|
|
307
|
+
export function choiceOf(value) {
|
|
308
|
+
if (!isRecord(value) || !Array.isArray(value.groups))
|
|
309
|
+
return null;
|
|
310
|
+
const groups = value.groups.filter(isRecord).flatMap((g) => {
|
|
311
|
+
const options = (Array.isArray(g.options) ? g.options : []).filter(isRecord).flatMap((o) => str(o.label) ? [{ id: str(o.id), label: o.label.trim(), price_delta_cents: num(o.price_delta_cents) }] : []);
|
|
312
|
+
return options.length ? [{ name: str(g.name) ?? '', required: g.required === true, min: num(g.min) ?? 0, max: num(g.max), options }] : [];
|
|
313
|
+
});
|
|
314
|
+
return groups.length ? { question: str(value.question), item: str(value.item), source: str(value.source) ?? '', groups } : null;
|
|
315
|
+
}
|
|
305
316
|
/** A cart being built, as an answer, a refusal or the conversation read names it. */
|
|
306
317
|
export function cartBuildingOf(value) {
|
|
307
318
|
return isRecord(value) && str(value.store) ? { store: value.store, started_at: str(value.started_at) ?? '' } : null;
|
|
@@ -524,6 +535,9 @@ async function readStreamedTurn(res, stream, timeoutMs) {
|
|
|
524
535
|
else if (event === 'segment' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onSegment) {
|
|
525
536
|
deliver(() => stream.onSegment({ conversationId: named, text: data.text }));
|
|
526
537
|
}
|
|
538
|
+
else if (event === 'ack' && typeof data.text === 'string' && data.text.trim() !== '' && stream.onAck) {
|
|
539
|
+
deliver(() => stream.onAck({ conversationId: named, text: data.text }));
|
|
540
|
+
}
|
|
527
541
|
else if (event === 'venue_availability' && stream.onAvailability && str(data.conversation_id) && Array.isArray(data.items)) {
|
|
528
542
|
const items = data.items.filter(isRecord).flatMap((i) => {
|
|
529
543
|
const id = str(i.id);
|
package/dist/definition.js
CHANGED
|
@@ -13,6 +13,7 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
13
13
|
'When the shopper sends a link to a product, such as a buy.trychannel3.com link, put the whole link in `ask` exactly as they sent it, query string included, with their words beside it: Agentcard buys that exact product from the link, so never describe the link in words instead.',
|
|
14
14
|
'Tell the shopper what `reply` says, in your own voice, except the products and stores it lists that reach the shopper as cards.',
|
|
15
15
|
"When `shopper_already_told_looking` is true, the shopper already knows you are working on their request, from a reply or a 👀 on their message, so never acknowledge it again: open with the answer or the question.",
|
|
16
|
+
'When the shopper asks you to find, buy, book or order something new, such as a product, a gift idea, a meal, a table, a flight or a store page they pasted a link to, first write one short line that says what you are about to look for, then call this tool in the same reply: only what they asked for, and the store or place they named, in their own words, such as "on it, looking for la croix at target", "one sec, checking nopa for a table for 2 at 7pm" or, for a pasted link, "one sec, checking that link", never a price, a total, stock, a result, a question or that anything was found, added or ordered. A store named by its site, such as "the polishing cloth from myrqvist.com", is a store, not a link. For a yes, a pick, a size, an address, a code, a change to a cart or a question about an order, write no line first, and after this tool answers, never say again that you are looking.',
|
|
16
17
|
'When `shopper_already_read_reply` is true, the shopper already read `reply` as Agentcard wrote it, so never say it again in any words: answer with only what `reply` leaves out, in one short line, or with nothing.',
|
|
17
18
|
'Show the shopper the cart before you confirm it.',
|
|
18
19
|
'When `cart_building` is set, the store is still building that cart: tell the shopper in one short line that its total is on its way, never ask them to confirm it, and confirm it only after they say yes to the total in the message Agentcard sends next.',
|
|
@@ -42,6 +43,7 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
42
43
|
"Talk to the shopper only about their order, in their terms: what is in the cart, what was found and what they can do next. Leave out how it was done, even when `reply` says it: never mention this tool, `reply`, a field, a system, \"my end\" or what you can or can't see, the searches run, the stores checked or not, a timeout, account setup, or a link that did not go out. When parts of the result disagree, say what you are sure of and ask, never which part said what.",
|
|
43
44
|
'Never tell the shopper what has not happened yet or where things stand inside, such as "nothing\'s been charged yet", "your order isn\'t placed yet" or "the address isn\'t locked in", even when `reply` says it: ask for what is needed instead, such as "should it ship to 1 ferry plz?".',
|
|
44
45
|
'When `next_action` is `store_login`, `add_card` or `approve`, the message that asks for it is one short line and nothing else, even when `reply` says more, such as "sign in to doordash to place your order", "add your card to place the order" or "approve the payment to place your order (up to $19.02)": no recap of the cart, the address or the price, nothing about tax, delivery, shipping, a hold or what was charged, no word on when the link runs out, and never a request to tell you once it is done. On `approve`, put the cart\'s `max_charge` in the parentheses when it is set, and no other amount. Only when the shopper\'s last message picked what went in the cart does a line about the cart come first, as its own message; after an address, a phone number, a yes or a saved card, the ask is the whole reply.',
|
|
46
|
+
'When `yes_carried` is set, the shopper already said yes to this cart, or picked it (`yes_carried.picked`), and Agentcard went ahead on it once they finished the step outside the chat, so never ask for their yes again: ask only for the step `next_action` names. When `yes_carried.total_changed` is true, that one short line opens with the new total, the one price it names beside `max_charge`, such as "it\'s $17.89 with fees and tax now. approve the payment to place your order (up to $19.02)".',
|
|
45
47
|
'When `next_action` is `confirm`, ask in one short message: the items, one total, and to say yes to place it, such as "1 carolina black socks pack from walmart, about $9.57. say yes to place it". Leave out the fees one by one, the hold, and the address unless the shopper just changed it.',
|
|
46
48
|
'Every line you write reaches the shopper, a line before you call this tool too: write to them as "you", never about "the shopper" or "the user", and never say what you are about to do with this tool, such as "I\'ll add them now".',
|
|
47
49
|
'Say as fact only what the result shows: never promise what a store will do later, such as confirm a size when the order goes through.',
|
|
@@ -52,6 +54,7 @@ export const BUY_TOOL_DESCRIPTION = [
|
|
|
52
54
|
"When the shopper asks what you can do or which stores you shop at, answer without calling this tool and with no line saying you are looking: one message that names at most four stores plus the kinds of things you can do, such as food delivery and restaurant reservations, then one that asks what they need. Every shopper can buy from Amazon, Walmart, Target and Best Buy, so name those, and no other store unless a result showed it.",
|
|
53
55
|
'Each paragraph you write reaches the shopper as its own message: give each one job and keep it under about 20 words. On a cart turn, write the confirmation and the one next step, such as "added a little cheeseburger from five guys, about $12.17 before delivery and tax" and then "next, sign in to doordash with the link so i can place it": one total, never a price breakdown your app\'s cart already shows (an app that shows no cart lists each line with its price) and never a fee by its name, rate or formula, such as an order fee, card processing or 2.9% + $0.30, even when `reply` or `fee_lines` names it, unless the shopper asks what the fees are, no promise to show the final total later, and nothing about passkeys, master passwords or Agentcard not seeing the card number unless the shopper asks.',
|
|
54
56
|
'Never listing products again is for offering choices: when you confirm what is in the cart, name each line by its item, such as "1 Dawn Ultra dish soap, 38 oz", never "the one you picked, shown in the card".',
|
|
57
|
+
'When you list options or choices for the shopper, such as sizes, breads, times, or stores no card shows, write one per line, each starting with "• ", in the same paragraph as the line that asks which one, so the question and its choices reach the shopper as one message. When the result has `choice`, list every value it holds that way, with its extra price, such as "• Gluten-free bread (+$2.46)", even when `reply` names them inside a sentence. The products and stores the cards show are still never listed again.',
|
|
55
58
|
'Say how long a link works only when the shopper asks, with `link_expires_in`, such as "it works for about 24 hours", never as a raw date and time or in UTC, such as `approval_expires_at` holds; when `link_expires_in` is null, say nothing about when it runs out.',
|
|
56
59
|
'Never call a product the shopper was sent "the card", in any language ("la tarjeta", "o cartão"): to the shopper a card is how they pay. Call it by its name, or "the one i sent".',
|
|
57
60
|
'When the cart has `total_leaves_out`, its `total` is before those: say so, such as "about $18.41 before tax and shipping", and never say it is with or includes tax, shipping or delivery.',
|
package/dist/direct.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { BuyNextAction } from './client.js';
|
|
2
2
|
import type { BuyToolInput } from './definition.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Why a message goes straight to Agentcard. `next_step` is a turn `buy.nextStep` opened: its call reads where the
|
|
5
|
+
* purchase stands and goes on from there whatever the model would write, so it never needs the model to decide it.
|
|
6
|
+
*/
|
|
7
|
+
export type DirectReason = 'product_ask' | 'pick' | 'answer' | 'confirm' | 'next_step';
|
|
5
8
|
/** The buy call for a message that goes straight to Agentcard, and why it does. */
|
|
6
9
|
export interface DirectCall {
|
|
7
10
|
input: BuyToolInput;
|
|
@@ -18,11 +21,21 @@ export interface DirectContext {
|
|
|
18
21
|
nextAction?: BuyNextAction | null;
|
|
19
22
|
/** Agentcard's last reply, the question a short answer answers. */
|
|
20
23
|
asked?: string | null;
|
|
21
|
-
/** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. */
|
|
24
|
+
/** The products Agentcard's last answer showed, in order, with the `product_id` a pick sends. A place (`the second one`) reads these. */
|
|
22
25
|
products?: ReadonlyArray<{
|
|
23
26
|
productId: string | null;
|
|
24
27
|
name: string;
|
|
25
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Every product card the shopper can tap by its name, each once: the answer's and the ones its searches sent as they answered,
|
|
31
|
+
* which an answer about one item can leave out (a menu turn that answered with the Burrito beside eight menu cards). A pick by
|
|
32
|
+
* name (a card's tap, a product's whole name, a word only one name holds) reads these; a place never does, so `the last one` is
|
|
33
|
+
* the last of `products`. Left out, `products` stands for both.
|
|
34
|
+
*/
|
|
35
|
+
tappable?: ReadonlyArray<{
|
|
36
|
+
productId: string | null;
|
|
37
|
+
name: string;
|
|
38
|
+
}>;
|
|
26
39
|
/** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
|
|
27
40
|
stores?: ReadonlyArray<{
|
|
28
41
|
storeId: string;
|
package/dist/direct.js
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* - a pick of what Agentcard just showed, by a card's tap (`I'll take the KODAK AA (24 Pack) ($13.49)`,
|
|
10
10
|
* `Order from Shake Shack`), its place (`the first one`, `#2`, `1 and 3`) or a reference (`that one`);
|
|
11
11
|
* - the answer to the question Agentcard just asked: the address, name or code it asked for, a short
|
|
12
|
-
* answer such as `medium` or `size 9`, or a plain yes to the total it asked the shopper to confirm
|
|
12
|
+
* answer such as `medium` or `size 9`, or a plain yes to the total it asked the shopper to confirm;
|
|
13
|
+
* - a price limit for the products Agentcard just showed: `only options under $20`, `under 20`, `cheaper ones`.
|
|
14
|
+
* Agentcard holds its cards to the limit and shows the ones inside it again as cards; your model, answering
|
|
15
|
+
* from the list it saw, named two products with no card to tap (2026-10-27).
|
|
13
16
|
*
|
|
14
17
|
* Everything else goes to the model: a question, small talk, a change of mind (`no`, `wait`, `cancel`),
|
|
15
18
|
* a command to the cart, and anything about the account, an order or how the chat works. A pick or an
|
|
@@ -21,6 +24,11 @@
|
|
|
21
24
|
* the 188 labelled messages in `test/fixtures/direct-messages.json`, it sends none it should not.
|
|
22
25
|
*/
|
|
23
26
|
import { ABOUT, ACKNOWLEDGED, CAPABILITY, CART_COMMAND, CHECKOUT, CHITCHAT, FILLER, asksForDetails, isNewRequest, isPickText, namesStore, plain } from './ack.js';
|
|
27
|
+
/**
|
|
28
|
+
* A price limit put on what was just shown: `under $20`, `under 20`, `less than 15 bucks`, `between 10 and 20`, `$20 or
|
|
29
|
+
* less`, `over $50`, `cheaper ones`, `menos de 20`. Only whether the message states one: Agentcard reads the limit itself.
|
|
30
|
+
*/
|
|
31
|
+
const PRICE_REFINE = /(?:\b(?:under|below|less than|cheaper than|up to|at most|no more than|max(?:imum)?|between|over|above|more than|at least|menos de|hasta|entre)\s+\$?\s?\d)|\$\s?\d+(?:\.\d{1,2})?\s*(?:or (?:less|under|below|more)|and (?:under|below|up)|max|tops|\+)|\b\d+\s*(?:dollars|bucks)\b|\b(?:cheaper|less expensive|more affordable|lower[- ]priced)\b(?!\s+than\b)|\bm[aá]s barat/;
|
|
24
32
|
/** The longest message that goes straight: a long one is read by the model. */
|
|
25
33
|
const MAX_CHARS = 500;
|
|
26
34
|
/** The steps a shopper answers with their own details or a code. */
|
|
@@ -143,9 +151,17 @@ function placesOf(bare, count) {
|
|
|
143
151
|
function nameWords(text) {
|
|
144
152
|
return String(text ?? '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
|
|
145
153
|
}
|
|
154
|
+
/** A card's tap as an app writes it, `I'll take the <name> (<price>)`: the name in it, or null for any other message. */
|
|
155
|
+
const TAP = /^(?:i'?ll (?:take|have|get)|i want|add)\s+(?:the\s+)?(.+?)\s*\(\s*(?:[$€£]\s?\d[\d,]*(?:\.\d{1,2})?|from [$€£]\s?\d[\d,]*(?:\.\d{1,2})?)\s*\)\s*[.!]?$/i;
|
|
156
|
+
function tappedName(raw) {
|
|
157
|
+
const name = TAP.exec(raw.trim())?.[1]?.trim();
|
|
158
|
+
return name ? name : null;
|
|
159
|
+
}
|
|
146
160
|
/** The pick a message makes of what Agentcard just showed, null when it picks nothing, or false when it counts past the list. */
|
|
147
161
|
function pickOf(raw, bare, context) {
|
|
148
162
|
const products = context.products ?? [];
|
|
163
|
+
// A pick by name reads every card the shopper can tap; a place reads `products`, in the order the answer showed them.
|
|
164
|
+
const byName = context.tappable?.length ? context.tappable : products;
|
|
149
165
|
const stores = context.stores ?? [];
|
|
150
166
|
const call = (fields) => ({ input: { ask: raw, confirm: false, new_order: false, product_id: null, store_id: null, ...fields }, reason: 'pick' });
|
|
151
167
|
if (stores.length > 0) {
|
|
@@ -159,18 +175,26 @@ function pickOf(raw, bare, context) {
|
|
|
159
175
|
if (named.length > 1)
|
|
160
176
|
return call({});
|
|
161
177
|
}
|
|
162
|
-
if (
|
|
163
|
-
// A card's tap names the product whole
|
|
178
|
+
if (byName.length > 0) {
|
|
179
|
+
// A card's tap names the product whole, `I'll take the Burrito Bowl ($15.15)`: the product of exactly that name is the pick,
|
|
180
|
+
// never the Burrito whose one word it shares, and a tap that names no product here (a card this list does not hold), or two
|
|
181
|
+
// alike, sends none, so Agentcard reads the words against every card it showed.
|
|
182
|
+
const tap = tappedName(raw);
|
|
183
|
+
if (tap !== null) {
|
|
184
|
+
const exact = byName.filter((product) => [nameWords(tap), nameWords(`the ${tap}`)].includes(nameWords(product.name)));
|
|
185
|
+
return call(exact.length === 1 ? { product_id: exact[0].productId } : {});
|
|
186
|
+
}
|
|
187
|
+
// A product's whole name inside the message: `I want the KODAK AA Alkaline Batteries (24 Pack)`.
|
|
164
188
|
const said = nameWords(raw);
|
|
165
|
-
const named =
|
|
189
|
+
const named = byName.filter((product) => nameWords(product.name).length >= 12 && said.includes(nameWords(product.name)));
|
|
166
190
|
if (named.length === 1)
|
|
167
191
|
return call({ product_id: named[0].productId });
|
|
168
192
|
if (named.length > 1)
|
|
169
193
|
return call({});
|
|
170
194
|
// A word only one product's name holds, such as its brand: `lets do the coppertop ones`, `duracell 24 count is fine`.
|
|
171
195
|
const words = new Set(said.split(' '));
|
|
172
|
-
const owners = (word) =>
|
|
173
|
-
const marked =
|
|
196
|
+
const owners = (word) => byName.filter((product) => nameWords(product.name).split(' ').includes(word));
|
|
197
|
+
const marked = byName.filter((product) => nameWords(product.name).split(' ').some((word) => word.length >= 4 && !/^\d/.test(word) && words.has(word) && owners(word).length === 1));
|
|
174
198
|
if (marked.length === 1 && said.split(' ').length <= 10 && !QUESTION_START.test(bare) && !DOUBT.test(said))
|
|
175
199
|
return call({ product_id: marked[0].productId });
|
|
176
200
|
const places = placesOf(bare, products.length);
|
|
@@ -182,7 +206,7 @@ function pickOf(raw, bare, context) {
|
|
|
182
206
|
if (/^(?:both|all|all of them|both of them|all three|all 3)$/.test(bare))
|
|
183
207
|
return call({});
|
|
184
208
|
}
|
|
185
|
-
if ((
|
|
209
|
+
if ((byName.length > 0 || stores.length > 0) && isPickText(raw))
|
|
186
210
|
return call({});
|
|
187
211
|
return null;
|
|
188
212
|
}
|
|
@@ -220,6 +244,9 @@ export function directCall(shopperText, context = {}) {
|
|
|
220
244
|
return null;
|
|
221
245
|
if (pick)
|
|
222
246
|
return pick;
|
|
247
|
+
// A price limit on the products just shown: Agentcard shows the ones inside it again, as cards.
|
|
248
|
+
if ((ctx.products ?? []).length > 0 && PRICE_REFINE.test(words) && words.split(' ').length <= 15)
|
|
249
|
+
return plainCall('product_ask');
|
|
223
250
|
// The address, name or code Agentcard asked for: what the shopper sends is the answer, unless it objects.
|
|
224
251
|
const details = (typeof ctx.nextAction === 'string' && DETAIL_STEPS.has(ctx.nextAction)) || asksForDetails(ctx.asked);
|
|
225
252
|
if (details && !OBJECTION.test(words) && !REQUEST_START.test(bare) && !QUESTION_START.test(bare) && !PAST.test(words) && !/\b(?:same as|on file|you have|the usual|ur|your)\b/.test(words) && !/^(?:resend|send|again|still)\b/.test(bare) && !NOT_SHOPPING.test(words) && words.split(' ').length <= 40)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatureReason, type AgentcardEvent, type VerifyOptions, } from './verify.js';
|
|
2
2
|
export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
|
|
3
3
|
export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type NextStepKind, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
|
|
4
|
+
export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
|
|
4
5
|
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 AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, 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
|
+
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type AgentcardTraceOptions, type OtelApi, type TraceContext, type FetchLike, type FetchLikeResponse, type BuyProgress, type BuyProgressStage, type BuyCatalogUpdate, type BuyAvailability, type BuyAvailabilityUpdate, type BuyFoundUpdate, type BuyTurnStatus, type BuySegmentUpdate, type BuyCartUpdate, type BuyDeltaUpdate, type BuyStreamHandlers, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartBuilding, 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, type BuyChoice, type BuyChoiceGroup, type BuyChoiceOption, } from './client.js';
|
|
6
7
|
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
8
|
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
8
9
|
export { buyTool, nextStepPrompt, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolFound, type BuyToolAvailability, type BuyToolProduct, type BuyToolProductAction, productAction, type BuyToolStore, type BuyToolTurn, type BuyToolTurnItem, type BuyToolTurnOptions, type BuyToolTurnMessage, type BuyAckMode, } from './buy-tool.js';
|
|
9
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
10
11
|
export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
|
|
11
|
-
export { forModel, forModelError,
|
|
12
|
+
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
13
|
+
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
|
|
12
14
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
|
13
15
|
export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
|
|
14
16
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { verifyAgentcardWebhook, AgentcardSignatureError, } from './verify.js';
|
|
2
2
|
export { readPurchaseMessage, } from './message.js';
|
|
3
3
|
export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, } from './shopper-message.js';
|
|
4
|
+
export { ORDER_STEPS, updateOrder, arrivalText, } from './order.js';
|
|
4
5
|
export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
|
|
5
6
|
export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
|
|
6
7
|
export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
|
|
@@ -8,7 +9,8 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
|
8
9
|
export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
|
|
9
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
|
|
10
11
|
export { directCall, isProductAsk } from './direct.js';
|
|
11
|
-
export { forModel, forModelError,
|
|
12
|
+
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
|
|
13
|
+
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
|
|
12
14
|
export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
|
|
13
15
|
export { listenToAgentcard, } from './listen.js';
|
|
14
16
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
|
package/dist/messages.d.ts
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Joins each message that leads in with a colon (`two options came in under $20:`) to the message after it, so a shopper
|
|
3
|
+
* never reads a bubble that ends on a colon: the lead-in and its first item or list arrive as one message, a line apart.
|
|
4
|
+
* `add` returns the message to send, or null while it holds a lead-in for the next one. `end` returns what a held lead-in
|
|
5
|
+
* leaves once nothing followed it, such as when every item it led into went because the cards show them: the text less
|
|
6
|
+
* its colon sentence (withoutLeadIn), or null when nothing is left. On 2026-10-27 a chat showed "two options came in
|
|
7
|
+
* under $20:" as a bubble of its own, above the item it led into.
|
|
6
8
|
*/
|
|
9
|
+
export declare function leadInJoiner(): {
|
|
10
|
+
add(message: string): string | null;
|
|
11
|
+
end(): string | null;
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* The text as separate messages, in order: one per paragraph, each trimmed, with empty ones left
|
|
9
15
|
* out. One or more blank lines end a paragraph. A fenced code block stays one message whatever blank
|
|
10
|
-
* lines it holds, and so does a list whose items blank lines separate.
|
|
16
|
+
* lines it holds, and so does a list whose items blank lines separate. A paragraph that ends with a
|
|
17
|
+
* colon goes with the paragraph after it, one line apart, and loses its colon sentence when nothing
|
|
18
|
+
* follows it, so no message ends on a colon.
|
|
11
19
|
*
|
|
12
20
|
* ```ts
|
|
13
21
|
* toMessages('Found two.\n\nWant the first one?'); // ['Found two.', 'Want the first one?']
|
|
22
|
+
* toMessages('two came in under $20:\n\n• Everydaze, $8.99'); // ['two came in under $20:\n• Everydaze, $8.99']
|
|
14
23
|
* ```
|
|
15
24
|
*/
|
|
16
25
|
export declare function toMessages(text: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* The text's paragraphs as `toMessages` cuts them, before any lead-in joins the paragraph after it: the units Agentcard
|
|
28
|
+
* and a model write, and what a count of the messages a shopper read is checked against, paragraph for paragraph.
|
|
29
|
+
*/
|
|
30
|
+
export declare function paragraphsOf(text: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Messages in order, each one that leads in with a colon joined to the ones after it by one joiner across them all
|
|
33
|
+
* (leadInJoiner), so a lead-in that is a whole message of its own, as one of Agentcard's `messages` can be, still goes
|
|
34
|
+
* with the message after it, and a lead-in nothing follows at the very end loses its colon sentences.
|
|
35
|
+
*/
|
|
36
|
+
export declare function joinLeadIns(messages: readonly string[]): string[];
|
|
17
37
|
/**
|
|
18
38
|
* How long someone texting takes to type `message`, in milliseconds: 300, plus 12 a character, at most
|
|
19
39
|
* 1,500. With `pace: true`, `messageStream` and `buy.turn` wait this long between two messages, so a
|
|
@@ -67,7 +87,8 @@ export interface MessageStreamOptions {
|
|
|
67
87
|
* moment the blank line that ends it arrives, whatever the pieces split it into, and goes to
|
|
68
88
|
* `onMessage` then, or, after the first, once its typing pause since the last one passed (`pace`). A
|
|
69
89
|
* paragraph inside a list waits for the first characters of the line after the blank, which say
|
|
70
|
-
* whether the list goes on
|
|
90
|
+
* whether the list goes on, and a paragraph that ends with a colon waits for the paragraph after it,
|
|
91
|
+
* so the two go out as one message. `end` hands over the last message and waits for every send.
|
|
71
92
|
*
|
|
72
93
|
* ```ts
|
|
73
94
|
* const paragraphs = messageStream({ onMessage: (text) => sendToShopper(text) });
|
package/dist/messages.js
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
* Text cut into the messages a shopper reads one at a time. A chat app sends each paragraph as its
|
|
3
3
|
* own message, so a reply written as three paragraphs arrives as three texts. A blank line ends a
|
|
4
4
|
* paragraph, with two exceptions that keep a block whole: a fenced code block, and a list whose
|
|
5
|
-
* items a blank line separates.
|
|
5
|
+
* items a blank line separates. A paragraph that leads in with a colon never stands alone: it goes out
|
|
6
|
+
* with the one after it (leadInJoiner).
|
|
6
7
|
*/
|
|
8
|
+
import { leadsIn, withoutLeadIn } from './sentences.js';
|
|
7
9
|
/** A line holding nothing but spaces or tabs. */
|
|
8
10
|
const BLANK = /^[ \t]*$/;
|
|
9
|
-
/** A list item: `-`,
|
|
10
|
-
const LIST_ITEM = /^ {0,3}(?:[
|
|
11
|
+
/** A list item: `-`, `*`, `+` or the bullet `•`, or a number followed by `.` or `)`, then a space or the line's end. */
|
|
12
|
+
const LIST_ITEM = /^ {0,3}(?:[-*+•]|\d{1,9}[.)])(?:[ \t]|$)/;
|
|
11
13
|
/** A list item whose marker a space already follows, the only kind a line still being written is sure to stay. */
|
|
12
|
-
const LIST_ITEM_SPACED = /^ {0,3}(?:[
|
|
14
|
+
const LIST_ITEM_SPACED = /^ {0,3}(?:[-*+•]|\d{1,9}[.)])[ \t]/;
|
|
13
15
|
/** A line indented under a list item: two spaces or a tab, then text. */
|
|
14
16
|
const INDENTED = /^(?: {2,}|\t)\S/;
|
|
15
17
|
/** The start of a line that can still grow into a list item or an indented line once more text arrives. */
|
|
16
|
-
const MAYBE_ITEM = /^ {0,3}(?:[
|
|
18
|
+
const MAYBE_ITEM = /^ {0,3}(?:[-*+•]|\d{1,9}[.)]?)?$|^[ \t]*$/;
|
|
17
19
|
/** A code fence: three or more backticks or tildes, indented by at most three spaces. */
|
|
18
20
|
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
19
21
|
/** How the line after a blank line inside a list reads: it continues the list, it ends it, or it is too short to tell. */
|
|
@@ -30,6 +32,29 @@ function closesFence(line, open) {
|
|
|
30
32
|
const match = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
31
33
|
return !!match && match[1][0] === open[0] && match[1].length >= open.length;
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Joins each message that leads in with a colon (`two options came in under $20:`) to the message after it, so a shopper
|
|
37
|
+
* never reads a bubble that ends on a colon: the lead-in and its first item or list arrive as one message, a line apart.
|
|
38
|
+
* `add` returns the message to send, or null while it holds a lead-in for the next one. `end` returns what a held lead-in
|
|
39
|
+
* leaves once nothing followed it, such as when every item it led into went because the cards show them: the text less
|
|
40
|
+
* its colon sentence (withoutLeadIn), or null when nothing is left. On 2026-10-27 a chat showed "two options came in
|
|
41
|
+
* under $20:" as a bubble of its own, above the item it led into.
|
|
42
|
+
*/
|
|
43
|
+
export function leadInJoiner() {
|
|
44
|
+
let held = null;
|
|
45
|
+
return {
|
|
46
|
+
add(message) {
|
|
47
|
+
const whole = held === null ? message : `${held}\n${message}`;
|
|
48
|
+
held = leadsIn(whole) ? whole : null;
|
|
49
|
+
return held === null ? whole : null;
|
|
50
|
+
},
|
|
51
|
+
end() {
|
|
52
|
+
const rest = held === null ? '' : withoutLeadIn(held);
|
|
53
|
+
held = null;
|
|
54
|
+
return rest || null;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
33
58
|
/**
|
|
34
59
|
* Reads text line by line and closes each message once nothing later can change it. It keeps only
|
|
35
60
|
* the message being built, so text fed in pieces costs what the text costs once.
|
|
@@ -102,19 +127,47 @@ class Scanner {
|
|
|
102
127
|
/**
|
|
103
128
|
* The text as separate messages, in order: one per paragraph, each trimmed, with empty ones left
|
|
104
129
|
* out. One or more blank lines end a paragraph. A fenced code block stays one message whatever blank
|
|
105
|
-
* lines it holds, and so does a list whose items blank lines separate.
|
|
130
|
+
* lines it holds, and so does a list whose items blank lines separate. A paragraph that ends with a
|
|
131
|
+
* colon goes with the paragraph after it, one line apart, and loses its colon sentence when nothing
|
|
132
|
+
* follows it, so no message ends on a colon.
|
|
106
133
|
*
|
|
107
134
|
* ```ts
|
|
108
135
|
* toMessages('Found two.\n\nWant the first one?'); // ['Found two.', 'Want the first one?']
|
|
136
|
+
* toMessages('two came in under $20:\n\n• Everydaze, $8.99'); // ['two came in under $20:\n• Everydaze, $8.99']
|
|
109
137
|
* ```
|
|
110
138
|
*/
|
|
111
139
|
export function toMessages(text) {
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
return joinLeadIns(paragraphsOf(text));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The text's paragraphs as `toMessages` cuts them, before any lead-in joins the paragraph after it: the units Agentcard
|
|
144
|
+
* and a model write, and what a count of the messages a shopper read is checked against, paragraph for paragraph.
|
|
145
|
+
*/
|
|
146
|
+
export function paragraphsOf(text) {
|
|
147
|
+
const paragraphs = [];
|
|
148
|
+
const scanner = new Scanner((paragraph) => paragraphs.push(paragraph));
|
|
114
149
|
for (const line of String(text ?? '').replace(/\r\n?/g, '\n').split('\n'))
|
|
115
150
|
scanner.line(line);
|
|
116
151
|
scanner.end();
|
|
117
|
-
return
|
|
152
|
+
return paragraphs;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Messages in order, each one that leads in with a colon joined to the ones after it by one joiner across them all
|
|
156
|
+
* (leadInJoiner), so a lead-in that is a whole message of its own, as one of Agentcard's `messages` can be, still goes
|
|
157
|
+
* with the message after it, and a lead-in nothing follows at the very end loses its colon sentences.
|
|
158
|
+
*/
|
|
159
|
+
export function joinLeadIns(messages) {
|
|
160
|
+
const leads = leadInJoiner();
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const message of messages) {
|
|
163
|
+
const ready = leads.add(message);
|
|
164
|
+
if (ready)
|
|
165
|
+
out.push(ready);
|
|
166
|
+
}
|
|
167
|
+
const rest = leads.end();
|
|
168
|
+
if (rest)
|
|
169
|
+
out.push(rest);
|
|
170
|
+
return out;
|
|
118
171
|
}
|
|
119
172
|
/** The pause before a message, as someone texting takes it: 300 ms, plus 12 ms a character, at most 1.5 seconds. */
|
|
120
173
|
const TYPING_BASE_MS = 300;
|
|
@@ -171,7 +224,8 @@ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
171
224
|
* moment the blank line that ends it arrives, whatever the pieces split it into, and goes to
|
|
172
225
|
* `onMessage` then, or, after the first, once its typing pause since the last one passed (`pace`). A
|
|
173
226
|
* paragraph inside a list waits for the first characters of the line after the blank, which say
|
|
174
|
-
* whether the list goes on
|
|
227
|
+
* whether the list goes on, and a paragraph that ends with a colon waits for the paragraph after it,
|
|
228
|
+
* so the two go out as one message. `end` hands over the last message and waits for every send.
|
|
175
229
|
*
|
|
176
230
|
* ```ts
|
|
177
231
|
* const paragraphs = messageStream({ onMessage: (text) => sendToShopper(text) });
|
|
@@ -247,10 +301,15 @@ export function messageStream(options) {
|
|
|
247
301
|
});
|
|
248
302
|
}
|
|
249
303
|
};
|
|
250
|
-
|
|
304
|
+
// A paragraph that leads in with a colon waits for the one after it, so the two go out as one message.
|
|
305
|
+
const leads = leadInJoiner();
|
|
306
|
+
const send = (message) => {
|
|
307
|
+
if (!message)
|
|
308
|
+
return;
|
|
251
309
|
queue.push(message);
|
|
252
310
|
kick();
|
|
253
|
-
}
|
|
311
|
+
};
|
|
312
|
+
const scanner = new Scanner((paragraph) => send(leads.add(paragraph)));
|
|
254
313
|
return {
|
|
255
314
|
push(delta) {
|
|
256
315
|
if (ended)
|
|
@@ -273,6 +332,7 @@ export function messageStream(options) {
|
|
|
273
332
|
scanner.line(line);
|
|
274
333
|
open = '';
|
|
275
334
|
scanner.end();
|
|
335
|
+
send(leads.end());
|
|
276
336
|
}
|
|
277
337
|
while (running)
|
|
278
338
|
await running;
|
package/dist/model-view.d.ts
CHANGED
|
@@ -107,6 +107,18 @@ export interface BuyToolModelView {
|
|
|
107
107
|
product_id: string | null;
|
|
108
108
|
in_cart: boolean;
|
|
109
109
|
} | null;
|
|
110
|
+
/**
|
|
111
|
+
* The choice the reply asks the shopper to make, absent when it asks none: each group's name and its values with their
|
|
112
|
+
* extra price, such as `Gluten-free bread (+$2.46)`. List them one per line, each starting with `• `, under the question.
|
|
113
|
+
*/
|
|
114
|
+
choice?: {
|
|
115
|
+
question: string | null;
|
|
116
|
+
groups: Array<{
|
|
117
|
+
name: string;
|
|
118
|
+
required: boolean;
|
|
119
|
+
options: string[];
|
|
120
|
+
}>;
|
|
121
|
+
};
|
|
110
122
|
/** The stores the reply presents to choose from, in the order it names them. */
|
|
111
123
|
stores: BuyToolModelStore[];
|
|
112
124
|
/** How many store pages of the stores above the shopper holds, 0 when none went out. */
|
|
@@ -116,6 +128,17 @@ export interface BuyToolModelView {
|
|
|
116
128
|
store_id: string;
|
|
117
129
|
selected: boolean;
|
|
118
130
|
} | null;
|
|
131
|
+
/**
|
|
132
|
+
* Present when this answer went ahead on the yes the shopper already gave in the chat, after a step they finished
|
|
133
|
+
* outside it, or on their pick of what is in the cart (`picked`): the total they said yes to, or the chat last showed
|
|
134
|
+
* them, the cart's total now, and whether it changed. Never ask for their yes again.
|
|
135
|
+
*/
|
|
136
|
+
yes_carried?: {
|
|
137
|
+
said_yes_to: string;
|
|
138
|
+
total: string;
|
|
139
|
+
total_changed: boolean;
|
|
140
|
+
picked?: true;
|
|
141
|
+
};
|
|
119
142
|
}
|
|
120
143
|
/**
|
|
121
144
|
* The text with every link taken out: a Markdown link keeps its label, an address with a path or a scheme goes
|
|
@@ -183,6 +206,16 @@ export declare function expiresIn(iso: string | null | undefined, now?: number):
|
|
|
183
206
|
* reads as `needs_input`.
|
|
184
207
|
*/
|
|
185
208
|
export declare function forModel(result: BuyToolResult): BuyToolModelView;
|
|
209
|
+
/**
|
|
210
|
+
* The fixed short line for an answer whose next step is a link the shopper already holds: `add your card to place the
|
|
211
|
+
* order`, `sign in to DoorDash to place your order`, or `approve the payment to place your order (up to $19.02)`, with
|
|
212
|
+
* the most the shopper can be charged in the parentheses when the cart names it. After the shopper's own words put the
|
|
213
|
+
* cart together, a line about the cart comes first, as its own message. After a yes the turn carried past a step outside
|
|
214
|
+
* the chat, a total that changed opens the ask: `it's $17.89 with fees and tax now. approve the payment ...`. Null for any
|
|
215
|
+
* other answer, when the link did not go out, when the shopper already read Agentcard's own ask through `onText`, when a
|
|
216
|
+
* card or approval step has no cart to place, and when the answer also showed products.
|
|
217
|
+
*/
|
|
218
|
+
export declare function stepAsWritten(result: BuyToolResult): string | null;
|
|
186
219
|
/**
|
|
187
220
|
* Agentcard's reply, ready for the shopper as written, when the answer only asks them something: a
|
|
188
221
|
* `needs_input` answer with a reply, such as a product list, a size or a store to pick, or a cart's
|
|
@@ -191,25 +224,39 @@ export declare function forModel(result: BuyToolResult): BuyToolModelView;
|
|
|
191
224
|
* itself for your model, such as the note that the cart changed or a `nextStep` turn's ask for the
|
|
192
225
|
* shopper's yes: only the reply Agentcard sent, as `envelope` holds it, is written for the shopper.
|
|
193
226
|
* Null as well once the shopper read the reply through `onText` (`shopper_already_read_reply`).
|
|
227
|
+
* An answer whose next step is a link the call handed over reads as that step's fixed short line
|
|
228
|
+
* instead (`stepAsWritten`), whoever wrote the reply.
|
|
194
229
|
*
|
|
195
230
|
* ```ts
|
|
196
231
|
* replyAsWritten(result); // 'found two at Amazon. want the first one?'
|
|
197
232
|
* ```
|
|
198
233
|
*/
|
|
199
234
|
export declare function replyAsWritten(result: BuyToolResult): string | null;
|
|
235
|
+
/** A card the shopper holds: a product's or a store's name and, for a product, its price as the model reads it (`$22.89`, `from $10.69`). */
|
|
236
|
+
export interface ShownCard {
|
|
237
|
+
name: string;
|
|
238
|
+
price?: string | null;
|
|
239
|
+
}
|
|
200
240
|
/**
|
|
201
241
|
* Agentcard's reply without the list its cards already show the shopper: each list line that names a product or
|
|
202
242
|
* store whose card went out, the line that led into that list (`here are a few:`, `other choices:`), a sentence that
|
|
203
|
-
* points at a number the list gave (`the dishwand in #8`),
|
|
204
|
-
*
|
|
205
|
-
*
|
|
243
|
+
* points at a number the list gave (`the dishwand in #8`), a sentence that goes on to more of the list (`other 6-packs
|
|
244
|
+
* are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
|
|
245
|
+
* further sentence that names another card. A list written on one line goes the same way, item by item (inlineList),
|
|
246
|
+
* and so does a list line written without a bullet, whole, up to the question on it. What is left is the reply around
|
|
247
|
+
* the list, such as `found these at amazon.` and `which one do you want?`. A reply whose cards went out names none of
|
|
248
|
+
* them twice over. A list that keeps an item for a product no card shows keeps its lead-in, and the two read as one
|
|
249
|
+
* message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
|
|
250
|
+
*
|
|
251
|
+
* Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,
|
|
252
|
+
* a reply's short name for the product, such as `Planet Oat Extra Creamy ($22.89)`, reads as its card (cardsNamed).
|
|
206
253
|
*
|
|
207
254
|
* ```ts
|
|
208
255
|
* withoutCardList('Two I'd suggest:\n- Duracell AA, 24 pack, $20.78\n\nWhich one?', ['Duracell AA Batteries, 24 Count']);
|
|
209
256
|
* // 'Which one?'
|
|
210
257
|
* ```
|
|
211
258
|
*/
|
|
212
|
-
export declare function withoutCardList(reply: string, cardNames: readonly string[]): string;
|
|
259
|
+
export declare function withoutCardList(reply: string, cardNames: readonly (string | ShownCard)[]): string;
|
|
213
260
|
/**
|
|
214
261
|
* A thrown error as the model may read it: the same message with every link
|
|
215
262
|
* taken out, and the original error on `cause` for your logs. A TypeError
|