@webex/internal-plugin-llm 3.11.0 → 3.12.0-next.2
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 +83 -12
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/llm.js +247 -51
- package/dist/llm.js.map +1 -1
- package/dist/llm.types.js +6 -0
- package/dist/llm.types.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.ts +12 -0
- package/src/index.ts +2 -0
- package/src/llm.ts +236 -33
- package/src/llm.types.ts +28 -5
- package/test/unit/spec/llm.js +349 -52
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"node": ">=18"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@webex/internal-plugin-mercury": "3.
|
|
16
|
+
"@webex/internal-plugin-mercury": "3.12.0-next.1"
|
|
17
17
|
},
|
|
18
18
|
"browserify": {
|
|
19
19
|
"transform": [
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"@webex/eslint-config-legacy": "0.0.0",
|
|
28
28
|
"@webex/jest-config-legacy": "0.0.0",
|
|
29
29
|
"@webex/legacy-tools": "0.0.0",
|
|
30
|
-
"@webex/test-helper-chai": "3.11.0",
|
|
31
|
-
"@webex/test-helper-mocha": "3.11.0",
|
|
32
|
-
"@webex/test-helper-mock-webex": "3.11.0",
|
|
33
|
-
"@webex/test-helper-test-users": "3.11.0",
|
|
30
|
+
"@webex/test-helper-chai": "3.11.0-next.1",
|
|
31
|
+
"@webex/test-helper-mocha": "3.11.0-next.1",
|
|
32
|
+
"@webex/test-helper-mock-webex": "3.11.0-next.1",
|
|
33
|
+
"@webex/test-helper-test-users": "3.11.0-next.1",
|
|
34
34
|
"eslint": "^8.24.0",
|
|
35
35
|
"prettier": "^2.7.1",
|
|
36
36
|
"sinon": "^9.2.4"
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"test:style": "eslint ./src/**/*.*",
|
|
45
45
|
"test:unit": "webex-legacy-tools test --unit --runner jest"
|
|
46
46
|
},
|
|
47
|
-
"version": "3.
|
|
47
|
+
"version": "3.12.0-next.2"
|
|
48
48
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
// eslint-disable-next-line import/prefer-default-export
|
|
2
2
|
export const LLM = 'llm';
|
|
3
|
+
|
|
4
|
+
export const LLM_DEFAULT_SESSION = 'llm-default-session';
|
|
5
|
+
|
|
6
|
+
export const DATA_CHANNEL_WITH_JWT_TOKEN = 'data-channel-with-jwt-token';
|
|
7
|
+
|
|
8
|
+
export const SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM = 'subscriptionAwareSubchannels';
|
|
9
|
+
|
|
10
|
+
export const DATA_CHNANEL_TYPE = {
|
|
11
|
+
TRANSCRIPTION: 'transcription',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const AWARE_DATA_CHANNEL = [DATA_CHNANEL_TYPE.TRANSCRIPTION];
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as WebexCore from '@webex/webex-core';
|
|
2
2
|
import LLMChannel, {config} from './llm';
|
|
3
|
+
import {DataChannelTokenType} from './llm.types';
|
|
3
4
|
|
|
4
5
|
WebexCore.registerInternalPlugin('llm', LLMChannel, {
|
|
5
6
|
config,
|
|
6
7
|
});
|
|
7
8
|
|
|
9
|
+
export {DataChannelTokenType};
|
|
8
10
|
export {default} from './llm';
|
package/src/llm.ts
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import Mercury from '@webex/internal-plugin-mercury';
|
|
4
4
|
|
|
5
|
-
import {LLM} from './constants';
|
|
6
5
|
// eslint-disable-next-line no-unused-vars
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
LLM,
|
|
8
|
+
DATA_CHANNEL_WITH_JWT_TOKEN,
|
|
9
|
+
AWARE_DATA_CHANNEL,
|
|
10
|
+
SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM,
|
|
11
|
+
LLM_DEFAULT_SESSION,
|
|
12
|
+
} from './constants';
|
|
13
|
+
import {ILLMChannel, DataChannelTokenType} from './llm.types';
|
|
8
14
|
|
|
9
15
|
export const config = {
|
|
10
16
|
llm: {
|
|
@@ -42,90 +48,287 @@ export const config = {
|
|
|
42
48
|
*/
|
|
43
49
|
export default class LLMChannel extends (Mercury as any) implements ILLMChannel {
|
|
44
50
|
namespace = LLM;
|
|
45
|
-
|
|
51
|
+
defaultSessionId = LLM_DEFAULT_SESSION;
|
|
46
52
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @
|
|
49
|
-
* @type {
|
|
50
|
-
* @public
|
|
53
|
+
* Map to store connection-specific data for multiple LLM connections
|
|
54
|
+
* @private
|
|
55
|
+
* @type {Map<string, {webSocketUrl?: string; binding?: string; locusUrl?: string; datachannelUrl?: string}>}
|
|
51
56
|
*/
|
|
57
|
+
private connections: Map<
|
|
58
|
+
string,
|
|
59
|
+
{
|
|
60
|
+
webSocketUrl?: string;
|
|
61
|
+
binding?: string;
|
|
62
|
+
locusUrl?: string;
|
|
63
|
+
datachannelUrl?: string;
|
|
64
|
+
datachannelToken?: string;
|
|
65
|
+
}
|
|
66
|
+
> = new Map();
|
|
52
67
|
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
private locusUrl?: string;
|
|
68
|
+
private datachannelTokens: Record<DataChannelTokenType, string> = {
|
|
69
|
+
[DataChannelTokenType.Default]: undefined,
|
|
70
|
+
[DataChannelTokenType.PracticeSession]: undefined,
|
|
71
|
+
};
|
|
58
72
|
|
|
59
|
-
private
|
|
73
|
+
private refreshHandler?: () => Promise<{
|
|
74
|
+
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
75
|
+
}>;
|
|
60
76
|
|
|
61
77
|
/**
|
|
62
78
|
* Register to the websocket
|
|
63
79
|
* @param {string} llmSocketUrl
|
|
80
|
+
* @param {string} datachannelToken
|
|
81
|
+
* @param {string} sessionId - Connection identifier
|
|
64
82
|
* @returns {Promise<void>}
|
|
65
83
|
*/
|
|
66
|
-
private register = (
|
|
67
|
-
|
|
84
|
+
private register = async (
|
|
85
|
+
llmSocketUrl: string,
|
|
86
|
+
datachannelToken?: string,
|
|
87
|
+
sessionId: string = LLM_DEFAULT_SESSION
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
const isDataChannelTokenEnabled = await this.isDataChannelTokenEnabled();
|
|
90
|
+
|
|
91
|
+
return this.request({
|
|
68
92
|
method: 'POST',
|
|
69
93
|
url: llmSocketUrl,
|
|
70
94
|
body: {deviceUrl: this.webex.internal.device.url},
|
|
95
|
+
headers:
|
|
96
|
+
isDataChannelTokenEnabled && datachannelToken
|
|
97
|
+
? {'Data-Channel-Auth-Token': datachannelToken}
|
|
98
|
+
: {},
|
|
71
99
|
})
|
|
72
100
|
.then((res: {body: {webSocketUrl: string; binding: string}}) => {
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
// Get or create connection data
|
|
102
|
+
const sessionData = this.connections.get(sessionId) || {};
|
|
103
|
+
sessionData.webSocketUrl = res.body.webSocketUrl;
|
|
104
|
+
sessionData.binding = res.body.binding;
|
|
105
|
+
this.connections.set(sessionId, sessionData);
|
|
75
106
|
})
|
|
76
107
|
.catch((error: any) => {
|
|
77
|
-
this.logger.error(`Error connecting to websocket: ${error}`);
|
|
108
|
+
this.logger.error(`Error connecting to websocket for ${sessionId}: ${error}`);
|
|
78
109
|
throw error;
|
|
79
110
|
});
|
|
111
|
+
};
|
|
80
112
|
|
|
81
113
|
/**
|
|
82
114
|
* Register and connect to the websocket
|
|
83
115
|
* @param {string} locusUrl
|
|
84
116
|
* @param {string} datachannelUrl
|
|
117
|
+
* @param {string} datachannelToken
|
|
118
|
+
* @param {string} sessionId - Connection identifier
|
|
85
119
|
* @returns {Promise<void>}
|
|
86
120
|
*/
|
|
87
|
-
public registerAndConnect = (
|
|
88
|
-
|
|
121
|
+
public registerAndConnect = (
|
|
122
|
+
locusUrl: string,
|
|
123
|
+
datachannelUrl: string,
|
|
124
|
+
datachannelToken?: string,
|
|
125
|
+
sessionId: string = LLM_DEFAULT_SESSION
|
|
126
|
+
): Promise<void> =>
|
|
127
|
+
this.register(datachannelUrl, datachannelToken, sessionId).then(async () => {
|
|
89
128
|
if (!locusUrl || !datachannelUrl) return undefined;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
129
|
+
|
|
130
|
+
// Get or create connection data
|
|
131
|
+
const sessionData = this.connections.get(sessionId) || {};
|
|
132
|
+
sessionData.locusUrl = locusUrl;
|
|
133
|
+
sessionData.datachannelUrl = datachannelUrl;
|
|
134
|
+
sessionData.datachannelToken = datachannelToken;
|
|
135
|
+
this.connections.set(sessionId, sessionData);
|
|
136
|
+
|
|
137
|
+
const isDataChannelTokenEnabled = await this.isDataChannelTokenEnabled();
|
|
138
|
+
|
|
139
|
+
const connectUrl = isDataChannelTokenEnabled
|
|
140
|
+
? LLMChannel.buildUrlWithAwareSubchannels(sessionData.webSocketUrl, AWARE_DATA_CHANNEL)
|
|
141
|
+
: sessionData.webSocketUrl;
|
|
142
|
+
|
|
143
|
+
return this.connect(connectUrl, sessionId);
|
|
93
144
|
});
|
|
94
145
|
|
|
95
146
|
/**
|
|
96
147
|
* Tells if LLM socket is connected
|
|
148
|
+
* @param {string} sessionId - Connection identifier
|
|
97
149
|
* @returns {boolean} connected
|
|
98
150
|
*/
|
|
99
|
-
public isConnected = (): boolean =>
|
|
151
|
+
public isConnected = (sessionId = LLM_DEFAULT_SESSION): boolean => {
|
|
152
|
+
const socket = this.getSocket(sessionId);
|
|
153
|
+
|
|
154
|
+
return socket ? socket.connected : false;
|
|
155
|
+
};
|
|
100
156
|
|
|
101
157
|
/**
|
|
102
158
|
* Tells if LLM socket is binding
|
|
159
|
+
* @param {string} sessionId - Connection identifier
|
|
103
160
|
* @returns {string} binding
|
|
104
161
|
*/
|
|
105
|
-
public getBinding = (): string =>
|
|
162
|
+
public getBinding = (sessionId = LLM_DEFAULT_SESSION): string => {
|
|
163
|
+
const sessionData = this.connections.get(sessionId);
|
|
164
|
+
|
|
165
|
+
return sessionData?.binding;
|
|
166
|
+
};
|
|
106
167
|
|
|
107
168
|
/**
|
|
108
169
|
* Get Locus URL for the connection
|
|
170
|
+
* @param {string} sessionId - Connection identifier
|
|
109
171
|
* @returns {string} locus Url
|
|
110
172
|
*/
|
|
111
|
-
public getLocusUrl = (): string =>
|
|
173
|
+
public getLocusUrl = (sessionId = LLM_DEFAULT_SESSION): string => {
|
|
174
|
+
const sessionData = this.connections.get(sessionId);
|
|
175
|
+
|
|
176
|
+
return sessionData?.locusUrl;
|
|
177
|
+
};
|
|
112
178
|
|
|
113
179
|
/**
|
|
114
180
|
* Get data channel URL for the connection
|
|
181
|
+
* @param {string} sessionId - Connection identifier
|
|
115
182
|
* @returns {string} data channel Url
|
|
116
183
|
*/
|
|
117
|
-
public getDatachannelUrl = (): string =>
|
|
184
|
+
public getDatachannelUrl = (sessionId = LLM_DEFAULT_SESSION): string => {
|
|
185
|
+
const sessionData = this.connections.get(sessionId);
|
|
186
|
+
|
|
187
|
+
return sessionData?.datachannelUrl;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get data channel token for the connection
|
|
192
|
+
* @param {DataChannelTokenType} dataChannelTokenType
|
|
193
|
+
* @returns {string} data channel token
|
|
194
|
+
*/
|
|
195
|
+
public getDatachannelToken = (
|
|
196
|
+
dataChannelTokenType: DataChannelTokenType = DataChannelTokenType.Default
|
|
197
|
+
): string => {
|
|
198
|
+
return this.datachannelTokens[dataChannelTokenType];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Set data channel token for the connection
|
|
203
|
+
* @param {string} datachannelToken - data channel token
|
|
204
|
+
* @param {DataChannelTokenType} dataChannelTokenType
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
public setDatachannelToken = (
|
|
208
|
+
datachannelToken: string,
|
|
209
|
+
dataChannelTokenType: DataChannelTokenType = DataChannelTokenType.Default
|
|
210
|
+
): void => {
|
|
211
|
+
this.datachannelTokens[dataChannelTokenType] = datachannelToken;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resets all data‑channel tokens to their initial undefined values.
|
|
216
|
+
* Used when leaving or disconnecting from a meeting.
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
219
|
+
public resetDatachannelTokens() {
|
|
220
|
+
this.datachannelTokens = {
|
|
221
|
+
[DataChannelTokenType.Default]: undefined,
|
|
222
|
+
[DataChannelTokenType.PracticeSession]: undefined,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Set the handler used to refresh the DataChannel token
|
|
228
|
+
*
|
|
229
|
+
* @param {function} handler - Function that returns a refreshed token
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
public setRefreshHandler(
|
|
233
|
+
handler: () => Promise<{
|
|
234
|
+
body: {datachannelToken: string; datachannelTokenType: DataChannelTokenType};
|
|
235
|
+
}>
|
|
236
|
+
) {
|
|
237
|
+
this.refreshHandler = handler;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Refresh the data channel token using the injected handler.
|
|
242
|
+
* Logs a descriptive error if the handler is missing or fails.
|
|
243
|
+
*
|
|
244
|
+
* @returns {Promise<string>} The refreshed token.
|
|
245
|
+
*/
|
|
246
|
+
public async refreshDataChannelToken() {
|
|
247
|
+
if (!this.refreshHandler) {
|
|
248
|
+
this.logger.warn(
|
|
249
|
+
'llm#refreshDataChannelToken --> LLM refreshHandler is not set, skipping token refresh'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const res = await this.refreshHandler();
|
|
257
|
+
|
|
258
|
+
return res;
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
this.logger.warn(
|
|
261
|
+
`llm#refreshDataChannelToken --> DataChannel token refresh failed (likely locus changed or participant left): ${
|
|
262
|
+
error?.message || error
|
|
263
|
+
}`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
118
269
|
|
|
119
270
|
/**
|
|
120
271
|
* Disconnects websocket connection
|
|
121
272
|
* @param {{code: number, reason: string}} options - The disconnect option object with code and reason
|
|
273
|
+
* @param {string} sessionId - Connection identifier
|
|
274
|
+
* @returns {Promise<void>}
|
|
275
|
+
*/
|
|
276
|
+
public disconnectLLM = (
|
|
277
|
+
options: {code: number; reason: string},
|
|
278
|
+
sessionId: string = LLM_DEFAULT_SESSION
|
|
279
|
+
): Promise<void> =>
|
|
280
|
+
this.disconnect(options, sessionId).then(() => {
|
|
281
|
+
// Clean up sessions data
|
|
282
|
+
this.connections.delete(sessionId);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Disconnects all LLM websocket connections
|
|
287
|
+
* @param {{code: number, reason: string}} options - The disconnect option object with code and reason
|
|
122
288
|
* @returns {Promise<void>}
|
|
123
289
|
*/
|
|
124
|
-
public
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
this.binding = undefined;
|
|
129
|
-
this.webSocketUrl = undefined;
|
|
290
|
+
public disconnectAllLLM = (options?: {code: number; reason: string}): Promise<void> =>
|
|
291
|
+
this.disconnectAll(options).then(() => {
|
|
292
|
+
// Clean up all connection data
|
|
293
|
+
this.connections.clear();
|
|
130
294
|
});
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get all active LLM connections
|
|
298
|
+
* @returns {Map} Map of sessionId to session data
|
|
299
|
+
*/
|
|
300
|
+
public getAllConnections = (): Map<
|
|
301
|
+
string,
|
|
302
|
+
{
|
|
303
|
+
webSocketUrl?: string;
|
|
304
|
+
binding?: string;
|
|
305
|
+
locusUrl?: string;
|
|
306
|
+
datachannelUrl?: string;
|
|
307
|
+
datachannelToken?: string;
|
|
308
|
+
}
|
|
309
|
+
> => new Map(this.connections);
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Returns true if data channel token is enabled, false otherwise
|
|
313
|
+
* @returns {Promise<boolean>} resolves with true if data channel token is enabled
|
|
314
|
+
*/
|
|
315
|
+
public isDataChannelTokenEnabled(): Promise<boolean> {
|
|
316
|
+
// @ts-ignore
|
|
317
|
+
return this.webex.internal.feature.getFeature('developer', DATA_CHANNEL_WITH_JWT_TOKEN);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Builds a WebSocket URL with the `subscriptionAwareSubchannels` query parameter.
|
|
322
|
+
*
|
|
323
|
+
* @param {string} baseUrl - The original WebSocket URL.
|
|
324
|
+
* @param {string[]} subchannels - List of subchannels to declare as subscription-aware.
|
|
325
|
+
* @returns {string} The final URL with updated query parameters.
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
public static buildUrlWithAwareSubchannels = (baseUrl: string, subchannels: string[]) => {
|
|
329
|
+
const urlObj = new URL(baseUrl);
|
|
330
|
+
urlObj.searchParams.set(SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM, subchannels.join(','));
|
|
331
|
+
|
|
332
|
+
return urlObj.toString();
|
|
333
|
+
};
|
|
131
334
|
}
|
package/src/llm.types.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
interface ILLMChannel {
|
|
2
|
-
registerAndConnect: (
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
registerAndConnect: (
|
|
3
|
+
locusUrl: string,
|
|
4
|
+
datachannelUrl: string,
|
|
5
|
+
datachannelToken?: string,
|
|
6
|
+
sessionId?: string
|
|
7
|
+
) => Promise<void>;
|
|
8
|
+
isConnected: (sessionId?: string) => boolean;
|
|
9
|
+
getBinding: (sessionId?: string) => string;
|
|
10
|
+
getLocusUrl: (sessionId?: string) => string;
|
|
11
|
+
getDatachannelUrl: (sessionId?: string) => string;
|
|
12
|
+
disconnectLLM: (options: {code: number; reason: string}, sessionId?: string) => Promise<void>;
|
|
13
|
+
disconnectAllLLM: (options?: {code: number; reason: string}) => Promise<void>;
|
|
14
|
+
getAllConnections: () => Map<
|
|
15
|
+
string,
|
|
16
|
+
{
|
|
17
|
+
webSocketUrl?: string;
|
|
18
|
+
binding?: string;
|
|
19
|
+
locusUrl?: string;
|
|
20
|
+
datachannelUrl?: string;
|
|
21
|
+
datachannelToken?: string;
|
|
22
|
+
}
|
|
23
|
+
>;
|
|
7
24
|
}
|
|
25
|
+
|
|
26
|
+
export enum DataChannelTokenType {
|
|
27
|
+
Default = 'llm-default-session',
|
|
28
|
+
PracticeSession = 'llm-practice-session',
|
|
29
|
+
}
|
|
30
|
+
|
|
8
31
|
// eslint-disable-next-line import/prefer-default-export
|
|
9
32
|
export type {ILLMChannel};
|