@webex/internal-plugin-call-ai-summary 3.12.0-next.6 → 3.12.0-next.61

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.
@@ -1,1189 +0,0 @@
1
- # AI Call Summary Architecture
2
-
3
- ## 1. Overview
4
-
5
- The Webex JS SDK will provide AI-generated call summary retrieval capabilities through a new **`internal-plugin-call-ai-summary`** internal plugin. This document describes the architecture for retrieving AI-generated notes, action items, and transcripts from completed calls.
6
-
7
- ### 1.1 Summary Discovery Flow
8
-
9
- AI summary content is discovered through a two-step lookup: **Janus** (call history) provides container IDs, and **Pragya** (AI container service) resolves those IDs into direct URLs for summary content.
10
-
11
- **Step 1: Get container IDs from Janus call history**
12
-
13
- The Janus `UserSession` response includes an `extensionPayload` field containing container IDs for AI artifacts related to a call:
14
-
15
- ```typescript
16
- export type UserSession = {
17
- id: string;
18
- sessionId: string;
19
- disposition: Disposition;
20
- startTime: string;
21
- endTime: string;
22
- url: string;
23
- durationSeconds: number;
24
- joinedDurationSeconds: number;
25
- participantCount: number;
26
- isDeleted: boolean;
27
- isPMR: boolean;
28
- correlationIds: string[];
29
- links: CallRecordLink;
30
- self: CallRecordSelf;
31
- other: CallRecordListOther;
32
- sessionType: SessionType;
33
- direction: string;
34
- callingSpecifics?: { redirectionDetails: RedirectionDetails };
35
- extensionPayload?: {
36
- callingContainerIds?: string[];
37
- };
38
- };
39
- ```
40
-
41
- > **Note:** The `extensionPayload.callingContainerIds` field is already present in the Janus API response but is not yet in the SDK's `UserSession` type definition at `packages/calling/src/Events/types.ts`. However, this plugin does **not** modify that type. It accepts a plain `containerId` string as input, keeping the plugin self-contained. The `UserSession` type update can be handled separately by the calling package team when convenient.
42
-
43
- **Step 2: Resolve container IDs via Pragya**
44
-
45
- For each `containerId`, call the Pragya container API:
46
-
47
- ```
48
- GET https://{pragya-host}/pragya/api/v1/containers/{containerId}
49
- ```
50
-
51
- **Pragya response:**
52
-
53
- > **Note:** The raw Pragya response nests summary URLs under `summaryData.data`. The plugin's `getContainer()` method flattens this so consumers can access `summaryData.summaryUrl` directly.
54
-
55
- ```json
56
- {
57
- "id": "34125120-13b5-11f1-9b36-adb685725098",
58
- "objectType": "callingAIContainer",
59
- "memberships": {
60
- "items": [
61
- { "id": "...", "roles": ["OWNER"], "objectType": "containerMembership" }
62
- ]
63
- },
64
- "summaryData": {
65
- "extensionId": "...",
66
- "objectType": "extension",
67
- "extensionType": "callingAISummary",
68
- "data": {
69
- "id": "...",
70
- "objectType": "callingAISummary",
71
- "status": "Active",
72
- "summaryUrl": "https://aibridge-url/summaries/c635e870-7b3b-4b3b-8b3b-7b3b7b3b7b3c",
73
- "transcriptUrl": "https://aibridge-url/summaries/c635e870-7b3b-4b3b-8b3b-7b3b7b3b7b3c/transcripts",
74
- "summarizeAfterCall": true,
75
- "aclUrl": "https://acl-a.wbx2.com/...",
76
- "kmsResourceObjectUrl": "kms://kms-cisco.wbx2.com/resources/...",
77
- "contentRetention": { ... }
78
- }
79
- },
80
- "encryptionKeyUrl": "kms://kms-cisco.wbx2.com/keys/897e4d2d-6219-433d-be77-7ec73fe1c0db",
81
- "kmsResourceObjectUrl": "kms://kms-cisco.wbx2.com/resources/f7316435-2147-4d23-bf4a-762d831cb58c",
82
- "aclUrl": "https://acl-a.wbx2.com/acl/api/v1/acls/78c4cd90-f880-11ee-96e9-3932dce37910",
83
- "forkSessionId": "123e4567-e89b-12d3-a456-426614174000",
84
- "callSessionId": "123e4567-e89b-12d3-a456-426614174000",
85
- "ownerUserId": "123e4567-e89b-12d3-a456-426614174000",
86
- "orgId": "123e4567-e89b-12d3-a456-426614174000",
87
- "start": "2023-10-01T12:00:00Z",
88
- "end": "2023-10-01T12:00:00Z"
89
- }
90
- ```
91
-
92
- **Step 3: Fetch summary content from the URLs**
93
-
94
- The `summaryData` object provides direct, region-correct URLs to each content type. The plugin fetches content from these URLs and decrypts it using the `encryptionKeyUrl` from the same Pragya response.
95
-
96
- ### 1.2 Key Design Decisions
97
-
98
- - **Self-contained plugin with zero changes to existing packages.** The plugin owns all of its types, constants, and logic. It does not modify `UserSession`, `CallHistory`, or any other existing code. Consumers pass a `containerId` string; how they obtain it (Janus, Mercury event, hard-coded for testing) is their concern.
99
- - **No separate service discovery for summary endpoints.** Pragya returns fully-qualified URLs that already include the correct regional host. The SDK fetches from these URLs directly using `uri:` rather than `service:` + `resource:`.
100
- - **Pragya is the source of truth** for both the content URLs and the encryption key.
101
- - **Pragya is discoverable via U2C** as `serviceName: "pragya"` (validated: e.g., load-us resolves to `https://pragya-loada.ciscospark.com/pragya/api/v1`).
102
-
103
- ### 1.3 Goals
104
-
105
- - Resolve AI summary container IDs from Janus call history via Pragya
106
- - Retrieve AI-generated notes (full notes) for a call
107
- - Retrieve AI-generated action items for a call
108
- - Retrieve transcript download URLs for a call
109
- - Handle encrypted content decryption via KMS
110
- - Maintain consistency with existing Webex JS SDK internal plugin patterns
111
- - Provide type-safe interfaces for all operations
112
- - Support both browser and Node.js environments
113
-
114
- ### 1.4 Non-Goals
115
-
116
- - Start/stop AI assistant during active calls (handled by Pragya start/stop APIs, out of scope)
117
- - Generate or regenerate summaries (backend-managed during/after calls)
118
- - Provide real-time in-call AI responses
119
- - Handle recording storage or deletion
120
- - Implement feedback UI components
121
-
122
- ### 1.5 Prerequisites
123
-
124
- 1. Janus API already returns `extensionPayload.callingContainerIds` in the response
125
- 2. Testing environment with AI-enabled calls that generate summaries
126
-
127
- ## 2. High-Level Design
128
-
129
- ### 2.1 Component Architecture
130
-
131
- ```
132
- +---------------------------------------------------------------+
133
- | Client Application |
134
- +-------------------------------+-------------------------------+
135
- |
136
- | webex.internal.aisummary.*
137
- |
138
- +-------------------------------v-------------------------------+
139
- | internal-plugin-call-ai-summary |
140
- | (Internal Plugin) |
141
- | +----------------------------------------------------------+ |
142
- | | Public API Methods | |
143
- | | - getContainer(containerId) | |
144
- | | - getSummary(containerInfo) | |
145
- | | - getNotes(containerInfo) | |
146
- | | - getActionItems(containerInfo) | |
147
- | | - getTranscriptUrl(containerInfo) | |
148
- | +----------------------------+-----------------------------+ |
149
- | | |
150
- | +----------------------------v-----------------------------+ |
151
- | | Internal Logic | |
152
- | | - Input validation | |
153
- | | - Content decryption (KMS via encryptionKeyUrl) | |
154
- | | - Response normalization | |
155
- | | - Error handling & mapping | |
156
- | +----------------------------+-----------------------------+ |
157
- +-------------------------------+-------------------------------+
158
- |
159
- +-----------------+-----------------+
160
- | |
161
- +-------------v--------------+ +-----------------v--------------+
162
- | Pragya Service | | Summary Content URLs |
163
- | (U2C: serviceName:pragya) | | (Direct URLs from Pragya) |
164
- | GET /containers/{id} | | GET {summaryUrl} |
165
- +----------------------------+ | GET {notesUrl} |
166
- | GET {actionItemsUrl} |
167
- +--------------------------------+
168
- |
169
- +-----------v--------------------+
170
- | internal-plugin-encryption |
171
- | decryptText(keyUrl, cipher) |
172
- +--------------------------------+
173
- ```
174
-
175
- ### 2.2 Key Components
176
-
177
- | Component | Responsibility |
178
- |-----------|----------------|
179
- | `internal-plugin-call-ai-summary` | Internal plugin; resolves Pragya containers, fetches and decrypts summary content |
180
- | `internal-plugin-encryption` | KMS integration for decrypting AI-generated content using `encryptionKeyUrl` |
181
- | `http-core` | HTTP transport; adds authorization headers, handles retries |
182
- | Pragya Service | Container metadata; provides content URLs and encryption key |
183
- | Summary Content Endpoints | Serve encrypted AI-generated content (notes, action items, transcripts) |
184
-
185
- ## 3. Data Flow
186
-
187
- ### 3.1 End-to-End Summary Retrieval Flow
188
-
189
- ```
190
- Client
191
- |
192
- | 1. Get call history
193
- +-> callHistory.getCallHistoryData()
194
- | +-> Janus API: GET /history/userSessions
195
- | +-> Response includes extensionPayload.callingContainerIds
196
- |
197
- | 2. Resolve container
198
- +-> webex.internal.aisummary.getContainer(containerId)
199
- | +-> Pragya API: GET /pragya/api/v1/containers/{containerId}
200
- | +-> Response: { summaryData: { summaryUrl, notesUrl, ... }, encryptionKeyUrl }
201
- |
202
- | 3. Fetch all summary content in one call
203
- +-> webex.internal.aisummary.getSummary({ containerInfo: container })
204
- +-> HTTP GET {summaryUrl}?fields=note,shortnote,actionitems
205
- +-> Response: { note: {...}, shortnote: {...}, actionitems: {...} }
206
- +-> Decrypt note, shortNote, and all action item snippets
207
- +-> Return { id, note, shortNote, actionItems, feedbackUrl }
208
- ```
209
-
210
- ### 3.2 Get Container Info Flow
211
-
212
- ```
213
- Client
214
- +-> webex.internal.aisummary.getContainer({ containerId })
215
- +-> Validate containerId (non-empty string)
216
- +-> webex.request({
217
- method: 'GET',
218
- service: 'pragya',
219
- resource: `containers/${containerId}`,
220
- })
221
- +-> Flatten: if body.summaryData.data exists, set body.summaryData = body.summaryData.data
222
- +-> Return PragyaContainerResponse (with flat summaryData)
223
- ```
224
-
225
- ### 3.3 Get Notes Flow
226
-
227
- ```
228
- Client
229
- +-> webex.internal.aisummary.getNotes(containerInfo)
230
- +-> Validate containerInfo has summaryData.notesUrl and encryptionKeyUrl
231
- +-> webex.request({
232
- method: 'GET',
233
- uri: containerInfo.summaryData.notesUrl,
234
- })
235
- +-> Response: { id, aiGeneratedContent: "<encrypted>", feedbackUrl?, keyUrl }
236
- +-> Decrypt aiGeneratedContent using containerInfo.encryptionKeyUrl
237
- +-> Return decrypted SummaryNotes
238
- ```
239
-
240
- ### 3.4 Get Action Items Flow
241
-
242
- ```
243
- Client
244
- +-> webex.internal.aisummary.getActionItems(containerInfo)
245
- +-> Validate containerInfo has summaryData.actionItemsUrl and encryptionKeyUrl
246
- +-> webex.request({
247
- method: 'GET',
248
- uri: containerInfo.summaryData.actionItemsUrl,
249
- })
250
- +-> Response: [{ id, keyUrl, snippets: [{ id, content, aiGeneratedContent }] }]
251
- +-> Decrypt all aiGeneratedContent fields using containerInfo.encryptionKeyUrl
252
- +-> Return decrypted SummaryActionItems
253
- ```
254
-
255
- ## 4. SDK Method Interfaces
256
-
257
- ### 4.1 Internal API Methods
258
-
259
- ```typescript
260
- /**
261
- * AISummary namespace accessible via webex.internal.aisummary
262
- */
263
- interface AISummary {
264
- /**
265
- * Resolve a Pragya container by ID to get summary URLs and encryption key.
266
- */
267
- getContainer(options: GetContainerOptions): Promise<PragyaContainerResponse>;
268
-
269
- /**
270
- * Get AI-generated full summary for a call.
271
- * Fetches from summaryUrl with ?fields=note,shortnote,actionitems and decrypts all content.
272
- * Returns note, shortNote, and actionItems in a single response.
273
- */
274
- getSummary(options: GetSummaryContentOptions): Promise<SummaryContent>;
275
-
276
- /**
277
- * Get AI-generated notes for a call.
278
- * Fetches from containerInfo.summaryData.notesUrl and decrypts content.
279
- * Only available if notesUrl is present in the Pragya response.
280
- */
281
- getNotes(options: GetSummaryContentOptions): Promise<SummaryNotes>;
282
-
283
- /**
284
- * Get AI-generated action items for a call.
285
- * Fetches from containerInfo.summaryData.actionItemsUrl and decrypts content.
286
- * Only available if actionItemsUrl is present in the Pragya response.
287
- */
288
- getActionItems(options: GetSummaryContentOptions): Promise<SummaryActionItems>;
289
-
290
- /**
291
- * Get the transcript URL for a call.
292
- * Returns the URL from containerInfo.summaryData.transcriptUrl.
293
- * Does not fetch or decrypt - the consumer uses this URL directly.
294
- */
295
- getTranscriptUrl(options: GetSummaryContentOptions): string;
296
-
297
- /**
298
- * Get decrypted transcript for a call.
299
- * Fetches from containerInfo.summaryData.transcriptUrl and decrypts each snippet.
300
- */
301
- getTranscript(options: GetSummaryContentOptions): Promise<TranscriptContent>;
302
- }
303
- ```
304
-
305
- ## 5. Data Transfer Objects (DTOs)
306
-
307
- ### 5.1 Request DTOs
308
-
309
- ```typescript
310
- /**
311
- * Options for resolving a Pragya container
312
- */
313
- export interface GetContainerOptions {
314
- /** Pragya container ID from Janus extensionPayload.callingContainerIds */
315
- containerId: string;
316
- }
317
-
318
- /**
319
- * Options for fetching summary content.
320
- * Requires the resolved Pragya container info.
321
- */
322
- export interface GetSummaryContentOptions {
323
- /** The resolved Pragya container response */
324
- containerInfo: PragyaContainerResponse;
325
- }
326
- ```
327
-
328
- ### 5.2 Pragya Response DTOs
329
-
330
- ```typescript
331
- /**
332
- * Summary data URLs from a Pragya container
333
- */
334
- export interface PragyaSummaryData {
335
- /** Status of the summary (e.g., "Active") */
336
- status: string;
337
- /** Full summary URL (AI Bridge) */
338
- summaryUrl: string;
339
- /** Transcript URL (AI Bridge) */
340
- transcriptUrl: string;
341
- /** Whether summarization runs after call ends */
342
- summarizeAfterCall: boolean;
343
- /** Notes-specific URL (may not be present in all API versions) */
344
- notesUrl?: string;
345
- /** Action items URL (may not be present in all API versions) */
346
- actionItemsUrl?: string;
347
- }
348
-
349
- /**
350
- * Complete Pragya container response
351
- */
352
- export interface PragyaContainerResponse {
353
- /** Summary data with content URLs */
354
- summaryData: PragyaSummaryData;
355
- /** KMS encryption key URL for decrypting content */
356
- encryptionKeyUrl: string;
357
- /** KMS resource object URL */
358
- kmsResourceObjectUrl: string;
359
- /** ACL URL for access control */
360
- aclUrl: string;
361
- /** Fork session ID */
362
- forkSessionId: string;
363
- /** Call session ID */
364
- callSessionId: string;
365
- /** Owner user ID */
366
- ownerUserId: string;
367
- /** Organization ID */
368
- orgId: string;
369
- /** Call start time */
370
- start: string;
371
- /** Call end time */
372
- end: string;
373
- }
374
- ```
375
-
376
- ### 5.3 Summary Response DTOs
377
-
378
- ```typescript
379
- /**
380
- * Decrypted AI-generated summary content.
381
- * Contains all three content types returned by the summary API.
382
- */
383
- export interface SummaryContent {
384
- /** Unique identifier */
385
- id: string;
386
- /** Decrypted full note content */
387
- note: string;
388
- /** Decrypted short note content */
389
- shortNote: string;
390
- /** Decrypted action item snippets */
391
- actionItems: ActionItemSnippet[];
392
- /** Feedback URL (if available) */
393
- feedbackUrl?: string;
394
- }
395
-
396
- /**
397
- * Decrypted AI-generated notes
398
- */
399
- export interface SummaryNotes {
400
- /** Unique identifier */
401
- id: string;
402
- /** Decrypted notes content */
403
- content: string;
404
- /** Feedback URL (if available) */
405
- feedbackUrl?: string;
406
- }
407
-
408
- /**
409
- * Single action item snippet
410
- */
411
- export interface ActionItemSnippet {
412
- /** Unique identifier */
413
- id: string;
414
- /** User-edited version (if available) */
415
- editedContent?: string;
416
- /** Decrypted AI-generated content */
417
- aiGeneratedContent: string;
418
- }
419
-
420
- /**
421
- * Decrypted AI-generated action items
422
- */
423
- export interface SummaryActionItems {
424
- /** Unique identifier (absent when no action items exist) */
425
- id?: string;
426
- /** Array of action item snippets */
427
- snippets: ActionItemSnippet[];
428
- /** Feedback URL (if available) */
429
- feedbackUrl?: string;
430
- }
431
-
432
- /**
433
- * Single decrypted transcript snippet
434
- */
435
- export interface TranscriptSnippet {
436
- /** Start time in milliseconds */
437
- startTime: string;
438
- /** End time in milliseconds */
439
- endTime: string;
440
- /** Decrypted transcript content */
441
- content: string;
442
- /** Audio CSI identifier */
443
- audioCSI?: string;
444
- /** Speaker information */
445
- speaker?: {
446
- speakerName: string;
447
- speakerId: string;
448
- };
449
- }
450
-
451
- /**
452
- * Decrypted transcript response
453
- */
454
- export interface TranscriptContent {
455
- /** Unique identifier */
456
- id: string;
457
- /** Total number of snippets */
458
- totalCount: number;
459
- /** Decrypted transcript snippets */
460
- snippets: TranscriptSnippet[];
461
- }
462
- ```
463
-
464
- ## 6. Low-Level Design & Pseudo Code
465
-
466
- ### 6.1 Plugin Registration
467
-
468
- ```typescript
469
- // packages/@webex/internal-plugin-call-ai-summary/src/index.ts
470
-
471
- import '@webex/internal-plugin-encryption';
472
- import {registerInternalPlugin} from '@webex/webex-core';
473
-
474
- import AISummary from './ai-summary';
475
- import config from './config';
476
-
477
- registerInternalPlugin('aisummary', AISummary, {config});
478
-
479
- export {default} from './ai-summary';
480
- ```
481
-
482
- ### 6.2 Config
483
-
484
- ```typescript
485
- // packages/@webex/internal-plugin-call-ai-summary/src/config.ts
486
-
487
- export default {
488
- aisummary: {},
489
- };
490
- ```
491
-
492
- ### 6.3 Constants
493
-
494
- ```typescript
495
- // packages/@webex/internal-plugin-call-ai-summary/src/constants.ts
496
-
497
- export const AI_SUMMARY_SERVICE = 'pragya';
498
- export const AI_SUMMARY_CONTAINERS_RESOURCE = 'containers';
499
-
500
- export const SUMMARY_STATUSES = {
501
- ACTIVE: 'Active',
502
- } as const;
503
-
504
- export const ERROR_MESSAGES = {
505
- INVALID_CONTAINER_ID: 'containerId is required and must be a non-empty string',
506
- INVALID_CONTAINER_INFO: 'containerInfo with valid summaryData and encryptionKeyUrl is required',
507
- CONTAINER_NOT_FOUND: 'Container not found',
508
- CONTENT_NOT_FOUND: 'Summary content not available or expired',
509
- ACCESS_DENIED: 'Access denied: User not authorized to view this summary',
510
- AUTHENTICATION_FAILED: 'Authentication failed: Invalid or expired token',
511
- } as const;
512
- ```
513
-
514
- ### 6.4 Plugin Implementation
515
-
516
- ```typescript
517
- // packages/@webex/internal-plugin-call-ai-summary/src/ai-summary.ts
518
-
519
- import {WebexPlugin} from '@webex/webex-core';
520
-
521
- import {AI_SUMMARY_SERVICE, AI_SUMMARY_CONTAINERS_RESOURCE, ERROR_MESSAGES} from './constants';
522
- import type {
523
- GetContainerOptions,
524
- GetSummaryContentOptions,
525
- PragyaContainerResponse,
526
- SummaryContent,
527
- SummaryNotes,
528
- SummaryActionItems,
529
- TranscriptContent,
530
- } from './types';
531
-
532
- const AISummary = WebexPlugin.extend({
533
- namespace: 'AISummary',
534
-
535
- /**
536
- * Resolve a Pragya container by ID.
537
- * Flattens the nested summaryData.data structure for consumer convenience.
538
- */
539
- getContainer(options: GetContainerOptions): Promise<PragyaContainerResponse> {
540
- const {containerId} = options;
541
- this._validateContainerId(containerId);
542
-
543
- return this.webex
544
- .request({
545
- method: 'GET',
546
- service: AI_SUMMARY_SERVICE,
547
- resource: `${AI_SUMMARY_CONTAINERS_RESOURCE}/${containerId}`,
548
- })
549
- .then(({body}) => {
550
- // Pragya API nests summary URLs under summaryData.data — flatten
551
- if (body.summaryData?.data) {
552
- body.summaryData = body.summaryData.data;
553
- }
554
- return body;
555
- })
556
- .catch((error) => {
557
- this.logger.error('AISummary->getContainer failed', {error, containerId});
558
- throw this._handleError(error, 'getContainer');
559
- });
560
- },
561
-
562
- /**
563
- * Get AI-generated full summary for a call.
564
- * Fetches note, shortNote, and actionItems in a single request via
565
- * summaryUrl?fields=note,shortnote,actionitems, then decrypts all content.
566
- */
567
- async getSummary(options: GetSummaryContentOptions): Promise<SummaryContent> {
568
- const {containerInfo} = options;
569
- this._validateContainerInfo(containerInfo, 'summaryUrl');
570
-
571
- try {
572
- const {body} = await this.webex.request({
573
- method: 'GET',
574
- uri: `${containerInfo.summaryData.summaryUrl}?fields=note,shortnote,actionitems`,
575
- });
576
-
577
- const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
578
- const decryptedNote = await this._decryptContent(body.note.aiGeneratedContent, keyUrl);
579
- const decryptedShortNote = await this._decryptContent(
580
- body.shortnote.aiGeneratedContent, keyUrl
581
- );
582
-
583
- const decryptedSnippets = await Promise.all(
584
- (body.actionitems?.snippets || []).map(async (snippet: any) => {
585
- const decryptedAiContent = await this._decryptContent(
586
- snippet.aiGeneratedContent, keyUrl
587
- );
588
- return {
589
- id: snippet.id,
590
- editedContent: snippet.content || undefined,
591
- aiGeneratedContent: decryptedAiContent,
592
- };
593
- })
594
- );
595
-
596
- const feedbackLink = (body.links || []).find((link: any) => link.rel === 'feedback');
597
-
598
- return {
599
- id: body.id,
600
- note: decryptedNote,
601
- shortNote: decryptedShortNote,
602
- actionItems: decryptedSnippets,
603
- feedbackUrl: feedbackLink?.href,
604
- };
605
- } catch (error) {
606
- this.logger.error('AISummary->getSummary failed', {error});
607
- throw this._handleError(error, 'getSummary');
608
- }
609
- },
610
-
611
- /**
612
- * Get AI-generated notes for a call (standalone endpoint).
613
- * Uses body.keyUrl as decryption key with fallback to containerInfo.encryptionKeyUrl.
614
- */
615
- async getNotes(options: GetSummaryContentOptions): Promise<SummaryNotes> {
616
- const {containerInfo} = options;
617
- this._validateContainerInfo(containerInfo, 'notesUrl');
618
-
619
- try {
620
- const {body} = await this.webex.request({
621
- method: 'GET',
622
- uri: containerInfo.summaryData.notesUrl,
623
- });
624
-
625
- const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
626
- const decryptedContent = await this._decryptContent(body.aiGeneratedContent, keyUrl);
627
-
628
- return { id: body.id, content: decryptedContent, feedbackUrl: body.feedbackUrl };
629
- } catch (error) {
630
- this.logger.error('AISummary->getNotes failed', {error});
631
- throw this._handleError(error, 'getNotes');
632
- }
633
- },
634
-
635
- /**
636
- * Get AI-generated action items for a call (standalone endpoint).
637
- * Response is an array; takes the first element and decrypts all snippets.
638
- */
639
- async getActionItems(options: GetSummaryContentOptions): Promise<SummaryActionItems> {
640
- const {containerInfo} = options;
641
- this._validateContainerInfo(containerInfo, 'actionItemsUrl');
642
-
643
- try {
644
- const {body} = await this.webex.request({
645
- method: 'GET',
646
- uri: containerInfo.summaryData.actionItemsUrl,
647
- });
648
-
649
- const actionItemsData = Array.isArray(body) ? body[0] : body;
650
- if (!actionItemsData) return {id: undefined, snippets: []};
651
-
652
- const keyUrl = actionItemsData.keyUrl || containerInfo.encryptionKeyUrl;
653
- const decryptedSnippets = await Promise.all(
654
- (actionItemsData.snippets || []).map(async (snippet: any) => {
655
- const decryptedAiContent = await this._decryptContent(
656
- snippet.aiGeneratedContent, keyUrl
657
- );
658
- return {
659
- id: snippet.id,
660
- editedContent: snippet.content || undefined,
661
- aiGeneratedContent: decryptedAiContent,
662
- };
663
- })
664
- );
665
-
666
- return {
667
- id: actionItemsData.id,
668
- snippets: decryptedSnippets,
669
- feedbackUrl: actionItemsData.feedbackUrl,
670
- };
671
- } catch (error) {
672
- this.logger.error('AISummary->getActionItems failed', {error});
673
- throw this._handleError(error, 'getActionItems');
674
- }
675
- },
676
-
677
- /** Returns the transcript URL string from the container info. */
678
- getTranscriptUrl(options: GetSummaryContentOptions): string {
679
- const {containerInfo} = options;
680
- this._validateContainerInfo(containerInfo, 'transcriptUrl');
681
- return containerInfo.summaryData.transcriptUrl;
682
- },
683
-
684
- /** Fetches and decrypts the full transcript, returning all snippets. */
685
- async getTranscript(options: GetSummaryContentOptions): Promise<TranscriptContent> {
686
- const {containerInfo} = options;
687
- this._validateContainerInfo(containerInfo, 'transcriptUrl');
688
-
689
- try {
690
- const {body} = await this.webex.request({
691
- method: 'GET',
692
- uri: containerInfo.summaryData.transcriptUrl,
693
- });
694
-
695
- const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
696
- const decryptedSnippets = await Promise.all(
697
- (body.transcriptSnippetList || []).map(async (snippet: any) => {
698
- const decryptedContent = await this._decryptContent(snippet.content, keyUrl);
699
- return {
700
- startTime: snippet.startTime,
701
- endTime: snippet.endTime,
702
- content: decryptedContent,
703
- audioCSI: snippet.audioCSI,
704
- speaker: snippet.speaker,
705
- };
706
- })
707
- );
708
-
709
- return { id: body.id, totalCount: body.totalCount, snippets: decryptedSnippets };
710
- } catch (error) {
711
- this.logger.error('AISummary->getTranscript failed', {error});
712
- throw this._handleError(error, 'getTranscript');
713
- }
714
- },
715
-
716
- // --- Private helpers ---
717
-
718
- _validateContainerId(containerId: string): void { /* ... */ },
719
- _validateContainerInfo(containerInfo: PragyaContainerResponse, urlField: string): void { /* ... */ },
720
- _decryptContent(encryptedContent: string, encryptionKeyUrl: string): Promise<string> {
721
- return this.webex.internal.encryption.decryptText(encryptionKeyUrl, encryptedContent);
722
- },
723
- _handleError(error: any, methodName: string): Error {
724
- if (error.statusCode === 404) {
725
- const msg = methodName === 'getContainer'
726
- ? ERROR_MESSAGES.CONTAINER_NOT_FOUND
727
- : ERROR_MESSAGES.CONTENT_NOT_FOUND;
728
- return new Error(msg);
729
- }
730
- if (error.statusCode === 403) return new Error(ERROR_MESSAGES.ACCESS_DENIED);
731
- if (error.statusCode === 401) return new Error(ERROR_MESSAGES.AUTHENTICATION_FAILED);
732
- return new Error(`${methodName} failed: ${error.message || 'Unknown error'}`);
733
- },
734
- });
735
-
736
- export default AISummary;
737
- ```
738
-
739
- ### 6.5 Usage Examples
740
-
741
- ```typescript
742
- // Step 1: Get call history (existing SDK API)
743
- const callHistory = await callHistoryInstance.getCallHistoryData(10, 50);
744
- const sessions = callHistory.data.userSessions;
745
-
746
- // Step 2: Find sessions with AI summaries
747
- const sessionWithSummary = sessions.find(
748
- (session) => session.extensionPayload?.callingContainerIds?.length > 0
749
- );
750
-
751
- if (!sessionWithSummary) {
752
- console.log('No AI summaries available');
753
- return;
754
- }
755
-
756
- // Step 3: Resolve the container (plugin flattens summaryData.data automatically)
757
- const containerId = sessionWithSummary.extensionPayload.callingContainerIds[0];
758
- const container = await webex.internal.aisummary.getContainer({ containerId });
759
-
760
- // Check if summary is available
761
- if (container.summaryData.status !== 'Active') {
762
- console.log('Summary is not yet ready');
763
- return;
764
- }
765
-
766
- // Step 4: Fetch all summary content (note + shortNote + actionItems) in one call
767
- const summary = await webex.internal.aisummary.getSummary({ containerInfo: container });
768
- console.log('Note:', summary.note);
769
- console.log('Short Note:', summary.shortNote);
770
- summary.actionItems.forEach((item, i) => {
771
- console.log(`Action Item ${i + 1}: ${item.aiGeneratedContent}`);
772
- });
773
-
774
- // Step 5: Get transcript URL (or fetch full transcript)
775
- const transcriptUrl = webex.internal.aisummary.getTranscriptUrl({ containerInfo: container });
776
- console.log('Transcript URL:', transcriptUrl);
777
-
778
- // Step 6: Fetch and decrypt full transcript
779
- const transcript = await webex.internal.aisummary.getTranscript({ containerInfo: container });
780
- transcript.snippets.forEach((snippet) => {
781
- console.log(`[${snippet.startTime}] ${snippet.speaker?.speakerName}: ${snippet.content}`);
782
- });
783
- ```
784
-
785
- ## 7. API Request/Response Details
786
-
787
- ### 7.1 Pragya Container Lookup
788
-
789
- **Request:**
790
- ```http
791
- GET /pragya/api/v1/containers/{containerId} HTTP/1.1
792
- Authorization: Bearer {user_access_token}
793
- Accept: application/json
794
- ```
795
-
796
- **Success Response (200 OK):**
797
-
798
- > The raw response nests URLs under `summaryData.data`. The plugin's `getContainer()` flattens this automatically.
799
-
800
- ```json
801
- {
802
- "id": "34125120-13b5-11f1-9b36-adb685725098",
803
- "objectType": "callingAIContainer",
804
- "memberships": {
805
- "items": [{ "id": "...", "roles": ["OWNER"], "objectType": "containerMembership" }]
806
- },
807
- "summaryData": {
808
- "extensionId": "...",
809
- "objectType": "extension",
810
- "extensionType": "callingAISummary",
811
- "data": {
812
- "id": "...",
813
- "objectType": "callingAISummary",
814
- "status": "Active",
815
- "summaryUrl": "https://aibridge-url/summaries/c635e870-...",
816
- "transcriptUrl": "https://aibridge-url/summaries/c635e870-.../transcripts",
817
- "summarizeAfterCall": true,
818
- "aclUrl": "https://acl-a.wbx2.com/...",
819
- "kmsResourceObjectUrl": "kms://kms-cisco.wbx2.com/resources/..."
820
- }
821
- },
822
- "encryptionKeyUrl": "kms://kms-cisco.wbx2.com/keys/897e4d2d-...",
823
- "kmsResourceObjectUrl": "kms://kms-cisco.wbx2.com/resources/f7316435-...",
824
- "aclUrl": "https://acl-a.wbx2.com/acl/api/v1/acls/78c4cd90-...",
825
- "forkSessionId": "123e4567-...",
826
- "callSessionId": "123e4567-...",
827
- "ownerUserId": "123e4567-...",
828
- "orgId": "123e4567-...",
829
- "start": "2023-10-01T12:00:00Z",
830
- "end": "2023-10-01T12:00:00Z"
831
- }
832
- ```
833
-
834
- **Error Responses:**
835
- - `401 Unauthorized` - Invalid or expired token
836
- - `403 Forbidden` - User not authorized to access this container
837
- - `404 Not Found` - Container not found
838
-
839
- ### 7.2 Summary Content (fetched via summaryUrl with fields query)
840
-
841
- The primary way to fetch all summary content is via `getSummary()`, which appends `?fields=note,shortnote,actionitems` to the `summaryUrl`.
842
-
843
- **Request:**
844
- ```http
845
- GET {summaryData.summaryUrl}?fields=note,shortnote,actionitems HTTP/1.1
846
- Authorization: Bearer {user_access_token}
847
- Accept: application/json
848
- ```
849
-
850
- **Success Response (200 OK):**
851
- ```json
852
- {
853
- "id": "10293-dk93-ddie-odir-did932j3kdde",
854
- "keyUrl": "kms://kms-us-int.wbx2.com/keys/f19d4d28-...",
855
- "note": {
856
- "aiGeneratedContent": "<encrypted_note_content>"
857
- },
858
- "shortnote": {
859
- "aiGeneratedContent": "<encrypted_short_note_content>"
860
- },
861
- "actionitems": {
862
- "snippets": [
863
- {
864
- "id": "394r0087-...",
865
- "content": "edited version",
866
- "aiGeneratedContent": "<encrypted_ai_generated_content>"
867
- }
868
- ]
869
- },
870
- "links": [
871
- { "rel": "feedback", "href": "https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/..." }
872
- ]
873
- }
874
- ```
875
-
876
- ### 7.3 Notes (standalone, fetched via notesUrl)
877
-
878
- **Request:**
879
- ```http
880
- GET {summaryData.notesUrl} HTTP/1.1
881
- Authorization: Bearer {user_access_token}
882
- Accept: application/json
883
- ```
884
-
885
- **Success Response (200 OK):**
886
- ```json
887
- {
888
- "id": "10293-dk93-ddie-odir-did932j3kdde",
889
- "aiGeneratedContent": "<encrypted_content>",
890
- "feedbackUrl": "https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/report/...",
891
- "keyUrl": "kms://kms-us-int.wbx2.com/keys/f19d4d28-..."
892
- }
893
- ```
894
-
895
- ### 7.4 Action Items (standalone, fetched via actionItemsUrl)
896
-
897
- **Request:**
898
- ```http
899
- GET {summaryData.actionItemsUrl} HTTP/1.1
900
- Authorization: Bearer {user_access_token}
901
- Accept: application/json
902
- ```
903
-
904
- **Success Response (200 OK):**
905
- ```json
906
- [
907
- {
908
- "id": "1234-dk93-ddie-odir-dk93dj33",
909
- "keyUrl": "kms://kms-us-int.wbx2.com/keys/f19d4d28-...",
910
- "snippets": [
911
- {
912
- "id": "394r0087-...",
913
- "content": "edited version",
914
- "aiGeneratedContent": "<encrypted_ai_generated_content>"
915
- }
916
- ]
917
- }
918
- ]
919
- ```
920
-
921
- ## 8. Encryption & Decryption
922
-
923
- ### 8.1 Content Encryption
924
-
925
- All AI-generated content is encrypted using KMS (Key Management Service):
926
-
927
- - **Encryption Key**: The `encryptionKeyUrl` from the Pragya container response (format: `kms://kms-{region}.wbx2.com/keys/{key-id}`)
928
- - **Encrypted Fields**: `aiGeneratedContent` in notes and action item snippets
929
- - **Decryption**: Uses `@webex/internal-plugin-encryption` via `decryptText()`
930
-
931
- ### 8.2 Decryption Pattern
932
-
933
- The SDK uses the existing `@webex/internal-plugin-encryption` plugin:
934
-
935
- ```typescript
936
- // Decrypt using the encryptionKeyUrl from the Pragya container response
937
- const decryptedContent = await this.webex.internal.encryption.decryptText(
938
- containerInfo.encryptionKeyUrl,
939
- body.aiGeneratedContent
940
- );
941
- ```
942
-
943
- This is the same pattern used by existing plugins:
944
-
945
- **AI Assistant Plugin** (`internal-plugin-ai-assistant/src/utils.ts`):
946
- ```typescript
947
- const decryptedValue = await webex.internal.encryption.decryptText(
948
- encryptionKeyUrl,
949
- encryptedValue
950
- );
951
- ```
952
-
953
- **Task Plugin** (`internal-plugin-task/src/helpers/decrypt.helper.js`):
954
- ```javascript
955
- ctx.webex.internal.encryption.decryptText(key.uri || key, object[name])
956
- ```
957
-
958
- ## 9. Error Handling
959
-
960
- ### 9.1 Error Scenarios
961
-
962
- | Error Type | HTTP Status | SDK Error Message | Recovery Action |
963
- |------------|-------------|-------------------|-----------------|
964
- | Invalid Container ID | N/A (client) | "containerId is required and must be a non-empty string" | Validate input |
965
- | Invalid Container Info | N/A (client) | "containerInfo with valid summaryData and encryptionKeyUrl is required" | Ensure getContainer was called first |
966
- | Authentication Failed | 401 | "Authentication failed: Invalid or expired token" | Re-authenticate user |
967
- | Access Denied | 403 | "Access denied: User not authorized to view this summary" | Check user permissions |
968
- | Container Not Found | 404 | "Container not found" | Verify containerId from Janus |
969
- | Content Not Found | 404 (non-getContainer) | "Summary content not available or expired" | Content may have been deleted or expired |
970
- | Summary Not Ready | N/A | summaryData.status !== "Active" | Retry after delay |
971
-
972
- ## 10. Security Considerations
973
-
974
- ### 10.1 Authentication
975
- - All API calls (Pragya and content URLs) require a valid user bearer token
976
- - Token is automatically attached by the SDK's HTTP layer
977
-
978
- ### 10.2 Authorization
979
- - Only call participants or authorized users can access containers and summaries
980
- - Org-level AI features must be enabled
981
- - Per-call consent: AI assistant must have been enabled during the call
982
-
983
- ### 10.3 Content Protection
984
- - All AI-generated content is encrypted at rest with KMS
985
- - `encryptionKeyUrl` from Pragya container is the decryption key
986
- - HTTPS required for all API calls
987
-
988
- ## 11. Testing Strategy
989
-
990
- ### 11.1 Unit Tests
991
-
992
- ```typescript
993
- import {assert, expect} from '@webex/test-helper-chai';
994
- import MockWebex from '@webex/test-helper-mock-webex';
995
- import sinon from 'sinon';
996
- import AISummary from '@webex/internal-plugin-call-ai-summary';
997
- import config from '@webex/internal-plugin-call-ai-summary/src/config';
998
-
999
- describe('internal-plugin-call-ai-summary', () => {
1000
- let webex;
1001
-
1002
- beforeEach(() => {
1003
- webex = MockWebex({
1004
- children: {
1005
- aisummary: AISummary,
1006
- },
1007
- });
1008
- webex.config.aisummary = config.aisummary;
1009
- webex.internal.encryption = {
1010
- decryptText: sinon.stub().resolves('decrypted content'),
1011
- };
1012
- });
1013
-
1014
- describe('#getContainer', () => {
1015
- it('should resolve a Pragya container by ID', async () => {
1016
- const mockContainer = {
1017
- summaryData: {
1018
- status: 'Active',
1019
- summaryUrl: 'https://aibridge-url/summaries/abc123',
1020
- notesUrl: 'https://aibridge-url/summaries/abc123/notes',
1021
- actionItemsUrl: 'https://aibridge-url/summaries/abc123/action-items',
1022
- transcriptUrl: 'https://aibridge-url/summaries/abc123/transcripts',
1023
- summarizeAfterCall: true,
1024
- },
1025
- encryptionKeyUrl: 'kms://kms.url/keys/key-id',
1026
- };
1027
-
1028
- webex.request = sinon.stub().resolves({body: mockContainer});
1029
-
1030
- const result = await webex.internal.aisummary.getContainer({
1031
- containerId: 'container-123',
1032
- });
1033
-
1034
- expect(result.summaryData.status).to.equal('Active');
1035
- assert.calledWith(webex.request, sinon.match({
1036
- method: 'GET',
1037
- service: 'pragya',
1038
- resource: 'containers/container-123',
1039
- }));
1040
- });
1041
-
1042
- it('should throw for empty containerId', async () => {
1043
- await expect(
1044
- webex.internal.aisummary.getContainer({containerId: ''})
1045
- ).to.be.rejectedWith('containerId is required');
1046
- });
1047
- });
1048
-
1049
- describe('#getNotes', () => {
1050
- const mockContainerInfo = {
1051
- summaryData: {
1052
- notesUrl: 'https://aibridge-url/summaries/abc123/notes',
1053
- },
1054
- encryptionKeyUrl: 'kms://kms.url/keys/key-id',
1055
- };
1056
-
1057
- it('should fetch and decrypt notes', async () => {
1058
- webex.request = sinon.stub().resolves({
1059
- body: {
1060
- id: 'note-id',
1061
- aiGeneratedContent: 'encrypted-notes',
1062
- feedbackUrl: 'https://feedback.url',
1063
- },
1064
- });
1065
-
1066
- const result = await webex.internal.aisummary.getNotes({
1067
- containerInfo: mockContainerInfo,
1068
- });
1069
-
1070
- expect(result.id).to.equal('note-id');
1071
- expect(result.content).to.equal('decrypted content');
1072
- expect(result.feedbackUrl).to.equal('https://feedback.url');
1073
- assert.calledWith(
1074
- webex.internal.encryption.decryptText,
1075
- 'kms://kms.url/keys/key-id',
1076
- 'encrypted-notes'
1077
- );
1078
- });
1079
-
1080
- it('should throw when containerInfo is missing notesUrl', async () => {
1081
- await expect(
1082
- webex.internal.aisummary.getNotes({containerInfo: {summaryData: {}}})
1083
- ).to.be.rejectedWith('containerInfo with valid summaryData');
1084
- });
1085
- });
1086
-
1087
- describe('#getActionItems', () => {
1088
- const mockContainerInfo = {
1089
- summaryData: {
1090
- actionItemsUrl: 'https://aibridge-url/summaries/abc123/action-items',
1091
- },
1092
- encryptionKeyUrl: 'kms://kms.url/keys/key-id',
1093
- };
1094
-
1095
- it('should fetch and decrypt all action item snippets', async () => {
1096
- webex.request = sinon.stub().resolves({
1097
- body: [{
1098
- id: 'action-items-id',
1099
- keyUrl: 'kms://kms.url/keys/key-id',
1100
- snippets: [
1101
- {id: 's1', aiGeneratedContent: 'encrypted-1'},
1102
- {id: 's2', content: 'edited', aiGeneratedContent: 'encrypted-2'},
1103
- ],
1104
- }],
1105
- });
1106
-
1107
- webex.internal.encryption.decryptText
1108
- .onFirstCall().resolves('Decrypted item 1')
1109
- .onSecondCall().resolves('Decrypted item 2');
1110
-
1111
- const result = await webex.internal.aisummary.getActionItems({
1112
- containerInfo: mockContainerInfo,
1113
- });
1114
-
1115
- expect(result.snippets).to.have.lengthOf(2);
1116
- expect(result.snippets[0].aiGeneratedContent).to.equal('Decrypted item 1');
1117
- expect(result.snippets[1].aiGeneratedContent).to.equal('Decrypted item 2');
1118
- expect(result.snippets[1].editedContent).to.equal('edited');
1119
- });
1120
- });
1121
-
1122
- describe('#getTranscriptUrl', () => {
1123
- it('should return the transcript URL', () => {
1124
- const containerInfo = {
1125
- summaryData: {
1126
- transcriptUrl: 'https://aibridge-url/summaries/abc123/transcripts',
1127
- },
1128
- encryptionKeyUrl: 'kms://kms.url/keys/key-id',
1129
- };
1130
-
1131
- const url = webex.internal.aisummary.getTranscriptUrl({containerInfo});
1132
-
1133
- expect(url).to.equal('https://aibridge-url/summaries/abc123/transcripts');
1134
- });
1135
- });
1136
- });
1137
- ```
1138
-
1139
- ## 12. Modularity & Existing Code Impact
1140
-
1141
- ### 12.1 Zero Changes to Existing Packages
1142
-
1143
- This plugin is fully self-contained. It does **not** require modifications to any existing package:
1144
-
1145
- | Concern | Approach |
1146
- |---------|----------|
1147
- | `UserSession` type in `@webex/calling` | **Not modified.** The plugin accepts a plain `containerId: string`. Consumers extract it from the Janus response at the application layer. The `UserSession` type update is a separate, optional task for the calling package team. |
1148
- | `packages/webex` bundle | **Not modified.** Consumers import `@webex/internal-plugin-call-ai-summary` directly, which self-registers via `registerInternalPlugin()`. No changes to the webex package index are needed. |
1149
- | `@webex/internal-plugin-encryption` | **Not modified.** Used as a runtime dependency via `this.webex.internal.encryption.decryptText()`. |
1150
-
1151
- ### 12.2 Plugin Package Structure
1152
-
1153
- ```
1154
- packages/@webex/internal-plugin-call-ai-summary/
1155
- src/
1156
- index.ts # registerInternalPlugin('aisummary', ...)
1157
- ai-summary.ts # WebexPlugin.extend({...})
1158
- config.ts # { aisummary: {} }
1159
- constants.ts # Service name, error messages
1160
- types.ts # All TypeScript interfaces
1161
- test/
1162
- unit/
1163
- spec/
1164
- ai-summary.ts
1165
- data/
1166
- responses.ts # Mock Pragya and content responses
1167
- package.json
1168
- jest.config.js
1169
- babel.config.js
1170
- .eslintrc.js
1171
- ```
1172
-
1173
- ## 13. Dependencies
1174
-
1175
- ### 13.1 Internal Dependencies
1176
-
1177
- | Package | Purpose |
1178
- |---------|---------|
1179
- | `@webex/webex-core` | Plugin infrastructure (`WebexPlugin`, `registerInternalPlugin`) |
1180
- | `@webex/internal-plugin-encryption` | Content decryption via `decryptText()` |
1181
-
1182
- ### 13.2 External Service Dependencies
1183
-
1184
- | Service | Purpose | Discovery |
1185
- |---------|---------|-----------|
1186
- | **Janus** | Call history; provides `extensionPayload.callingContainerIds` | U2C: `serviceName: "janus"` |
1187
- | **Pragya** | Container metadata; provides content URLs and encryption key | U2C: `serviceName: "pragya"` |
1188
- | **Summary Content Endpoints** | Serve encrypted AI-generated content | Direct URLs from Pragya response |
1189
- | **KMS** | Encryption key management | Via `encryptionKeyUrl` from Pragya |