@webex/internal-plugin-call-ai-summary 3.12.0-next.6 → 3.12.0-next.60
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/.repo-context.json +381 -0
- package/.sdd/manifest.json +408 -0
- package/AGENTS.md +218 -0
- package/README.md +17 -12
- package/dist/ai-summary.js +1 -1
- package/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/manual-integration-test.js +3 -1
- package/dist/manual-integration-test.js.map +1 -1
- package/dist/manual-pragya-api-test.js +3 -1
- package/dist/manual-pragya-api-test.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md +115 -0
- package/docs/adr/index.md +26 -0
- package/docs/architecture.md +287 -0
- package/docs/getting-started.md +147 -0
- package/docs/index.md +53 -0
- package/docs/specs/README.md +83 -0
- package/package.json +6 -6
- package/schemas/repo-context.schema.json +796 -0
- package/src/docs/README.md +372 -0
- package/src/index.ts +1 -1
- package/src/manual-integration-test.js +3 -1
- package/src/manual-pragya-api-test.js +3 -1
- package/src/types.ts +12 -12
- package/test/unit/fixture/responses.ts +123 -0
- package/test/unit/spec/ai-summary.ts +388 -0
- package/ai-docs/AGENTS.md +0 -300
- package/ai-docs/ARCHITECTURE.md +0 -1189
package/ai-docs/ARCHITECTURE.md
DELETED
|
@@ -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 |
|