@webex/internal-plugin-llm 3.11.0 → 3.12.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/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "node": ">=18"
14
14
  },
15
15
  "dependencies": {
16
- "@webex/internal-plugin-mercury": "3.11.0"
16
+ "@webex/internal-plugin-mercury": "3.12.0"
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.12.0",
31
+ "@webex/test-helper-mocha": "3.12.0",
32
+ "@webex/test-helper-mock-webex": "3.12.0",
33
+ "@webex/test-helper-test-users": "3.12.0",
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.11.0"
47
+ "version": "3.12.0"
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 {ILLMChannel} from './llm.types';
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,289 @@ 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
- * If the LLM plugin has been registered and listening
48
- * @instance
49
- * @type {Boolean}
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 webSocketUrl?: string;
54
-
55
- private binding?: string;
56
-
57
- private locusUrl?: string;
68
+ private datachannelTokens: Record<DataChannelTokenType, string> = {
69
+ [DataChannelTokenType.Default]: undefined,
70
+ [DataChannelTokenType.PracticeSession]: undefined,
71
+ };
58
72
 
59
- private datachannelUrl?: string;
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 = (llmSocketUrl: string): Promise<void> =>
67
- this.request({
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
- this.webSocketUrl = res.body.webSocketUrl;
74
- this.binding = res.body.binding;
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 = (locusUrl: string, datachannelUrl: string): Promise<void> =>
88
- this.register(datachannelUrl).then(() => {
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
- this.locusUrl = locusUrl;
91
- this.datachannelUrl = datachannelUrl;
92
- this.connect(this.webSocketUrl);
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 => this.connected;
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 => this.binding;
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 => this.locusUrl;
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 => this.datachannelUrl;
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
+ private 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
+ this.datachannelTokens[sessionId] = undefined;
284
+ });
285
+
286
+ /**
287
+ * Disconnects all LLM websocket connections
288
+ * @param {{code: number, reason: string}} options - The disconnect option object with code and reason
122
289
  * @returns {Promise<void>}
123
290
  */
124
- public disconnectLLM = (options: object): Promise<void> =>
125
- this.disconnect(options).then(() => {
126
- this.locusUrl = undefined;
127
- this.datachannelUrl = undefined;
128
- this.binding = undefined;
129
- this.webSocketUrl = undefined;
291
+ public disconnectAllLLM = (options?: {code: number; reason: string}): Promise<void> =>
292
+ this.disconnectAll(options).then(() => {
293
+ // Clean up all connection data
294
+ this.connections.clear();
295
+ this.resetDatachannelTokens();
130
296
  });
297
+
298
+ /**
299
+ * Get all active LLM connections
300
+ * @returns {Map} Map of sessionId to session data
301
+ */
302
+ public getAllConnections = (): Map<
303
+ string,
304
+ {
305
+ webSocketUrl?: string;
306
+ binding?: string;
307
+ locusUrl?: string;
308
+ datachannelUrl?: string;
309
+ datachannelToken?: string;
310
+ }
311
+ > => new Map(this.connections);
312
+
313
+ /**
314
+ * Returns true if data channel token is enabled, false otherwise
315
+ * @returns {Promise<boolean>} resolves with true if data channel token is enabled
316
+ */
317
+ public isDataChannelTokenEnabled(): Promise<boolean> {
318
+ // @ts-ignore
319
+ return this.webex.internal.feature.getFeature('developer', DATA_CHANNEL_WITH_JWT_TOKEN);
320
+ }
321
+
322
+ /**
323
+ * Builds a WebSocket URL with the `subscriptionAwareSubchannels` query parameter.
324
+ *
325
+ * @param {string} baseUrl - The original WebSocket URL.
326
+ * @param {string[]} subchannels - List of subchannels to declare as subscription-aware.
327
+ * @returns {string} The final URL with updated query parameters.
328
+ */
329
+
330
+ public static buildUrlWithAwareSubchannels = (baseUrl: string, subchannels: string[]) => {
331
+ const urlObj = new URL(baseUrl);
332
+ urlObj.searchParams.set(SUBSCRIPTION_AWARE_SUBCHANNELS_PARAM, subchannels.join(','));
333
+
334
+ return urlObj.toString();
335
+ };
131
336
  }
package/src/llm.types.ts CHANGED
@@ -1,9 +1,32 @@
1
1
  interface ILLMChannel {
2
- registerAndConnect: (locusUrl: string, datachannelUrl: string) => Promise<void>;
3
- isConnected: () => boolean;
4
- getBinding: () => string;
5
- getLocusUrl: () => string;
6
- disconnectLLM: (options: {code: number; reason: string}) => Promise<void>;
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};