@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.
- package/README.md +96 -11
- package/dist/ack.d.ts +36 -8
- package/dist/ack.js +179 -16
- package/dist/adapters.js +1 -0
- package/dist/buy-tool.d.ts +79 -5
- package/dist/buy-tool.js +206 -29
- package/dist/client.d.ts +93 -7
- package/dist/client.js +158 -12
- package/dist/definition.d.ts +14 -6
- package/dist/definition.js +24 -8
- package/dist/events.d.ts +12 -0
- package/dist/events.js +79 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/listen.d.ts +93 -0
- package/dist/listen.js +496 -0
- package/dist/model-view.d.ts +38 -6
- package/dist/model-view.js +51 -9
- package/package.json +2 -2
package/dist/model-view.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
64
|
-
* are taken out
|
|
65
|
-
*
|
|
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.
|
package/dist/model-view.js
CHANGED
|
@@ -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
|
|
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
|
|
64
|
-
* are taken out
|
|
65
|
-
*
|
|
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.
|
|
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",
|