@webex/internal-plugin-ai-assistant 0.0.0-next.1

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.
@@ -0,0 +1,448 @@
1
+ /*!
2
+ * Copyright (c) 2015-2022 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+ import uuid from 'uuid';
5
+ import {WebexPlugin} from '@webex/webex-core';
6
+ import '@webex/internal-plugin-mercury';
7
+ import {get} from 'lodash';
8
+ import {Timer} from '@webex/common-timers';
9
+
10
+ import {
11
+ MakeMeetingRequestOptions,
12
+ RequestOptions,
13
+ RequestResponse,
14
+ SummarizeMeetingOptions,
15
+ } from './types';
16
+ import {
17
+ AI_ASSISTANT_ERROR_CODES,
18
+ AI_ASSISTANT_ERRORS,
19
+ AI_ASSISTANT_REGISTERED,
20
+ AI_ASSISTANT_RESULT,
21
+ AI_ASSISTANT_STREAM,
22
+ AI_ASSISTANT_UNREGISTERED,
23
+ AI_ASSISTANT_SERVICE_NAME,
24
+ ASSISTANT_API_RESPONSE_EVENT,
25
+ ACTION_TYPES,
26
+ CONTENT_TYPES,
27
+ CONTEXT_RESOURCE_TYPES,
28
+ } from './constants';
29
+
30
+ const AIAssistant = WebexPlugin.extend({
31
+ namespace: 'AIAssistant',
32
+
33
+ /**
34
+ * registered value indicating events registration is successful
35
+ * @instance
36
+ * @type {Boolean}
37
+ * @memberof AIAssistant
38
+ */
39
+ registered: false,
40
+
41
+ /**
42
+ * Initializer
43
+ * @private
44
+ * @param {Object} attrs
45
+ * @param {Object} options
46
+ * @returns {undefined}
47
+ */
48
+ initialize(...args) {
49
+ Reflect.apply(WebexPlugin.prototype.initialize, this, args);
50
+ },
51
+
52
+ /**
53
+ * Explicitly sets up the AI assistant plugin by connecting to mercury, and listening for AI assistant events.
54
+ * @returns {Promise}
55
+ * @public
56
+ * @memberof AIAssistant
57
+ */
58
+ register() {
59
+ if (!this.webex.canAuthorize) {
60
+ this.logger.error('AI assistant->register#ERROR, Unable to register, SDK cannot authorize');
61
+
62
+ return Promise.reject(new Error('SDK cannot authorize'));
63
+ }
64
+
65
+ if (this.registered) {
66
+ this.logger.info('AI assistant->register#INFO, AI assistant plugin already registered');
67
+
68
+ return Promise.resolve();
69
+ }
70
+
71
+ return this.webex.internal.mercury
72
+ .connect()
73
+ .then(() => {
74
+ this.listenForEvents();
75
+ this.trigger(AI_ASSISTANT_REGISTERED);
76
+ this.registered = true;
77
+ })
78
+ .catch((error) => {
79
+ this.logger.error(`AI assistant->register#ERROR, Unable to register, ${error.message}`);
80
+
81
+ return Promise.reject(error);
82
+ });
83
+ },
84
+
85
+ /**
86
+ * Explicitly tears down the AI assistant plugin by disconnecting from mercury, and stops listening to AI assistant events
87
+ * @returns {Promise}
88
+ * @public
89
+ * @memberof AIAssistant
90
+ */
91
+ unregister() {
92
+ if (!this.registered) {
93
+ this.logger.info('AI assistant->unregister#INFO, AI assistant plugin already unregistered');
94
+
95
+ return Promise.resolve();
96
+ }
97
+
98
+ this.stopListeningForEvents();
99
+
100
+ return this.webex.internal.mercury.disconnect().then(() => {
101
+ this.trigger(AI_ASSISTANT_UNREGISTERED);
102
+ this.registered = false;
103
+ });
104
+ },
105
+
106
+ /**
107
+ * registers for Assistant API events through mercury
108
+ * @returns {undefined}
109
+ * @private
110
+ */
111
+ listenForEvents() {
112
+ this.webex.internal.mercury.on(ASSISTANT_API_RESPONSE_EVENT, (envelope) => {
113
+ this._handleEvent(envelope.data);
114
+ });
115
+ },
116
+
117
+ /**
118
+ * unregisteres all the Assistant API events from mercury
119
+ * @returns {undefined}
120
+ * @private
121
+ */
122
+ stopListeningForEvents() {
123
+ this.webex.internal.mercury.off(ASSISTANT_API_RESPONSE_EVENT);
124
+ },
125
+
126
+ /**
127
+ * constructs the event name based on request id
128
+ * This is used by the plugin to listen for the result of a particular request
129
+ * @param {UUID} requestId the id of the request
130
+ * @returns {string}
131
+ */
132
+ _getResultEventName(requestId: string) {
133
+ return `${AI_ASSISTANT_RESULT}:${requestId}`;
134
+ },
135
+
136
+ /**
137
+ * constructs the stream event name based on request id
138
+ * This is used by the consumer to listen for the stream (i.e. the data) of a particular request
139
+ * @param {UUID} requestId the id of the request
140
+ * @returns {string}
141
+ */
142
+ _getStreamEventName(requestId: string) {
143
+ return `${AI_ASSISTANT_STREAM}:${requestId}`;
144
+ },
145
+
146
+ /**
147
+ * Takes incoming data and triggers correct events
148
+ * @param {Object} data the event data
149
+ * @returns {undefined}
150
+ */
151
+ _handleEvent(data) {
152
+ this.trigger(this._getResultEventName(data.clientRequestId), data);
153
+ },
154
+
155
+ /**
156
+ * Decrypts the encrypted value using the encryption key URL
157
+ * @param {Object} options
158
+ * @param {string} options.value the encrypted value to decrypt
159
+ * @param {string} options.encryptionKeyUrl the encryption key URL to use for
160
+ * @returns {Promise<Object>} returns a promise that resolves with the decrypted value
161
+ */
162
+ async _decryptData({value, encryptionKeyUrl}) {
163
+ const result = await this.webex.internal.encryption.decryptText(encryptionKeyUrl, value);
164
+
165
+ return result;
166
+ },
167
+
168
+ /**
169
+ * Makes the request to the AI assistant service
170
+ * @param {Object} options
171
+ * @param {string} options.resource the URL to query
172
+ * @param {Mixed} options.params additional params for the body of the request
173
+ * @param {string} options.dataPath the path to get the data in the result object
174
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
175
+ */
176
+ _request(options: RequestOptions): Promise<RequestResponse> {
177
+ const {resource, params, dataPath} = options;
178
+
179
+ const timeout = this.config.requestTimeout;
180
+ const requestId = uuid.v4();
181
+ const eventName = this._getResultEventName(requestId);
182
+ const streamEventName = this._getStreamEventName(requestId);
183
+ let concatenatedMessage = '';
184
+
185
+ // eslint-disable-next-line no-async-promise-executor
186
+ return new Promise((resolve, reject) => {
187
+ const timer = new Timer(() => {
188
+ this.stopListening(this, eventName);
189
+ this.trigger(streamEventName, {
190
+ requestId,
191
+ finished: true,
192
+ errorMessage: AI_ASSISTANT_ERRORS.AI_ASSISTANT_TIMEOUT,
193
+ errorCode: AI_ASSISTANT_ERROR_CODES.AI_ASSISTANT_TIMEOUT,
194
+ });
195
+ }, timeout);
196
+
197
+ this.listenTo(this, eventName, async (data) => {
198
+ timer.reset();
199
+ const resultData = get(data, dataPath, []);
200
+ const errorMessage = get(data, 'response.errorMessage');
201
+ const errorCode = get(data, 'response.errorCode');
202
+
203
+ if (data.finished) {
204
+ // For finished messages, decrypt and emit the final complete message
205
+ timer.cancel();
206
+
207
+ try {
208
+ let decryptedMessage;
209
+ if (resultData?.value) {
210
+ decryptedMessage = await this._decryptData(resultData);
211
+ }
212
+
213
+ // Emit the final message
214
+ this.trigger(streamEventName, {
215
+ message: decryptedMessage || '',
216
+ requestId,
217
+ finished: true,
218
+ errorMessage,
219
+ errorCode,
220
+ });
221
+
222
+ this.stopListening(this, eventName);
223
+ } catch (decryptError) {
224
+ this.trigger(streamEventName, {
225
+ message: concatenatedMessage,
226
+ requestId,
227
+ finished: true,
228
+ errorMessage: errorMessage || decryptError.message,
229
+ errorCode,
230
+ });
231
+ }
232
+ } else {
233
+ // For non-finished messages, concatenate and emit the accumulated message
234
+ try {
235
+ let decryptedMessage = '';
236
+ if (resultData?.value) {
237
+ decryptedMessage = await this._decryptData(resultData);
238
+ }
239
+
240
+ concatenatedMessage += decryptedMessage;
241
+
242
+ // Emit the concatenated message so far
243
+ this.trigger(streamEventName, {
244
+ message: concatenatedMessage,
245
+ requestId,
246
+ finished: false,
247
+ errorMessage,
248
+ errorCode,
249
+ });
250
+ } catch (decryptError) {
251
+ // If decryption fails, we still want to continue listening for more messages
252
+ this.trigger(streamEventName, {
253
+ message: concatenatedMessage,
254
+ requestId,
255
+ finished: false,
256
+ errorMessage: errorMessage || decryptError.message,
257
+ errorCode,
258
+ });
259
+ }
260
+ }
261
+ });
262
+
263
+ this.webex
264
+ .request({
265
+ service: AI_ASSISTANT_SERVICE_NAME,
266
+ resource,
267
+ method: 'POST',
268
+ contentType: 'application/json',
269
+ body: {clientRequestId: requestId, ...params},
270
+ })
271
+ .catch((error) => {
272
+ reject(error);
273
+ })
274
+ .then(({body}) => {
275
+ const {sessionId} = body;
276
+
277
+ resolve({
278
+ requestId,
279
+ sessionId,
280
+ streamEventName,
281
+ });
282
+ timer.start();
283
+ });
284
+ });
285
+ },
286
+
287
+ /**
288
+ * Common method to make AI assistant requests for meeting analysis
289
+ * @param {Object} options
290
+ * @param {string} options.contextResources array of context resources to include in the request
291
+ * @param {string} options.sessionId the session ID for subsequent requests, not required for the first request
292
+ * @param {string} options.encryptionKeyUrl the encryption key URL for this meeting summary
293
+ * @param {string} options.contentType the type of content ('action' or 'message')
294
+ * @param {string} options.contentValue the value to use (action name or message text)
295
+ * @param {Object} options.parameters optional parameters to include in the request (for action type only)
296
+ * @param {Object} options.assistant optional parameter to specify the assistant to use
297
+ * @param {Object} options.locale optional locale to use for the request, defaults to 'en_US'
298
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
299
+ */
300
+ async _makeMeetingRequest(options: MakeMeetingRequestOptions): Promise<RequestResponse> {
301
+ let value = options.contentValue;
302
+
303
+ if (options.contentType === 'message') {
304
+ value = await this._encryptData({
305
+ text: options.contentValue,
306
+ encryptionKeyUrl: options.encryptionKeyUrl,
307
+ });
308
+ }
309
+
310
+ const content: any = {
311
+ context: {
312
+ resources: options.contextResources,
313
+ },
314
+ encryptionKeyUrl: options.encryptionKeyUrl,
315
+ type: options.contentType,
316
+ value,
317
+ };
318
+
319
+ if (options.contentType === 'action' && options.parameters) {
320
+ content.parameters = options.parameters;
321
+ }
322
+
323
+ return this._request({
324
+ resource: options.sessionId ? `sessions/${options.sessionId}/messages` : 'sessions/messages',
325
+ dataPath: 'response.content',
326
+ params: {
327
+ async: 'chunked',
328
+ locale: options.locale || 'en_US',
329
+ content,
330
+ ...(options.assistant ? {assistant: options.assistant} : {}),
331
+ },
332
+ });
333
+ },
334
+
335
+ /**
336
+ * Returns the summary of a meeting
337
+ * @param {Object} options
338
+ * @param {string} options.meetingInstanceId the meeting instance ID for the meeting from locus
339
+ * @param {string} options.meetingSite the name.webex.com site for the meeting
340
+ * @param {string} options.sessionId the session ID for subsequent requests, not required for the first request
341
+ * @param {string} options.encryptionKeyUrl the encryption key URL for this meeting summary
342
+ * @param {number} options.lastMinutes Optional number of minutes to summarize from the end of the meeting. If not included, summarizes from the start.
343
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
344
+ */
345
+ summarizeMeeting(options: SummarizeMeetingOptions): Promise<RequestResponse> {
346
+ return this._makeMeetingRequest({
347
+ ...options,
348
+ contentType: CONTENT_TYPES.ACTION,
349
+ contentValue: ACTION_TYPES.SUMMARIZE_FOR_ME,
350
+ contextResources: [
351
+ {
352
+ id: options.meetingInstanceId,
353
+ type: CONTEXT_RESOURCE_TYPES.MEETING,
354
+ url: options.meetingSite,
355
+ },
356
+ ],
357
+ ...(options.lastMinutes ? {parameters: {lastMinutes: options.lastMinutes}} : {}),
358
+ });
359
+ },
360
+
361
+ /**
362
+ * Checks if the user's name was mentioned in a meeting
363
+ * @param {Object} options
364
+ * @param {string} options.meetingInstanceId the meeting instance ID for the meeting from locus
365
+ * @param {string} options.meetingSite the name.webex.com site for the meeting
366
+ * @param {string} options.sessionId the session ID for subsequent requests, not required for the first request
367
+ * @param {string} options.encryptionKeyUrl the encryption key URL for this meeting summary
368
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
369
+ */
370
+ wasMyNameMentioned(options: SummarizeMeetingOptions): Promise<RequestResponse> {
371
+ return this._makeMeetingRequest({
372
+ ...options,
373
+ contextResources: [
374
+ {
375
+ id: options.meetingInstanceId,
376
+ type: CONTEXT_RESOURCE_TYPES.MEETING,
377
+ url: options.meetingSite,
378
+ },
379
+ ],
380
+ contentType: CONTENT_TYPES.ACTION,
381
+ contentValue: ACTION_TYPES.WAS_MY_NAME_MENTIONED,
382
+ });
383
+ },
384
+
385
+ /**
386
+ * Returns all action items from a meeting
387
+ * @param {Object} options
388
+ * @param {string} options.meetingInstanceId the meeting instance ID for the meeting from locus
389
+ * @param {string} options.meetingSite the name.webex.com site for the meeting
390
+ * @param {string} options.sessionId the session ID for subsequent requests, not required for the first request
391
+ * @param {string} options.encryptionKeyUrl the encryption key URL for this meeting summary
392
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
393
+ */
394
+ showAllActionItems(options: SummarizeMeetingOptions): Promise<RequestResponse> {
395
+ return this._makeMeetingRequest({
396
+ ...options,
397
+ contextResources: [
398
+ {
399
+ id: options.meetingInstanceId,
400
+ type: CONTEXT_RESOURCE_TYPES.MEETING,
401
+ url: options.meetingSite,
402
+ },
403
+ ],
404
+ contentType: CONTENT_TYPES.ACTION,
405
+ contentValue: ACTION_TYPES.SHOW_ALL_ACTION_ITEMS,
406
+ });
407
+ },
408
+
409
+ /**
410
+ * Helper method to encrypt text using the encryption key URL
411
+ * @param {Object} options
412
+ * @param {string} options.text the text to encrypt
413
+ * @param {string} options.encryptionKeyUrl the encryption key URL to use for encryption
414
+ * @returns {Promise<string>} returns a promise that resolves with the encrypted text
415
+ */
416
+ async _encryptData({text, encryptionKeyUrl}) {
417
+ const result = await this.webex.internal.encryption.encryptText(encryptionKeyUrl, text);
418
+
419
+ return result;
420
+ },
421
+
422
+ /**
423
+ * Ask any question about the meeting content
424
+ * @param {Object} options
425
+ * @param {string} options.meetingInstanceId the meeting instance ID for the meeting from locus
426
+ * @param {string} options.meetingSite the name.webex.com site for the meeting
427
+ * @param {string} options.sessionId the session ID for subsequent requests, not required for the first request
428
+ * @param {string} options.encryptionKeyUrl the encryption key URL for this meeting summary
429
+ * @param {string} options.question the question to ask about the meeting content
430
+ * @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
431
+ */
432
+ askMeAnything(options: SummarizeMeetingOptions & {question: string}): Promise<RequestResponse> {
433
+ return this._makeMeetingRequest({
434
+ ...options,
435
+ contextResources: [
436
+ {
437
+ id: options.meetingInstanceId,
438
+ type: CONTEXT_RESOURCE_TYPES.MEETING,
439
+ url: options.meetingSite,
440
+ },
441
+ ],
442
+ contentType: CONTENT_TYPES.MESSAGE,
443
+ contentValue: options.question,
444
+ });
445
+ },
446
+ });
447
+
448
+ export default AIAssistant;
package/src/config.ts ADDED
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ export default {
6
+ aiassistant: {
7
+ /**
8
+ * Timeout before AI Assistant request fails, in milliseconds.
9
+ * @type {Number}
10
+ */
11
+ requestTimeout: 6000,
12
+ },
13
+ };
@@ -0,0 +1,38 @@
1
+ export const AI_ASSISTANT_REGISTERED = 'aiassistant:registered';
2
+ export const AI_ASSISTANT_UNREGISTERED = 'aiassistant:unregistered';
3
+ export const ASSISTANT_API_RESPONSE_EVENT = 'event:assistant-api.response';
4
+ export const AI_ASSISTANT_SERVICE_NAME = 'assistant-api';
5
+ export const AI_ASSISTANT_RESULT = 'aiassistant:result';
6
+ export const AI_ASSISTANT_STREAM = 'aiassistant:stream';
7
+
8
+ export enum AI_ASSISTANT_ERRORS {
9
+ NOT_ENOUGH_CONTENT = 'NO_ENOUGH_MEETING_TRANSCRIPT',
10
+ TRANSCRIPT_AUTH_ERROR = 'TRANSCRIPT_AUTH_ERROR',
11
+ AI_ASSISTANT_TIMEOUT = 'AI_ASSISTANT_TIMEOUT', // This one is generated by the plugin
12
+ }
13
+
14
+ export enum AI_ASSISTANT_ERROR_CODES {
15
+ AI_ASSISTANT_TIMEOUT = 9408, // This one is generated by the plugin
16
+ NOT_ENOUGH_CONTENT = 204,
17
+ FORBIDDEN = 403,
18
+ LLM_TIMEOUT = 408,
19
+ RATE_LIMIT = 429,
20
+ GENERIC_ERROR = 10000,
21
+ WAIT_PREVIOUS = 10001,
22
+ EMPTY_SEARCH_RESULT = 10002,
23
+ }
24
+
25
+ export enum ACTION_TYPES {
26
+ SHOW_ALL_ACTION_ITEMS = 'SHOW_ALL_ACTION_ITEMS',
27
+ WAS_MY_NAME_MENTIONED = 'WAS_MY_NAME_MENTIONED',
28
+ SUMMARIZE_FOR_ME = 'SUMMARIZE_FOR_ME',
29
+ }
30
+
31
+ export enum CONTENT_TYPES {
32
+ ACTION = 'action',
33
+ MESSAGE = 'message',
34
+ }
35
+
36
+ export enum CONTEXT_RESOURCE_TYPES {
37
+ MEETING = 'meeting',
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import {registerInternalPlugin} from '@webex/webex-core';
2
+
3
+ import AIAssistant from './ai-assistant';
4
+ import config from './config';
5
+
6
+ registerInternalPlugin('aiassistant', AIAssistant, {config});
7
+
8
+ export {default} from './ai-assistant';
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ export interface RequestResponse {
2
+ sessionId: string;
3
+ requestId: string;
4
+ streamEventName: string;
5
+ }
6
+
7
+ export interface StreamEvent {
8
+ message: string;
9
+ requestId: string;
10
+ finished: boolean;
11
+ error: string | null;
12
+ }
13
+
14
+ export interface RequestOptions {
15
+ resource: string;
16
+ dataPath: string;
17
+ foundPath?: string;
18
+ notFoundPath?: string;
19
+ params?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface ContextResource {
23
+ id: string;
24
+ type: string;
25
+ url: string;
26
+ }
27
+
28
+ export interface SummarizeMeetingOptions {
29
+ assistant?: string;
30
+ meetingInstanceId: string;
31
+ meetingSite: string;
32
+ sessionId: string;
33
+ encryptionKeyUrl: string;
34
+ lastMinutes?: number;
35
+ }
36
+
37
+ export interface MakeMeetingRequestOptions {
38
+ sessionId: string;
39
+ encryptionKeyUrl: string;
40
+ contextResources: ContextResource[];
41
+ contentType: 'action' | 'message';
42
+ contentValue: string;
43
+ parameters?: any;
44
+ assistant?: string;
45
+ locale?: string;
46
+ }