@webex/internal-plugin-call-ai-summary 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.
- package/.eslintrc.js +6 -0
- package/README.md +257 -0
- package/ai-docs/AGENTS.md +300 -0
- package/ai-docs/ARCHITECTURE.md +1189 -0
- package/babel.config.js +3 -0
- package/dist/ai-summary.js +418 -0
- package/dist/ai-summary.js.map +1 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.js +25 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/manual-integration-test.js +154 -0
- package/dist/manual-integration-test.js.map +1 -0
- package/dist/manual-pragya-api-test.js +176 -0
- package/dist/manual-pragya-api-test.js.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +47 -0
- package/src/ai-summary.ts +318 -0
- package/src/config.ts +7 -0
- package/src/constants.ts +19 -0
- package/src/index.ts +13 -0
- package/src/manual-integration-test.js +129 -0
- package/src/manual-pragya-api-test.js +166 -0
- package/src/types.ts +154 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {WebexPlugin} from '@webex/webex-core';
|
|
6
|
+
|
|
7
|
+
import {AI_SUMMARY_SERVICE, AI_SUMMARY_CONTAINERS_RESOURCE, ERROR_MESSAGES} from './constants';
|
|
8
|
+
import type {
|
|
9
|
+
GetContainerOptions,
|
|
10
|
+
GetSummaryContentOptions,
|
|
11
|
+
PragyaContainerResponse,
|
|
12
|
+
SummaryContent,
|
|
13
|
+
SummaryNotes,
|
|
14
|
+
SummaryActionItems,
|
|
15
|
+
TranscriptContent,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
const AISummary = WebexPlugin.extend({
|
|
19
|
+
namespace: 'AISummary',
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a Pragya container by ID.
|
|
23
|
+
* Returns container metadata including summary URLs and encryption key.
|
|
24
|
+
*
|
|
25
|
+
* @param {GetContainerOptions} options
|
|
26
|
+
* @returns {Promise<PragyaContainerResponse>}
|
|
27
|
+
*/
|
|
28
|
+
getContainer(options: GetContainerOptions): Promise<PragyaContainerResponse> {
|
|
29
|
+
const {containerId} = options;
|
|
30
|
+
|
|
31
|
+
this._validateContainerId(containerId);
|
|
32
|
+
|
|
33
|
+
return this.webex
|
|
34
|
+
.request({
|
|
35
|
+
method: 'GET',
|
|
36
|
+
service: AI_SUMMARY_SERVICE,
|
|
37
|
+
resource: `${AI_SUMMARY_CONTAINERS_RESOURCE}/${containerId}`,
|
|
38
|
+
})
|
|
39
|
+
.then(({body}) => {
|
|
40
|
+
// Pragya API nests summary URLs under summaryData.data — flatten
|
|
41
|
+
// so consumers can access summaryData.summaryUrl directly.
|
|
42
|
+
if (body.summaryData?.data) {
|
|
43
|
+
body.summaryData = body.summaryData.data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return body;
|
|
47
|
+
})
|
|
48
|
+
.catch((error) => {
|
|
49
|
+
this.logger.error('AISummary->getContainer failed', {error, containerId});
|
|
50
|
+
throw this._handleError(error, 'getContainer');
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get AI-generated full summary for a call.
|
|
56
|
+
* Fetches from containerInfo.summaryData.summaryUrl and decrypts content.
|
|
57
|
+
*
|
|
58
|
+
* @param {GetSummaryContentOptions} options
|
|
59
|
+
* @returns {Promise<SummaryContent>}
|
|
60
|
+
*/
|
|
61
|
+
async getSummary(options: GetSummaryContentOptions): Promise<SummaryContent> {
|
|
62
|
+
const {containerInfo} = options;
|
|
63
|
+
|
|
64
|
+
this._validateContainerInfo(containerInfo, 'summaryUrl');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const {body} = await this.webex.request({
|
|
68
|
+
method: 'GET',
|
|
69
|
+
uri: `${containerInfo.summaryData.summaryUrl}?fields=note,shortnote,actionitems`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
|
|
73
|
+
|
|
74
|
+
const decryptedNote = await this._decryptContent(body.note.aiGeneratedContent, keyUrl);
|
|
75
|
+
|
|
76
|
+
const decryptedShortNote = await this._decryptContent(
|
|
77
|
+
body.shortnote.aiGeneratedContent,
|
|
78
|
+
keyUrl
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const decryptedSnippets = await Promise.all(
|
|
82
|
+
(body.actionitems?.snippets || []).map(async (snippet: any) => {
|
|
83
|
+
const decryptedAiContent = await this._decryptContent(snippet.aiGeneratedContent, keyUrl);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: snippet.id,
|
|
87
|
+
editedContent: snippet.content || undefined,
|
|
88
|
+
aiGeneratedContent: decryptedAiContent,
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// feedbackUrl may be in the links array as rel="feedback"
|
|
94
|
+
const feedbackLink = (body.links || []).find((link: any) => link.rel === 'feedback');
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: body.id,
|
|
98
|
+
note: decryptedNote,
|
|
99
|
+
shortNote: decryptedShortNote,
|
|
100
|
+
actionItems: decryptedSnippets,
|
|
101
|
+
feedbackUrl: feedbackLink?.href,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logger.error('AISummary->getSummary failed', {error});
|
|
105
|
+
throw this._handleError(error, 'getSummary');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get AI-generated notes for a call.
|
|
111
|
+
* Fetches from containerInfo.summaryData.notesUrl and decrypts content.
|
|
112
|
+
*
|
|
113
|
+
* @param {GetSummaryContentOptions} options
|
|
114
|
+
* @returns {Promise<SummaryNotes>}
|
|
115
|
+
*/
|
|
116
|
+
async getNotes(options: GetSummaryContentOptions): Promise<SummaryNotes> {
|
|
117
|
+
const {containerInfo} = options;
|
|
118
|
+
|
|
119
|
+
this._validateContainerInfo(containerInfo, 'notesUrl');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const {body} = await this.webex.request({
|
|
123
|
+
method: 'GET',
|
|
124
|
+
uri: containerInfo.summaryData.notesUrl,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
|
|
128
|
+
|
|
129
|
+
const decryptedContent = await this._decryptContent(body.aiGeneratedContent, keyUrl);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id: body.id,
|
|
133
|
+
content: decryptedContent,
|
|
134
|
+
feedbackUrl: body.feedbackUrl,
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.logger.error('AISummary->getNotes failed', {error});
|
|
138
|
+
throw this._handleError(error, 'getNotes');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get AI-generated action items for a call.
|
|
144
|
+
* Fetches from containerInfo.summaryData.actionItemsUrl and decrypts content.
|
|
145
|
+
*
|
|
146
|
+
* @param {GetSummaryContentOptions} options
|
|
147
|
+
* @returns {Promise<SummaryActionItems>}
|
|
148
|
+
*/
|
|
149
|
+
async getActionItems(options: GetSummaryContentOptions): Promise<SummaryActionItems> {
|
|
150
|
+
const {containerInfo} = options;
|
|
151
|
+
|
|
152
|
+
this._validateContainerInfo(containerInfo, 'actionItemsUrl');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const {body} = await this.webex.request({
|
|
156
|
+
method: 'GET',
|
|
157
|
+
uri: containerInfo.summaryData.actionItemsUrl,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Action items response is an array; take the first element
|
|
161
|
+
const actionItemsData = Array.isArray(body) ? body[0] : body;
|
|
162
|
+
|
|
163
|
+
if (!actionItemsData) {
|
|
164
|
+
return {id: undefined, snippets: []};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const keyUrl = actionItemsData.keyUrl || containerInfo.encryptionKeyUrl;
|
|
168
|
+
|
|
169
|
+
const decryptedSnippets = await Promise.all(
|
|
170
|
+
(actionItemsData.snippets || []).map(async (snippet: any) => {
|
|
171
|
+
const decryptedAiContent = await this._decryptContent(snippet.aiGeneratedContent, keyUrl);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
id: snippet.id,
|
|
175
|
+
editedContent: snippet.content || undefined,
|
|
176
|
+
aiGeneratedContent: decryptedAiContent,
|
|
177
|
+
};
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: actionItemsData.id,
|
|
183
|
+
snippets: decryptedSnippets,
|
|
184
|
+
feedbackUrl: actionItemsData.feedbackUrl,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error('AISummary->getActionItems failed', {error});
|
|
188
|
+
throw this._handleError(error, 'getActionItems');
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the transcript URL for a call.
|
|
194
|
+
* Returns the URL string from the container info.
|
|
195
|
+
*
|
|
196
|
+
* @param {GetSummaryContentOptions} options
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
getTranscriptUrl(options: GetSummaryContentOptions): string {
|
|
200
|
+
const {containerInfo} = options;
|
|
201
|
+
|
|
202
|
+
this._validateContainerInfo(containerInfo, 'transcriptUrl');
|
|
203
|
+
|
|
204
|
+
return containerInfo.summaryData.transcriptUrl;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get decrypted transcript for a call.
|
|
209
|
+
* Fetches from containerInfo.summaryData.transcriptUrl and decrypts each snippet.
|
|
210
|
+
*
|
|
211
|
+
* @param {GetSummaryContentOptions} options
|
|
212
|
+
* @returns {Promise<TranscriptContent>}
|
|
213
|
+
*/
|
|
214
|
+
async getTranscript(options: GetSummaryContentOptions): Promise<TranscriptContent> {
|
|
215
|
+
const {containerInfo} = options;
|
|
216
|
+
|
|
217
|
+
this._validateContainerInfo(containerInfo, 'transcriptUrl');
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const {body} = await this.webex.request({
|
|
221
|
+
method: 'GET',
|
|
222
|
+
uri: containerInfo.summaryData.transcriptUrl,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
|
|
226
|
+
|
|
227
|
+
const decryptedSnippets = await Promise.all(
|
|
228
|
+
(body.transcriptSnippetList || []).map(async (snippet: any) => {
|
|
229
|
+
const decryptedContent = await this._decryptContent(snippet.content, keyUrl);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
startTime: snippet.startTime,
|
|
233
|
+
endTime: snippet.endTime,
|
|
234
|
+
content: decryptedContent,
|
|
235
|
+
audioCSI: snippet.audioCSI,
|
|
236
|
+
speaker: snippet.speaker,
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
id: body.id,
|
|
243
|
+
totalCount: body.totalCount,
|
|
244
|
+
snippets: decryptedSnippets,
|
|
245
|
+
};
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.logger.error('AISummary->getTranscript failed', {error});
|
|
248
|
+
throw this._handleError(error, 'getTranscript');
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate containerId parameter.
|
|
254
|
+
* @param {string} containerId - The container ID to validate.
|
|
255
|
+
* @returns {void}
|
|
256
|
+
* @private
|
|
257
|
+
*/
|
|
258
|
+
_validateContainerId(containerId: string): void {
|
|
259
|
+
if (!containerId || typeof containerId !== 'string' || containerId.trim().length === 0) {
|
|
260
|
+
throw new Error(ERROR_MESSAGES.INVALID_CONTAINER_ID);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validate containerInfo has the required URL field and encryption key.
|
|
266
|
+
* @param {PragyaContainerResponse} containerInfo - The container info to validate.
|
|
267
|
+
* @param {string} urlField - The summaryData field name to check.
|
|
268
|
+
* @returns {void}
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_validateContainerInfo(containerInfo: PragyaContainerResponse, urlField: string): void {
|
|
272
|
+
if (!containerInfo?.summaryData?.[urlField] || !containerInfo?.encryptionKeyUrl) {
|
|
273
|
+
throw new Error(ERROR_MESSAGES.INVALID_CONTAINER_INFO);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Decrypt encrypted content using KMS.
|
|
279
|
+
* Delegates to the internal encryption plugin.
|
|
280
|
+
* @param {string} encryptedContent - The encrypted text (JWE format).
|
|
281
|
+
* @param {string} encryptionKeyUrl - KMS key URL.
|
|
282
|
+
* @returns {Promise<string>} Decrypted plaintext.
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
_decryptContent(encryptedContent: string, encryptionKeyUrl: string): Promise<string> {
|
|
286
|
+
return this.webex.internal.encryption.decryptText(encryptionKeyUrl, encryptedContent);
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Handle and normalize errors.
|
|
291
|
+
* @param {object} error - The error object from the request.
|
|
292
|
+
* @param {string} methodName - The name of the calling method.
|
|
293
|
+
* @returns {Error} A normalized Error instance.
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_handleError(error: any, methodName: string): Error {
|
|
297
|
+
if (error.statusCode === 404) {
|
|
298
|
+
const message =
|
|
299
|
+
methodName === 'getContainer'
|
|
300
|
+
? ERROR_MESSAGES.CONTAINER_NOT_FOUND
|
|
301
|
+
: ERROR_MESSAGES.CONTENT_NOT_FOUND;
|
|
302
|
+
|
|
303
|
+
return new Error(message);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (error.statusCode === 403) {
|
|
307
|
+
return new Error(ERROR_MESSAGES.ACCESS_DENIED);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (error.statusCode === 401) {
|
|
311
|
+
return new Error(ERROR_MESSAGES.AUTHENTICATION_FAILED);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return new Error(`${methodName} failed: ${error.message || 'Unknown error'}`);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
export default AISummary;
|
package/src/config.ts
ADDED
package/src/constants.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const AI_SUMMARY_SERVICE = 'pragya';
|
|
6
|
+
export const AI_SUMMARY_CONTAINERS_RESOURCE = 'containers';
|
|
7
|
+
|
|
8
|
+
export const SUMMARY_STATUSES = {
|
|
9
|
+
ACTIVE: 'Active',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const ERROR_MESSAGES = {
|
|
13
|
+
INVALID_CONTAINER_ID: 'containerId is required and must be a non-empty string',
|
|
14
|
+
INVALID_CONTAINER_INFO: 'containerInfo with valid summaryData and encryptionKeyUrl is required',
|
|
15
|
+
CONTAINER_NOT_FOUND: 'Container not found',
|
|
16
|
+
CONTENT_NOT_FOUND: 'Summary content not available or expired',
|
|
17
|
+
ACCESS_DENIED: 'Access denied: User not authorized to view this summary',
|
|
18
|
+
AUTHENTICATION_FAILED: 'Authentication failed: Invalid or expired token',
|
|
19
|
+
} as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '@webex/internal-plugin-encryption';
|
|
6
|
+
import {registerInternalPlugin} from '@webex/webex-core';
|
|
7
|
+
|
|
8
|
+
import AISummary from './ai-summary';
|
|
9
|
+
import config from './config';
|
|
10
|
+
|
|
11
|
+
registerInternalPlugin('aisummary', AISummary, {config});
|
|
12
|
+
|
|
13
|
+
export {default} from './ai-summary';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*
|
|
4
|
+
* Manual integration test for internal-plugin-call-ai-summary
|
|
5
|
+
* Tests the full flow using the SDK service catalog (WDM):
|
|
6
|
+
* device.register() -> getContainer -> getSummary (with KMS decryption)
|
|
7
|
+
*
|
|
8
|
+
* The SDK resolves `service: 'pragya'` to the correct base URL via the
|
|
9
|
+
* service catalog populated during device registration.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* WEBEX_TOKEN='<token>' node src/manual-integration-test.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/* eslint-disable no-console, require-jsdoc */
|
|
16
|
+
|
|
17
|
+
require('@webex/internal-plugin-call-ai-summary');
|
|
18
|
+
|
|
19
|
+
const WebexCore = require('@webex/webex-core').default;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Configuration
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const WEBEX_TOKEN = process.env.WEBEX_TOKEN || '<PASTE_YOUR_TOKEN_HERE>';
|
|
25
|
+
const CONTAINER_ID = process.env.CONTAINER_ID || '<PASTE_CONTAINER_ID_HERE>';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Main
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
async function main() {
|
|
31
|
+
console.log('=== Step 1: Create WebexCore ===\n');
|
|
32
|
+
const webex = new WebexCore({
|
|
33
|
+
credentials: {
|
|
34
|
+
access_token: WEBEX_TOKEN,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Step 2: Register device to populate service catalog
|
|
39
|
+
console.log('=== Step 2: Register device (WDM) ===\n');
|
|
40
|
+
await webex.internal.device.register();
|
|
41
|
+
console.log('Device registered successfully.');
|
|
42
|
+
console.log('Device URL:', webex.internal.device.url);
|
|
43
|
+
|
|
44
|
+
// Log the pragya service URL from the service catalog
|
|
45
|
+
try {
|
|
46
|
+
const pragyaUrl = webex.internal.services.get('pragya');
|
|
47
|
+
console.log('Pragya service URL (from catalog):', pragyaUrl);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.log('Could not resolve pragya from service catalog:', e.message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Step 3: Get container via plugin (uses service: 'pragya' + resource)
|
|
53
|
+
console.log('\n=== Step 3: getContainer via plugin ===\n');
|
|
54
|
+
const container = await webex.internal.aisummary.getContainer({
|
|
55
|
+
containerId: CONTAINER_ID,
|
|
56
|
+
});
|
|
57
|
+
console.log(
|
|
58
|
+
'Container Info:',
|
|
59
|
+
JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
id: container.id,
|
|
62
|
+
objectType: container.objectType,
|
|
63
|
+
encryptionKeyUrl: container.encryptionKeyUrl,
|
|
64
|
+
summaryUrl: container.summaryData?.summaryUrl,
|
|
65
|
+
transcriptUrl: container.summaryData?.transcriptUrl,
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
2
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Step 4: Call getSummary via plugin (fetches + decrypts all content)
|
|
73
|
+
console.log('\n=== Step 4: getSummary via plugin ===\n');
|
|
74
|
+
const summaryResult = await webex.internal.aisummary.getSummary({
|
|
75
|
+
containerInfo: container,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log('=== getSummary return structure ===');
|
|
79
|
+
const noteStr = summaryResult.note || '';
|
|
80
|
+
const shortNoteStr = summaryResult.shortNote || '';
|
|
81
|
+
const truncNote = noteStr.length > 200 ? `${noteStr.substring(0, 200)}...` : noteStr;
|
|
82
|
+
const truncShort =
|
|
83
|
+
shortNoteStr.length > 200 ? `${shortNoteStr.substring(0, 200)}...` : shortNoteStr;
|
|
84
|
+
const truncated = {
|
|
85
|
+
id: summaryResult.id,
|
|
86
|
+
note: truncNote,
|
|
87
|
+
shortNote: truncShort,
|
|
88
|
+
actionItems: (summaryResult.actionItems || []).map((item) => {
|
|
89
|
+
const content = item.aiGeneratedContent || '';
|
|
90
|
+
const truncContent = content.length > 100 ? `${content.substring(0, 100)}...` : content;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: item.id,
|
|
94
|
+
aiGeneratedContent: truncContent,
|
|
95
|
+
editedContent: item.editedContent,
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
feedbackUrl: summaryResult.feedbackUrl,
|
|
99
|
+
};
|
|
100
|
+
console.log(JSON.stringify(truncated, null, 2));
|
|
101
|
+
|
|
102
|
+
// Step 5: Get transcript URL via plugin
|
|
103
|
+
console.log('\n=== Step 5: getTranscriptUrl via plugin ===\n');
|
|
104
|
+
const transcriptUrl = webex.internal.aisummary.getTranscriptUrl({
|
|
105
|
+
containerInfo: container,
|
|
106
|
+
});
|
|
107
|
+
console.log('Transcript URL:', transcriptUrl);
|
|
108
|
+
|
|
109
|
+
// Step 6: Fetch transcript content
|
|
110
|
+
console.log('\n=== Step 6: Fetch transcript content ===\n');
|
|
111
|
+
try {
|
|
112
|
+
const {body: transcriptBody} = await webex.request({
|
|
113
|
+
method: 'GET',
|
|
114
|
+
uri: `${transcriptUrl}?fields=id,content`,
|
|
115
|
+
});
|
|
116
|
+
console.log('Transcript response keys:', Object.keys(transcriptBody));
|
|
117
|
+
console.log(JSON.stringify(transcriptBody, null, 2).substring(0, 500));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Transcript fetch failed:', err.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('\nDone.');
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch((err) => {
|
|
127
|
+
console.error('FATAL:', err.message || err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*
|
|
4
|
+
* Manual test for internal-plugin-call-ai-summary
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* WEBEX_TOKEN='<your-token>' node manual-test.js
|
|
8
|
+
*
|
|
9
|
+
* Or paste your token directly into WEBEX_TOKEN below.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* eslint-disable no-console, require-jsdoc */
|
|
13
|
+
|
|
14
|
+
require('@webex/internal-plugin-call-ai-summary');
|
|
15
|
+
|
|
16
|
+
const WebexCore = require('@webex/webex-core').default;
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Configuration
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const WEBEX_TOKEN = process.env.WEBEX_TOKEN || '<PASTE_YOUR_TOKEN_HERE>';
|
|
22
|
+
const CONTAINER_ID = '<PASTE_CONTAINER_ID_HERE>';
|
|
23
|
+
const PRAGYA_BASE_URL = '<PASTE_PRAGYA_BASE_URL_HERE>';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Main
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
async function main() {
|
|
29
|
+
if (WEBEX_TOKEN === '<PASTE_YOUR_TOKEN_HERE>') {
|
|
30
|
+
console.error('ERROR: Set WEBEX_TOKEN env var or paste your token in the script.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const webex = new WebexCore({
|
|
35
|
+
credentials: {
|
|
36
|
+
access_token: WEBEX_TOKEN,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log('--- Fetching container', CONTAINER_ID, '(SDK auth) ---\n');
|
|
41
|
+
|
|
42
|
+
const response = await webex.request({
|
|
43
|
+
method: 'GET',
|
|
44
|
+
uri: `${PRAGYA_BASE_URL}/containers/${CONTAINER_ID}`,
|
|
45
|
+
headers: {
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const container = response.body;
|
|
51
|
+
let passed = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
function check(label, actual, expected) {
|
|
55
|
+
if (actual === expected) {
|
|
56
|
+
console.log(` PASS: ${label}`);
|
|
57
|
+
passed += 1;
|
|
58
|
+
} else {
|
|
59
|
+
console.log(
|
|
60
|
+
` FAIL: ${label} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
|
61
|
+
);
|
|
62
|
+
failed += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function checkExists(label, value) {
|
|
67
|
+
if (value !== undefined && value !== null) {
|
|
68
|
+
console.log(` PASS: ${label} exists`);
|
|
69
|
+
passed += 1;
|
|
70
|
+
} else {
|
|
71
|
+
console.log(` FAIL: ${label} is missing`);
|
|
72
|
+
failed += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkType(label, value, type) {
|
|
77
|
+
const actual = Array.isArray(value) ? 'array' : typeof value;
|
|
78
|
+
const expected = type;
|
|
79
|
+
if (type === 'array' ? Array.isArray(value) : actual === expected) {
|
|
80
|
+
console.log(` PASS: ${label} is ${type}`);
|
|
81
|
+
passed += 1;
|
|
82
|
+
} else {
|
|
83
|
+
console.log(` FAIL: ${label} — expected ${type}, got ${actual}`);
|
|
84
|
+
failed += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkMatch(label, value, regex) {
|
|
89
|
+
if (regex.test(value)) {
|
|
90
|
+
console.log(` PASS: ${label} matches ${regex}`);
|
|
91
|
+
passed += 1;
|
|
92
|
+
} else {
|
|
93
|
+
console.log(` FAIL: ${label} — ${JSON.stringify(value)} does not match ${regex}`);
|
|
94
|
+
failed += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Top-level container ---
|
|
99
|
+
console.log('\n--- Top-level container ---');
|
|
100
|
+
checkType('container', container, 'object');
|
|
101
|
+
check('container.id', container.id, CONTAINER_ID);
|
|
102
|
+
check('container.objectType', container.objectType, 'callingAIContainer');
|
|
103
|
+
checkExists('container.summaryData', container.summaryData);
|
|
104
|
+
checkExists('container.memberships', container.memberships);
|
|
105
|
+
checkMatch('container.encryptionKeyUrl', container.encryptionKeyUrl, /^kms:\/\//);
|
|
106
|
+
checkMatch('container.kmsResourceObjectUrl', container.kmsResourceObjectUrl, /^kms:\/\//);
|
|
107
|
+
checkMatch('container.aclUrl', container.aclUrl, /^https?:\/\//);
|
|
108
|
+
checkExists('container.start', container.start);
|
|
109
|
+
checkExists('container.end', container.end);
|
|
110
|
+
checkExists('container.forkSessionId', container.forkSessionId);
|
|
111
|
+
checkExists('container.callSessionId', container.callSessionId);
|
|
112
|
+
checkExists('container.ownerUserId', container.ownerUserId);
|
|
113
|
+
checkExists('container.orgId', container.orgId);
|
|
114
|
+
|
|
115
|
+
// --- summaryData ---
|
|
116
|
+
console.log('\n--- summaryData ---');
|
|
117
|
+
const {summaryData} = container;
|
|
118
|
+
checkType('summaryData', summaryData, 'object');
|
|
119
|
+
checkExists('summaryData.extensionId', summaryData.extensionId);
|
|
120
|
+
check('summaryData.objectType', summaryData.objectType, 'extension');
|
|
121
|
+
check('summaryData.extensionType', summaryData.extensionType, 'callingAISummary');
|
|
122
|
+
|
|
123
|
+
// --- summaryData.data ---
|
|
124
|
+
console.log('\n--- summaryData.data ---');
|
|
125
|
+
const summaryDataData = summaryData.data;
|
|
126
|
+
checkType('summaryData.data', summaryDataData, 'object');
|
|
127
|
+
checkExists('summaryData.data.id', summaryDataData.id);
|
|
128
|
+
check('summaryData.data.objectType', summaryDataData.objectType, 'callingAISummary');
|
|
129
|
+
checkExists('summaryData.data.status', summaryDataData.status);
|
|
130
|
+
checkMatch('summaryData.data.summaryUrl', summaryDataData.summaryUrl, /^https?:\/\//);
|
|
131
|
+
checkMatch('summaryData.data.transcriptUrl', summaryDataData.transcriptUrl, /^https?:\/\//);
|
|
132
|
+
checkMatch('summaryData.data.aclUrl', summaryDataData.aclUrl, /^https?:\/\//);
|
|
133
|
+
checkMatch(
|
|
134
|
+
'summaryData.data.kmsResourceObjectUrl',
|
|
135
|
+
summaryDataData.kmsResourceObjectUrl,
|
|
136
|
+
/^kms:\/\//
|
|
137
|
+
);
|
|
138
|
+
checkType('summaryData.data.summarizeAfterCall', summaryDataData.summarizeAfterCall, 'boolean');
|
|
139
|
+
checkType('summaryData.data.contentRetention', summaryDataData.contentRetention, 'object');
|
|
140
|
+
|
|
141
|
+
// --- memberships ---
|
|
142
|
+
console.log('\n--- memberships ---');
|
|
143
|
+
const {memberships} = container;
|
|
144
|
+
checkType('memberships', memberships, 'object');
|
|
145
|
+
checkType('memberships.items', memberships.items, 'array');
|
|
146
|
+
if (memberships.items.length > 0) {
|
|
147
|
+
console.log(` PASS: memberships.items has ${memberships.items.length} member(s)`);
|
|
148
|
+
passed += 1;
|
|
149
|
+
const first = memberships.items[0];
|
|
150
|
+
checkExists('memberships.items[0].id', first.id);
|
|
151
|
+
checkType('memberships.items[0].roles', first.roles, 'array');
|
|
152
|
+
check('memberships.items[0].objectType', first.objectType, 'containerMembership');
|
|
153
|
+
} else {
|
|
154
|
+
console.log(' FAIL: memberships.items is empty');
|
|
155
|
+
failed += 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Summary ---
|
|
159
|
+
console.log(`\n=== ${passed} passed, ${failed} failed ===`);
|
|
160
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
main().catch((err) => {
|
|
164
|
+
console.error('FAILED:', err.message || err);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|