@nlxai/core 1.1.8-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/.eslintrc.cjs +5 -0
- package/.prettierrc +1 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +407 -0
- package/README.md +112 -0
- package/lib/index.cjs +736 -0
- package/lib/index.d.ts +755 -0
- package/lib/index.esm.js +729 -0
- package/lib/index.umd.js +15 -0
- package/package.json +53 -0
- package/rollup.config.ts +9 -0
- package/tsconfig.json +9 -0
- package/tsdoc.json +6 -0
- package/typedoc.cjs +13 -0
package/lib/index.esm.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import fetch from 'isomorphic-fetch';
|
|
2
|
+
import { equals, adjust } from 'ramda';
|
|
3
|
+
import ReconnectingWebSocket from 'reconnecting-websocket';
|
|
4
|
+
import { v4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
var name = "@nlxai/core";
|
|
7
|
+
var version$1 = "1.1.8-alpha.0";
|
|
8
|
+
var description = "Low-level SDK for building NLX experiences";
|
|
9
|
+
var type = "module";
|
|
10
|
+
var main = "lib/index.cjs";
|
|
11
|
+
var module = "lib/index.esm.js";
|
|
12
|
+
var browser = "lib/index.umd.js";
|
|
13
|
+
var types = "lib/index.d.ts";
|
|
14
|
+
var exports = {
|
|
15
|
+
".": {
|
|
16
|
+
types: "./lib/index.d.ts",
|
|
17
|
+
"import": "./lib/index.esm.js",
|
|
18
|
+
require: "./lib/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var scripts = {
|
|
22
|
+
build: "rm -rf lib && rollup -c --configPlugin typescript --configImportAttributesKey with",
|
|
23
|
+
docs: "rm -rf docs/ && typedoc && concat-md --decrease-title-levels --dir-name-as-title docs/ > docs/index.md",
|
|
24
|
+
"lint:check": "eslint src/ --ext .ts,.tsx,.js,.jsx --max-warnings 0",
|
|
25
|
+
lint: "eslint src/ --ext .ts,.tsx,.js,.jsx --fix",
|
|
26
|
+
prepublish: "npm run build",
|
|
27
|
+
"preview-docs": "npm run docs && comrak --unsafe --gfm -o docs/index.html docs/index.md && open docs/index.html",
|
|
28
|
+
"publish-docs": "npm run docs && mv docs/index.md ../website/src/content/headless-api-reference.md",
|
|
29
|
+
test: "typedoc --emit none",
|
|
30
|
+
tsc: "tsc"
|
|
31
|
+
};
|
|
32
|
+
var author = "Peter Szerzo <peter@nlx.ai>";
|
|
33
|
+
var license = "MIT";
|
|
34
|
+
var devDependencies = {
|
|
35
|
+
"@types/isomorphic-fetch": "^0.0.36",
|
|
36
|
+
"@types/node": "^22.10.1",
|
|
37
|
+
"@types/ramda": "0.29.1",
|
|
38
|
+
"@types/uuid": "^9.0.7",
|
|
39
|
+
"concat-md": "^0.5.1",
|
|
40
|
+
"eslint-config-nlx": "*",
|
|
41
|
+
prettier: "^3.1.0",
|
|
42
|
+
"rollup-config-nlx": "*",
|
|
43
|
+
typedoc: "^0.25.13",
|
|
44
|
+
"typedoc-plugin-markdown": "^3.17.1",
|
|
45
|
+
typescript: "^5.5.4"
|
|
46
|
+
};
|
|
47
|
+
var dependencies = {
|
|
48
|
+
"isomorphic-fetch": "^3.0.0",
|
|
49
|
+
ramda: "^0.29.1",
|
|
50
|
+
"reconnecting-websocket": "^4.4.0",
|
|
51
|
+
uuid: "^9.0.1"
|
|
52
|
+
};
|
|
53
|
+
var publishConfig = {
|
|
54
|
+
access: "public"
|
|
55
|
+
};
|
|
56
|
+
var gitHead = "8f4341c889c73a3b681dcdf25420ecec285c1ef6";
|
|
57
|
+
var packageJson = {
|
|
58
|
+
name: name,
|
|
59
|
+
version: version$1,
|
|
60
|
+
description: description,
|
|
61
|
+
type: type,
|
|
62
|
+
main: main,
|
|
63
|
+
module: module,
|
|
64
|
+
browser: browser,
|
|
65
|
+
types: types,
|
|
66
|
+
exports: exports,
|
|
67
|
+
scripts: scripts,
|
|
68
|
+
author: author,
|
|
69
|
+
license: license,
|
|
70
|
+
devDependencies: devDependencies,
|
|
71
|
+
dependencies: dependencies,
|
|
72
|
+
publishConfig: publishConfig,
|
|
73
|
+
gitHead: gitHead
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Package version
|
|
78
|
+
*/
|
|
79
|
+
const version = packageJson.version;
|
|
80
|
+
// use a custom Console to indicate we really want to log to the console and it's not incidental. `console.log` causes an eslint error
|
|
81
|
+
const Console = console;
|
|
82
|
+
/**
|
|
83
|
+
* Response type
|
|
84
|
+
*/
|
|
85
|
+
var ResponseType;
|
|
86
|
+
(function (ResponseType) {
|
|
87
|
+
/**
|
|
88
|
+
* Response from the application
|
|
89
|
+
*/
|
|
90
|
+
ResponseType["Application"] = "bot";
|
|
91
|
+
/**
|
|
92
|
+
* Response from the user
|
|
93
|
+
*/
|
|
94
|
+
ResponseType["User"] = "user";
|
|
95
|
+
/**
|
|
96
|
+
* Generic failure (cannot be attributed to the application)
|
|
97
|
+
*/
|
|
98
|
+
ResponseType["Failure"] = "failure";
|
|
99
|
+
})(ResponseType || (ResponseType = {}));
|
|
100
|
+
const welcomeIntent = "NLX.Welcome";
|
|
101
|
+
const defaultFailureMessage = "We encountered an issue. Please try again soon.";
|
|
102
|
+
const normalizeSlots = (slotsRecordOrArray) => {
|
|
103
|
+
if (Array.isArray(slotsRecordOrArray)) {
|
|
104
|
+
return slotsRecordOrArray;
|
|
105
|
+
}
|
|
106
|
+
return Object.entries(slotsRecordOrArray).map(([key, value]) => ({
|
|
107
|
+
slotId: key,
|
|
108
|
+
value,
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
const normalizeStructuredRequest = (structured) => ({
|
|
112
|
+
...structured,
|
|
113
|
+
intentId: structured.flowId ?? structured.intentId,
|
|
114
|
+
slots: structured.slots != null
|
|
115
|
+
? normalizeSlots(structured.slots)
|
|
116
|
+
: structured.slots,
|
|
117
|
+
});
|
|
118
|
+
const fromInternal = (internalState) => internalState.responses;
|
|
119
|
+
const safeJsonParse = (val) => {
|
|
120
|
+
try {
|
|
121
|
+
const json = JSON.parse(val);
|
|
122
|
+
return json;
|
|
123
|
+
}
|
|
124
|
+
catch (_err) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Helper method to decide when a new {@link Config} requires creating a new {@link ConversationHandler} or whether the old `Config`'s
|
|
130
|
+
* `ConversationHandler` can be used.
|
|
131
|
+
*
|
|
132
|
+
* The order of configs doesn't matter.
|
|
133
|
+
* @param config1 -
|
|
134
|
+
* @param config2 -
|
|
135
|
+
* @returns true if `createConversation` should be called again
|
|
136
|
+
*/
|
|
137
|
+
const shouldReinitialize = (config1, config2) => {
|
|
138
|
+
return !equals(config1, config2);
|
|
139
|
+
};
|
|
140
|
+
const getBaseDomain = (url) => url.match(/(bots\.dev\.studio\.nlx\.ai|bots\.studio\.nlx\.ai|apps\.nlx\.ai|dev\.apps\.nlx\.ai)/g)?.[0] ?? "apps.nlx.ai";
|
|
141
|
+
/**
|
|
142
|
+
* When a HTTP URL is provided, deduce the websocket URL. Otherwise, return the argument.
|
|
143
|
+
* @param applicationUrl - the websocket URL
|
|
144
|
+
* @returns httpUrl - the HTTP URL
|
|
145
|
+
*/
|
|
146
|
+
const normalizeToWebsocket = (applicationUrl) => {
|
|
147
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
148
|
+
return applicationUrl;
|
|
149
|
+
}
|
|
150
|
+
const base = getBaseDomain(applicationUrl);
|
|
151
|
+
const url = new URL(applicationUrl);
|
|
152
|
+
const pathChunks = url.pathname.split("/");
|
|
153
|
+
const deploymentKey = pathChunks[2];
|
|
154
|
+
const channelKey = pathChunks[3];
|
|
155
|
+
return `wss://us-east-1-ws.${base}?deploymentKey=${deploymentKey}&channelKey=${channelKey}`;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* When a websocket URL is provided, deduce the HTTP URL. Otherwise, return the argument.
|
|
159
|
+
* @param applicationUrl - the websocket URL
|
|
160
|
+
* @returns httpUrl - the HTTP URL
|
|
161
|
+
*/
|
|
162
|
+
const normalizeToHttp = (applicationUrl) => {
|
|
163
|
+
if (!isWebsocketUrl(applicationUrl)) {
|
|
164
|
+
return applicationUrl;
|
|
165
|
+
}
|
|
166
|
+
const base = getBaseDomain(applicationUrl);
|
|
167
|
+
const url = new URL(applicationUrl);
|
|
168
|
+
const params = new URLSearchParams(url.search);
|
|
169
|
+
const channelKey = params.get("channelKey");
|
|
170
|
+
const deploymentKey = params.get("deploymentKey");
|
|
171
|
+
return `https://${base}/c/${deploymentKey}/${channelKey}`;
|
|
172
|
+
};
|
|
173
|
+
const isWebsocketUrl = (url) => {
|
|
174
|
+
return url.indexOf("wss://") === 0;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Check whether a configuration is value.
|
|
178
|
+
* @param config - Chat configuration
|
|
179
|
+
* @returns isValid - Whether the configuration is valid
|
|
180
|
+
*/
|
|
181
|
+
const isConfigValid = (config) => {
|
|
182
|
+
const applicationUrl = config.applicationUrl ?? "";
|
|
183
|
+
return applicationUrl.length > 0;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Call this to create a conversation handler.
|
|
187
|
+
* @param config -
|
|
188
|
+
* @returns The {@link ConversationHandler} is a bundle of functions to interact with the conversation.
|
|
189
|
+
*/
|
|
190
|
+
function createConversation(config) {
|
|
191
|
+
let socket;
|
|
192
|
+
let socketMessageQueue = [];
|
|
193
|
+
let socketMessageQueueCheckInterval = null;
|
|
194
|
+
let voicePlusSocket;
|
|
195
|
+
let voicePlusSocketMessageQueue = [];
|
|
196
|
+
let voicePlusSocketMessageQueueCheckInterval = null;
|
|
197
|
+
const applicationUrl = config.applicationUrl ?? "";
|
|
198
|
+
// Check if the application URL has a language code appended to it
|
|
199
|
+
if (/[-|_][a-z]{2,}[-|_][A-Z]{2,}$/.test(applicationUrl)) {
|
|
200
|
+
Console.warn("Since v1.0.0, the language code is no longer added at the end of the application URL. Please remove the modifier (e.g. '-en-US') from the URL, and specify it in the `languageCode` parameter instead.");
|
|
201
|
+
}
|
|
202
|
+
const eventListeners = { voicePlusCommand: [] };
|
|
203
|
+
const initialConversationId = config.conversationId ?? v4();
|
|
204
|
+
let state = {
|
|
205
|
+
responses: config.responses ?? [],
|
|
206
|
+
languageCode: config.languageCode,
|
|
207
|
+
userId: config.userId,
|
|
208
|
+
conversationId: initialConversationId,
|
|
209
|
+
};
|
|
210
|
+
const fullApplicationHttpUrl = () => `${normalizeToHttp(applicationUrl)}${config.experimental?.completeApplicationUrl === true
|
|
211
|
+
? ""
|
|
212
|
+
: `-${state.languageCode}`}`;
|
|
213
|
+
const setState = (change,
|
|
214
|
+
// Optionally send the response that causes the current state change, to be sent to subscribers
|
|
215
|
+
newResponse) => {
|
|
216
|
+
state = {
|
|
217
|
+
...state,
|
|
218
|
+
...change,
|
|
219
|
+
};
|
|
220
|
+
subscribers.forEach((subscriber) => {
|
|
221
|
+
subscriber(fromInternal(state), newResponse);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const failureHandler = () => {
|
|
225
|
+
const newResponse = {
|
|
226
|
+
type: ResponseType.Failure,
|
|
227
|
+
receivedAt: new Date().getTime(),
|
|
228
|
+
payload: {
|
|
229
|
+
text: config.failureMessage ?? defaultFailureMessage,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
setState({
|
|
233
|
+
responses: [...state.responses, newResponse],
|
|
234
|
+
}, newResponse);
|
|
235
|
+
};
|
|
236
|
+
const messageResponseHandler = (response) => {
|
|
237
|
+
if (response?.messages.length > 0) {
|
|
238
|
+
const newResponse = {
|
|
239
|
+
type: ResponseType.Application,
|
|
240
|
+
receivedAt: new Date().getTime(),
|
|
241
|
+
payload: {
|
|
242
|
+
...response,
|
|
243
|
+
messages: response.messages.map((message) => ({
|
|
244
|
+
nodeId: message.nodeId,
|
|
245
|
+
messageId: message.messageId,
|
|
246
|
+
text: message.text,
|
|
247
|
+
choices: message.choices ?? [],
|
|
248
|
+
})),
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
setState({
|
|
252
|
+
responses: [...state.responses, newResponse],
|
|
253
|
+
}, newResponse);
|
|
254
|
+
if (response.metadata.hasPendingDataRequest) {
|
|
255
|
+
appendStructuredUserResponse({ poll: true });
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
void sendToApplication({
|
|
258
|
+
request: {
|
|
259
|
+
structured: {
|
|
260
|
+
poll: true,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}, 1500);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
Console.warn("Invalid message structure, expected object with field 'messages'.");
|
|
269
|
+
failureHandler();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
let requestOverride;
|
|
273
|
+
const sendVoicePlusMessage = (message) => {
|
|
274
|
+
if (voicePlusSocket?.readyState === 1) {
|
|
275
|
+
voicePlusSocket.send(JSON.stringify(message));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
voicePlusSocketMessageQueue = [...voicePlusSocketMessageQueue, message];
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const sendToApplication = async (body) => {
|
|
282
|
+
if (requestOverride != null) {
|
|
283
|
+
requestOverride(body, (payload) => {
|
|
284
|
+
const newResponse = {
|
|
285
|
+
type: ResponseType.Application,
|
|
286
|
+
receivedAt: new Date().getTime(),
|
|
287
|
+
payload,
|
|
288
|
+
};
|
|
289
|
+
setState({
|
|
290
|
+
responses: [...state.responses, newResponse],
|
|
291
|
+
}, newResponse);
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const bodyWithContext = {
|
|
296
|
+
userId: state.userId,
|
|
297
|
+
conversationId: state.conversationId,
|
|
298
|
+
...body,
|
|
299
|
+
languageCode: state.languageCode,
|
|
300
|
+
channelType: config.experimental?.channelType,
|
|
301
|
+
environment: config.environment,
|
|
302
|
+
};
|
|
303
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
304
|
+
if (socket?.readyState === 1) {
|
|
305
|
+
socket.send(JSON.stringify(bodyWithContext));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
socketMessageQueue = [...socketMessageQueue, bodyWithContext];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(fullApplicationHttpUrl(), {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: {
|
|
316
|
+
...(config.headers ?? {}),
|
|
317
|
+
Accept: "application/json",
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
|
+
"nlx-sdk-version": packageJson.version,
|
|
320
|
+
},
|
|
321
|
+
body: JSON.stringify(bodyWithContext),
|
|
322
|
+
});
|
|
323
|
+
if (res.status >= 400) {
|
|
324
|
+
throw new Error(`Responded with ${res.status}`);
|
|
325
|
+
}
|
|
326
|
+
const json = await res.json();
|
|
327
|
+
messageResponseHandler(json);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
Console.warn(err);
|
|
331
|
+
failureHandler();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
let subscribers = [];
|
|
336
|
+
const checkSocketQueue = async () => {
|
|
337
|
+
if (socket?.readyState === 1 && socketMessageQueue[0] != null) {
|
|
338
|
+
await sendToApplication(socketMessageQueue[0]);
|
|
339
|
+
socketMessageQueue = socketMessageQueue.slice(1);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const checkVoicePlusSocketQueue = () => {
|
|
343
|
+
if (voicePlusSocket?.readyState === 1 &&
|
|
344
|
+
voicePlusSocketMessageQueue[0] != null) {
|
|
345
|
+
sendVoicePlusMessage(voicePlusSocketMessageQueue[0]);
|
|
346
|
+
voicePlusSocketMessageQueue = voicePlusSocketMessageQueue.slice(1);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const setupWebsocket = () => {
|
|
350
|
+
// If the socket is already set up, tear it down first
|
|
351
|
+
teardownWebsocket();
|
|
352
|
+
const url = new URL(applicationUrl);
|
|
353
|
+
if (config.experimental?.completeApplicationUrl !== true) {
|
|
354
|
+
url.searchParams.set("languageCode", state.languageCode);
|
|
355
|
+
url.searchParams.set("channelKey", `${url.searchParams.get("channelKey") ?? ""}-${state.languageCode}`);
|
|
356
|
+
}
|
|
357
|
+
url.searchParams.set("conversationId", state.conversationId);
|
|
358
|
+
socket = new ReconnectingWebSocket(url.href);
|
|
359
|
+
socketMessageQueueCheckInterval = setInterval(() => {
|
|
360
|
+
void checkSocketQueue();
|
|
361
|
+
}, 500);
|
|
362
|
+
socket.onmessage = function (e) {
|
|
363
|
+
if (typeof e?.data === "string") {
|
|
364
|
+
messageResponseHandler(safeJsonParse(e.data));
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
url.searchParams.set("voice-plus", "true");
|
|
368
|
+
voicePlusSocket = new ReconnectingWebSocket(url.href);
|
|
369
|
+
voicePlusSocketMessageQueueCheckInterval = setInterval(() => {
|
|
370
|
+
checkVoicePlusSocketQueue();
|
|
371
|
+
}, 500);
|
|
372
|
+
voicePlusSocket.onmessage = (e) => {
|
|
373
|
+
if (typeof e?.data === "string") {
|
|
374
|
+
const command = safeJsonParse(e.data);
|
|
375
|
+
if (command != null) {
|
|
376
|
+
eventListeners.voicePlusCommand.forEach((listener) => {
|
|
377
|
+
listener(command);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
const setupCommandWebsocket = () => {
|
|
384
|
+
// If the socket is already set up, tear it down first
|
|
385
|
+
teardownCommandWebsocket();
|
|
386
|
+
if (config.bidirectional !== true) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const url = new URL(normalizeToWebsocket(applicationUrl));
|
|
390
|
+
if (config.experimental?.completeApplicationUrl !== true) {
|
|
391
|
+
url.searchParams.set("languageCode", state.languageCode);
|
|
392
|
+
url.searchParams.set("channelKey", `${url.searchParams.get("channelKey") ?? ""}-${state.languageCode}`);
|
|
393
|
+
}
|
|
394
|
+
url.searchParams.set("conversationId", state.conversationId);
|
|
395
|
+
url.searchParams.set("type", "voice-plus");
|
|
396
|
+
const apiKey = config.headers["nlx-api-key"];
|
|
397
|
+
if (!isWebsocketUrl(applicationUrl) && apiKey != null) {
|
|
398
|
+
url.searchParams.set("apiKey", apiKey);
|
|
399
|
+
}
|
|
400
|
+
voicePlusSocket = new ReconnectingWebSocket(url.href);
|
|
401
|
+
voicePlusSocketMessageQueueCheckInterval = setInterval(() => {
|
|
402
|
+
checkVoicePlusSocketQueue();
|
|
403
|
+
}, 500);
|
|
404
|
+
voicePlusSocket.onmessage = (e) => {
|
|
405
|
+
if (typeof e?.data === "string") {
|
|
406
|
+
const command = safeJsonParse(e.data);
|
|
407
|
+
if (command != null) {
|
|
408
|
+
eventListeners.voicePlusCommand.forEach((listener) => {
|
|
409
|
+
listener(command);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
const teardownWebsocket = () => {
|
|
416
|
+
if (socketMessageQueueCheckInterval != null) {
|
|
417
|
+
clearInterval(socketMessageQueueCheckInterval);
|
|
418
|
+
}
|
|
419
|
+
if (socket != null) {
|
|
420
|
+
socket.onmessage = null;
|
|
421
|
+
socket.close();
|
|
422
|
+
socket = undefined;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const teardownCommandWebsocket = () => {
|
|
426
|
+
if (voicePlusSocketMessageQueueCheckInterval != null) {
|
|
427
|
+
clearInterval(voicePlusSocketMessageQueueCheckInterval);
|
|
428
|
+
}
|
|
429
|
+
if (voicePlusSocket != null) {
|
|
430
|
+
voicePlusSocket.onmessage = null;
|
|
431
|
+
voicePlusSocket.close();
|
|
432
|
+
voicePlusSocket = undefined;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
436
|
+
setupWebsocket();
|
|
437
|
+
}
|
|
438
|
+
setupCommandWebsocket();
|
|
439
|
+
const appendStructuredUserResponse = (structured, context) => {
|
|
440
|
+
const newResponse = {
|
|
441
|
+
type: ResponseType.User,
|
|
442
|
+
receivedAt: new Date().getTime(),
|
|
443
|
+
payload: {
|
|
444
|
+
type: "structured",
|
|
445
|
+
...normalizeStructuredRequest(structured),
|
|
446
|
+
context,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
setState({
|
|
450
|
+
responses: [...state.responses, newResponse],
|
|
451
|
+
}, newResponse);
|
|
452
|
+
};
|
|
453
|
+
const sendFlow = (intentId, context) => {
|
|
454
|
+
appendStructuredUserResponse({ intentId }, context);
|
|
455
|
+
void sendToApplication({
|
|
456
|
+
context,
|
|
457
|
+
request: {
|
|
458
|
+
structured: {
|
|
459
|
+
intentId,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const sendText = (text, context) => {
|
|
465
|
+
const newResponse = {
|
|
466
|
+
type: ResponseType.User,
|
|
467
|
+
receivedAt: new Date().getTime(),
|
|
468
|
+
payload: {
|
|
469
|
+
type: "text",
|
|
470
|
+
text,
|
|
471
|
+
context,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
setState({
|
|
475
|
+
responses: [...state.responses, newResponse],
|
|
476
|
+
}, newResponse);
|
|
477
|
+
void sendToApplication({
|
|
478
|
+
context,
|
|
479
|
+
request: {
|
|
480
|
+
unstructured: {
|
|
481
|
+
text,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
const sendChoice = (choiceId, context, metadata) => {
|
|
487
|
+
let newResponses = [...state.responses];
|
|
488
|
+
const choiceResponse = {
|
|
489
|
+
type: ResponseType.User,
|
|
490
|
+
receivedAt: new Date().getTime(),
|
|
491
|
+
payload: {
|
|
492
|
+
type: "choice",
|
|
493
|
+
choiceId,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const responseIndex = metadata?.responseIndex ?? -1;
|
|
497
|
+
const messageIndex = metadata?.messageIndex ?? -1;
|
|
498
|
+
if (responseIndex > -1 && messageIndex > -1) {
|
|
499
|
+
newResponses = adjust(responseIndex, (response) => response.type === ResponseType.Application
|
|
500
|
+
? {
|
|
501
|
+
...response,
|
|
502
|
+
payload: {
|
|
503
|
+
...response.payload,
|
|
504
|
+
messages: adjust(messageIndex, (message) => ({ ...message, selectedChoiceId: choiceId }), response.payload.messages),
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
: response, newResponses);
|
|
508
|
+
}
|
|
509
|
+
newResponses = [...newResponses, choiceResponse];
|
|
510
|
+
setState({
|
|
511
|
+
responses: newResponses,
|
|
512
|
+
}, choiceResponse);
|
|
513
|
+
void sendToApplication({
|
|
514
|
+
context,
|
|
515
|
+
request: {
|
|
516
|
+
structured: {
|
|
517
|
+
nodeId: metadata?.nodeId,
|
|
518
|
+
intentId: metadata?.intentId,
|
|
519
|
+
choiceId,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
const unsubscribe = (subscriber) => {
|
|
525
|
+
subscribers = subscribers.filter((fn) => fn !== subscriber);
|
|
526
|
+
};
|
|
527
|
+
const subscribe = (subscriber) => {
|
|
528
|
+
subscribers = [...subscribers, subscriber];
|
|
529
|
+
subscriber(fromInternal(state));
|
|
530
|
+
return () => {
|
|
531
|
+
unsubscribe(subscriber);
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
sendText,
|
|
536
|
+
sendContext: async (context) => {
|
|
537
|
+
const res = await fetch(`${fullApplicationHttpUrl()}/context`, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: {
|
|
540
|
+
...(config.headers ?? {}),
|
|
541
|
+
Accept: "application/json",
|
|
542
|
+
"Content-Type": "application/json",
|
|
543
|
+
"nlx-conversation-id": state.conversationId,
|
|
544
|
+
"nlx-sdk-version": packageJson.version,
|
|
545
|
+
},
|
|
546
|
+
body: JSON.stringify({
|
|
547
|
+
languageCode: state.languageCode,
|
|
548
|
+
conversationId: state.conversationId,
|
|
549
|
+
userId: state.userId,
|
|
550
|
+
context,
|
|
551
|
+
}),
|
|
552
|
+
});
|
|
553
|
+
if (res.status >= 400) {
|
|
554
|
+
throw new Error(`Responded with ${res.status}`);
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
sendStructured: (structured, context) => {
|
|
558
|
+
appendStructuredUserResponse(structured, context);
|
|
559
|
+
void sendToApplication({
|
|
560
|
+
context,
|
|
561
|
+
request: {
|
|
562
|
+
structured: normalizeStructuredRequest(structured),
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
sendSlots: (slots, context) => {
|
|
567
|
+
appendStructuredUserResponse({ slots }, context);
|
|
568
|
+
void sendToApplication({
|
|
569
|
+
context,
|
|
570
|
+
request: {
|
|
571
|
+
structured: {
|
|
572
|
+
slots: normalizeSlots(slots),
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
sendFlow,
|
|
578
|
+
sendIntent: (intentId, context) => {
|
|
579
|
+
Console.warn("Calling `sendIntent` is deprecated and will be removed in a future version of the SDK. Use `sendFlow` instead.");
|
|
580
|
+
sendFlow(intentId, context);
|
|
581
|
+
},
|
|
582
|
+
sendWelcomeFlow: (context) => {
|
|
583
|
+
sendFlow(welcomeIntent, context);
|
|
584
|
+
},
|
|
585
|
+
sendWelcomeIntent: (context) => {
|
|
586
|
+
Console.warn("Calling `sendWelcomeIntent` is deprecated and will be removed in a future version of the SDK. Use `sendWelcomeFlow` instead.");
|
|
587
|
+
sendFlow(welcomeIntent, context);
|
|
588
|
+
},
|
|
589
|
+
sendChoice,
|
|
590
|
+
currentConversationId: () => {
|
|
591
|
+
return state.conversationId;
|
|
592
|
+
},
|
|
593
|
+
setLanguageCode: (languageCode) => {
|
|
594
|
+
if (languageCode === state.languageCode) {
|
|
595
|
+
Console.warn("Attempted to set language code to the one already active.");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
599
|
+
setupWebsocket();
|
|
600
|
+
}
|
|
601
|
+
setupCommandWebsocket();
|
|
602
|
+
setState({ languageCode });
|
|
603
|
+
},
|
|
604
|
+
currentLanguageCode: () => {
|
|
605
|
+
return state.languageCode;
|
|
606
|
+
},
|
|
607
|
+
getVoiceCredentials: async (context) => {
|
|
608
|
+
const url = normalizeToHttp(applicationUrl);
|
|
609
|
+
const res = await fetch(`${url}-${state.languageCode}/requestToken`, {
|
|
610
|
+
method: "POST",
|
|
611
|
+
headers: {
|
|
612
|
+
...(config.headers ?? {}),
|
|
613
|
+
Accept: "application/json",
|
|
614
|
+
"Content-Type": "application/json",
|
|
615
|
+
"nlx-conversation-id": state.conversationId,
|
|
616
|
+
"nlx-sdk-version": packageJson.version,
|
|
617
|
+
},
|
|
618
|
+
body: JSON.stringify({
|
|
619
|
+
languageCode: state.languageCode,
|
|
620
|
+
conversationId: state.conversationId,
|
|
621
|
+
userId: state.userId,
|
|
622
|
+
requestToken: true,
|
|
623
|
+
context,
|
|
624
|
+
}),
|
|
625
|
+
});
|
|
626
|
+
if (res.status >= 400) {
|
|
627
|
+
throw new Error(`Responded with ${res.status}`);
|
|
628
|
+
}
|
|
629
|
+
const data = await res.json();
|
|
630
|
+
if (data?.url == null) {
|
|
631
|
+
throw new Error("Invalid response");
|
|
632
|
+
}
|
|
633
|
+
return data;
|
|
634
|
+
},
|
|
635
|
+
subscribe,
|
|
636
|
+
unsubscribe,
|
|
637
|
+
unsubscribeAll: () => {
|
|
638
|
+
subscribers = [];
|
|
639
|
+
},
|
|
640
|
+
reset: (options) => {
|
|
641
|
+
setState({
|
|
642
|
+
conversationId: v4(),
|
|
643
|
+
responses: options?.clearResponses === true ? [] : state.responses,
|
|
644
|
+
});
|
|
645
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
646
|
+
setupWebsocket();
|
|
647
|
+
}
|
|
648
|
+
setupCommandWebsocket();
|
|
649
|
+
},
|
|
650
|
+
destroy: () => {
|
|
651
|
+
subscribers = [];
|
|
652
|
+
if (isWebsocketUrl(applicationUrl)) {
|
|
653
|
+
teardownWebsocket();
|
|
654
|
+
}
|
|
655
|
+
teardownCommandWebsocket();
|
|
656
|
+
},
|
|
657
|
+
setRequestOverride: (val) => {
|
|
658
|
+
requestOverride = val;
|
|
659
|
+
},
|
|
660
|
+
addEventListener: (event, listener) => {
|
|
661
|
+
eventListeners[event] = [...eventListeners[event], listener];
|
|
662
|
+
},
|
|
663
|
+
removeEventListener: (event, listener) => {
|
|
664
|
+
eventListeners[event] = eventListeners[event].filter((l) => l !== listener);
|
|
665
|
+
},
|
|
666
|
+
sendVoicePlusContext: (context) => {
|
|
667
|
+
sendVoicePlusMessage({ context });
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get current expiration timestamp from the current list of reponses
|
|
673
|
+
* @param responses - the current list of user and application responses (first argument in the subscribe callback)
|
|
674
|
+
* @returns an expiration timestamp in Unix Epoch (`new Date().getTime()`), or `null` if this is not known (typically occurs if the application has not responded yet)
|
|
675
|
+
*/
|
|
676
|
+
const getCurrentExpirationTimestamp = (responses) => {
|
|
677
|
+
let expirationTimestamp = null;
|
|
678
|
+
responses.forEach((response) => {
|
|
679
|
+
if (response.type === ResponseType.Application &&
|
|
680
|
+
response.payload.expirationTimestamp != null) {
|
|
681
|
+
expirationTimestamp = response.payload.expirationTimestamp;
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
return expirationTimestamp;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* This package is intentionally designed with a subscription-based API as opposed to a promise-based one where each message corresponds to a single application response, available asynchronously.
|
|
688
|
+
*
|
|
689
|
+
* If you need a promise-based wrapper, you can use the `promisify` helper available in the package:
|
|
690
|
+
* @example
|
|
691
|
+
* ```typescript
|
|
692
|
+
* import { createConversation, promisify } from "@nlxai/core";
|
|
693
|
+
*
|
|
694
|
+
* const convo = createConversation(config);
|
|
695
|
+
*
|
|
696
|
+
* const sendTextWrapped = promisify(convo.sendText, convo);
|
|
697
|
+
*
|
|
698
|
+
* sendTextWrapped("Hello").then((response) => {
|
|
699
|
+
* console.log(response);
|
|
700
|
+
* });
|
|
701
|
+
* ```
|
|
702
|
+
* @typeParam T - the type of the function's params, e.g. for `sendText` it's `text: string, context?: Context`
|
|
703
|
+
* @param fn - the function to wrap (e.g. `convo.sendText`, `convo.sendChoice`, etc.)
|
|
704
|
+
* @param convo - the `ConversationHandler` (from {@link createConversation})
|
|
705
|
+
* @param timeout - the timeout in milliseconds
|
|
706
|
+
* @returns A promise-wrapped version of the function. The function, when called, returns a promise that resolves to the Conversation's next response.
|
|
707
|
+
*/
|
|
708
|
+
function promisify(fn, convo, timeout = 10000) {
|
|
709
|
+
return async (payload) => {
|
|
710
|
+
return await new Promise((resolve, reject) => {
|
|
711
|
+
const timeoutId = setTimeout(() => {
|
|
712
|
+
reject(new Error("The request timed out."));
|
|
713
|
+
convo.unsubscribe(subscription);
|
|
714
|
+
}, timeout);
|
|
715
|
+
const subscription = (_responses, newResponse) => {
|
|
716
|
+
if (newResponse?.type === ResponseType.Application ||
|
|
717
|
+
newResponse?.type === ResponseType.Failure) {
|
|
718
|
+
clearTimeout(timeoutId);
|
|
719
|
+
convo.unsubscribe(subscription);
|
|
720
|
+
resolve(newResponse);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
convo.subscribe(subscription);
|
|
724
|
+
fn(payload);
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export { ResponseType, createConversation, getCurrentExpirationTimestamp, isConfigValid, promisify, shouldReinitialize, version };
|