@agent-cards/commerce-sdk 0.17.0 → 0.18.2
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 +21 -20
- package/dist/ack.d.ts +7 -0
- package/dist/ack.js +42 -0
- package/dist/buy-tool.d.ts +22 -1
- package/dist/buy-tool.js +120 -25
- package/dist/client.d.ts +39 -0
- package/dist/client.js +2 -1
- package/dist/direct.js +15 -2
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ console.log(await result.getText());
|
|
|
136
136
|
|
|
137
137
|
A purchase needs a link at three moments: the shopper approves the purchase on an approval page, a new shopper adds a card on a Vault page, and a shopper signs in to a store's own account on its login page before Agentcard can buy there. Each link's `kind` says which: `approval`, `vault` or `store_login`, and a `store_login` link names the store in `store`. The tool hands each link to `sendLink` and waits for it, so the shopper holds the link before the model answers. The model reads `link_sent: true` and never the link itself, because a tool runner puts each answer straight into the model's context, and a model that holds a link writes it into the chat.
|
|
138
138
|
|
|
139
|
-
The tool also hands `sendLink` a `product` link for each product a search finds, up to eight per answer. The link carries the product's page at its store in `url`, the product's name in `title`, its price in `price`, such as `$12.99`, or `from $10.69` for a product priced by a size or another choice the shopper makes, such as a burger whose patty count sets its price, Agentcard's id for it in `productId`, what taking it does in `action` (`reserve` for a restaurant table, `book` for a flight, room, class or ticket, `buy` for anything else, so a button can say Reserve instead of Buy this), its picture in `imageUrl`, or null when the store sent none, and every photo of the product in `images`, `imageUrl` first and at most eight, so a card can show the others as thumbnails that swap the main picture. `images` holds one entry for a product with one photo and none for a product with none. Every price the tool writes, in a link or in the model's view, uses the currency Agentcard names for the cart or the product, in that currency's own minor units: `1331` is `$13.31` in USD, `CA$13.31` in CAD and `¥1,331` in JPY. A price whose currency Agentcard does not name shows with no symbol. The link arrives beside the reply, and you decide how to show it: as a card, in a list, or as a text with the URL. Each product gets its own link, so the three dishes a restaurant's reply offers are three cards, though they share the store's one page. A flight has no page at a store, so each flight a search finds is a `product` link whose `url` is null, with the airline, the route and its times in `title`: show it as a card with no link to open, and hand its `productId` back as the shopper's pick. The model reads `product_links_sent` and never a URL. The tool's description tells it that the shopper already sees those products as cards, so its reply never lists them again by name or price: it writes one short line, such as which one they want, and a shopper who asks for links hears that the links are there. When a later answer in the conversation only stands beside the same products, as a cart answer does, the tool sends none of their links again. When a later turn's reply presents them again, as the answer to a repeated ask does, the tool sends their links again, so every product the reply names arrives with its link. A product link your `sendLink` throws on counts as not sent, and the call goes on.
|
|
139
|
+
The tool also hands `sendLink` a `product` link for each product a search finds, up to eight per answer. The link carries the product's page at its store in `url`, the product's name in `title`, its price in `price`, such as `$12.99`, or `from $10.69` for a product priced by a size or another choice the shopper makes, such as a burger whose patty count sets its price, Agentcard's id for it in `productId`, what taking it does in `action` (`reserve` for a restaurant table, `book` for a flight, room, class or ticket, `buy` for anything else, so a button can say Reserve instead of Buy this), its picture in `imageUrl`, or null when the store sent none, and every photo of the product in `images`, `imageUrl` first and at most eight, so a card can show the others as thumbnails that swap the main picture. `images` holds one entry for a product with one photo and none for a product with none. The link names the store that sells the product in `store`, such as `Walmart`, and the store's own website in `storeDomain`, such as `walmart.com` or `jadeyoga.com`, or null when Agentcard knows none, such as a restaurant's: show `storeDomain` on the card as where the product comes from, never the host of `url`, which can be a tracking link. Every price the tool writes, in a link or in the model's view, uses the currency Agentcard names for the cart or the product, in that currency's own minor units: `1331` is `$13.31` in USD, `CA$13.31` in CAD and `¥1,331` in JPY. A price whose currency Agentcard does not name shows with no symbol. The link arrives beside the reply, and you decide how to show it: as a card, in a list, or as a text with the URL. Each product gets its own link, so the three dishes a restaurant's reply offers are three cards, though they share the store's one page. A flight has no page at a store, so each flight a search finds is a `product` link whose `url` is null, with the airline, the route and its times in `title`: show it as a card with no link to open, and hand its `productId` back as the shopper's pick. The model reads `product_links_sent` and never a URL. The tool's description tells it that the shopper already sees those products as cards, so its reply never lists them again by name or price: it writes one short line, such as which one they want, and a shopper who asks for links hears that the links are there. When a later answer in the conversation only stands beside the same products, as a cart answer does, the tool sends none of their links again. When a later turn's reply presents them again, as the answer to a repeated ask does, the tool sends their links again, so every product the reply names arrives with its link. A product link your `sendLink` throws on counts as not sent, and the call goes on.
|
|
140
140
|
|
|
141
141
|
`productId` holds no link: it is the same `product_id` the model reads for that product, and the page is `url`. An answer that only adds a product the shopper picked lists that product alone, so it sends no card for a product nobody offered on that turn, and the pages the search already sent still count as sent on the answers after it.
|
|
142
142
|
|
|
@@ -242,29 +242,30 @@ Every list in the reply comes as bullet lines that start with `• `, the choice
|
|
|
242
242
|
|
|
243
243
|
An answer that asks the shopper to choose, such as a sandwich's bread, a shirt's size or a store's own size question, carries `result.choice`: each group with every value, its option id and its extra price in `price_delta_cents`. The reply shown as written lists each value on its own `• ` line under the question, with its extra price, and so does one more `onText` message when the streamed reply named the values inside a sentence. `withChoice(text, result.choice)` lists them under any text of your own.
|
|
244
244
|
|
|
245
|
-
###
|
|
245
|
+
### Let Agentcard answer shopping first
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
Send the shopper's message to Agentcard before your model runs, and run your model only when the message is yours:
|
|
248
248
|
|
|
249
249
|
```ts
|
|
250
|
-
const turn = buy.turn(message, {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
history = [...messages, { role: 'assistant', content: turn.replyAsWritten }];
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
250
|
+
const turn = buy.turn(message, {
|
|
251
|
+
onItem: (item) => (item.kind === 'text' ? sendText(shopper.id, item.text) : sendCard(shopper.id, item.link)), // your code
|
|
252
|
+
});
|
|
253
|
+
if (await turn.handled()) {
|
|
254
|
+
// Agentcard answered the shopper: keep the exchange, so your model reads it on the next message.
|
|
255
|
+
const said = turn.items.flatMap((item) => (item.kind === 'text' ? [item.text] : [])).join('\n\n');
|
|
256
|
+
history = [...turn.messages(history), { role: 'assistant', content: said }];
|
|
257
|
+
return;
|
|
261
258
|
}
|
|
262
|
-
// Otherwise run your model on
|
|
259
|
+
// Otherwise run your model on turn.messages(history), as before.
|
|
263
260
|
```
|
|
264
261
|
|
|
265
|
-
|
|
262
|
+
Agentcard answers a request to buy or book, with or without a verb (`Printer paper, one ream`), a pick of what it just showed (a card's tap, `the second one`, `1 and 3`), an answer to its question (the address, a code, `medium`) and a yes to a total. When its reply only asks the shopper something, the turn sends it through `onItem`, after the cards and the card, sign-in or approval link it handed over, and `handled()` resolves `true`: no model call before the search, and none after it.
|
|
263
|
+
|
|
264
|
+
Every other message comes back from Agentcard as yours in about 0.1 to 0.2 seconds, with no search, and `handled()` resolves `false`: small talk, a question, a change of mind such as `no` or `wait`, and anything about the account or an order. An answer your model has to word, such as an order placed or a refusal, resolves `false` too, and `turn.messages(history)` hands your model that answer with the shopper's message, so it words it and calls the tool no second time.
|
|
265
|
+
|
|
266
|
+
`handled()` sends one call, once, and a second call resolves as the first did. A turn `buy.nextStep` opened resolves `false` and sends nothing: take it as "Take the next step once the shopper finishes one outside the chat" shows.
|
|
266
267
|
|
|
267
|
-
|
|
268
|
+
You no longer need `direct` or `replyAsWritten` for this. They still work as they did: `direct: true` sets `turn.direct` to the call the tool's own rule reads in a message (`directCall(text, context)`), and `replyAsWritten: true` sets `turn.replyAsWritten`, for an app that sends the call itself. An app that never calls `handled()` sends nothing until its model calls the tool.
|
|
268
269
|
|
|
269
270
|
### Hand the app the turn in order
|
|
270
271
|
|
|
@@ -361,7 +362,7 @@ async function runModel(shopperId: string, turn: BuyToolTurn) {
|
|
|
361
362
|
| Saving a card, with no yes and no pick standing | The total and the ask for the yes: `your Souvla chicken salad comes to $17.43 with fees. should i go ahead and place the order?` |
|
|
362
363
|
| Signing in to the store, with a card still to add | The add-card link: `next, add a card with the link i sent. it works for about 24 hours.` |
|
|
363
364
|
| Signing in to the store, with a card on file | The total and the ask for the yes, or the approval link after a yes that stopped for the sign-in |
|
|
364
|
-
| Signing in or saving a card, after a yes, when the store priced its fees and tax in since | The new total
|
|
365
|
+
| Signing in or saving a card, after a yes, when the store priced its fees and tax in since | The new total and the ask for the yes, with no link: `The cart changed after your yes: it comes to $17.89 with tax and shipping now. Nothing was paid. Say yes to pay $17.89.` |
|
|
365
366
|
| Declining the approval | `you declined the payment, so you haven't been charged. your chicken salad is still in the cart, so just say yes if you want a new approval link, or tell me what to change` |
|
|
366
367
|
|
|
367
368
|
With `direct` and `replyAsWritten` on the tool, a step needs no model when its answer is a link: send `step.run(step.direct.input)` first, and when `step.replyAsWritten` is set, post it, with `step.links`, in place of the model's answer. When it is null, as for the ask for a yes, run your model as above.
|
|
@@ -369,8 +370,8 @@ With `direct` and `replyAsWritten` on the tool, a step needs no model when its a
|
|
|
369
370
|
The turn moves the purchase only up to the shopper's own decision, and the tool holds it there whatever the model calls:
|
|
370
371
|
|
|
371
372
|
- No call of the turn sends an ask to Agentcard, which could read the model's words as the shopper's go-ahead. Each call reads where the purchase stands and answers with the next step: the card link the sign-in held back, or the cart and the ask for the shopper's yes.
|
|
372
|
-
- A confirm goes in only for the yes the shopper already gave in the chat, such as "lets place it", when that confirm stopped for the card or the sign-in they just finished, and the shopper is never asked for that yes again. It goes on past each step, the sign-in and then the card, as long as the cart holds the same items, in the same quantities and at the same prices, at the same store: a total that moved
|
|
373
|
-
- A shopper who picked what is in the cart, by tapping a product's card or asking to buy it, and since answered only the checkout's questions (an address, a phone number, a name, an email, a yes to an address read back to them) or said a plain go-ahead, gave the same go-ahead: saving the card or signing in to the store confirms that cart by its hash as it stands, with no ask for a yes (`yes_carried.picked`). A cart whose items, quantities, item prices or store changed since the pick, any other message, such as "actually wait" or a question, and a declined approval take it back.
|
|
373
|
+
- A confirm goes in only for the yes the shopper already gave in the chat, such as "lets place it", when that confirm stopped for the card or the sign-in they just finished, and the shopper is never asked for that yes again. It goes on past each step, the sign-in and then the card, as long as the cart holds the same items, in the same quantities and at the same prices, at the same store. Each confirm names the cart's hash as it stands, and in `carried` the hash of the cart the shopper said yes to: Agentcard pays only that cart, so a total that moved because the store priced its fees, tax or delivery in comes back with the new total and the ask for the shopper's yes, and nothing is paid. A cart whose items, quantities or store changed, a shopper message after the yes that is not a plain go-ahead, such as "wait, make it two", and a declined approval each take the yes back, and the shopper is asked again. Any other confirm the model sends is refused by the tool, and nothing reaches Agentcard.
|
|
374
|
+
- A shopper who picked what is in the cart, by tapping a product's card or asking to buy it, and since answered only the checkout's questions (an address, a phone number, a name, an email, a yes to an address read back to them) or said a plain go-ahead, gave the same go-ahead: saving the card or signing in to the store confirms that cart by its hash as it stands, with no ask for a yes (`yes_carried.picked`). The confirm names in `carried` the cart the chat showed, and a cart the store priced again since comes back with its total and the ask for a yes. A cart whose items, quantities, item prices or store changed since the pick, any other message, such as "actually wait" or a question, and a declined approval take it back.
|
|
374
375
|
- The approval in the Vault is still the shopper's: the confirm sends the approval link, which names the most they can be charged, and the order goes in only once they approve it.
|
|
375
376
|
- A shopper who was not connected yet when they asked gets their own first ask run, in their words, once their card is saved.
|
|
376
377
|
|
package/dist/ack.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export declare const CHITCHAT: RegExp;
|
|
|
20
20
|
* can you buy aa batteries from`, is a search and keeps its reply.
|
|
21
21
|
*/
|
|
22
22
|
export declare const CAPABILITY: RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* True when the shopper's words ask for other products like one they saw, such as `More like the KODAK AA Alkaline Batteries (60
|
|
25
|
+
* Pack)`, `something similar to the first one`, `alternatives to the Duracell` or `cheaper than the KODAK one`: a new search,
|
|
26
|
+
* never a pick of the product it names. `names` are the products the shopper saw: their words are read out first, so a product
|
|
27
|
+
* whose own name says `alternative` or `similar` is still picked by its tap.
|
|
28
|
+
*/
|
|
29
|
+
export declare function asksForSimilar(text: string, names?: readonly string[]): boolean;
|
|
23
30
|
/**
|
|
24
31
|
* True when the message names one of these stores and nothing else to buy: the store's whole name, or
|
|
25
32
|
* two words of it in a row, such as `el grullense` for `Tacos El Grullense`, alone or after a frame such
|
package/dist/ack.js
CHANGED
|
@@ -140,6 +140,45 @@ const PLACE = '(?:first|second|third|fourth|fifth|sixth|last|other|\\d{1,2}(?:st
|
|
|
140
140
|
const PICK_REF = new RegExp(`^(?:(?:the|that|this) (?:[\\w'$.-]+ ){0,3}one\\b|(?:that|this|those|these|both|it)(?: ones?)?$|the ${PLACE}(?: (?:one|option|item|product|choice|result))?$|${PLACE}(?: (?:one|option|item|product|choice|result)\\b|$)|(?:option|number|item|choice|#) ?#?\\d{1,2}\\b|\\d{1,2}$)`);
|
|
141
141
|
/** A budget word in front of a price, so `under $13.49` stays a search. */
|
|
142
142
|
const BUDGET = /(?:under|below|than|max|maximum|around|about|to|budget|within|over|above|for|most)\s*$/;
|
|
143
|
+
/** What a request for other products opens with before `more like`: `show me more like the KODAK one`, `any more like it`. */
|
|
144
|
+
const MORE_LIKE_LEAD = "(?:(?:ok|okay|hey|hi|so|um|uh|also|and|cool|great|nice|hmm|actually|yes|yeah|can you|could you|can i|could i|i'?d like to|i want to|please)\\s+)*(?:(?:show|find|get|give|send)(?: me| us)?\\s+|(?:i want|i'?d like|i would like|i need|i'?m looking for|im looking for|looking for|search for|look for|do you have|got|have you got|are there|is there|see)\\s+)?(?:some\\s+|any\\s+)?";
|
|
145
|
+
/**
|
|
146
|
+
* Words that ask for other products like one the shopper saw, never for that product: `more like the KODAK AA (60 Pack)`,
|
|
147
|
+
* `something similar`, `alternatives to the Duracell`, `cheaper than the KODAK one`, `like the first one but in blue`, and in
|
|
148
|
+
* Spanish `algo parecido al KODAK`, `más como este`. A number or a price after the words is a correction (`more like $20`,
|
|
149
|
+
* `something like 2`), and stays out.
|
|
150
|
+
*/
|
|
151
|
+
const SIMILAR = [
|
|
152
|
+
new RegExp(`^${MORE_LIKE_LEAD}more (?:like|similar)\\b(?!\\s*(?:\\$|\\d))`),
|
|
153
|
+
/\bmore like (?:the|this|that|these|those|it|them|one|ones)\b/,
|
|
154
|
+
/\b(?:something|anything|stuff|things|ones|others|other ones|other options|options|products|items|other products|other items|more)(?: else)? (?:like|similar to)\b(?!\s*(?:\$|\d))/,
|
|
155
|
+
/\bsimilar (?:to|ones?|items?|products?|options?|stuff|things|styles?|kinds?|types?|brands?|models?)\b|\b(?:something|anything|more|some|any|show me|find|see|got|have|want|need|others?) similar\b|^similar\b/,
|
|
156
|
+
/\b(?:an?|some|any|other|cheaper|good|better|similar) alternatives?\b|\balternatives? (?:to|for|like)\b|^alternatives?\b/,
|
|
157
|
+
/\b(?:cheaper|less expensive|more affordable|lower[- ]priced|lower cost) than\b(?!\s*(?:\$|\d|(?:that|this|my|the) (?:price|budget)))/,
|
|
158
|
+
/\blike (?:the|this|that|these|those|it)\b.*\bbut\b|\bbut (?:cheaper|less expensive|more affordable)\b/,
|
|
159
|
+
/\b(?:anything|something|others?|options?|ones?|products?|items?)(?: else)? (?:other than|besides|apart from)\b/,
|
|
160
|
+
/\bparecid[oa]s?\b|\bsimilar(?:es)? a[ls]?\b|\balgo (?:como|parecido|similar|as[ií])\b(?!\s*(?:\$|\d))|\bm[aá]s (?:como|del estilo)\b|\botr[oa]s? (?:opciones |cosas |productos |marcas )?(?:como|parecid[oa]s?|similares)\b|\balternativas?\b|\bm[aá]s barat[oa]s? que\b|\bcomo (?:el|la|los|las|este|esta|estos|estas|ese|esa)\b.*\bpero\b/,
|
|
161
|
+
];
|
|
162
|
+
/** Text as lowercase words with single spaces, accents kept, as `asksForSimilar` reads it. */
|
|
163
|
+
function similarWords(text) {
|
|
164
|
+
return plain(String(text ?? '')).replace(/[^\p{L}\p{N}\s$#'-]+/gu, ' ').replace(/\s+/g, ' ').trim();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* True when the shopper's words ask for other products like one they saw, such as `More like the KODAK AA Alkaline Batteries (60
|
|
168
|
+
* Pack)`, `something similar to the first one`, `alternatives to the Duracell` or `cheaper than the KODAK one`: a new search,
|
|
169
|
+
* never a pick of the product it names. `names` are the products the shopper saw: their words are read out first, so a product
|
|
170
|
+
* whose own name says `alternative` or `similar` is still picked by its tap.
|
|
171
|
+
*/
|
|
172
|
+
export function asksForSimilar(text, names = []) {
|
|
173
|
+
let said = ` ${similarWords(text)} `;
|
|
174
|
+
for (const name of names) {
|
|
175
|
+
const own = similarWords(name);
|
|
176
|
+
if (own.length >= 3)
|
|
177
|
+
said = said.split(` ${own} `).join(' ');
|
|
178
|
+
}
|
|
179
|
+
said = said.replace(/\s+/g, ' ').trim();
|
|
180
|
+
return said !== '' && SIMILAR.some((pattern) => pattern.test(said));
|
|
181
|
+
}
|
|
143
182
|
/**
|
|
144
183
|
* True for a pick of a product the shopper already saw: a frame such as `i'll take` or `i want the`, a
|
|
145
184
|
* reference such as `that one`, `the second` or `#2`, or a product title with its price, as a product
|
|
@@ -147,6 +186,9 @@ const BUDGET = /(?:under|below|than|max|maximum|around|about|to|budget|within|ov
|
|
|
147
186
|
* gets no instant reply.
|
|
148
187
|
*/
|
|
149
188
|
function isPick(line, words) {
|
|
189
|
+
// `More like the KODAK AA (60 Pack) ($26.05)` names a card to search for others like it: a new search, with its instant reply.
|
|
190
|
+
if (asksForSimilar(line))
|
|
191
|
+
return false;
|
|
150
192
|
if (/\(\s*\$\s*\d[\d,]*(?:\.\d{2})?\s*\)/.test(line))
|
|
151
193
|
return true;
|
|
152
194
|
const price = line.match(/\$\s*\d[\d,]*\.\d{2}\s*$/);
|
package/dist/buy-tool.d.ts
CHANGED
|
@@ -76,9 +76,16 @@ export interface BuyToolLink {
|
|
|
76
76
|
expiresAt: string | null;
|
|
77
77
|
/**
|
|
78
78
|
* On a store login link: the store's name, or null when Agentcard named none. On an approval link:
|
|
79
|
-
* the store whose cart it approves, when the answer names the cart.
|
|
79
|
+
* the store whose cart it approves, when the answer names the cart. On a product link: the name of
|
|
80
|
+
* the store that sells it, such as `Walmart` or `jadeyoga.com`, or null when Agentcard named none.
|
|
80
81
|
*/
|
|
81
82
|
store?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* On a product link: the website of the store that sells it, such as `walmart.com` or `jadeyoga.com`, to show on the card
|
|
85
|
+
* as where the product comes from. It is never the host of `url`, which can be a tracking link that names no store. Null
|
|
86
|
+
* when Agentcard knows no website for the store, such as a restaurant, a venue or an airline.
|
|
87
|
+
*/
|
|
88
|
+
storeDomain?: string | null;
|
|
82
89
|
/** On a product link: the product's name. */
|
|
83
90
|
title?: string;
|
|
84
91
|
/**
|
|
@@ -156,6 +163,8 @@ export interface BuyToolProduct {
|
|
|
156
163
|
/** Every photo of the product, `imageUrl` first, at most eight; empty when it has none. */
|
|
157
164
|
images: string[];
|
|
158
165
|
store: string | null;
|
|
166
|
+
/** The website of the store that sells it, such as `walmart.com`, or null when Agentcard knows none. */
|
|
167
|
+
storeDomain?: string | null;
|
|
159
168
|
/** The product's page at its store, or null when Agentcard sent none. */
|
|
160
169
|
url: string | null;
|
|
161
170
|
/**
|
|
@@ -677,6 +686,16 @@ export interface BuyToolTurn extends Omit<BuyTool, 'instantAck' | 'instantReacti
|
|
|
677
686
|
* is set; when it is null, hand your model the call and its result, and its words answer the shopper.
|
|
678
687
|
*/
|
|
679
688
|
readonly direct: DirectCall | null;
|
|
689
|
+
/**
|
|
690
|
+
* Sends the shopper's message to Agentcard and resolves `true` when Agentcard answered the shopper: a request to buy, a
|
|
691
|
+
* pick of what it showed, an answer to its question or a yes to a total, whose reply only asks the shopper something
|
|
692
|
+
* and went out with `say()`. Resolves `false` when your model answers: Agentcard read the message as yours, such as
|
|
693
|
+
* small talk or a question about an order, in well under a second and with no model call, or it answered with an order
|
|
694
|
+
* placed or a refusal for your model to word, which `messages(history)` then hands it. Call it once, before your model
|
|
695
|
+
* runs; a second call resolves as the first did. A turn `buy.nextStep` opened, or one opened while a call still runs,
|
|
696
|
+
* resolves `false` and sends nothing.
|
|
697
|
+
*/
|
|
698
|
+
handled(): Promise<boolean>;
|
|
680
699
|
/**
|
|
681
700
|
* Every message and link the turn handed over, in the order they went out: the instant reply, each
|
|
682
701
|
* link, each message of Agentcard's that `onText` took, and each message written through `messageStream()`
|
|
@@ -712,6 +731,8 @@ export type BuyToolTurnMessage = {
|
|
|
712
731
|
* or an OpenAI Responses item, they come as an assistant message of their own.
|
|
713
732
|
*/
|
|
714
733
|
export declare function turnMessages<M extends object>(history: readonly M[] | null | undefined, told: readonly string[], shopperText: string): Array<M | BuyToolTurnMessage>;
|
|
734
|
+
/** The note a message carries to the model when Agentcard already answered it and left the words to the model (`handled()`). */
|
|
735
|
+
export declare function handedNote(answer: string): string;
|
|
715
736
|
/**
|
|
716
737
|
* The words a `nextStep` turn hands the model as the user's message: the event, marked as no one's
|
|
717
738
|
* typing, and the instruction to take the next step now in one short message, without the filler of
|
package/dist/buy-tool.js
CHANGED
|
@@ -39,7 +39,7 @@ import { choiceLines, leadsIntoList, messageForShopper, messagesListChoice, repl
|
|
|
39
39
|
import { joinLeadIns, leadInJoiner, messageStream, paceOf, paragraphsOf, sleep, toMessages, typingHook } from './messages.js';
|
|
40
40
|
import { isStale } from './shopper-message.js';
|
|
41
41
|
import { actionLinkId, isActionLink } from './action-links.js';
|
|
42
|
-
import { ACK_REACTION, ackAt, answersCheckout, answersQuestion, holdsBack, instantAck as instantAckOf, isNewRequest, isPickText, keepsYes, namesStore, nextTemplate, readsAsAckLine, slowStepLine } from './ack.js';
|
|
42
|
+
import { ACK_REACTION, ackAt, answersCheckout, answersQuestion, asksForSimilar, holdsBack, instantAck as instantAckOf, isNewRequest, isPickText, keepsYes, namesStore, nextTemplate, readsAsAckLine, slowStepLine } from './ack.js';
|
|
43
43
|
/** The action a product of `merchant` takes (BuyToolProductAction). */
|
|
44
44
|
export function productAction(merchant) {
|
|
45
45
|
if (merchant === 'reservations')
|
|
@@ -75,6 +75,10 @@ export function turnMessages(history, told, shopperText) {
|
|
|
75
75
|
out.push({ role: 'user', content: shopperText });
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
|
+
/** The note a message carries to the model when Agentcard already answered it and left the words to the model (`handled()`). */
|
|
79
|
+
export function handedNote(answer) {
|
|
80
|
+
return `[Agentcard already answered this message with the buy tool. Its answer, for you to tell the shopper in your words, without calling the tool again for it: ${answer}]`;
|
|
81
|
+
}
|
|
78
82
|
/** What the model reads of a message Agentcard posted into the chat: its text with every link taken out, as `forModel` takes them out. */
|
|
79
83
|
const modelText = (message) => stripLinks(String(message.text ?? ''));
|
|
80
84
|
/** How often the history held a message's text when the message first went in to the model. */
|
|
@@ -206,7 +210,7 @@ function yesAfter(result, yes) {
|
|
|
206
210
|
const seen = yes.shown ?? stopped;
|
|
207
211
|
if (!seen || (yes.shown && stopped && !sameOrder(yes.shown, stopped)))
|
|
208
212
|
return null;
|
|
209
|
-
return { conversationId: result.conversation_id, cart: stopped ?? seen, totalCents: seen.totalCents };
|
|
213
|
+
return { conversationId: result.conversation_id, cart: stopped ?? seen, totalCents: seen.totalCents, consentedHash: seen.hash };
|
|
210
214
|
}
|
|
211
215
|
/** The words of the call a `nextStep` turn's `direct` makes. They never reach Agentcard: the turn's calls send no ask. */
|
|
212
216
|
const NEXT_STEP_ASK = 'take the next step';
|
|
@@ -266,7 +270,22 @@ export function productsOf(turn) {
|
|
|
266
270
|
/** A product's card as `sendLink` hears it: the id the model reads and a pick takes, never the store page, which is `url`. A flight option has no page at a store: its card carries no url, and its productId picks it. */
|
|
267
271
|
function productLink(product) {
|
|
268
272
|
const productId = pickId(product);
|
|
269
|
-
return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), expiresAt: null, ...(product.availability ? { availability: product.availability } : {}) };
|
|
273
|
+
return { kind: 'product', url: product.url || null, title: product.name, price: productPrice(product), ...(productId ? { productId } : {}), action: product.action ?? 'buy', imageUrl: product.imageUrl, images: product.images ?? (product.imageUrl ? [product.imageUrl] : []), store: product.store ?? null, storeDomain: product.storeDomain ?? null, expiresAt: null, ...(product.availability ? { availability: product.availability } : {}) };
|
|
274
|
+
}
|
|
275
|
+
/** A host name: dot-separated labels of letters, digits and hyphens, ending in a letters-only top-level label. */
|
|
276
|
+
const STORE_HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
277
|
+
/**
|
|
278
|
+
* The website of the store a catalog item is at: the `domain` Agentcard sent, else, from an older API that sends none, the
|
|
279
|
+
* store's id when the id is itself a website (`jadeyoga.com`). Never Channel3's tracking host (buy.trychannel3.com). Null
|
|
280
|
+
* when neither names one: a store id such as `walmart` names no website by itself.
|
|
281
|
+
*/
|
|
282
|
+
function storeDomainOf(store) {
|
|
283
|
+
for (const raw of [store?.domain, store?.id]) {
|
|
284
|
+
const host = typeof raw === 'string' ? raw.trim().toLowerCase().replace(/^www\./, '').replace(/\.$/, '') : '';
|
|
285
|
+
if (STORE_HOST.test(host) && host !== 'trychannel3.com' && !host.endsWith('.trychannel3.com'))
|
|
286
|
+
return host;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
270
289
|
}
|
|
271
290
|
function productsOfCatalog(catalog) {
|
|
272
291
|
return (catalog.items ?? [])
|
|
@@ -282,6 +301,7 @@ function productsOfCatalog(catalog) {
|
|
|
282
301
|
images: item.image_urls ?? (item.image_url ? [item.image_url] : []),
|
|
283
302
|
...(item.flight ? { flight: item.flight } : {}),
|
|
284
303
|
store: item.store?.name ?? item.store?.id ?? catalog.merchant_name ?? null,
|
|
304
|
+
storeDomain: storeDomainOf(item.store),
|
|
285
305
|
url: item.url ?? null,
|
|
286
306
|
action: productAction(catalog.merchant),
|
|
287
307
|
linkSent: false,
|
|
@@ -533,6 +553,13 @@ function needsCard(link, body) {
|
|
|
533
553
|
envelope: body,
|
|
534
554
|
};
|
|
535
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* One shopper message's turn: the calls the model makes for it share its number, its instant reply and
|
|
558
|
+
* the callbacks its links and answers go to. `links` and `results` collect for a turn `buy.turn` opened,
|
|
559
|
+
* and are null for a call made straight on the tool.
|
|
560
|
+
*/
|
|
561
|
+
/** What a call throws when Agentcard handed the message back to the app's model (`BuyToolTurn.handled`). */
|
|
562
|
+
const HANDED_BACK = Symbol('handed back');
|
|
536
563
|
/** The way a turn hands over a link, or undefined when it has nowhere to send one. */
|
|
537
564
|
function senderOf(t) {
|
|
538
565
|
if (!t.sendLink && !t.links)
|
|
@@ -904,8 +931,10 @@ export function buyTool(options) {
|
|
|
904
931
|
let shownStoreNames = [];
|
|
905
932
|
/** True for a pick of a product or a store the shopper saw, by its name. */
|
|
906
933
|
const namesShown = (text) => namesShownProduct(text) || namesStore(text, shownStoreNames);
|
|
934
|
+
/** Whether a message asks for other products like one shown (`More like the <card's name>`): a new search, never a pick. */
|
|
935
|
+
const asksLikeShown = (text) => asksForSimilar(text, [...lastTappable, ...lastAnswerProducts].map((product) => product.name));
|
|
907
936
|
/** What a message picks: a product it names (`product`), a store or a place in a list (`pick`), or nothing (null). */
|
|
908
|
-
const pickOf = (text) => namesStore(text, shownStoreNames) ? 'pick' : namesShownProduct(text) ? 'product' : isPickText(text) ? 'pick' : null;
|
|
937
|
+
const pickOf = (text) => asksLikeShown(text) ? null : namesStore(text, shownStoreNames) ? 'pick' : namesShownProduct(text) ? 'product' : isPickText(text) ? 'pick' : null;
|
|
909
938
|
/** How the shopper's message reads for the instant reply: what it picks, and the reply it gets, or null. */
|
|
910
939
|
const ackOfMessage = (shopperText) => {
|
|
911
940
|
const text = String(shopperText ?? '');
|
|
@@ -1009,7 +1038,7 @@ export function buyTool(options) {
|
|
|
1009
1038
|
};
|
|
1010
1039
|
}
|
|
1011
1040
|
// A confirm, or a pick of what the shopper saw, gets fixed words that never say `looking for`.
|
|
1012
|
-
const slow = step ?? t.picked ?? (!t.neutral && (namesShown(ask) || isPickText(ask)) ? 'pick' : null);
|
|
1041
|
+
const slow = step ?? t.picked ?? (!t.neutral && !asksLikeShown(ask) && (namesShown(ask) || isPickText(ask)) ? 'pick' : null);
|
|
1013
1042
|
// A short answer to the question the last reply asked, such as `medium` after `what size?`, is no
|
|
1014
1043
|
// search: like `yes`, a slow call still tells the shopper, in words that look for nothing.
|
|
1015
1044
|
const neutral = t.neutral || answersQuestion(ask, lastReply);
|
|
@@ -1574,7 +1603,7 @@ export function buyTool(options) {
|
|
|
1574
1603
|
const inPurchase = (at) => at === null || at === conversationId;
|
|
1575
1604
|
if (yes && one && conversationId && inPurchase(yes.conversationId) && sameOrder(one, yes.cart)) {
|
|
1576
1605
|
carriedYes = null;
|
|
1577
|
-
return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', ...who }, yes: { ...yes, cart: one, conversationId } };
|
|
1606
|
+
return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', carried: { from: yes.picked ? 'pick' : 'yes', cart_hash: yes.consentedHash }, ...who }, yes: { ...yes, cart: one, conversationId } };
|
|
1578
1607
|
}
|
|
1579
1608
|
// The shopper picked what is in the cart and finished the step outside the chat: finishing it is their go-ahead for the
|
|
1580
1609
|
// same order, confirmed by the cart's hash as it stands now, and the approval link stays their consent to the payment
|
|
@@ -1586,7 +1615,7 @@ export function buyTool(options) {
|
|
|
1586
1615
|
if (picked?.cart && one && conversationId && inPurchase(picked.conversationId) && (event.kind === 'card_added' || event.kind === 'store_connected')) {
|
|
1587
1616
|
pickedOrder = null;
|
|
1588
1617
|
if (sameOrder(one, picked.cart)) {
|
|
1589
|
-
return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', ...who }, yes: { conversationId, cart: one, totalCents: picked.cart.totalCents, picked: true } };
|
|
1618
|
+
return { body: { confirm: one.hash, conversation_id: conversationId, payment_source: 'vault', carried: { from: 'pick', cart_hash: picked.cart.hash }, ...who }, yes: { conversationId, cart: one, totalCents: picked.cart.totalCents, consentedHash: picked.cart.hash, picked: true } };
|
|
1590
1619
|
}
|
|
1591
1620
|
}
|
|
1592
1621
|
// A checkout the shopper already moved past their yes (an approval link waiting, or the order going through) is reported as
|
|
@@ -1629,7 +1658,9 @@ export function buyTool(options) {
|
|
|
1629
1658
|
if (!confirm && ask === '')
|
|
1630
1659
|
throw new TypeError("Put the shopper's words in ask.");
|
|
1631
1660
|
// A pick chooses from the products or stores this conversation showed: never on a confirm, and never on a new order, which shows none yet.
|
|
1632
|
-
|
|
1661
|
+
// A request for other products like one shown is a new search: a `product_id` or `store_id` the model sent beside it would
|
|
1662
|
+
// put that product in the cart (2026-10-27, "More like <a card's whole title>" on 0.18.0), so none goes.
|
|
1663
|
+
const picks = !confirm && !newOrder && !asksLikeShown(ask);
|
|
1633
1664
|
const pick = picks && input.product_id ? { product_id: input.product_id } : picks && input.store_id ? { store_id: input.store_id } : null;
|
|
1634
1665
|
const body = {
|
|
1635
1666
|
resume: true,
|
|
@@ -1674,7 +1705,9 @@ export function buyTool(options) {
|
|
|
1674
1705
|
// A turn the tool took on its own sends no ask: it reads where the purchase stands, and only the
|
|
1675
1706
|
// shopper's own yes to this exact cart goes in, for that cart's hash.
|
|
1676
1707
|
const plan = t.event ? await stepPlan(t.event) : null;
|
|
1677
|
-
const
|
|
1708
|
+
const handBack = t.handBack ?? null;
|
|
1709
|
+
t.handBack = null;
|
|
1710
|
+
const planned = plan && 'body' in plan ? plan.body : handBack ? { ...body, hand_back: true, last_answer: handBack } : body;
|
|
1678
1711
|
const confirmSent = plan ? 'body' in plan && planned.confirm !== undefined : confirm;
|
|
1679
1712
|
// Agentcard writes the line that says what it is looking for (onAck) only where the shopper can read it before the
|
|
1680
1713
|
// cards and has read nothing yet: a streamed ask, with somewhere to show it, that no reaction, reply or model's words
|
|
@@ -1949,6 +1982,9 @@ export function buyTool(options) {
|
|
|
1949
1982
|
catch (error) {
|
|
1950
1983
|
refusal = error;
|
|
1951
1984
|
}
|
|
1985
|
+
// Agentcard read the message as your model's: nothing ran, so the call ends here and the turn's model answers it.
|
|
1986
|
+
if (turn?.handed_back)
|
|
1987
|
+
throw HANDED_BACK;
|
|
1952
1988
|
}
|
|
1953
1989
|
// The store was still building the last request's cart: once it is in, the request that was refused goes as it was
|
|
1954
1990
|
// sent, a step's own request too (a saved ask is no longer saved once its step sent it).
|
|
@@ -2433,6 +2469,18 @@ export function buyTool(options) {
|
|
|
2433
2469
|
// the app's own error. A call that ends stops it too, since a call started means the timer would send nothing.
|
|
2434
2470
|
state.dropWaitLine = () => clearTimeout(waitTimer);
|
|
2435
2471
|
}
|
|
2472
|
+
// What Agentcard last showed and asked when the turn opened, for `handled()`: its call changes them once it answers.
|
|
2473
|
+
const lastAnswer = {
|
|
2474
|
+
answered,
|
|
2475
|
+
next_action: lastNextAction ?? null,
|
|
2476
|
+
asked: lastReply ?? null,
|
|
2477
|
+
products: lastAnswerProducts.map((product) => ({ product_id: product.productId, name: product.name })),
|
|
2478
|
+
stores: lastAnswerStores.map((store) => ({ store_id: store.storeId, name: store.name })),
|
|
2479
|
+
};
|
|
2480
|
+
const openedWhileRunning = running > 0;
|
|
2481
|
+
const openedNextAction = lastNextAction;
|
|
2482
|
+
const openedProducts = lastAnswerProducts;
|
|
2483
|
+
const openedStores = lastAnswerStores;
|
|
2436
2484
|
if (!event)
|
|
2437
2485
|
previousTurn = state;
|
|
2438
2486
|
// What Agentcard told the shopper that no history the model ran with holds yet.
|
|
@@ -2471,9 +2519,70 @@ export function buyTool(options) {
|
|
|
2471
2519
|
if (saved.length > 0)
|
|
2472
2520
|
client.forgetMessages(externalUserId, saved);
|
|
2473
2521
|
handed = fresh;
|
|
2474
|
-
|
|
2522
|
+
// A message Agentcard answered for the model to word goes in with that answer, so the model words it and calls nothing again.
|
|
2523
|
+
return turnMessages(history, fresh.map(modelText), handedAnswer === null ? shopperWords : `${shopperWords}\n\n${handedNote(handedAnswer)}`);
|
|
2524
|
+
};
|
|
2525
|
+
/** Says a whole text as the turn's messages, after every item before it (`say()`). */
|
|
2526
|
+
const sayWhole = (whole) => {
|
|
2527
|
+
writesThroughTurn = true;
|
|
2528
|
+
firstWords(whole);
|
|
2529
|
+
if (state.modelTurn && /\S/.test(String(whole ?? '')))
|
|
2530
|
+
state.modelTurn.spoke = true;
|
|
2531
|
+
const reply = feedReply(text);
|
|
2532
|
+
for (const message of toMessages(whole))
|
|
2533
|
+
reply.add(message);
|
|
2534
|
+
const released = releaseForReply();
|
|
2535
|
+
released.catch(() => { });
|
|
2536
|
+
return reply.settled().then(() => released);
|
|
2537
|
+
};
|
|
2538
|
+
/** Agentcard's answer, as the model reads it, when `handled()` left the words to the model. */
|
|
2539
|
+
let handedAnswer = null;
|
|
2540
|
+
let handledRun = null;
|
|
2541
|
+
/**
|
|
2542
|
+
* One call to Agentcard with the shopper's message (`BuyToolTurn.handled`). A pick by its id or a yes to the total the
|
|
2543
|
+
* tool reads itself goes as that call; every other message goes as its words, with `hand_back`, and Agentcard decides.
|
|
2544
|
+
*/
|
|
2545
|
+
const runHandled = async () => {
|
|
2546
|
+
const words = String(shopperText ?? '');
|
|
2547
|
+
if (event || openedWhileRunning || words.trim() === '')
|
|
2548
|
+
return false;
|
|
2549
|
+
const local = directCall(words, { answered, nextAction: openedNextAction, asked: lastAnswer.asked, products: openedProducts, stores: openedStores });
|
|
2550
|
+
const exact = !!local && (local.input.confirm || local.input.product_id !== null || local.input.store_id !== null);
|
|
2551
|
+
const input = exact ? local.input : { ask: words, confirm: false, new_order: false, product_id: null, store_id: null };
|
|
2552
|
+
if (!exact) {
|
|
2553
|
+
state.handBack = lastAnswer;
|
|
2554
|
+
// Agentcard writes the line that says what it is looking for on a search this call starts, as on a search the turn
|
|
2555
|
+
// sends straight (`direct`); a message it hands back gets none, since nothing runs.
|
|
2556
|
+
state.agentcardAcks = (ask) => ask.trim() === words.trim();
|
|
2557
|
+
}
|
|
2558
|
+
let result;
|
|
2559
|
+
try {
|
|
2560
|
+
result = await answerIn(input, state);
|
|
2561
|
+
}
|
|
2562
|
+
catch (error) {
|
|
2563
|
+
state.handBack = null;
|
|
2564
|
+
if (error === HANDED_BACK)
|
|
2565
|
+
return false;
|
|
2566
|
+
throw error;
|
|
2567
|
+
}
|
|
2568
|
+
if (result.shopper_already_read_reply === true)
|
|
2569
|
+
return true;
|
|
2570
|
+
const reply = replyAsWritten(result, { textingStyle: texting });
|
|
2571
|
+
// The reply goes out where the app hears the turn's messages: onItem or onMessage, else onText. With none of them, the
|
|
2572
|
+
// shopper would read nothing, so the app's model words the answer instead.
|
|
2573
|
+
if (reply !== null && (onItem || onMessage)) {
|
|
2574
|
+
await sayWhole(reply);
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
if (reply !== null && state.onText) {
|
|
2578
|
+
await state.onText(reply, { conversationId: result.conversation_id });
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2581
|
+
handedAnswer = JSON.stringify(forModel(result));
|
|
2582
|
+
return false;
|
|
2475
2583
|
};
|
|
2476
2584
|
return toolOf((raw) => answerIn(raw, state), {}, {
|
|
2585
|
+
handled: { value: () => (handledRun ??= runHandled()), enumerable: false },
|
|
2477
2586
|
agentcardMessages: { get: () => [...(handed ?? kept)], enumerable: false },
|
|
2478
2587
|
event: { value: event, enumerable: false },
|
|
2479
2588
|
release: {
|
|
@@ -2525,21 +2634,7 @@ export function buyTool(options) {
|
|
|
2525
2634
|
},
|
|
2526
2635
|
enumerable: false,
|
|
2527
2636
|
},
|
|
2528
|
-
say: {
|
|
2529
|
-
value: (whole) => {
|
|
2530
|
-
writesThroughTurn = true;
|
|
2531
|
-
firstWords(whole);
|
|
2532
|
-
if (state.modelTurn && /\S/.test(String(whole ?? '')))
|
|
2533
|
-
state.modelTurn.spoke = true;
|
|
2534
|
-
const reply = feedReply(text);
|
|
2535
|
-
for (const message of toMessages(whole))
|
|
2536
|
-
reply.add(message);
|
|
2537
|
-
const released = releaseForReply();
|
|
2538
|
-
released.catch(() => { });
|
|
2539
|
-
return reply.settled().then(() => released);
|
|
2540
|
-
},
|
|
2541
|
-
enumerable: false,
|
|
2542
|
-
},
|
|
2637
|
+
say: { value: sayWhole, enumerable: false },
|
|
2543
2638
|
});
|
|
2544
2639
|
};
|
|
2545
2640
|
return toolOf(answer, { instantAck, instantReaction, turn, nextStep }, {});
|
package/dist/client.d.ts
CHANGED
|
@@ -242,11 +242,48 @@ export interface BuyRequest {
|
|
|
242
242
|
user_id?: string;
|
|
243
243
|
/** `false` asks for no line that says what Agentcard is looking for (`onAck`): the shopper already read one. */
|
|
244
244
|
ack?: false;
|
|
245
|
+
/**
|
|
246
|
+
* `true` asks Agentcard to answer at once, with `handed_back` and no turn run, when the ask is not shopping: a request to
|
|
247
|
+
* buy, a pick of what it showed, an answer to its question or a yes to a total runs as any ask, and every other message
|
|
248
|
+
* is your model's. `buy.turn`'s `handled()` sends it. An older API ignores it and runs the ask.
|
|
249
|
+
*/
|
|
250
|
+
hand_back?: true;
|
|
251
|
+
/** With `hand_back`: what Agentcard's last answer showed and asked, which decides whether a pick or an answer is shopping. */
|
|
252
|
+
last_answer?: BuyLastAnswer;
|
|
253
|
+
/**
|
|
254
|
+
* On a confirm the app sends on its own after a step outside the chat (a saved card, a store sign-in): the go-ahead it
|
|
255
|
+
* carries, a `yes` to a cart or the `pick` of what is in it, and `cart_hash`, the cart it was given for. Agentcard pays
|
|
256
|
+
* only that cart; a cart that changed since is answered with its total and `next_action: 'confirm'`, and nothing is paid.
|
|
257
|
+
*/
|
|
258
|
+
carried?: {
|
|
259
|
+
from: 'yes' | 'pick';
|
|
260
|
+
cart_hash: string;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** What the shopper's last answer from Agentcard showed and asked, sent with `hand_back`. */
|
|
264
|
+
export interface BuyLastAnswer {
|
|
265
|
+
/** True when the shopper's last turn ended on Agentcard's answer, with no turn your model answered alone since. */
|
|
266
|
+
answered: boolean;
|
|
267
|
+
next_action: string | null;
|
|
268
|
+
asked: string | null;
|
|
269
|
+
products: Array<{
|
|
270
|
+
product_id: string | null;
|
|
271
|
+
name: string;
|
|
272
|
+
}>;
|
|
273
|
+
stores: Array<{
|
|
274
|
+
store_id: string;
|
|
275
|
+
name: string;
|
|
276
|
+
}>;
|
|
245
277
|
}
|
|
246
278
|
export type BuyStatus = 'needs_input' | 'needs_approval' | 'order_placed' | 'partially_placed' | 'declined';
|
|
279
|
+
/**
|
|
280
|
+
* The store a product or a cart is at. `domain` is the store's own website, such as `walmart.com` or `jadeyoga.com`: on a
|
|
281
|
+
* catalog item, null when Agentcard knows none (a restaurant, a venue, an airline), and absent from an older API and on a cart.
|
|
282
|
+
*/
|
|
247
283
|
export interface BuyStore {
|
|
248
284
|
id: string;
|
|
249
285
|
name?: string;
|
|
286
|
+
domain?: string | null;
|
|
250
287
|
}
|
|
251
288
|
export interface BuyCartLine {
|
|
252
289
|
name: string;
|
|
@@ -435,6 +472,8 @@ export interface BuyTurn {
|
|
|
435
472
|
conversation_id: string;
|
|
436
473
|
/** A status Agentcard adds later reads as `needs_input`. */
|
|
437
474
|
status: BuyStatus;
|
|
475
|
+
/** True when Agentcard answered a `hand_back` ask as your model's to answer: no turn ran, and the other fields are empty. */
|
|
476
|
+
handed_back?: true;
|
|
438
477
|
reply: string;
|
|
439
478
|
messages: string[];
|
|
440
479
|
cart: BuyCart | null;
|
package/dist/client.js
CHANGED
|
@@ -136,7 +136,7 @@ function imagesOf(main, list) {
|
|
|
136
136
|
function storeOf(v) {
|
|
137
137
|
if (!isRecord(v) || !str(v.id))
|
|
138
138
|
return null;
|
|
139
|
-
return { id: v.id, ...(str(v.name) ? { name: v.name } : {}) };
|
|
139
|
+
return { id: v.id, ...(str(v.name) ? { name: v.name } : {}), ...(Object.hasOwn(v, 'domain') ? { domain: str(v.domain) } : {}) };
|
|
140
140
|
}
|
|
141
141
|
function feeLinesOf(v) {
|
|
142
142
|
if (!Array.isArray(v))
|
|
@@ -272,6 +272,7 @@ export function turnOf(body) {
|
|
|
272
272
|
return {
|
|
273
273
|
conversation_id: str(body.conversation_id) ?? '',
|
|
274
274
|
status: typeof body.status === 'string' && STATUSES.has(body.status) ? body.status : 'needs_input',
|
|
275
|
+
...(body.handed_back === true ? { handed_back: true } : {}),
|
|
275
276
|
reply: typeof body.reply === 'string' ? body.reply : '',
|
|
276
277
|
messages: Array.isArray(body.messages) ? body.messages.filter((m) => typeof m === 'string') : [],
|
|
277
278
|
cart: cartOf(body.cart),
|
package/dist/direct.js
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* answer such as `medium` or `size 9`, or a plain yes to the total it asked the shopper to confirm;
|
|
13
13
|
* - a price limit for the products Agentcard just showed: `only options under $20`, `under 20`, `cheaper ones`.
|
|
14
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)
|
|
15
|
+
* from the list it saw, named two products with no card to tap (2026-10-27);
|
|
16
|
+
* - a request for other products like one Agentcard just showed: `More like the KODAK AA Alkaline Batteries (60 Pack)`,
|
|
17
|
+
* `something similar to the first one`, `cheaper than the KODAK one`. It goes as a new search with no `product_id`:
|
|
18
|
+
* on 2026-10-27 (the playground chat on 0.18.0, production /buy) "More like <a card's whole title>" went as a pick of
|
|
19
|
+
* that card, and Agentcard put it in the cart and asked for the shipping address.
|
|
16
20
|
*
|
|
17
21
|
* Everything else goes to the model: a question, small talk, a change of mind (`no`, `wait`, `cancel`),
|
|
18
22
|
* a command to the cart, and anything about the account, an order or how the chat works. A pick or an
|
|
@@ -23,7 +27,7 @@
|
|
|
23
27
|
* purchase reply to small talk, while one the model reads by mistake costs only the model's call. On
|
|
24
28
|
* the 188 labelled messages in `test/fixtures/direct-messages.json`, it sends none it should not.
|
|
25
29
|
*/
|
|
26
|
-
import { ABOUT, ACKNOWLEDGED, CAPABILITY, CART_COMMAND, CHECKOUT, CHITCHAT, FILLER, asksForDetails, isNewRequest, isPickText, namesStore, plain } from './ack.js';
|
|
30
|
+
import { ABOUT, ACKNOWLEDGED, CAPABILITY, CART_COMMAND, CHECKOUT, CHITCHAT, FILLER, asksForDetails, asksForSimilar, isNewRequest, isPickText, namesStore, plain } from './ack.js';
|
|
27
31
|
/**
|
|
28
32
|
* A price limit put on what was just shown: `under $20`, `under 20`, `less than 15 bucks`, `between 10 and 20`, `$20 or
|
|
29
33
|
* less`, `over $50`, `cheaper ones`, `menos de 20`. Only whether the message states one: Agentcard reads the limit itself.
|
|
@@ -164,6 +168,9 @@ function pickOf(raw, bare, context) {
|
|
|
164
168
|
const byName = context.tappable?.length ? context.tappable : products;
|
|
165
169
|
const stores = context.stores ?? [];
|
|
166
170
|
const call = (fields) => ({ input: { ask: raw, confirm: false, new_order: false, product_id: null, store_id: null, ...fields }, reason: 'pick' });
|
|
171
|
+
// `More like the <card's whole name>` names a card to find others like it: it picks nothing.
|
|
172
|
+
if (asksForSimilar(raw, [...byName, ...products].map((product) => product.name)))
|
|
173
|
+
return null;
|
|
167
174
|
if (stores.length > 0) {
|
|
168
175
|
const said = ` ${nameWords(raw)} `;
|
|
169
176
|
// A store's card names it whole; a store named with the dishes to order from it (`five guys, bacon cheeseburger`) picks it too.
|
|
@@ -236,6 +243,12 @@ export function directCall(shopperText, context = {}) {
|
|
|
236
243
|
const told = CHITCHAT.test(words) || ABOUT.test(words) || CART_COMMAND.test(words) || CAPABILITY.test(words);
|
|
237
244
|
if (ctx.nextAction === 'confirm' && isYes(words))
|
|
238
245
|
return plainCall('confirm', true);
|
|
246
|
+
// A request for other products like one just shown is a new search, never a pick of the one it names: it goes with no
|
|
247
|
+
// `product_id`, and Agentcard searches for products like it and leaves it out.
|
|
248
|
+
const shown = [...(ctx.tappable ?? []), ...(ctx.products ?? [])];
|
|
249
|
+
if (!told && (shown.length > 0 || (ctx.stores ?? []).length > 0) && asksForSimilar(raw, shown.map((product) => product.name))) {
|
|
250
|
+
return NOT_SHOPPING.test(words) || TAKEBACK.test(words) ? null : plainCall('product_ask');
|
|
251
|
+
}
|
|
239
252
|
// A question, a story of what happened, an objection or a change of mind is the model's to answer, whatever came before.
|
|
240
253
|
const aside = QUESTION_START.test(bare) || REFERS.test(words) || COMPLAINT.test(words) || PAST.test(words) || NOT_SHOPPING.test(words) || OBJECTION.test(words) || CORRECTION.test(words);
|
|
241
254
|
if (!told && !aside && !TAKEBACK.test(words)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, ty
|
|
|
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
4
|
export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
|
|
5
5
|
export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.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
|
+
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 BuyLastAnswer, 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';
|
|
7
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';
|
|
8
8
|
export { toMessages, messageStream, typingDelay, type MessagePace, type MessageStream, type MessageStreamOptions, } from './messages.js';
|
|
9
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/commerce-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|