@agent-cards/commerce-sdk 0.18.5 → 0.18.6
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 +34 -3
- package/dist/action-links.js +1 -0
- package/dist/app-instructions.d.ts +17 -0
- package/dist/app-instructions.generated.d.ts +9 -0
- package/dist/app-instructions.generated.js +10 -0
- package/dist/app-instructions.js +22 -0
- package/dist/buy-tool.d.ts +14 -2
- package/dist/buy-tool.js +76 -43
- package/dist/client.d.ts +37 -0
- package/dist/client.js +16 -0
- package/dist/definition.js +2 -1
- package/dist/direct.d.ts +8 -0
- package/dist/direct.js +13 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/message.d.ts +2 -1
- package/dist/model-view.d.ts +29 -4
- package/dist/model-view.js +93 -13
- package/dist/plan.d.ts +41 -0
- package/dist/plan.js +34 -0
- package/dist/store-wait.d.ts +27 -0
- package/dist/store-wait.js +41 -0
- package/dist/trip-ask.d.ts +1 -1
- package/dist/trip-ask.js +4 -4
- package/examples/anthropic.mjs +3 -2
- package/package.json +4 -3
package/dist/definition.js
CHANGED
|
@@ -52,7 +52,8 @@ export const BUY_TOOL_RULES = [
|
|
|
52
52
|
'Name each link by what it is for, from `next_action`: the link to add a card, to sign in to the store, or to approve the payment.',
|
|
53
53
|
'Within a step, ask for one detail per message, such as the delivery address before the name for the label.',
|
|
54
54
|
'Write every sentence in your own style, casing included, even where `reply` is written another way.',
|
|
55
|
-
"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
|
|
55
|
+
"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, Best Buy and thousands more stores, so name those four and say \"and thousands more stores\" every time you name the stores you shop, in a reply about one kind of thing too, such as \"travel gear from Amazon, Target and thousands more stores\": never let the four read as the whole list, never say \"plenty more\", \"many more\" or \"most online stores\", and never give a number of stores. Name no other store unless a result showed it.",
|
|
56
|
+
"Never tell the shopper you can't render or show Markdown, a table or other formatting, and never say you keep to plain text: how your words look is your app's to decide.",
|
|
56
57
|
'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.',
|
|
57
58
|
'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".',
|
|
58
59
|
'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.',
|
package/dist/direct.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface DirectContext {
|
|
|
36
36
|
productId: string | null;
|
|
37
37
|
name: string;
|
|
38
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Every product card an earlier answer of this conversation showed, each once, newest first. A card's tap whose name no card
|
|
41
|
+
* of `tappable` holds is read against these, so a tap on a card two answers back still sends its `product_id`.
|
|
42
|
+
*/
|
|
43
|
+
shownBefore?: ReadonlyArray<{
|
|
44
|
+
productId: string | null;
|
|
45
|
+
name: string;
|
|
46
|
+
}>;
|
|
39
47
|
/** The stores Agentcard's last answer showed, with the `store_id` a pick sends. */
|
|
40
48
|
stores?: ReadonlyArray<{
|
|
41
49
|
storeId: string;
|
package/dist/direct.js
CHANGED
|
@@ -185,14 +185,25 @@ function pickOf(raw, bare, context) {
|
|
|
185
185
|
if (named.length > 1)
|
|
186
186
|
return call({});
|
|
187
187
|
}
|
|
188
|
+
// A tap on a card an earlier answer showed, after an answer that showed no cards: it is read against every card shown before.
|
|
189
|
+
const tapNow = byName.length === 0 ? tappedName(raw) : null;
|
|
190
|
+
if (tapNow !== null) {
|
|
191
|
+
const before = (context.shownBefore ?? []).filter((product) => product.productId && [nameWords(tapNow), nameWords(`the ${tapNow}`)].includes(nameWords(product.name)));
|
|
192
|
+
if (before.length > 0)
|
|
193
|
+
return call(new Set(before.map((product) => product.productId)).size === 1 ? { product_id: before[0].productId } : {});
|
|
194
|
+
}
|
|
188
195
|
if (byName.length > 0) {
|
|
189
196
|
// 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,
|
|
190
197
|
// 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
|
|
191
198
|
// alike, sends none, so Agentcard reads the words against every card it showed.
|
|
192
199
|
const tap = tappedName(raw);
|
|
193
200
|
if (tap !== null) {
|
|
194
|
-
const
|
|
195
|
-
|
|
201
|
+
const named = (cards) => cards.filter((product) => [nameWords(tap), nameWords(`the ${tap}`)].includes(nameWords(product.name)));
|
|
202
|
+
const exact = named(byName);
|
|
203
|
+
// A tap on a card an earlier answer showed names a card of no newer list: it is read against every card shown before.
|
|
204
|
+
const before = exact.length === 0 ? named(context.shownBefore ?? []).filter((product) => product.productId) : [];
|
|
205
|
+
const card = exact.length === 1 ? exact[0] : exact.length === 0 && new Set(before.map((product) => product.productId)).size === 1 ? before[0] : null;
|
|
206
|
+
return call(card ? { product_id: card.productId } : {});
|
|
196
207
|
}
|
|
197
208
|
// A product's whole name inside the message: `I want the KODAK AA Alkaline Batteries (24 Pack)`.
|
|
198
209
|
const said = nameWords(raw);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,17 +3,20 @@ 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 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 BuyChoiceDelivery, 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 BuyLatestReply, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, type BuyStoreChoice, type BuyStorePick, type BuyStores, type BuyChoice, type BuyChoiceGroup, type BuyChoiceDelivery, 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';
|
|
10
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION, type AckForOptions } from './ack.js';
|
|
11
11
|
export { directCall, isProductAsk, type DirectCall, type DirectContext, type DirectReason } from './direct.js';
|
|
12
|
-
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
12
|
+
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolGrandTotal, type BuyToolModelProduct, type BuyToolModelStore, type ShownCard, } from './model-view.js';
|
|
13
13
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice, type ReplyAsWrittenOptions } from './texting.js';
|
|
14
|
+
export { appInstructions, APP_INSTRUCTIONS_VERSION, type AppInstructionsOptions } from './app-instructions.js';
|
|
14
15
|
export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
|
|
15
16
|
export { listenToAgentcard, type AgentcardListener, type ListenOptions, type ListenCursorStore, type StreamFetch, } from './listen.js';
|
|
16
17
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, type ActionLinkKind, type ActionLinkMessage, type ActionLinkOutcome, } from './action-links.js';
|
|
17
18
|
export { AGENTCARD_EVENT_TYPES, type AgentcardEventType, } from './events.js';
|
|
18
19
|
export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS, type TurnStream, type TurnStreamOptions } from './turn-stream.js';
|
|
19
20
|
export { TURN_RECOVERY_ON, isTurnId, type TurnEvent, type TurnRecord, type TurnRecordStore } from './turn-record.js';
|
|
21
|
+
export type { BuyLateResults, BuyStoreWait } from './store-wait.js';
|
|
22
|
+
export { planOf, type BuyPlan, type BuyPlanItem, type BuyPlanItemState, type BuyPlanStatus } from './plan.js';
|
package/dist/index.js
CHANGED
|
@@ -9,11 +9,13 @@ export { toMessages, messageStream, typingDelay, } from './messages.js';
|
|
|
9
9
|
export { buyTool, nextStepPrompt, productAction, } from './buy-tool.js';
|
|
10
10
|
export { instantAck, instantReaction, ackFor, keepsYes, ACK_REACTION } from './ack.js';
|
|
11
11
|
export { directCall, isProductAsk } from './direct.js';
|
|
12
|
-
export { forModel, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
|
|
12
|
+
export { forModel, grandTotals, forModelError, stepAsWritten, withoutCardList, stripLinks, } from './model-view.js';
|
|
13
13
|
export { asBullets, choiceLines, replyAsWritten, textingStyle, withChoice } from './texting.js';
|
|
14
|
+
export { appInstructions, APP_INSTRUCTIONS_VERSION } from './app-instructions.js';
|
|
14
15
|
export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
|
|
15
16
|
export { listenToAgentcard, } from './listen.js';
|
|
16
17
|
export { ACTION_LINK_ENDINGS, ACTION_LINK_KINDS, actionLinkId, actionLinkOutcome, isActionLink, } from './action-links.js';
|
|
17
18
|
export { AGENTCARD_EVENT_TYPES, } from './events.js';
|
|
18
19
|
export { turnStream, readTurn, TURN_RECORD_TTL_SECONDS } from './turn-stream.js';
|
|
19
20
|
export { TURN_RECOVERY_ON, isTurnId } from './turn-record.js';
|
|
21
|
+
export { planOf } from './plan.js';
|
package/dist/message.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type AgentcardEvent } from './verify.js';
|
|
|
10
10
|
* - `order_failed`: the order failed after the approval, and nothing more happens for that approval.
|
|
11
11
|
* - `order_not_placed_yet`: the shopper approved, and the order is not placed yet.
|
|
12
12
|
* - `order_unconfirmed`: the store's reply to the order was lost, so the order may exist; Agentcard is confirming it.
|
|
13
|
+
* - `order_placing`: the shopper approved, and Agentcard is placing the order now; one of the order kinds above follows.
|
|
13
14
|
* - `approval_expiring`: the approval link expires soon; `text` carries the link.
|
|
14
15
|
* - `approval_expired`: the approval expired before the shopper answered, and nothing was charged.
|
|
15
16
|
* - `approval_declined`: the shopper declined the approval, and nothing was charged.
|
|
@@ -25,7 +26,7 @@ import { type AgentcardEvent } from './verify.js';
|
|
|
25
26
|
* - `cart_needs_input`: that checkout asks the shopper something first, such as a size, a delivery address, a sign-in to the store or an approval of the most the order can cost.
|
|
26
27
|
* - `cart_failed`: that checkout built no cart, and nothing was ordered or charged.
|
|
27
28
|
*/
|
|
28
|
-
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
|
|
29
|
+
export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'order_placing' | 'approval_expiring' | 'approval_expired' | 'approval_declined' | 'payment_declined' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added' | 'store_connected' | 'store_connect_failed' | 'store_connect_expired' | 'cart_ready' | 'cart_needs_input' | 'cart_failed';
|
|
29
30
|
/** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
|
|
30
31
|
export interface PurchaseFacts {
|
|
31
32
|
merchantName?: string;
|
package/dist/model-view.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `product_links_sent` for the products' store pages, and `link_sent` on each
|
|
8
8
|
* store and `store_links_sent` for the pages of the stores to pick from.
|
|
9
9
|
*/
|
|
10
|
-
import type { BuyNextAction } from './client.js';
|
|
10
|
+
import type { BuyCart, BuyNextAction } from './client.js';
|
|
11
11
|
import type { BuyToolProduct, BuyToolResult, BuyToolStatus, BuyToolStore } from './buy-tool.js';
|
|
12
12
|
export interface BuyToolModelCart {
|
|
13
13
|
store: string;
|
|
@@ -60,6 +60,22 @@ export interface BuyToolModelStore {
|
|
|
60
60
|
/** True when the shopper holds this store's page, which `sendLink` delivered beside the reply. */
|
|
61
61
|
link_sent: boolean;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* What every store cart on the table adds up to, in one currency: the stores' totals summed in minor units, each store's
|
|
65
|
+
* tip, fees and tax counted once as its `total` counts them. `is_estimate` is true when any store's total is an estimate,
|
|
66
|
+
* and `leaves_out` names what the estimated totals leave out together; null when every store priced its cart.
|
|
67
|
+
*/
|
|
68
|
+
export interface BuyToolGrandTotal {
|
|
69
|
+
amount: string;
|
|
70
|
+
/** The sum in the currency's minor unit, such as cents. */
|
|
71
|
+
amount_cents: number;
|
|
72
|
+
/** The ISO 4217 code, upper case, or null when Agentcard named none for these carts. */
|
|
73
|
+
currency: string | null;
|
|
74
|
+
is_estimate: boolean;
|
|
75
|
+
leaves_out: string | null;
|
|
76
|
+
/** How many store carts this total adds up. */
|
|
77
|
+
stores: number;
|
|
78
|
+
}
|
|
63
79
|
/** The buy tool's answer as the model reads it. */
|
|
64
80
|
export interface BuyToolModelView {
|
|
65
81
|
status: BuyToolStatus;
|
|
@@ -78,6 +94,8 @@ export interface BuyToolModelView {
|
|
|
78
94
|
cart: BuyToolModelCart | null;
|
|
79
95
|
/** Every cart on the table, the newest last, when the shopper is buying from more than one store. */
|
|
80
96
|
carts: BuyToolModelCart[];
|
|
97
|
+
/** What the carts add up to, one entry per currency, in the order the carts first name it; empty when no cart is on the table. */
|
|
98
|
+
grand_totals: BuyToolGrandTotal[];
|
|
81
99
|
products: BuyToolModelProduct[];
|
|
82
100
|
approval_expires_at: string | null;
|
|
83
101
|
/**
|
|
@@ -197,6 +215,11 @@ export declare function cardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'ur
|
|
|
197
215
|
* and a place's card is where the shopper reads whether it has room.
|
|
198
216
|
*/
|
|
199
217
|
export declare function productCardKey(product: Pick<BuyToolProduct, 'id' | 'ref' | 'url' | 'flight' | 'availability'>): string | null;
|
|
218
|
+
/**
|
|
219
|
+
* The carts' grand totals, one per currency, summed from each cart's `totalCents`, which already holds its tip, fees and
|
|
220
|
+
* tax: a display string is never parsed. Carts in two currencies are never added together.
|
|
221
|
+
*/
|
|
222
|
+
export declare function grandTotals(carts: readonly BuyCart[]): BuyToolGrandTotal[];
|
|
200
223
|
/** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
|
|
201
224
|
export declare function expiresIn(iso: string | null | undefined, now?: number): string | null;
|
|
202
225
|
/**
|
|
@@ -243,9 +266,11 @@ export interface ShownCard {
|
|
|
243
266
|
* 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
267
|
* are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
|
|
245
268
|
* 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.
|
|
247
|
-
*
|
|
248
|
-
*
|
|
269
|
+
* and so does a list line written without a bullet, whole, up to the question on it. The lines set in under a list
|
|
270
|
+
* line that goes, its details such as ` - ships from London`, go with it, and once the list went, a sentence that only
|
|
271
|
+
* points back at it, `both are priced in foreign currency`, goes when the cards show another number than it counts.
|
|
272
|
+
* What is left is the reply around the list, such as `found these at amazon.` and `which one do you want?`. A reply
|
|
273
|
+
* whose cards went out names none of 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
274
|
* message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
|
|
250
275
|
*
|
|
251
276
|
* Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,
|
package/dist/model-view.js
CHANGED
|
@@ -196,12 +196,42 @@ function cartView(cart) {
|
|
|
196
196
|
/** What an estimated total leaves out: tax, and shipping or delivery, less what the store's own fee lines already carry
|
|
197
197
|
* (a 1-800-Flowers estimate holds its Florist Delivery and Sales Tax). Null when the lines carry both. */
|
|
198
198
|
function leftOut(lines) {
|
|
199
|
+
return leftOutWords(leftOutParts(lines), 1);
|
|
200
|
+
}
|
|
201
|
+
function leftOutParts(lines) {
|
|
199
202
|
const parts = [];
|
|
200
203
|
if (!lines.some((l) => /\btax\b/i.test(l.label)))
|
|
201
204
|
parts.push('tax');
|
|
202
205
|
if (!lines.some((l) => /\b(?:deliver(?:y|ies)?|shipping)\b/i.test(l.label)))
|
|
203
206
|
parts.push('shipping or delivery');
|
|
204
|
-
return parts
|
|
207
|
+
return parts;
|
|
208
|
+
}
|
|
209
|
+
function leftOutWords(parts, stores) {
|
|
210
|
+
return parts.length ? `${parts.join(' and ')}, which the ${stores === 1 ? 'store adds' : 'stores add'} at checkout` : null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The carts' grand totals, one per currency, summed from each cart's `totalCents`, which already holds its tip, fees and
|
|
214
|
+
* tax: a display string is never parsed. Carts in two currencies are never added together.
|
|
215
|
+
*/
|
|
216
|
+
export function grandTotals(carts) {
|
|
217
|
+
const groups = new Map();
|
|
218
|
+
for (const cart of carts) {
|
|
219
|
+
const code = typeof cart.currency === 'string' && cart.currency.trim() !== '' ? cart.currency.trim().toUpperCase() : null;
|
|
220
|
+
groups.set(code, [...(groups.get(code) ?? []), cart]);
|
|
221
|
+
}
|
|
222
|
+
return [...groups].map(([currency, group]) => {
|
|
223
|
+
const cents = group.reduce((sum, cart) => sum + cart.totalCents, 0);
|
|
224
|
+
const estimates = group.filter((cart) => cart.totalIsEstimate === true);
|
|
225
|
+
const parts = new Set(estimates.flatMap((cart) => leftOutParts(cart.merchantFeeLines ?? [])));
|
|
226
|
+
return {
|
|
227
|
+
amount: money(cents, currency),
|
|
228
|
+
amount_cents: cents,
|
|
229
|
+
currency,
|
|
230
|
+
is_estimate: estimates.length > 0,
|
|
231
|
+
leaves_out: leftOutWords(['tax', 'shipping or delivery'].filter((part) => parts.has(part)), estimates.length),
|
|
232
|
+
stores: group.length,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
205
235
|
}
|
|
206
236
|
/** How long until `iso`, from `now`, as the shopper reads it: `about 24 hours`, `about 15 minutes`, `no longer`, or null. */
|
|
207
237
|
export function expiresIn(iso, now = Date.now()) {
|
|
@@ -267,6 +297,7 @@ export function forModel(result) {
|
|
|
267
297
|
store_pick: result.store_pick && stripLinks(result.store_pick.store_id) === result.store_pick.store_id
|
|
268
298
|
? { store_id: result.store_pick.store_id, selected: result.store_pick.selected === true }
|
|
269
299
|
: null,
|
|
300
|
+
grand_totals: grandTotals(result.carts ?? []),
|
|
270
301
|
...(yesCarriedView(result) ? { yes_carried: yesCarriedView(result) } : {}),
|
|
271
302
|
};
|
|
272
303
|
}
|
|
@@ -393,6 +424,17 @@ const LIST_NUMBER = /(?<!\ba )(?<!\ban )(?:#(\d{1,2})\b|\bnumber (\d{1,2})\b)/i;
|
|
|
393
424
|
* `there's also ...`, `you could also ...`. One that names a card is the list, whatever the reply recommended before it.
|
|
394
425
|
*/
|
|
395
426
|
const MORE_OF_LIST = /^[\s*_"'(]*(?:(?:and|plus|but|or)\s+)?(?:others?\b[^.!?:]{0,40}?\b(?:are|is|include|includes|would be)\b|there(?:'s|’s|\s+is|\s+are)\s+also\b|(?:you|we)\s+(?:could|can|might)\s+also\b)/i;
|
|
427
|
+
/**
|
|
428
|
+
* A sentence whose subject is the list's items, named by nothing but a pointer back at them and followed by its verb:
|
|
429
|
+
* `both are ...`, `these ship ...`, `the two cost ...`, `all three come ...`, `they're ...`. A pointer that leads a
|
|
430
|
+
* noun, `Both Visa and Mastercard are accepted.`, `Both taste and price matter.` or `Either way, ...`, points at no item and never
|
|
431
|
+
* matches: a word read as the verb but joined to another by `and` or `or` is a noun.
|
|
432
|
+
*/
|
|
433
|
+
const POINTS_BACK = /^[\s*_"'(]*(?:(?:and|but|so|plus)\s+)?(?:both|these|those|either|neither|they|the (?:two|three|four|five)|all (?:two|three|four|five|of (?:them|these|those))|each of (?:them|these|those))(?:\s+of\s+(?:them|these|those))?(?:'re|’re|\s+(?:(?:also|all|only|each|usually|both)\s+)?(?:are|is|were|was|have|has|had|come|comes|ship|ships|cost|costs|run|runs|sell|sells|look|looks|work|works|seem|seems|can|could|will|would|should|might|do|does|offer|offers|include|includes|take|takes|use|uses|weigh|weighs|arrive|arrives|carry|carries|fit|fits|need|needs|sit|sits|taste|tastes|smell|smells|feel|feels|make|makes|go|goes|pair|pairs|brew|brews|roast|roasts|stay|stays|last|lasts|hold|holds|don['’]t|doesn['’]t|didn['’]t|aren['’]t|isn['’]t|won['’]t|can['’]t|wouldn['’]t)\b(?!\s+(?:and|or)\b))/i;
|
|
434
|
+
/** How far a line is set in, a tab counting as four spaces, so a nested list line reads as deeper than its item. */
|
|
435
|
+
function indentOf(line) {
|
|
436
|
+
return (/^[ \t]*/.exec(line)?.[0] ?? '').replace(/\t/g, ' ').length;
|
|
437
|
+
}
|
|
396
438
|
/** An amount a text gives after a currency sign, such as `$22.89` or `¥1,331`. */
|
|
397
439
|
const AMOUNT = /[$£€¥]\s?(\d{1,3}(?:,\d{3})+|\d+)(\.\d+)?/g;
|
|
398
440
|
/** Words that tell no product from another: a card's brand or its distinctive words are never one of these. */
|
|
@@ -629,9 +671,11 @@ function inlineList(line, cards) {
|
|
|
629
671
|
* points at a number the list gave (`the dishwand in #8`), a sentence that goes on to more of the list (`other 6-packs
|
|
630
672
|
* are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
|
|
631
673
|
* further sentence that names another card. A list written on one line goes the same way, item by item (inlineList),
|
|
632
|
-
* and so does a list line written without a bullet, whole, up to the question on it.
|
|
633
|
-
*
|
|
634
|
-
*
|
|
674
|
+
* and so does a list line written without a bullet, whole, up to the question on it. The lines set in under a list
|
|
675
|
+
* line that goes, its details such as ` - ships from London`, go with it, and once the list went, a sentence that only
|
|
676
|
+
* points back at it, `both are priced in foreign currency`, goes when the cards show another number than it counts.
|
|
677
|
+
* What is left is the reply around the list, such as `found these at amazon.` and `which one do you want?`. A reply
|
|
678
|
+
* whose cards went out names none of 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
|
|
635
679
|
* message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
|
|
636
680
|
*
|
|
637
681
|
* Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,
|
|
@@ -653,10 +697,14 @@ export function withoutCardList(reply, cardNames) {
|
|
|
653
697
|
};
|
|
654
698
|
// The numbers the dropped list gave, so a number of a product's own, `a #2 Phillips head`, keeps its sentence.
|
|
655
699
|
const numbers = new Set();
|
|
700
|
+
// How many items of a list written inline went: with the list lines that went, the count a pointer such as `these` means.
|
|
701
|
+
let inlineGone = 0;
|
|
702
|
+
// The lines whose inline list went whole: the lines set in under one describe its items, and go with them.
|
|
703
|
+
const inlineEmptied = new Set();
|
|
656
704
|
// A list a line writes inline goes the way a list of lines does: each item that names a card, and the sentence that
|
|
657
705
|
// led into the list once every item went. An item for a product no card shows stays, with the lead, and so does a
|
|
658
706
|
// list that opens the line, `1) a mug; 2) a candle; 3) a soap box`, item by item rather than as one list line.
|
|
659
|
-
const unlisted = (line) => {
|
|
707
|
+
const unlisted = (line, index) => {
|
|
660
708
|
const list = inlineList(line, cards);
|
|
661
709
|
const gone = list?.items.map((item) => cardsNamed(item.body, cards, true).length > 0) ?? [];
|
|
662
710
|
if (!list || !gone.some(Boolean))
|
|
@@ -665,6 +713,9 @@ export function withoutCardList(reply, cardNames) {
|
|
|
665
713
|
if (gone[k] && item.number !== null)
|
|
666
714
|
numbers.add(item.number);
|
|
667
715
|
});
|
|
716
|
+
inlineGone += gone.filter(Boolean).length;
|
|
717
|
+
if (gone.every(Boolean))
|
|
718
|
+
inlineEmptied.add(index);
|
|
668
719
|
const after = line.slice(list.end).trimStart();
|
|
669
720
|
if (gone.every(Boolean))
|
|
670
721
|
return `${line.slice(0, list.lead).trimEnd()} ${after}`.trim();
|
|
@@ -672,22 +723,37 @@ export function withoutCardList(reply, cardNames) {
|
|
|
672
723
|
const close = /[.!?]$/.test(left[left.length - 1]) ? '' : '.';
|
|
673
724
|
return [line.slice(0, list.items[0].start).trimEnd(), `${left.join('; ')}${close}`, after].filter((part) => part !== '').join(' ');
|
|
674
725
|
};
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
726
|
+
const written = reply.split('\n');
|
|
727
|
+
const lines = written.map(unlisted);
|
|
728
|
+
// A line set in deeper than a list line that names a card is that item's detail, such as ` - ships from London`
|
|
729
|
+
// under `- Square Mile Red Brick, £13.50`: it goes with its item (`under`), since what it describes is the card.
|
|
730
|
+
let item = null;
|
|
731
|
+
const kind = lines.map((line, i) => {
|
|
732
|
+
if (inlineEmptied.has(i)) {
|
|
733
|
+
// A list nested under an item already dropped keeps that item's depth, so the lines after it still go with their item.
|
|
734
|
+
const at = indentOf(written[i]);
|
|
735
|
+
if (item === null || at <= item)
|
|
736
|
+
item = at;
|
|
737
|
+
return 'kept';
|
|
738
|
+
}
|
|
739
|
+
if (line.trim() === '')
|
|
740
|
+
return 'kept';
|
|
741
|
+
if (item !== null && indentOf(line) > item)
|
|
742
|
+
return 'under';
|
|
743
|
+
const of = LIST_ITEM.test(line) ? (cardsNamed(line.replace(LIST_ITEM, ''), cards, true).length > 0 ? 'item' : 'kept') : opensCard(line) ? 'named' : 'kept';
|
|
744
|
+
item = of === 'kept' ? null : indentOf(line);
|
|
745
|
+
return of;
|
|
680
746
|
});
|
|
681
747
|
const dropped = kind.map((of) => of !== 'kept');
|
|
682
748
|
lines.forEach((line, i) => {
|
|
683
|
-
const number = dropped[i] ? Number(/^\s*(\d{1,2})[.)]\s/.exec(line)?.[1]) : NaN;
|
|
749
|
+
const number = dropped[i] && kind[i] !== 'under' ? Number(/^\s*(\d{1,2})[.)]\s/.exec(line)?.[1]) : NaN;
|
|
684
750
|
if (Number.isInteger(number))
|
|
685
751
|
numbers.add(number);
|
|
686
752
|
});
|
|
687
753
|
// The list a line leads into: the lines from the next one with words on it to the first that is not a list line, so a
|
|
688
754
|
// question on the line after the last item, `Which one?`, is no part of the list. A blank line between two list lines
|
|
689
755
|
// is no end of it, as toMessages reads one, so a list that keeps an item past the blank keeps its lead.
|
|
690
|
-
const listLine = (j) => LIST_ITEM.test(lines[j]) || kind[j] === 'named';
|
|
756
|
+
const listLine = (j) => LIST_ITEM.test(lines[j]) || kind[j] === 'named' || kind[j] === 'under';
|
|
691
757
|
const listAfter = (line) => {
|
|
692
758
|
const start = lines.findIndex((after, j) => j > line && after.trim() !== '');
|
|
693
759
|
if (start === -1)
|
|
@@ -709,8 +775,20 @@ export function withoutCardList(reply, cardNames) {
|
|
|
709
775
|
};
|
|
710
776
|
const named = new Set();
|
|
711
777
|
const kept = [];
|
|
778
|
+
// Once a list went, a sentence that only points back at its items, `both are priced in foreign currency` or `these ship
|
|
779
|
+
// free`, reads against the cards. It stays when it counts what the cards show, `both` beside two cards, and goes when the
|
|
780
|
+
// cards show another number, as eight cards beside `both` do: the shopper cannot tell which two it means.
|
|
781
|
+
const listed = kind.filter((of) => of === 'item' || of === 'named').length + inlineGone;
|
|
782
|
+
const pointsElsewhere = (sentence) => {
|
|
783
|
+
const pointer = POINTS_BACK.exec(sentence);
|
|
784
|
+
if (!pointer || listed === 0)
|
|
785
|
+
return false;
|
|
786
|
+
const counted = /\b(both|either|neither|two|three|four|five)\b/i.exec(pointer[0])?.[1]?.toLowerCase();
|
|
787
|
+
const count = counted ? { both: 2, either: 2, neither: 2, two: 2, three: 3, four: 4, five: 5 }[counted] : listed;
|
|
788
|
+
return count !== cards.length;
|
|
789
|
+
};
|
|
712
790
|
lines.forEach((line, i) => {
|
|
713
|
-
if (kind[i] === 'item')
|
|
791
|
+
if (kind[i] === 'item' || kind[i] === 'under')
|
|
714
792
|
return;
|
|
715
793
|
let text = line;
|
|
716
794
|
// The line that leads into a list whose every line went loses the sentence that leads in: `Two I'd suggest:` or
|
|
@@ -727,6 +805,8 @@ export function withoutCardList(reply, cardNames) {
|
|
|
727
805
|
if (place && numbers.has(Number(place[1] ?? place[2])))
|
|
728
806
|
return [];
|
|
729
807
|
const found = cardsNamed(sentence, cards, false);
|
|
808
|
+
if (found.length === 0 && !ASKS.test(sentence) && !/:\s*$/.test(sentence) && pointsElsewhere(sentence))
|
|
809
|
+
return [];
|
|
730
810
|
if (kind[i] === 'named') {
|
|
731
811
|
const card = found.length > 0 || opensCard(sentence);
|
|
732
812
|
asked ||= ASKS.test(sentence);
|
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A checkout plan on a purchase answer: the items a shopper chose to book together, booked in the order they depend on each
|
|
3
|
+
* other on one approval, such as a trip's flight first, then its hotel and car. The same object rides `plan.updated`, so a
|
|
4
|
+
* tracker redraws from either. Null on an answer with no plan, and on an older API.
|
|
5
|
+
*/
|
|
6
|
+
export type BuyPlanStatus = 'preparing' | 'running' | 'done' | 'partly_done' | 'stopped';
|
|
7
|
+
export type BuyPlanItemState = 'draft' | 'quoted' | 'approved' | 'booking' | 'confirmed' | 'pending' | 'needs_input' | 'failed' | 'skipped' | 'cancelled';
|
|
8
|
+
export interface BuyPlanItem {
|
|
9
|
+
/** The item's key in its plan: `flight`, `hotel`, `car`. */
|
|
10
|
+
key: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
state: BuyPlanItemState;
|
|
13
|
+
name: string;
|
|
14
|
+
/** What the shopper pays for it: the confirmed price once booked, the quote before. */
|
|
15
|
+
price_cents: number | null;
|
|
16
|
+
/** A changed price waiting for the shopper's answer, or null. */
|
|
17
|
+
asked_cents: number | null;
|
|
18
|
+
/** The supplier's confirmation number once booked. */
|
|
19
|
+
confirmation: string | null;
|
|
20
|
+
/** The keys of the items it books after. */
|
|
21
|
+
depends_on: string[];
|
|
22
|
+
/** Why it waits or did not book, such as `price_changed` or `approval_required`, or null. */
|
|
23
|
+
reason: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface BuyPlan {
|
|
26
|
+
id: string;
|
|
27
|
+
status: BuyPlanStatus;
|
|
28
|
+
/** Moves on every change: keep the highest one seen. */
|
|
29
|
+
version: number;
|
|
30
|
+
currency: string;
|
|
31
|
+
total_cents: number;
|
|
32
|
+
charged_cents: number;
|
|
33
|
+
/** When the items waiting on a paused one are let go, or null. */
|
|
34
|
+
hold_until: string | null;
|
|
35
|
+
items: BuyPlanItem[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The plan an answer carries, or null when it carries none or one this version cannot read: a status or an item state this
|
|
39
|
+
* version does not know, a missing total, or an item it cannot read. A tracker never shows a guessed state or a zero total.
|
|
40
|
+
*/
|
|
41
|
+
export declare function planOf(value: unknown): BuyPlan | null;
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
3
|
+
const text = (v) => (typeof v === 'string' ? v : null);
|
|
4
|
+
const STATUSES = new Set(['preparing', 'running', 'done', 'partly_done', 'stopped']);
|
|
5
|
+
const STATES = new Set(['draft', 'quoted', 'approved', 'booking', 'confirmed', 'pending', 'needs_input', 'failed', 'skipped', 'cancelled']);
|
|
6
|
+
function itemOf(i) {
|
|
7
|
+
if (!isRecord(i) || !text(i.key) || !text(i.state) || !STATES.has(i.state))
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
key: i.key, kind: text(i.kind) ?? '', state: i.state, name: text(i.name) ?? '',
|
|
11
|
+
price_cents: num(i.price_cents), asked_cents: num(i.asked_cents), confirmation: text(i.confirmation),
|
|
12
|
+
depends_on: Array.isArray(i.depends_on) ? i.depends_on.filter((d) => typeof d === 'string') : [], reason: text(i.reason),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The plan an answer carries, or null when it carries none or one this version cannot read: a status or an item state this
|
|
17
|
+
* version does not know, a missing total, or an item it cannot read. A tracker never shows a guessed state or a zero total.
|
|
18
|
+
*/
|
|
19
|
+
export function planOf(value) {
|
|
20
|
+
if (!isRecord(value) || !text(value.id) || !Array.isArray(value.items) || !STATUSES.has(text(value.status) ?? ''))
|
|
21
|
+
return null;
|
|
22
|
+
const total = num(value.total_cents);
|
|
23
|
+
const charged = num(value.charged_cents);
|
|
24
|
+
const version = num(value.version);
|
|
25
|
+
if (total === null || charged === null || version === null)
|
|
26
|
+
return null;
|
|
27
|
+
const items = value.items.map(itemOf);
|
|
28
|
+
if (items.some((i) => i === null))
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
id: value.id, status: value.status, version, currency: text(value.currency) ?? 'USD',
|
|
32
|
+
total_cents: total, charged_cents: charged, hold_until: text(value.hold_until), items: items,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BuyToolProduct } from './buy-tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* What happens to a store that answers after the reply: `stream` sends its cards after the reply, in the same turn, as
|
|
4
|
+
* the card events every streamed turn carries; `hold` keeps them out of the turn, so the reply is the turn's last word.
|
|
5
|
+
*/
|
|
6
|
+
export type BuyLateResults = 'stream' | 'hold';
|
|
7
|
+
/** The store wait of a tool or a turn. */
|
|
8
|
+
export interface BuyStoreWait {
|
|
9
|
+
/** How long, in milliseconds, the reply waits for store searches before Agentcard writes it. */
|
|
10
|
+
storeWaitMs?: number;
|
|
11
|
+
/** What happens to a store that answers after the reply. */
|
|
12
|
+
lateResults?: BuyLateResults;
|
|
13
|
+
}
|
|
14
|
+
/** The request fields a store wait sends, none when it names nothing. */
|
|
15
|
+
export interface BuyStoreWaitFields {
|
|
16
|
+
store_wait_ms?: number;
|
|
17
|
+
late_results?: BuyLateResults;
|
|
18
|
+
}
|
|
19
|
+
/** Checks a store wait passed to `buyTool` (`where` is empty) or to a turn (`where` reads ' to the turn'), and throws a TypeError for one that cannot work. */
|
|
20
|
+
export declare function checkStoreWait(wait: BuyStoreWait, where?: string): void;
|
|
21
|
+
/** The fields a request carries for a turn's store wait: the turn's own, else the tool's. */
|
|
22
|
+
export declare function storeWaitFields(turn: BuyStoreWait, tool: BuyStoreWait): BuyStoreWaitFields;
|
|
23
|
+
/**
|
|
24
|
+
* The answer's products with the cards of a store that answered after the reply added at the end, each once: the answer
|
|
25
|
+
* can list them already, when the turn's last word came after them, and a late card it lists is left as the answer has it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withLateCards(products: readonly BuyToolProduct[], late: readonly BuyToolProduct[]): BuyToolProduct[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a reply waits for store searches, and what happens to a store that answers after it: the `storeWaitMs` and
|
|
3
|
+
* `lateResults` options of `buyTool` and `buy.turn`, sent on every request as `store_wait_ms` and `late_results`. Left
|
|
4
|
+
* out, nothing is sent and Agentcard keeps its own wait.
|
|
5
|
+
*/
|
|
6
|
+
import { pickId, productCardKey } from './model-view.js';
|
|
7
|
+
/** Checks a store wait passed to `buyTool` (`where` is empty) or to a turn (`where` reads ' to the turn'), and throws a TypeError for one that cannot work. */
|
|
8
|
+
export function checkStoreWait(wait, where = '') {
|
|
9
|
+
const ms = wait.storeWaitMs;
|
|
10
|
+
if (ms !== undefined && (typeof ms !== 'number' || !Number.isInteger(ms) || ms < 0)) {
|
|
11
|
+
throw new TypeError(`Pass storeWaitMs${where} as a whole number of milliseconds, 0 or more, or leave it out.`);
|
|
12
|
+
}
|
|
13
|
+
const late = wait.lateResults;
|
|
14
|
+
if (late !== undefined && late !== 'stream' && late !== 'hold') {
|
|
15
|
+
throw new TypeError(`Pass lateResults${where} as 'stream' or 'hold', or leave it out.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** The fields a request carries for a turn's store wait: the turn's own, else the tool's. */
|
|
19
|
+
export function storeWaitFields(turn, tool) {
|
|
20
|
+
const ms = turn.storeWaitMs ?? tool.storeWaitMs;
|
|
21
|
+
const late = turn.lateResults ?? tool.lateResults;
|
|
22
|
+
return { ...(ms !== undefined ? { store_wait_ms: ms } : {}), ...(late !== undefined ? { late_results: late } : {}) };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The answer's products with the cards of a store that answered after the reply added at the end, each once: the answer
|
|
26
|
+
* can list them already, when the turn's last word came after them, and a late card it lists is left as the answer has it.
|
|
27
|
+
*/
|
|
28
|
+
export function withLateCards(products, late) {
|
|
29
|
+
// A product with no link-free id is told apart by its page, so two stores' offers of one title stay two.
|
|
30
|
+
const keyOf = (product) => pickId(product) ?? productCardKey(product) ?? `name:${product.name}`;
|
|
31
|
+
const held = new Set(products.map(keyOf));
|
|
32
|
+
const added = [];
|
|
33
|
+
for (const product of late) {
|
|
34
|
+
const key = keyOf(product);
|
|
35
|
+
if (held.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
held.add(key);
|
|
38
|
+
added.push(product);
|
|
39
|
+
}
|
|
40
|
+
return [...products, ...added];
|
|
41
|
+
}
|
package/dist/trip-ask.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* an ask goes straight only with a shopping verb (`book a flight to Loreto`), as before.
|
|
14
14
|
*
|
|
15
15
|
* `inCapabilities` on: the tool's description says it plans and books trips, flights and hotels, tells the model to call it
|
|
16
|
-
* for a trip ask with no dates or airport, and
|
|
16
|
+
* for a trip ask with no dates or airport, and leads with a whole trip when the shopper asks what it can do for travel. Off: the
|
|
17
17
|
* description is the text it was before, word for word.
|
|
18
18
|
*/
|
|
19
19
|
export declare const TRAVEL: {
|
package/dist/trip-ask.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* an ask goes straight only with a shopping verb (`book a flight to Loreto`), as before.
|
|
14
14
|
*
|
|
15
15
|
* `inCapabilities` on: the tool's description says it plans and books trips, flights and hotels, tells the model to call it
|
|
16
|
-
* for a trip ask with no dates or airport, and
|
|
16
|
+
* for a trip ask with no dates or airport, and leads with a whole trip when the shopper asks what it can do for travel. Off: the
|
|
17
17
|
* description is the text it was before, word for word.
|
|
18
18
|
*/
|
|
19
19
|
export const TRAVEL = { goStraight: true, inCapabilities: true };
|
|
@@ -52,9 +52,9 @@ export function withoutTripVerb(text) {
|
|
|
52
52
|
return TRAVEL.goStraight && isTripAsk(text) ? text.replace(TRIP_VERB, '') : text;
|
|
53
53
|
}
|
|
54
54
|
const OPENING_LINE = 'Buy what this shopper asks for at real stores.';
|
|
55
|
-
const TRAVEL_LINES = 'You also plan and book trips: flights and
|
|
56
|
-
const CAPABILITY_END = '
|
|
57
|
-
const CAPABILITY_TRAVEL = 'Name
|
|
55
|
+
const TRAVEL_LINES = 'You also plan and book trips: flights, hotels and rental cars. When the shopper asks to plan a trip, or for flights or a hotel, such as "help me plan a trip to Loreto" or "flights and hotels in loreto", call this tool with their words, even when they name no dates and no place they leave from: Agentcard asks them for what it needs.';
|
|
56
|
+
const CAPABILITY_END = 'Name no other store unless a result showed it.';
|
|
57
|
+
const CAPABILITY_TRAVEL = 'Name whole trips among the kinds of things you can do. When the shopper asks what you can do for travel, lead with planning a whole trip, the flights, a hotel and a rental car together, then name the parts they can book on their own: flights, hotels and rental cars.';
|
|
58
58
|
/** The tool's description with travel named, when `inCapabilities` is on (or `on` says so); the description as given when off. */
|
|
59
59
|
export function withTravel(description, on = TRAVEL.inCapabilities) {
|
|
60
60
|
if (!on)
|
package/examples/anthropic.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Sandbox credentials place no real order. SHOPPER_ID is your own id for the shopper you play.
|
|
7
7
|
import readline from 'node:readline';
|
|
8
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
9
|
-
import { AgentcardClient, buyTool } from '@agent-cards/commerce-sdk';
|
|
9
|
+
import { AgentcardClient, appInstructions, buyTool } from '@agent-cards/commerce-sdk';
|
|
10
10
|
|
|
11
11
|
const agentcard = new AgentcardClient({
|
|
12
12
|
clientId: process.env.AGENTCARD_CLIENT_ID ?? '',
|
|
@@ -33,7 +33,8 @@ for await (const line of terminal) {
|
|
|
33
33
|
const runner = anthropic.beta.messages.toolRunner({
|
|
34
34
|
model: 'claude-opus-5-5',
|
|
35
35
|
max_tokens: 16000,
|
|
36
|
-
|
|
36
|
+
// Agentcard's recommended instructions: what it can do and how to use the buy tool. A terminal shows plain text.
|
|
37
|
+
system: appInstructions({ markdown: false }),
|
|
37
38
|
tools: [buy],
|
|
38
39
|
messages: [...messages, { role: 'user', content: said }],
|
|
39
40
|
// When a safety classifier declines a request, Anthropic answers it with a fallback model instead.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/commerce-sdk",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.6",
|
|
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
|
"homepage": "https://opencommerce.sh",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"_comment_build": "Zero runtime dependencies: node:crypto is the one import, and the buy tool calls the global fetch. TypeScript is pinned, not installed, as in @agent-cards/sdk. types/node.d.ts declares the two node:crypto functions the source calls, so the build needs no @types package and the published declarations name no Node type; FetchLike names AbortSignal, which every runtime with fetch declares.",
|
|
30
|
-
"build": "npx -y -p typescript@5.9.3 tsc",
|
|
30
|
+
"build": "node scripts/app-instructions.mjs && npx -y -p typescript@5.9.3 tsc",
|
|
31
31
|
"prepublishOnly": "pnpm build",
|
|
32
32
|
"test": "pnpm build && node --test test/*.test.mjs",
|
|
33
33
|
"verify:pack": "node scripts/verify-pack.mjs",
|
|
34
|
-
"_comment_pack": "verify:pack lists what npm would publish (npm pack --dry-run) and fails on a missing manifest target, README or example, or on a test, source, shim or build-cache file; then it loads the built root and checks the names the README imports. The release workflow runs it after the build."
|
|
34
|
+
"_comment_pack": "verify:pack lists what npm would publish (npm pack --dry-run) and fails on a missing manifest target, README or example, or on a test, source, shim or build-cache file; then it loads the built root and checks the names the README imports. The release workflow runs it after the build.",
|
|
35
|
+
"_comment_instructions": "The build first copies the instructions for an app's own model (appInstructions) from apps/agent-voice/kb/app.md into src/app-instructions.generated.ts, which git ignores, so the published text always matches the voice package at the commit it was built from."
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
38
|
"agentcard",
|