@agent-cards/commerce-sdk 0.5.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,10 +4,11 @@
4
4
  * model that holds a link writes it into the chat. So a link never enters
5
5
  * this view: `sendLink` delivers it, and the model reads `link_sent` for the
6
6
  * Vault and approval links, and `link_sent` on each product and
7
- * `product_links_sent` for the products' store pages.
7
+ * `product_links_sent` for the products' store pages, and `link_sent` on each
8
+ * store and `store_links_sent` for the pages of the stores to pick from.
8
9
  */
9
10
  import type { BuyNextAction } from './client.js';
10
- import type { BuyToolProduct, BuyToolResult, BuyToolStatus } from './buy-tool.js';
11
+ import type { BuyToolProduct, BuyToolResult, BuyToolStatus, BuyToolStore } from './buy-tool.js';
11
12
  export interface BuyToolModelCart {
12
13
  store: string;
13
14
  items: Array<{
@@ -23,10 +24,21 @@ export interface BuyToolModelProduct {
23
24
  /** What the model passes as `product_id` when the shopper picks this product: Agentcard's ref, never a link. Null when there is none. */
24
25
  product_id: string | null;
25
26
  name: string;
27
+ /** The store that sells it, by name, never a link: a product with no store is not shown to the model. */
28
+ store: string;
26
29
  price: string | null;
27
30
  /** True when the shopper holds this product's store page, which `sendLink` delivered beside the reply. */
28
31
  link_sent: boolean;
29
32
  }
33
+ export interface BuyToolModelStore {
34
+ /** What the model passes as `store_id` when the shopper picks this store: Agentcard's id, never a link. */
35
+ store_id: string;
36
+ name: string;
37
+ /** The delivery estimate in minutes, or null when the store showed none. */
38
+ eta_minutes: number | null;
39
+ /** True when the shopper holds this store's page, which `sendLink` delivered beside the reply. */
40
+ link_sent: boolean;
41
+ }
30
42
  /** The buy tool's answer as the model reads it. */
31
43
  export interface BuyToolModelView {
32
44
  status: BuyToolStatus;
@@ -41,6 +53,8 @@ export interface BuyToolModelView {
41
53
  products: BuyToolModelProduct[];
42
54
  approval_expires_at: string | null;
43
55
  shopper_need_not_reply: boolean;
56
+ /** True when the shopper already got a message this turn saying their request is being worked on, so the reply opens with no acknowledgement of its own. */
57
+ shopper_already_told_looking: boolean;
44
58
  /** True when the shopper already has the link this answer needed. */
45
59
  link_sent: boolean;
46
60
  /** How many store pages of the products above the shopper holds, 0 when none went out. */
@@ -50,8 +64,20 @@ export interface BuyToolModelView {
50
64
  product_id: string | null;
51
65
  in_cart: boolean;
52
66
  } | null;
67
+ /** The stores the reply presents to choose from, in the order it names them. */
68
+ stores: BuyToolModelStore[];
69
+ /** How many store pages of the stores above the shopper holds, 0 when none went out. */
70
+ store_links_sent: number;
71
+ /** The store this call's `store_id` (or an answer such as `the second one`) picked, and whether Agentcard selected it; null when none was picked. */
72
+ store_pick: {
73
+ store_id: string;
74
+ selected: boolean;
75
+ } | null;
53
76
  }
54
- /** The text with every link taken out: a Markdown link keeps its label, and a bare address goes entirely. */
77
+ /**
78
+ * The text with every link taken out: a Markdown link keeps its label, an address with a path or a scheme goes
79
+ * entirely, and a bare address reads as the site's name (siteName), so the store a reply names stays in it.
80
+ */
55
81
  export declare function stripLinks(text: string): string;
56
82
  /**
57
83
  * An amount in its currency's minor unit, as the model reads it: the currency's own minor units place the
@@ -60,11 +86,17 @@ export declare function stripLinks(text: string): string;
60
86
  */
61
87
  export declare function money(minor: number | null | undefined, currency: string | null | undefined): string | null;
62
88
  /**
63
- * The products the model reads: the first twenty that keep a name once links
64
- * are taken out. The tool sends store pages for these same twenty, so the shopper
65
- * never holds a product link the model cannot name.
89
+ * The products the model reads: the first twenty that keep a name and a store
90
+ * once links are taken out, so the model never offers a product without saying
91
+ * where it is sold. The tool sends store pages for these same twenty, so the
92
+ * shopper never holds a product link the model cannot name.
66
93
  */
67
94
  export declare function shownProducts(products: readonly BuyToolProduct[] | null | undefined): BuyToolProduct[];
95
+ /**
96
+ * The stores the model reads: each one whose id and name hold no link, so the model never reads a
97
+ * link and never offers a store without its name. The tool sends pages for these same stores.
98
+ */
99
+ export declare function shownStores(stores: readonly BuyToolStore[] | null | undefined): BuyToolStore[];
68
100
  /**
69
101
  * The id the model passes back for a product: Agentcard's link-free ref, else the product's own id when
70
102
  * it holds no link, else null. A retail product's id is its store page, and a link never reaches the model.
@@ -10,15 +10,40 @@ const HOST_PATH = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}[/?#][^\s<>"'`)\]]*/gi;
10
10
  * A bare host on a common top-level domain, such as `agentcard.sh` or `amazon.com`, which a chat app
11
11
  * turns into a link. The list keeps words with a dot, such as `Node.js`, and an email address's host.
12
12
  */
13
- const BARE_HOST = /(?<![@\w.-])(?:[a-z0-9-]+\.)+(?:com|net|org|io|co|sh|ai|app|dev|me|us|uk|ca|de|fr|es|it|nl|eu|au|in|jp|br|mx|info|biz|shop|store|site|online|link|page|xyz|ly|gg|to|tv|fm|so)(?![\w-])/gi;
14
- /** The text with every link taken out: a Markdown link keeps its label, and a bare address goes entirely. */
13
+ const BARE_HOST = /(?<![@\w.-])(?:[a-z0-9-]+\.)+(?:com|net|org|io|co|sh|ai|app|dev|me|us|uk|ca|de|fr|es|it|nl|eu|au|in|jp|br|mx|info|biz|shop|store|site|online|link|page|xyz|ly|gg|to|tv|fm|so)(?:\.[a-z]{2})?(?![\w-])/gi;
14
+ /** The labels of a domain ending under a country's (amazon.co.uk), which name no store. */
15
+ const SECOND_LEVEL = new Set(['co', 'com', 'org', 'net', 'ac', 'gov', 'edu']);
16
+ /** Labels before a store's own that name a part of its site: shop.nike.com is Nike's, ca.mavi.com is Mavi's. */
17
+ const SITE_PART = new Set(['www', 'shop', 'store', 'stores', 'm', 'mobile', 'en', 'us', 'ca', 'uk']);
18
+ /**
19
+ * The name a bare address stands for, as the model reads a store in `reply`: walmart.com reads Walmart, shop.nike.com
20
+ * Nike, amazon.co.uk Amazon. Agentcard names a store by its domain, and a removed domain left the model with "Buy it at
21
+ * for $54.00" and no store to tell the shopper.
22
+ */
23
+ function siteName(host) {
24
+ const labels = host.toLowerCase().split('.').filter(Boolean);
25
+ labels.pop();
26
+ if (labels.length > 1 && SECOND_LEVEL.has(labels[labels.length - 1]))
27
+ labels.pop();
28
+ const name = labels.find((label, i) => i === labels.length - 1 || !(SITE_PART.has(label) || label.length <= 2)) ?? '';
29
+ return name ? name[0].toUpperCase() + name.slice(1) : '';
30
+ }
31
+ /** A www address: the site's name when it is bare (the closing mark it ends a sentence with stays), nothing when it carries a path. */
32
+ function wwwName(address) {
33
+ const [, host, closing] = /^(.*?)([.,;:!?]*)$/.exec(address) ?? [];
34
+ return host && !/[/?#]/.test(host) ? siteName(host) + (closing ?? '') : '';
35
+ }
36
+ /**
37
+ * The text with every link taken out: a Markdown link keeps its label, an address with a path or a scheme goes
38
+ * entirely, and a bare address reads as the site's name (siteName), so the store a reply names stays in it.
39
+ */
15
40
  export function stripLinks(text) {
16
41
  return String(text ?? '')
17
42
  .replace(MARKDOWN_LINK, '$1')
18
43
  .replace(SCHEME_URL, '')
19
- .replace(WWW_URL, '')
44
+ .replace(WWW_URL, wwwName)
20
45
  .replace(HOST_PATH, '')
21
- .replace(BARE_HOST, '')
46
+ .replace(BARE_HOST, siteName)
22
47
  .replace(/\(\s*\)|\[\s*\]|<\s*>/g, '')
23
48
  .replace(/[ \t]+([.,;:!?])/g, '$1')
24
49
  .replace(/[ \t]{2,}/g, ' ')
@@ -60,12 +85,20 @@ export function money(minor, currency) {
60
85
  return format.format(minor / 10 ** minorUnits(format));
61
86
  }
62
87
  /**
63
- * The products the model reads: the first twenty that keep a name once links
64
- * are taken out. The tool sends store pages for these same twenty, so the shopper
65
- * never holds a product link the model cannot name.
88
+ * The products the model reads: the first twenty that keep a name and a store
89
+ * once links are taken out, so the model never offers a product without saying
90
+ * where it is sold. The tool sends store pages for these same twenty, so the
91
+ * shopper never holds a product link the model cannot name.
66
92
  */
67
93
  export function shownProducts(products) {
68
- return (products ?? []).filter((product) => stripLinks(product.name) !== '').slice(0, MAX_PRODUCTS);
94
+ return (products ?? []).filter((product) => stripLinks(product.name) !== '' && stripLinks(product.store ?? '') !== '').slice(0, MAX_PRODUCTS);
95
+ }
96
+ /**
97
+ * The stores the model reads: each one whose id and name hold no link, so the model never reads a
98
+ * link and never offers a store without its name. The tool sends pages for these same stores.
99
+ */
100
+ export function shownStores(stores) {
101
+ return (stores ?? []).filter((store) => store.id !== '' && stripLinks(store.id) === store.id && stripLinks(store.name) !== '');
69
102
  }
70
103
  /**
71
104
  * The id the model passes back for a product: Agentcard's link-free ref, else the product's own id when
@@ -95,20 +128,29 @@ export function forModel(result) {
95
128
  const status = typeof result.status === 'string' && MODEL_STATUSES.has(result.status) ? result.status : 'needs_input';
96
129
  const products = shownProducts(result.products);
97
130
  const held = (product) => product.linkSent === true && typeof product.url === 'string' && product.url !== '';
131
+ const stores = shownStores(result.stores);
132
+ const heldStore = (store) => store.linkSent === true && typeof store.url === 'string' && store.url !== '';
98
133
  return {
99
134
  status,
100
135
  next_action: result.next_action ?? null,
101
136
  reply: stripLinks(result.reply),
102
137
  cart: result.cart ? cartView(result.cart) : null,
103
138
  carts: (result.carts ?? []).map(cartView),
104
- products: products.map((product) => ({ product_id: pickId(product), name: stripLinks(product.name), price: money(product.priceCents, product.currency), link_sent: held(product) })),
139
+ products: products.map((product) => ({ product_id: pickId(product), name: stripLinks(product.name), store: stripLinks(product.store ?? ''), price: money(product.priceCents, product.currency), link_sent: held(product) })),
105
140
  approval_expires_at: typeof result.approval_expires_at === 'string' ? stripLinks(result.approval_expires_at) || null : null,
106
141
  shopper_need_not_reply: result.shopper_need_not_reply === true,
142
+ shopper_already_told_looking: result.shopper_already_told_looking === true,
107
143
  link_sent: result.link_sent === true,
108
144
  // Two products on one page share one link, so the count is of pages.
109
145
  product_links_sent: new Set(products.filter(held).map((product) => product.url)).size,
110
146
  // The id the model sent, never the catalog id, which is a store page for a retail product.
111
147
  pick: result.pick ? { product_id: pickId({ id: result.pick.product_id, ref: result.pick.ref }), in_cart: result.pick.in_cart === true } : null,
148
+ stores: stores.map((store) => ({ store_id: store.id, name: stripLinks(store.name), eta_minutes: store.etaMinutes, link_sent: heldStore(store) })),
149
+ store_links_sent: new Set(stores.filter(heldStore).map((store) => store.url)).size,
150
+ // A store id with a link in it is never read back to the model.
151
+ store_pick: result.store_pick && stripLinks(result.store_pick.store_id) === result.store_pick.store_id
152
+ ? { store_id: result.store_pick.store_id, selected: result.store_pick.selected === true }
153
+ : null,
112
154
  };
113
155
  }
114
156
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.5.0",
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.",
3
+ "version": "0.9.0",
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",
7
7
  "types": "dist/index.d.ts",