@brinkcommerce/agentic-shopping-sdk 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/chunk-2MMTZSYZ.js +26 -0
- package/dist/errors-CYbkUYHr.d.cts +126 -0
- package/dist/errors-CYbkUYHr.d.ts +126 -0
- package/dist/index.cjs +168 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +127 -0
- package/dist/server.cjs +244 -0
- package/dist/server.d.cts +103 -0
- package/dist/server.d.ts +103 -0
- package/dist/server.js +204 -0
- package/package.json +54 -0
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __typeError = (msg) => {
|
|
7
|
+
throw TypeError(msg);
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
23
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
24
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
25
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
26
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
27
|
+
|
|
28
|
+
// src/server.ts
|
|
29
|
+
var server_exports = {};
|
|
30
|
+
__export(server_exports, {
|
|
31
|
+
AgentError: () => AgentError,
|
|
32
|
+
AgentShoppingClient: () => AgentShoppingClient,
|
|
33
|
+
createChatRoute: () => createChatRoute,
|
|
34
|
+
createTokenSource: () => createTokenSource
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(server_exports);
|
|
37
|
+
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
var AgentError = class extends Error {
|
|
40
|
+
constructor(statusCode, body) {
|
|
41
|
+
super(`Agent error: ${statusCode}`);
|
|
42
|
+
this.statusCode = statusCode;
|
|
43
|
+
this.body = body;
|
|
44
|
+
this.name = "AgentError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/server/token.ts
|
|
49
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
50
|
+
function createTokenSource({ tokenUrl, clientId, clientSecret, scope }) {
|
|
51
|
+
let token;
|
|
52
|
+
let expiresAt = 0;
|
|
53
|
+
let inFlight;
|
|
54
|
+
async function mint() {
|
|
55
|
+
const response = await fetch(tokenUrl, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
58
|
+
body: new URLSearchParams({
|
|
59
|
+
grant_type: "client_credentials",
|
|
60
|
+
scope,
|
|
61
|
+
client_id: clientId,
|
|
62
|
+
client_secret: clientSecret
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) throw new AgentError(response.status, void 0);
|
|
66
|
+
const minted = await response.json();
|
|
67
|
+
if (typeof minted.access_token !== "string" || minted.access_token === "") {
|
|
68
|
+
throw new AgentError(response.status, void 0);
|
|
69
|
+
}
|
|
70
|
+
const lifetimeMs = typeof minted.expires_in === "number" ? minted.expires_in * 1e3 : 0;
|
|
71
|
+
token = minted.access_token;
|
|
72
|
+
expiresAt = Date.now() + Math.max(lifetimeMs - EXPIRY_SKEW_MS, 0);
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
token() {
|
|
77
|
+
if (token !== void 0 && Date.now() < expiresAt) return Promise.resolve(token);
|
|
78
|
+
inFlight ?? (inFlight = mint().finally(() => {
|
|
79
|
+
inFlight = void 0;
|
|
80
|
+
}));
|
|
81
|
+
return inFlight;
|
|
82
|
+
},
|
|
83
|
+
invalidate() {
|
|
84
|
+
token = void 0;
|
|
85
|
+
expiresAt = 0;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/server/client.ts
|
|
91
|
+
var DEFAULT_SCOPE = "agentic-shopping/invoke";
|
|
92
|
+
var RUNTIME_ARN = /^arn:aws:bedrock-agentcore:([a-z0-9-]+):\d{12}:runtime\/[^/]+$/;
|
|
93
|
+
function invocationUrl({ runtimeArn, qualifier = "DEFAULT", agentUrl }) {
|
|
94
|
+
if (runtimeArn !== void 0 && agentUrl !== void 0) throw new TypeError("pass runtimeArn or agentUrl, not both");
|
|
95
|
+
if (agentUrl !== void 0) return agentUrl;
|
|
96
|
+
if (runtimeArn === void 0) throw new TypeError("runtimeArn is required, or agentUrl instead of it");
|
|
97
|
+
const region = RUNTIME_ARN.exec(runtimeArn)?.[1];
|
|
98
|
+
if (region === void 0) throw new TypeError(`runtimeArn is not a bedrock-agentcore runtime ARN: ${runtimeArn}`);
|
|
99
|
+
const path = `/runtimes/${encodeURIComponent(runtimeArn)}/invocations?qualifier=${encodeURIComponent(qualifier)}`;
|
|
100
|
+
return `https://bedrock-agentcore.${region}.amazonaws.com${path}`;
|
|
101
|
+
}
|
|
102
|
+
var SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id";
|
|
103
|
+
var _agentUrl, _tokens, _AgentShoppingClient_instances, send_fn, invoke_fn;
|
|
104
|
+
var AgentShoppingClient = class {
|
|
105
|
+
constructor(options) {
|
|
106
|
+
__privateAdd(this, _AgentShoppingClient_instances);
|
|
107
|
+
__privateAdd(this, _agentUrl);
|
|
108
|
+
__privateAdd(this, _tokens);
|
|
109
|
+
const { tokenUrl, clientId, clientSecret, scope = DEFAULT_SCOPE } = options;
|
|
110
|
+
__privateSet(this, _agentUrl, invocationUrl(options));
|
|
111
|
+
__privateSet(this, _tokens, createTokenSource({ tokenUrl, clientId, clientSecret, scope }));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The raw response, body unread. Pass `response.body` straight through to the browser, or read it with
|
|
115
|
+
* `parseWireEvents` from the core entry point.
|
|
116
|
+
*/
|
|
117
|
+
stream(input, options = {}) {
|
|
118
|
+
return __privateMethod(this, _AgentShoppingClient_instances, invoke_fn).call(this, input, true, options);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One turn as a whole reply. The non-streaming branch **never carries product cards** — the agent writes
|
|
122
|
+
* them only onto the stream. If you want cards, use `stream()`.
|
|
123
|
+
*/
|
|
124
|
+
async ask(input, options = {}) {
|
|
125
|
+
const response = await __privateMethod(this, _AgentShoppingClient_instances, invoke_fn).call(this, input, false, options);
|
|
126
|
+
const body = await response.json();
|
|
127
|
+
const result = body.result;
|
|
128
|
+
return {
|
|
129
|
+
text: typeof result?.text === "string" ? result.text : "",
|
|
130
|
+
suggestions: Array.isArray(result?.suggestions) ? result.suggestions : []
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
_agentUrl = new WeakMap();
|
|
135
|
+
_tokens = new WeakMap();
|
|
136
|
+
_AgentShoppingClient_instances = new WeakSet();
|
|
137
|
+
send_fn = async function(input, streaming, signal) {
|
|
138
|
+
return fetch(__privateGet(this, _agentUrl), {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Authorization: `Bearer ${await __privateGet(this, _tokens).token()}`,
|
|
143
|
+
[SESSION_HEADER]: input.sessionId
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
prompt: input.prompt,
|
|
147
|
+
streaming,
|
|
148
|
+
storeGroupId: input.storeGroupId,
|
|
149
|
+
market: input.market,
|
|
150
|
+
// `JSON.stringify` drops an undefined value, so no cart sends no field — which is what tells the
|
|
151
|
+
// agent the cart is invisible rather than empty.
|
|
152
|
+
cart: input.cart
|
|
153
|
+
}),
|
|
154
|
+
signal
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
invoke_fn = async function(input, streaming, options) {
|
|
158
|
+
let response = await __privateMethod(this, _AgentShoppingClient_instances, send_fn).call(this, input, streaming, options.signal);
|
|
159
|
+
if (response.status === 401) {
|
|
160
|
+
__privateGet(this, _tokens).invalidate();
|
|
161
|
+
response = await __privateMethod(this, _AgentShoppingClient_instances, send_fn).call(this, input, streaming, options.signal);
|
|
162
|
+
}
|
|
163
|
+
if (!response.ok) throw new AgentError(response.status, await response.text());
|
|
164
|
+
return response;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/server/route.ts
|
|
168
|
+
function jsonError(status, message) {
|
|
169
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
170
|
+
status,
|
|
171
|
+
headers: { "Content-Type": "application/json" }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function readCart(value) {
|
|
175
|
+
if (value === void 0) return void 0;
|
|
176
|
+
if (typeof value !== "object" || value === null) return "cart must be an object";
|
|
177
|
+
const { items, totalAmount, currencyCode } = value;
|
|
178
|
+
if (!Array.isArray(items)) return "cart.items must be an array";
|
|
179
|
+
if (typeof totalAmount !== "number" || !Number.isInteger(totalAmount) || totalAmount < 0) {
|
|
180
|
+
return "cart.totalAmount must be a whole number of minor units";
|
|
181
|
+
}
|
|
182
|
+
if (typeof currencyCode !== "string" || currencyCode.length !== 3) {
|
|
183
|
+
return "cart.currencyCode must be a three-letter code";
|
|
184
|
+
}
|
|
185
|
+
const lines = [];
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
if (typeof item !== "object" || item === null) return "each cart item must be an object";
|
|
188
|
+
const { name, size, quantity } = item;
|
|
189
|
+
if (typeof name !== "string" || name === "") return "each cart item needs a name";
|
|
190
|
+
if (size !== void 0 && typeof size !== "string") return "a cart item size must be a string";
|
|
191
|
+
if (typeof quantity !== "number" || !Number.isInteger(quantity) || quantity < 1) {
|
|
192
|
+
return "each cart item needs a quantity of at least 1";
|
|
193
|
+
}
|
|
194
|
+
lines.push(size === void 0 ? { name, quantity } : { name, size, quantity });
|
|
195
|
+
}
|
|
196
|
+
return { items: lines, totalAmount, currencyCode };
|
|
197
|
+
}
|
|
198
|
+
function readAskInput(payload) {
|
|
199
|
+
if (typeof payload !== "object" || payload === null) return "body must be a JSON object";
|
|
200
|
+
const { prompt, sessionId, storeGroupId, market, cart } = payload;
|
|
201
|
+
for (const [field, value] of Object.entries({ prompt, sessionId, storeGroupId, market })) {
|
|
202
|
+
if (typeof value !== "string" || value === "") return `${field} is required`;
|
|
203
|
+
}
|
|
204
|
+
const parsed = readCart(cart);
|
|
205
|
+
if (typeof parsed === "string") return parsed;
|
|
206
|
+
const input = { prompt, sessionId, storeGroupId, market };
|
|
207
|
+
return parsed === void 0 ? input : { ...input, cart: parsed };
|
|
208
|
+
}
|
|
209
|
+
function createChatRoute(client) {
|
|
210
|
+
return async (request) => {
|
|
211
|
+
let payload;
|
|
212
|
+
try {
|
|
213
|
+
payload = await request.json();
|
|
214
|
+
} catch {
|
|
215
|
+
return jsonError(400, "body is not valid JSON");
|
|
216
|
+
}
|
|
217
|
+
const input = readAskInput(payload);
|
|
218
|
+
if (typeof input === "string") return jsonError(400, input);
|
|
219
|
+
let upstream;
|
|
220
|
+
try {
|
|
221
|
+
upstream = await client.stream(input, { signal: request.signal });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (request.signal.aborted) return new Response(null, { status: 499 });
|
|
224
|
+
if (error instanceof AgentError) return jsonError(502, `the agent answered ${error.statusCode}`);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
return new Response(upstream.body, {
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": "application/x-ndjson",
|
|
230
|
+
// Two hints for anything between this route and the shopper: do not store the reply, and do not
|
|
231
|
+
// hold it back waiting for a full buffer. A proxy that buffers turns a stream into a long pause.
|
|
232
|
+
"Cache-Control": "no-store",
|
|
233
|
+
"X-Accel-Buffering": "no"
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
239
|
+
0 && (module.exports = {
|
|
240
|
+
AgentError,
|
|
241
|
+
AgentShoppingClient,
|
|
242
|
+
createChatRoute,
|
|
243
|
+
createTokenSource
|
|
244
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { C as CartContext } from './errors-CYbkUYHr.cjs';
|
|
2
|
+
export { A as AgentError, a as CartLine } from './errors-CYbkUYHr.cjs';
|
|
3
|
+
|
|
4
|
+
/** The credentials Brink provides, whichever way the agent itself is named. */
|
|
5
|
+
interface Credentials {
|
|
6
|
+
/** The OAuth2 token endpoint the client credentials are minted against. */
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
/** Server-side only. This must never reach a browser bundle. */
|
|
10
|
+
clientSecret: string;
|
|
11
|
+
scope?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Name the agent one of two ways: the runtime ARN Brink hands over, or a URL you built yourself — your own
|
|
15
|
+
* gateway in front of the agent, or a container running locally.
|
|
16
|
+
*/
|
|
17
|
+
type AgentShoppingClientOptions = Credentials & ({
|
|
18
|
+
runtimeArn: string;
|
|
19
|
+
qualifier?: string;
|
|
20
|
+
} | {
|
|
21
|
+
agentUrl: string;
|
|
22
|
+
});
|
|
23
|
+
interface AskInput {
|
|
24
|
+
prompt: string;
|
|
25
|
+
/** One conversation. `createSessionId()` from the core entry point makes one. */
|
|
26
|
+
sessionId: string;
|
|
27
|
+
/**
|
|
28
|
+
* Required. The agent answers a request naming no store with one sentence and no search, at HTTP 200 — a
|
|
29
|
+
* failure that looks exactly like success. Requiring it here turns that into a compile error.
|
|
30
|
+
*/
|
|
31
|
+
storeGroupId: string;
|
|
32
|
+
/** Required, for the same reason. Two-letter country code. */
|
|
33
|
+
market: string;
|
|
34
|
+
/**
|
|
35
|
+
* What the shopper already owns, so the agent can answer "what goes with what I have?". Optional, and
|
|
36
|
+
* absent is not empty: send `items: []` for an empty cart, and omit `cart` to leave it invisible.
|
|
37
|
+
*/
|
|
38
|
+
cart?: CartContext;
|
|
39
|
+
}
|
|
40
|
+
interface AskOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Cancels the turn. A shopper who navigates away mid-answer leaves the agent generating one otherwise,
|
|
43
|
+
* and a stop button has nothing to call.
|
|
44
|
+
*/
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
declare class AgentShoppingClient {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(options: AgentShoppingClientOptions);
|
|
50
|
+
/**
|
|
51
|
+
* The raw response, body unread. Pass `response.body` straight through to the browser, or read it with
|
|
52
|
+
* `parseWireEvents` from the core entry point.
|
|
53
|
+
*/
|
|
54
|
+
stream(input: AskInput, options?: AskOptions): Promise<Response>;
|
|
55
|
+
/**
|
|
56
|
+
* One turn as a whole reply. The non-streaming branch **never carries product cards** — the agent writes
|
|
57
|
+
* them only onto the stream. If you want cards, use `stream()`.
|
|
58
|
+
*/
|
|
59
|
+
ask(input: AskInput, options?: AskOptions): Promise<{
|
|
60
|
+
text: string;
|
|
61
|
+
suggestions: string[];
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A route handler over web-standard `Request`/`Response`, so it drops into a Next.js App Router file as
|
|
67
|
+
* `export const POST = createChatRoute(client)` without this SDK depending on Next.js.
|
|
68
|
+
*
|
|
69
|
+
* The upstream body is passed through untransformed — reading it here would buffer the whole reply and undo
|
|
70
|
+
* the streaming. A payload missing a field is rejected with a 400 rather than forwarded: the agent would
|
|
71
|
+
* answer it with one sentence at HTTP 200, which no caller can tell from a real reply.
|
|
72
|
+
*
|
|
73
|
+
* A transport failure comes back as a 502 carrying JSON, so a browser reading the error body finds the
|
|
74
|
+
* status rather than whatever error page the framework would otherwise have rendered. The upstream body is
|
|
75
|
+
* not forwarded: a failed token mint's body can quote the client secret.
|
|
76
|
+
*/
|
|
77
|
+
declare function createChatRoute(client: AgentShoppingClient): (request: Request) => Promise<Response>;
|
|
78
|
+
|
|
79
|
+
interface TokenSourceOptions {
|
|
80
|
+
tokenUrl: string;
|
|
81
|
+
clientId: string;
|
|
82
|
+
clientSecret: string;
|
|
83
|
+
scope: string;
|
|
84
|
+
}
|
|
85
|
+
interface TokenSource {
|
|
86
|
+
/** A valid access token: minted on first use, then cached until it is about to expire. */
|
|
87
|
+
token(): Promise<string>;
|
|
88
|
+
/** Drops the cached token, so the next call mints a fresh one. For when the agent rejects the cached one. */
|
|
89
|
+
invalidate(): void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* An OAuth2 client-credentials mint, cached in process until it is about to expire. One token serves every
|
|
93
|
+
* turn — minting per request is a round trip the agent does not need.
|
|
94
|
+
*
|
|
95
|
+
* Concurrent callers arriving on a cold cache share one mint rather than each starting their own, so a
|
|
96
|
+
* process that takes twenty requests at once still asks the authorization server for a single token.
|
|
97
|
+
*
|
|
98
|
+
* A failed mint throws `AgentError` carrying the status and **no body**: an `invalid_client` response can
|
|
99
|
+
* echo the client secret back in its description.
|
|
100
|
+
*/
|
|
101
|
+
declare function createTokenSource({ tokenUrl, clientId, clientSecret, scope }: TokenSourceOptions): TokenSource;
|
|
102
|
+
|
|
103
|
+
export { AgentShoppingClient, type AgentShoppingClientOptions, type AskInput, type AskOptions, CartContext, type TokenSource, type TokenSourceOptions, createChatRoute, createTokenSource };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { C as CartContext } from './errors-CYbkUYHr.js';
|
|
2
|
+
export { A as AgentError, a as CartLine } from './errors-CYbkUYHr.js';
|
|
3
|
+
|
|
4
|
+
/** The credentials Brink provides, whichever way the agent itself is named. */
|
|
5
|
+
interface Credentials {
|
|
6
|
+
/** The OAuth2 token endpoint the client credentials are minted against. */
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
/** Server-side only. This must never reach a browser bundle. */
|
|
10
|
+
clientSecret: string;
|
|
11
|
+
scope?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Name the agent one of two ways: the runtime ARN Brink hands over, or a URL you built yourself — your own
|
|
15
|
+
* gateway in front of the agent, or a container running locally.
|
|
16
|
+
*/
|
|
17
|
+
type AgentShoppingClientOptions = Credentials & ({
|
|
18
|
+
runtimeArn: string;
|
|
19
|
+
qualifier?: string;
|
|
20
|
+
} | {
|
|
21
|
+
agentUrl: string;
|
|
22
|
+
});
|
|
23
|
+
interface AskInput {
|
|
24
|
+
prompt: string;
|
|
25
|
+
/** One conversation. `createSessionId()` from the core entry point makes one. */
|
|
26
|
+
sessionId: string;
|
|
27
|
+
/**
|
|
28
|
+
* Required. The agent answers a request naming no store with one sentence and no search, at HTTP 200 — a
|
|
29
|
+
* failure that looks exactly like success. Requiring it here turns that into a compile error.
|
|
30
|
+
*/
|
|
31
|
+
storeGroupId: string;
|
|
32
|
+
/** Required, for the same reason. Two-letter country code. */
|
|
33
|
+
market: string;
|
|
34
|
+
/**
|
|
35
|
+
* What the shopper already owns, so the agent can answer "what goes with what I have?". Optional, and
|
|
36
|
+
* absent is not empty: send `items: []` for an empty cart, and omit `cart` to leave it invisible.
|
|
37
|
+
*/
|
|
38
|
+
cart?: CartContext;
|
|
39
|
+
}
|
|
40
|
+
interface AskOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Cancels the turn. A shopper who navigates away mid-answer leaves the agent generating one otherwise,
|
|
43
|
+
* and a stop button has nothing to call.
|
|
44
|
+
*/
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
declare class AgentShoppingClient {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(options: AgentShoppingClientOptions);
|
|
50
|
+
/**
|
|
51
|
+
* The raw response, body unread. Pass `response.body` straight through to the browser, or read it with
|
|
52
|
+
* `parseWireEvents` from the core entry point.
|
|
53
|
+
*/
|
|
54
|
+
stream(input: AskInput, options?: AskOptions): Promise<Response>;
|
|
55
|
+
/**
|
|
56
|
+
* One turn as a whole reply. The non-streaming branch **never carries product cards** — the agent writes
|
|
57
|
+
* them only onto the stream. If you want cards, use `stream()`.
|
|
58
|
+
*/
|
|
59
|
+
ask(input: AskInput, options?: AskOptions): Promise<{
|
|
60
|
+
text: string;
|
|
61
|
+
suggestions: string[];
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A route handler over web-standard `Request`/`Response`, so it drops into a Next.js App Router file as
|
|
67
|
+
* `export const POST = createChatRoute(client)` without this SDK depending on Next.js.
|
|
68
|
+
*
|
|
69
|
+
* The upstream body is passed through untransformed — reading it here would buffer the whole reply and undo
|
|
70
|
+
* the streaming. A payload missing a field is rejected with a 400 rather than forwarded: the agent would
|
|
71
|
+
* answer it with one sentence at HTTP 200, which no caller can tell from a real reply.
|
|
72
|
+
*
|
|
73
|
+
* A transport failure comes back as a 502 carrying JSON, so a browser reading the error body finds the
|
|
74
|
+
* status rather than whatever error page the framework would otherwise have rendered. The upstream body is
|
|
75
|
+
* not forwarded: a failed token mint's body can quote the client secret.
|
|
76
|
+
*/
|
|
77
|
+
declare function createChatRoute(client: AgentShoppingClient): (request: Request) => Promise<Response>;
|
|
78
|
+
|
|
79
|
+
interface TokenSourceOptions {
|
|
80
|
+
tokenUrl: string;
|
|
81
|
+
clientId: string;
|
|
82
|
+
clientSecret: string;
|
|
83
|
+
scope: string;
|
|
84
|
+
}
|
|
85
|
+
interface TokenSource {
|
|
86
|
+
/** A valid access token: minted on first use, then cached until it is about to expire. */
|
|
87
|
+
token(): Promise<string>;
|
|
88
|
+
/** Drops the cached token, so the next call mints a fresh one. For when the agent rejects the cached one. */
|
|
89
|
+
invalidate(): void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* An OAuth2 client-credentials mint, cached in process until it is about to expire. One token serves every
|
|
93
|
+
* turn — minting per request is a round trip the agent does not need.
|
|
94
|
+
*
|
|
95
|
+
* Concurrent callers arriving on a cold cache share one mint rather than each starting their own, so a
|
|
96
|
+
* process that takes twenty requests at once still asks the authorization server for a single token.
|
|
97
|
+
*
|
|
98
|
+
* A failed mint throws `AgentError` carrying the status and **no body**: an `invalid_client` response can
|
|
99
|
+
* echo the client secret back in its description.
|
|
100
|
+
*/
|
|
101
|
+
declare function createTokenSource({ tokenUrl, clientId, clientSecret, scope }: TokenSourceOptions): TokenSource;
|
|
102
|
+
|
|
103
|
+
export { AgentShoppingClient, type AgentShoppingClientOptions, type AskInput, type AskOptions, CartContext, type TokenSource, type TokenSourceOptions, createChatRoute, createTokenSource };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentError,
|
|
3
|
+
__privateAdd,
|
|
4
|
+
__privateGet,
|
|
5
|
+
__privateMethod,
|
|
6
|
+
__privateSet
|
|
7
|
+
} from "./chunk-2MMTZSYZ.js";
|
|
8
|
+
|
|
9
|
+
// src/server/token.ts
|
|
10
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
11
|
+
function createTokenSource({ tokenUrl, clientId, clientSecret, scope }) {
|
|
12
|
+
let token;
|
|
13
|
+
let expiresAt = 0;
|
|
14
|
+
let inFlight;
|
|
15
|
+
async function mint() {
|
|
16
|
+
const response = await fetch(tokenUrl, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
19
|
+
body: new URLSearchParams({
|
|
20
|
+
grant_type: "client_credentials",
|
|
21
|
+
scope,
|
|
22
|
+
client_id: clientId,
|
|
23
|
+
client_secret: clientSecret
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) throw new AgentError(response.status, void 0);
|
|
27
|
+
const minted = await response.json();
|
|
28
|
+
if (typeof minted.access_token !== "string" || minted.access_token === "") {
|
|
29
|
+
throw new AgentError(response.status, void 0);
|
|
30
|
+
}
|
|
31
|
+
const lifetimeMs = typeof minted.expires_in === "number" ? minted.expires_in * 1e3 : 0;
|
|
32
|
+
token = minted.access_token;
|
|
33
|
+
expiresAt = Date.now() + Math.max(lifetimeMs - EXPIRY_SKEW_MS, 0);
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
token() {
|
|
38
|
+
if (token !== void 0 && Date.now() < expiresAt) return Promise.resolve(token);
|
|
39
|
+
inFlight ?? (inFlight = mint().finally(() => {
|
|
40
|
+
inFlight = void 0;
|
|
41
|
+
}));
|
|
42
|
+
return inFlight;
|
|
43
|
+
},
|
|
44
|
+
invalidate() {
|
|
45
|
+
token = void 0;
|
|
46
|
+
expiresAt = 0;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/server/client.ts
|
|
52
|
+
var DEFAULT_SCOPE = "agentic-shopping/invoke";
|
|
53
|
+
var RUNTIME_ARN = /^arn:aws:bedrock-agentcore:([a-z0-9-]+):\d{12}:runtime\/[^/]+$/;
|
|
54
|
+
function invocationUrl({ runtimeArn, qualifier = "DEFAULT", agentUrl }) {
|
|
55
|
+
if (runtimeArn !== void 0 && agentUrl !== void 0) throw new TypeError("pass runtimeArn or agentUrl, not both");
|
|
56
|
+
if (agentUrl !== void 0) return agentUrl;
|
|
57
|
+
if (runtimeArn === void 0) throw new TypeError("runtimeArn is required, or agentUrl instead of it");
|
|
58
|
+
const region = RUNTIME_ARN.exec(runtimeArn)?.[1];
|
|
59
|
+
if (region === void 0) throw new TypeError(`runtimeArn is not a bedrock-agentcore runtime ARN: ${runtimeArn}`);
|
|
60
|
+
const path = `/runtimes/${encodeURIComponent(runtimeArn)}/invocations?qualifier=${encodeURIComponent(qualifier)}`;
|
|
61
|
+
return `https://bedrock-agentcore.${region}.amazonaws.com${path}`;
|
|
62
|
+
}
|
|
63
|
+
var SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id";
|
|
64
|
+
var _agentUrl, _tokens, _AgentShoppingClient_instances, send_fn, invoke_fn;
|
|
65
|
+
var AgentShoppingClient = class {
|
|
66
|
+
constructor(options) {
|
|
67
|
+
__privateAdd(this, _AgentShoppingClient_instances);
|
|
68
|
+
__privateAdd(this, _agentUrl);
|
|
69
|
+
__privateAdd(this, _tokens);
|
|
70
|
+
const { tokenUrl, clientId, clientSecret, scope = DEFAULT_SCOPE } = options;
|
|
71
|
+
__privateSet(this, _agentUrl, invocationUrl(options));
|
|
72
|
+
__privateSet(this, _tokens, createTokenSource({ tokenUrl, clientId, clientSecret, scope }));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The raw response, body unread. Pass `response.body` straight through to the browser, or read it with
|
|
76
|
+
* `parseWireEvents` from the core entry point.
|
|
77
|
+
*/
|
|
78
|
+
stream(input, options = {}) {
|
|
79
|
+
return __privateMethod(this, _AgentShoppingClient_instances, invoke_fn).call(this, input, true, options);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* One turn as a whole reply. The non-streaming branch **never carries product cards** — the agent writes
|
|
83
|
+
* them only onto the stream. If you want cards, use `stream()`.
|
|
84
|
+
*/
|
|
85
|
+
async ask(input, options = {}) {
|
|
86
|
+
const response = await __privateMethod(this, _AgentShoppingClient_instances, invoke_fn).call(this, input, false, options);
|
|
87
|
+
const body = await response.json();
|
|
88
|
+
const result = body.result;
|
|
89
|
+
return {
|
|
90
|
+
text: typeof result?.text === "string" ? result.text : "",
|
|
91
|
+
suggestions: Array.isArray(result?.suggestions) ? result.suggestions : []
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
_agentUrl = new WeakMap();
|
|
96
|
+
_tokens = new WeakMap();
|
|
97
|
+
_AgentShoppingClient_instances = new WeakSet();
|
|
98
|
+
send_fn = async function(input, streaming, signal) {
|
|
99
|
+
return fetch(__privateGet(this, _agentUrl), {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
Authorization: `Bearer ${await __privateGet(this, _tokens).token()}`,
|
|
104
|
+
[SESSION_HEADER]: input.sessionId
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
prompt: input.prompt,
|
|
108
|
+
streaming,
|
|
109
|
+
storeGroupId: input.storeGroupId,
|
|
110
|
+
market: input.market,
|
|
111
|
+
// `JSON.stringify` drops an undefined value, so no cart sends no field — which is what tells the
|
|
112
|
+
// agent the cart is invisible rather than empty.
|
|
113
|
+
cart: input.cart
|
|
114
|
+
}),
|
|
115
|
+
signal
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
invoke_fn = async function(input, streaming, options) {
|
|
119
|
+
let response = await __privateMethod(this, _AgentShoppingClient_instances, send_fn).call(this, input, streaming, options.signal);
|
|
120
|
+
if (response.status === 401) {
|
|
121
|
+
__privateGet(this, _tokens).invalidate();
|
|
122
|
+
response = await __privateMethod(this, _AgentShoppingClient_instances, send_fn).call(this, input, streaming, options.signal);
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) throw new AgentError(response.status, await response.text());
|
|
125
|
+
return response;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/server/route.ts
|
|
129
|
+
function jsonError(status, message) {
|
|
130
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
131
|
+
status,
|
|
132
|
+
headers: { "Content-Type": "application/json" }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function readCart(value) {
|
|
136
|
+
if (value === void 0) return void 0;
|
|
137
|
+
if (typeof value !== "object" || value === null) return "cart must be an object";
|
|
138
|
+
const { items, totalAmount, currencyCode } = value;
|
|
139
|
+
if (!Array.isArray(items)) return "cart.items must be an array";
|
|
140
|
+
if (typeof totalAmount !== "number" || !Number.isInteger(totalAmount) || totalAmount < 0) {
|
|
141
|
+
return "cart.totalAmount must be a whole number of minor units";
|
|
142
|
+
}
|
|
143
|
+
if (typeof currencyCode !== "string" || currencyCode.length !== 3) {
|
|
144
|
+
return "cart.currencyCode must be a three-letter code";
|
|
145
|
+
}
|
|
146
|
+
const lines = [];
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
if (typeof item !== "object" || item === null) return "each cart item must be an object";
|
|
149
|
+
const { name, size, quantity } = item;
|
|
150
|
+
if (typeof name !== "string" || name === "") return "each cart item needs a name";
|
|
151
|
+
if (size !== void 0 && typeof size !== "string") return "a cart item size must be a string";
|
|
152
|
+
if (typeof quantity !== "number" || !Number.isInteger(quantity) || quantity < 1) {
|
|
153
|
+
return "each cart item needs a quantity of at least 1";
|
|
154
|
+
}
|
|
155
|
+
lines.push(size === void 0 ? { name, quantity } : { name, size, quantity });
|
|
156
|
+
}
|
|
157
|
+
return { items: lines, totalAmount, currencyCode };
|
|
158
|
+
}
|
|
159
|
+
function readAskInput(payload) {
|
|
160
|
+
if (typeof payload !== "object" || payload === null) return "body must be a JSON object";
|
|
161
|
+
const { prompt, sessionId, storeGroupId, market, cart } = payload;
|
|
162
|
+
for (const [field, value] of Object.entries({ prompt, sessionId, storeGroupId, market })) {
|
|
163
|
+
if (typeof value !== "string" || value === "") return `${field} is required`;
|
|
164
|
+
}
|
|
165
|
+
const parsed = readCart(cart);
|
|
166
|
+
if (typeof parsed === "string") return parsed;
|
|
167
|
+
const input = { prompt, sessionId, storeGroupId, market };
|
|
168
|
+
return parsed === void 0 ? input : { ...input, cart: parsed };
|
|
169
|
+
}
|
|
170
|
+
function createChatRoute(client) {
|
|
171
|
+
return async (request) => {
|
|
172
|
+
let payload;
|
|
173
|
+
try {
|
|
174
|
+
payload = await request.json();
|
|
175
|
+
} catch {
|
|
176
|
+
return jsonError(400, "body is not valid JSON");
|
|
177
|
+
}
|
|
178
|
+
const input = readAskInput(payload);
|
|
179
|
+
if (typeof input === "string") return jsonError(400, input);
|
|
180
|
+
let upstream;
|
|
181
|
+
try {
|
|
182
|
+
upstream = await client.stream(input, { signal: request.signal });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (request.signal.aborted) return new Response(null, { status: 499 });
|
|
185
|
+
if (error instanceof AgentError) return jsonError(502, `the agent answered ${error.statusCode}`);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
return new Response(upstream.body, {
|
|
189
|
+
headers: {
|
|
190
|
+
"Content-Type": "application/x-ndjson",
|
|
191
|
+
// Two hints for anything between this route and the shopper: do not store the reply, and do not
|
|
192
|
+
// hold it back waiting for a full buffer. A proxy that buffers turns a stream into a long pause.
|
|
193
|
+
"Cache-Control": "no-store",
|
|
194
|
+
"X-Accel-Buffering": "no"
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
AgentError,
|
|
201
|
+
AgentShoppingClient,
|
|
202
|
+
createChatRoute,
|
|
203
|
+
createTokenSource
|
|
204
|
+
};
|