@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.
- 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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Module Spec
|
|
3
|
+
title: 'AI call summary plugin specification'
|
|
4
|
+
description: Responsibilities, boundaries, design, invariants, and verification for the AI call summary plugin.
|
|
5
|
+
tags: [module, specification]
|
|
6
|
+
---
|
|
7
|
+
<!-- sdd-generated-metadata
|
|
8
|
+
doc_kind: module-spec
|
|
9
|
+
generated_from: module-spec@0.3.0
|
|
10
|
+
generated_by: claude-code
|
|
11
|
+
approved_by: "@riag"
|
|
12
|
+
updated_at: 2026-09-23T14:03:22Z
|
|
13
|
+
validation_status: pass
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
# AI call summary plugin
|
|
17
|
+
|
|
18
|
+
This source-local document at `src/docs/README.md` owns the stable
|
|
19
|
+
specification for **the AI call summary plugin**. Ground every claim in repository evidence
|
|
20
|
+
and link to the
|
|
21
|
+
[repository architecture](../../docs/architecture.md)
|
|
22
|
+
instead of repeating broader facts.
|
|
23
|
+
|
|
24
|
+
Related context: [documentation index](../../docs/index.md) ·
|
|
25
|
+
[repository agent instructions](../../AGENTS.md)
|
|
26
|
+
|
|
27
|
+
## Metadata
|
|
28
|
+
|
|
29
|
+
| Field | Value |
|
|
30
|
+
| ------------- | ------------------------------------------------------------ |
|
|
31
|
+
| Owner | Webex JS SDK Team |
|
|
32
|
+
| Source path | `src/` |
|
|
33
|
+
| Resource kind | package |
|
|
34
|
+
| Status | Active |
|
|
35
|
+
| Last verified | 2026-09-23 at `bc61c78ba5` |
|
|
36
|
+
| Module id | `src/` |
|
|
37
|
+
| Parent spec | — |
|
|
38
|
+
| Doc kind | Module spec |
|
|
39
|
+
| Coverage score | 100% assessed 2026-09-23 — six of six public methods and ten of ten exported types specced; independent validation found no drift |
|
|
40
|
+
| Validation status | pass, validator codex, assessed 2026-09-23 |
|
|
41
|
+
|
|
42
|
+
## Applicability
|
|
43
|
+
|
|
44
|
+
| Condition ID | Status | Evidence or reason | Owned section |
|
|
45
|
+
| ------------------------------------ | ------ | ------------------ | ----------------------------- |
|
|
46
|
+
| `module.has_tiers` | N/A | The repository assigns no operational tiers to packages. `package.json` | Tier |
|
|
47
|
+
| `module.has_ui` | N/A | No UI surface; the module returns data only. `src/ai-summary.ts` | UI use-case flow |
|
|
48
|
+
| `module.crosses_service_boundaries` | Applicable | Calls the Pragya and AI Bridge services over HTTPS. `src/ai-summary.ts` | Cross-boundary use-case flow |
|
|
49
|
+
| `module.holds_client_state` | N/A | No state is retained between method calls. `src/ai-summary.ts` | Client state model |
|
|
50
|
+
| `module.enforces_domain_rules` | N/A | Validation is input checking, not domain-rule enforcement. `src/ai-summary.ts` | Business rules and invariants |
|
|
51
|
+
| `module.is_concurrent_async` | Applicable | Snippet decryption runs concurrently via Promise.all. `src/ai-summary.ts` | Concurrency and reactive flow |
|
|
52
|
+
| `module.owns_persistence` | N/A | The module persists nothing. `src/ai-summary.ts` | Data, schema, and migration |
|
|
53
|
+
| `module.stateful_transitions` | N/A | No state machine; each method is a single request-response. `src/ai-summary.ts` | State machine |
|
|
54
|
+
| `module.exposes_wire_protocol` | N/A | Consumes upstream HTTP; exposes no wire format of its own. `src/ai-summary.ts` | Protocol and wire format |
|
|
55
|
+
| `module.ui_multi_screen` | N/A | No UI surface. `src/ai-summary.ts` | UI flow |
|
|
56
|
+
| `module.large_data_model` | N/A | Ten interfaces, all flat DTOs. `src/types.ts` | Data model |
|
|
57
|
+
| `module.returns_caller_errors` | Applicable | The _handleError helper normalizes upstream failures into caller-facing Error messages. `src/ai-summary.ts` | Caller-visible failure modes |
|
|
58
|
+
| `module.module_specific_conventions` | N/A | Follows repository-wide plugin conventions only. `src/index.ts` | Module-specific rules |
|
|
59
|
+
| `module.published_package` | Applicable | Published to npm via the deploy:npm script. `package.json` | Export stability |
|
|
60
|
+
| `module.embedded_in_host` | Applicable | Registers into a host Webex SDK instance. `src/index.ts` | Host integration and theming |
|
|
61
|
+
| `module.has_design_tradeoff` | Applicable | Response flattening and single-request summary retrieval are deliberate trade-offs. `src/ai-summary.ts` | Key design trade-off |
|
|
62
|
+
| `module.has_submodules` | N/A | Computed has_submodules false; the module is a flat set of files with no child module owning its own spec. `src/ai-summary.ts` | Sub-modules |
|
|
63
|
+
|
|
64
|
+
## Evidence register
|
|
65
|
+
|
|
66
|
+
| Evidence | What it establishes |
|
|
67
|
+
| ---------------- | ------------------- |
|
|
68
|
+
| `src/ai-summary.ts` | The six public methods, their validation, request, decryption, and error-normalization behavior |
|
|
69
|
+
| `src/types.ts` | The ten exported request and response interfaces and their optionality |
|
|
70
|
+
| `src/index.ts` | Plugin registration as `aisummary` and the side-effect import of the encryption plugin |
|
|
71
|
+
| `src/constants.ts` | Service name, container resource path, and the exact caller-visible error message strings |
|
|
72
|
+
| `src/config.ts` | The plugin config namespace, currently empty |
|
|
73
|
+
| `package.json` | Published package identity, engines, dependencies, and the documented command set |
|
|
74
|
+
| `src/manual-pragya-api-test.js` | A manual script validating the Pragya container response structure |
|
|
75
|
+
| `src/manual-integration-test.js` | A manual script exercising the end-to-end flow against live services |
|
|
76
|
+
|
|
77
|
+
Prior source basis: repository design and agent-guidance documentation, reconciled under the
|
|
78
|
+
`reconcile` policy; three stale testing claims were corrected against the current tree and one
|
|
79
|
+
omitted method was restored from code.
|
|
80
|
+
|
|
81
|
+
## Purpose and boundary
|
|
82
|
+
|
|
83
|
+
- Responsibility: resolve a Pragya container for a call and return decrypted AI-generated summary, notes, action items, and transcript content.
|
|
84
|
+
- In scope: container resolution, content retrieval from Pragya-supplied URLs, KMS decryption of every content field, and normalization of upstream errors.
|
|
85
|
+
- Out of scope: starting or stopping the AI assistant during a call, generating or regenerating summaries, real-time in-call AI responses, recording storage or deletion, and feedback UI components.
|
|
86
|
+
- Consumers: Webex SDK applications reaching the module through `webex.internal.aisummary`.
|
|
87
|
+
|
|
88
|
+
This plugin provides methods to:
|
|
89
|
+
|
|
90
|
+
1. Resolve a **Pragya container** by ID (returns metadata, summary URLs, and encryption key)
|
|
91
|
+
2. Fetch and decrypt **AI-generated summaries** (note, short note, action items) in a single call
|
|
92
|
+
3. Fetch and decrypt **AI-generated notes** via a dedicated notes endpoint
|
|
93
|
+
4. Fetch and decrypt **AI-generated action items** via a dedicated action items endpoint
|
|
94
|
+
5. Retrieve the **transcript URL** for a call
|
|
95
|
+
6. Fetch and decrypt the full call **transcript**
|
|
96
|
+
|
|
97
|
+
This is an internal Cisco Webex plugin. As such, it does not strictly adhere to semantic versioning. Use at your own risk.
|
|
98
|
+
|
|
99
|
+
## Structure and key files
|
|
100
|
+
|
|
101
|
+
| File | Description |
|
|
102
|
+
| ---- | ----------- |
|
|
103
|
+
| `src/index.ts` | Entry point. Registers the plugin via `registerInternalPlugin('aisummary', ...)`. |
|
|
104
|
+
| `src/ai-summary.ts` | Main plugin class extending `WebexPlugin`. Contains all public and private methods. |
|
|
105
|
+
| `src/types.ts` | TypeScript interfaces for request/response DTOs. |
|
|
106
|
+
| `src/constants.ts` | Service name, resource path, and error message constants. |
|
|
107
|
+
| `src/config.ts` | Plugin configuration (currently empty). |
|
|
108
|
+
| `src/manual-pragya-api-test.js` | Manual script validating the Pragya container response structure. |
|
|
109
|
+
| `src/manual-integration-test.js` | Manual script exercising device registration through transcript fetch. |
|
|
110
|
+
|
|
111
|
+
Unit tests live at `test/unit/spec/ai-summary.ts` with upstream wire fixtures at `test/unit/fixture/responses.ts`. The two
|
|
112
|
+
manual scripts remain the only verification against live services.
|
|
113
|
+
|
|
114
|
+
## Public surface
|
|
115
|
+
|
|
116
|
+
All methods are accessible via `webex.internal.aisummary`. Exact declarations are authoritative in
|
|
117
|
+
the linked sources and are not restated here.
|
|
118
|
+
|
|
119
|
+
| Surface | Consumer | Compatibility commitment | Source |
|
|
120
|
+
| -------- | ---------- | ------------------------ | -------- |
|
|
121
|
+
| `getContainer` | Webex SDK consumers | Internal; no semantic-versioning guarantee | `src/ai-summary.ts` |
|
|
122
|
+
| `getSummary` | Webex SDK consumers | Internal; recommended entry point for summary content | `src/ai-summary.ts` |
|
|
123
|
+
| `getNotes` | Webex SDK consumers | Internal; depends on optional `notesUrl` | `src/ai-summary.ts` |
|
|
124
|
+
| `getActionItems` | Webex SDK consumers | Internal; depends on optional `actionItemsUrl` | `src/ai-summary.ts` |
|
|
125
|
+
| `getTranscriptUrl` | Webex SDK consumers | Internal; synchronous, returns a string | `src/ai-summary.ts` |
|
|
126
|
+
| `getTranscript` | Webex SDK consumers | Internal; returns decrypted transcript snippets | `src/ai-summary.ts` |
|
|
127
|
+
| Request and response types | TypeScript consumers | Internal; shipped as declarations | `src/types.ts` |
|
|
128
|
+
|
|
129
|
+
Request shapes, all issued through `this.webex.request` with the SDK auth interceptor supplying
|
|
130
|
+
`Authorization: Bearer {user_access_token}` and `Accept: application/json`:
|
|
131
|
+
|
|
132
|
+
- `getContainer` — `GET /pragya/api/v1/containers/{containerId} HTTP/1.1`, resolved through the service catalog as `service: 'pragya'`.
|
|
133
|
+
- `getSummary` — `GET {summaryData.summaryUrl}?fields=note,shortnote,actionitems HTTP/1.1`, as an absolute URI.
|
|
134
|
+
- `getNotes` — `GET {summaryData.notesUrl} HTTP/1.1`, as an absolute URI.
|
|
135
|
+
- `getActionItems` — `GET {summaryData.actionItemsUrl} HTTP/1.1`, as an absolute URI.
|
|
136
|
+
- `getTranscript` — `GET {summaryData.transcriptUrl} HTTP/1.1`, as an absolute URI.
|
|
137
|
+
|
|
138
|
+
Behavior of each method, in current-code terms:
|
|
139
|
+
|
|
140
|
+
- `getContainer` resolves a Pragya container by ID. Returns container metadata including summary URLs and the KMS encryption key URL. The raw response nests URLs under `summaryData.data`. The plugin's `getContainer()` flattens this automatically. `summaryData` contains summary URLs (`summaryUrl`, `transcriptUrl`, `status`, `summarizeAfterCall`); `encryptionKeyUrl` is the KMS key URL for decrypting content; and the response also carries `kmsResourceObjectUrl`, `aclUrl`, `forkSessionId`, `callSessionId`, `ownerUserId`, `orgId`, `start`, `end`. **Returns:** `Promise<PragyaContainerResponse>`
|
|
141
|
+
- `getSummary` fetches all AI-generated summary content (note, short note, and action items) from a single request to the summary URL, and decrypts each field. It issues `GET {summaryUrl}?fields=note,shortnote,actionitems`. It returns `id` (summary identifier), `note` (decrypted full note, HTML string), `shortNote` (decrypted short note, HTML string), `actionItems` (array of `ActionItemSnippet` objects), and `feedbackUrl` extracted from the `links` array (`rel: "feedback"`), if available. This is the **recommended** method for retrieving summary content.
|
|
142
|
+
- `getNotes` fetches AI-generated notes from the dedicated notes endpoint and decrypts via KMS. It issues `GET {notesUrl}` and requires `notesUrl` to be present in the container's `summaryData`.
|
|
143
|
+
- `getActionItems` fetches AI-generated action items from the dedicated action items endpoint and decrypts each snippet via KMS. It issues `GET {actionItemsUrl}` and requires `actionItemsUrl` to be present. When the response array is empty it returns `{id: undefined, snippets: []}`.
|
|
144
|
+
- `getTranscriptUrl` returns the transcript URL from the container info. Does not fetch or decrypt content. It is synchronous and returns a plain `string`.
|
|
145
|
+
- `getTranscript` fetches and decrypts the full call transcript, returning `id`, `totalCount`, and decrypted `snippets` each carrying `startTime`, `endTime`, `content`, `audioCSI`, and `speaker`.
|
|
146
|
+
|
|
147
|
+
Private helpers `_validateContainerId`, `_validateContainerInfo`, `_decryptContent`, and
|
|
148
|
+
`_handleError` are not part of the public surface.
|
|
149
|
+
|
|
150
|
+
## Dependencies
|
|
151
|
+
|
|
152
|
+
| Dependency | Why it is required | Failure behavior |
|
|
153
|
+
| --------------------- | ------------------ | ----------------------------------- |
|
|
154
|
+
| `@webex/webex-core` | Base plugin class, request handling, auth interceptor | Request rejections are normalized by `_handleError` |
|
|
155
|
+
| `@webex/internal-plugin-encryption` | KMS decryption | Decryption rejection propagates to the caller through `_handleError` |
|
|
156
|
+
| Pragya service | Container metadata; provides content URLs and encryption key | 401/403/404 mapped to normalized error messages |
|
|
157
|
+
| AI Bridge content endpoints | Serve encrypted AI-generated content | 404 becomes `Summary content not available or expired` |
|
|
158
|
+
| KMS | Encryption key management | Key fetch failure rejects the calling method |
|
|
159
|
+
|
|
160
|
+
The Pragya and AI Bridge APIs require a valid Webex access token. The SDK's auth interceptor automatically attaches the token for URLs in the service catalog and for the absolute content URLs returned by Pragya.
|
|
161
|
+
|
|
162
|
+
## Requirements
|
|
163
|
+
|
|
164
|
+
| ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
|
|
165
|
+
| --------- | ---------------------------------------------- | ------------------------------ | --------------- | ----------------------------------- | ------------------- | --------------------------------- |
|
|
166
|
+
| `MOD-001` | `getContainer` flattens `summaryData.data` onto `summaryData` before returning | Consumers access `summaryData.summaryUrl` directly without knowing the upstream nesting | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
167
|
+
| `MOD-002` | Every content field is decrypted before it is returned to the caller | Callers must never receive JWE ciphertext | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
168
|
+
| `MOD-003` | A per-response `keyUrl` takes precedence over `containerInfo.encryptionKeyUrl` | Content responses may be sealed under a different key than the container | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
169
|
+
| `MOD-004` | Invalid input throws synchronously before any request is issued | Callers get immediate, actionable validation errors | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
170
|
+
| `MOD-005` | Upstream 401, 403, and 404 responses are normalized to fixed caller-facing messages | Callers branch on stable messages rather than transport details | `src/constants.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
171
|
+
| `MOD-006` | `getTranscriptUrl` is synchronous and performs no network or decryption work | Callers can obtain the URL for downstream processing without cost | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
172
|
+
| `MOD-007` | The plugin self-registers as `aisummary` on import with zero changes to other packages | Consumers import the package directly; no bundle edit is required | `src/index.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
|
|
173
|
+
|
|
174
|
+
## Design overview
|
|
175
|
+
|
|
176
|
+
The module is a single `WebexPlugin.extend` object with six public methods, four private helpers, and
|
|
177
|
+
no retained state. Each public method follows the same three-step shape: validate the caller's input,
|
|
178
|
+
issue exactly one HTTP request through `this.webex.request`, then decrypt every content field before
|
|
179
|
+
returning a typed DTO. Configuration is an empty `aisummary` namespace, so behavior is fully
|
|
180
|
+
determined by the container passed in by the caller.
|
|
181
|
+
|
|
182
|
+
Two upstream services are involved and the split matters. Pragya owns container metadata and is the
|
|
183
|
+
source of truth for both the content URLs and the encryption key. Because Pragya returns
|
|
184
|
+
fully-qualified, region-correct URLs, the module performs no separate service discovery for the
|
|
185
|
+
content endpoints — it fetches whatever absolute URL the container supplies. Only the container
|
|
186
|
+
lookup itself resolves through the service catalog, as `service: 'pragya'`.
|
|
187
|
+
|
|
188
|
+
The module deliberately owns all of its types, constants, and logic rather than extending shared SDK
|
|
189
|
+
types, which keeps it self-contained; the rationale and its costs are recorded in
|
|
190
|
+
[ADR-0001](../../docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md).
|
|
191
|
+
|
|
192
|
+
The complete implementation — the per-method validate/request/decrypt bodies, the
|
|
193
|
+
`summaryData.data` flattening, the `Promise.all` snippet decryption and the `keyUrl` fallback — is
|
|
194
|
+
authoritative in `src/ai-summary.ts` and is linked rather than restated here, so this specification
|
|
195
|
+
does not become a second contract surface that drifts from the code.
|
|
196
|
+
|
|
197
|
+
## Data flow and sequence coverage
|
|
198
|
+
|
|
199
|
+
The transport is HTTPS via `this.webex.request`, once per public method.
|
|
200
|
+
|
|
201
|
+
| Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
|
|
202
|
+
| --------------- | ----------------- | ------------------- | ----------------------------- |
|
|
203
|
+
| Container resolution | `getContainer({containerId})` → flattened `PragyaContainerResponse` | `src/ai-summary.ts` | Empty id throws; 401/403/404 normalized |
|
|
204
|
+
| Single-request summary | `getSummary({containerInfo})` → decrypted note, short note, action items | `src/ai-summary.ts` | Missing `summaryUrl` or key throws; 404 normalized |
|
|
205
|
+
| Standalone notes | `getNotes({containerInfo})` → decrypted note content | `src/ai-summary.ts` | Missing `notesUrl` throws before any request |
|
|
206
|
+
| Standalone action items | `getActionItems({containerInfo})` → decrypted snippets | `src/ai-summary.ts` | Empty array returns `{id: undefined, snippets: []}` |
|
|
207
|
+
| Transcript URL | `getTranscriptUrl({containerInfo})` → URL string | `src/ai-summary.ts` | Missing `transcriptUrl` throws; no network call |
|
|
208
|
+
| Transcript content | `getTranscript({containerInfo})` → decrypted snippets | `src/ai-summary.ts` | Missing `transcriptUrl` throws; 404 normalized |
|
|
209
|
+
|
|
210
|
+
```mermaid
|
|
211
|
+
flowchart LR
|
|
212
|
+
Caller[Caller] --> Validate[Validate options]
|
|
213
|
+
Validate -->|invalid| Throw[Throw validation Error]
|
|
214
|
+
Validate -->|valid| Request[webex.request]
|
|
215
|
+
Request -->|error| Normalize[_handleError]
|
|
216
|
+
Request -->|body| Decrypt[_decryptContent per field]
|
|
217
|
+
Decrypt --> Result[Typed DTO]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Each operation group keeps its own sequence rather than being collapsed into the shape above.
|
|
221
|
+
|
|
222
|
+
**Get container info flow.** Client calls `webex.internal.aisummary.getContainer({ containerId })`;
|
|
223
|
+
Validate containerId (non-empty string); `webex.request` with `method: 'GET'`, `service: 'pragya'`,
|
|
224
|
+
`resource: containers/${containerId}`; Flatten: if body.summaryData.data exists, set
|
|
225
|
+
body.summaryData = body.summaryData.data; Return PragyaContainerResponse (with flat summaryData).
|
|
226
|
+
|
|
227
|
+
**Get notes flow.** Client calls `webex.internal.aisummary.getNotes(containerInfo)`; Validate
|
|
228
|
+
containerInfo has summaryData.notesUrl and encryptionKeyUrl; `webex.request` with `method: 'GET'`,
|
|
229
|
+
`uri: containerInfo.summaryData.notesUrl`; Response: `{ id, aiGeneratedContent: "<encrypted>",
|
|
230
|
+
feedbackUrl?, keyUrl }`; Decrypt aiGeneratedContent using containerInfo.encryptionKeyUrl; Return
|
|
231
|
+
decrypted SummaryNotes.
|
|
232
|
+
|
|
233
|
+
**Get action items flow.** Client calls `webex.internal.aisummary.getActionItems(containerInfo)`;
|
|
234
|
+
Validate containerInfo has summaryData.actionItemsUrl and encryptionKeyUrl; `webex.request` with
|
|
235
|
+
`method: 'GET'`, `uri: containerInfo.summaryData.actionItemsUrl`; Response: `[{ id, keyUrl,
|
|
236
|
+
snippets: [{ id, content, aiGeneratedContent }] }]`; Decrypt all aiGeneratedContent fields using
|
|
237
|
+
containerInfo.encryptionKeyUrl; Return decrypted SummaryActionItems.
|
|
238
|
+
|
|
239
|
+
## Class and component relationships
|
|
240
|
+
|
|
241
|
+
```mermaid
|
|
242
|
+
classDiagram
|
|
243
|
+
class AISummary {
|
|
244
|
+
+getContainer()
|
|
245
|
+
+getSummary()
|
|
246
|
+
+getNotes()
|
|
247
|
+
+getActionItems()
|
|
248
|
+
+getTranscriptUrl()
|
|
249
|
+
+getTranscript()
|
|
250
|
+
-_validateContainerId()
|
|
251
|
+
-_validateContainerInfo()
|
|
252
|
+
-_decryptContent()
|
|
253
|
+
-_handleError()
|
|
254
|
+
}
|
|
255
|
+
WebexPlugin <|-- AISummary
|
|
256
|
+
AISummary --> EncryptionPlugin : decryptText
|
|
257
|
+
AISummary --> WebexRequest : request
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The module is registered onto the host SDK by `registerInternalPlugin('aisummary', AISummary, {config})` in `src/index.ts`, which also imports `@webex/internal-plugin-encryption` for its side effects and re-exports the plugin as the package default. Importing the package is what performs that registration; the exact entry-point declarations are authoritative in `src/index.ts`.
|
|
261
|
+
|
|
262
|
+
## Use cases and flows
|
|
263
|
+
|
|
264
|
+
| Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
|
|
265
|
+
| -------- | --------------- | ----------------------------- | ---------------------------- | ------------------------- |
|
|
266
|
+
| `UC-001` | SDK consumer | Read `extensionPayload.callingContainerIds` from call history, call `getContainer`, then `getSummary`; receive decrypted note, short note, and action items | Any upstream 401/403/404 surfaces as a normalized `Error` | `src/ai-summary.ts` |
|
|
267
|
+
| `UC-002` | SDK consumer | Call `getNotes` for note content only | Throws when `notesUrl` is absent from the container | `src/ai-summary.ts` |
|
|
268
|
+
| `UC-003` | SDK consumer | Call `getActionItems` for action items only | Returns an empty snippet list when the response array is empty | `src/ai-summary.ts` |
|
|
269
|
+
| `UC-004` | SDK consumer | Call `getTranscriptUrl` to hand the URL to downstream processing | Synchronous throw when `transcriptUrl` is absent | `src/ai-summary.ts` |
|
|
270
|
+
| `UC-005` | SDK consumer | Call `getTranscript` to obtain timed, decrypted transcript snippets | Throws when `transcriptUrl` or the key is absent | `src/ai-summary.ts` |
|
|
271
|
+
|
|
272
|
+
### Cross-boundary use-case flow
|
|
273
|
+
|
|
274
|
+
Every use case crosses a network boundary twice: once to Pragya through the service catalog, and once
|
|
275
|
+
to an AI Bridge URL supplied by that container. Ordering is strict — a container must be resolved
|
|
276
|
+
before any content call, because the content URLs and the encryption key both come from it. There is
|
|
277
|
+
no retry, timeout, or circuit-breaking logic in this module; those are inherited from the SDK HTTP
|
|
278
|
+
layer. Compatibility is loose by design: `notesUrl` and `actionItemsUrl` are optional, so the module
|
|
279
|
+
must tolerate their absence rather than assume a fixed upstream version.
|
|
280
|
+
|
|
281
|
+
Decryption depends on a prerequisite chain that the SDK satisfies automatically: a registered device (`webex.internal.device.register()`), a Mercury WebSocket connection (initiated automatically during KMS key fetch), an ECDHE key exchange with KMS, and key retrieval from KMS using the `encryptionKeyUrl`. The SDK handles these steps automatically when `decryptText` is called.
|
|
282
|
+
|
|
283
|
+
## Concurrency and reactive flow
|
|
284
|
+
|
|
285
|
+
- Execution model: promise-based async methods on a stateless plugin object; no workers, timers, or subscriptions.
|
|
286
|
+
- Ordering guarantees: container resolution must precede content retrieval; within a method, decryption follows the single request.
|
|
287
|
+
- Idempotency and retry: every method is a read and is therefore naturally idempotent; the module itself performs no retries.
|
|
288
|
+
- Shared-state protection: none required — the module holds no mutable state between calls.
|
|
289
|
+
- Blocking restrictions: snippet collections are decrypted concurrently with `Promise.all` rather than sequentially, so a large transcript does not serialize KMS round-trips.
|
|
290
|
+
|
|
291
|
+
## Caller-visible failure modes
|
|
292
|
+
|
|
293
|
+
The plugin normalizes HTTP errors into descriptive messages. Validation errors are thrown synchronously, before any request is issued.
|
|
294
|
+
|
|
295
|
+
| Error Type | HTTP Status | SDK Error Message | Recovery Action |
|
|
296
|
+
| ---------- | ----------- | ----------------- | --------------- |
|
|
297
|
+
| Invalid Container ID | N/A (client) | "containerId is required and must be a non-empty string" | Validate input |
|
|
298
|
+
| Invalid Container Info | N/A (client) | "containerInfo with valid summaryData and encryptionKeyUrl is required" | Ensure getContainer was called first |
|
|
299
|
+
| Authentication Failed | 401 | "Authentication failed: Invalid or expired token" | Re-authenticate user |
|
|
300
|
+
| Access Denied | 403 | "Access denied: User not authorized to view this summary" | Check user permissions |
|
|
301
|
+
| Container Not Found | 404 | "Container not found" | Verify containerId from Janus |
|
|
302
|
+
| Content Not Found | 404 (non-getContainer) | "Summary content not available or expired" | Content may have been deleted or expired |
|
|
303
|
+
| Summary Not Ready | N/A | summaryData.status !== "Active" | Retry after delay |
|
|
304
|
+
| Unmapped failure | Any other | "{methodName} failed: {error.message}" | Depends on the underlying cause |
|
|
305
|
+
|
|
306
|
+
The exact message strings are declared in `src/constants.ts` and the status mapping in
|
|
307
|
+
`_handleError` in `src/ai-summary.ts`; callers branch on these strings, so changing one is a
|
|
308
|
+
breaking change.
|
|
309
|
+
|
|
310
|
+
## Pitfalls and constraints
|
|
311
|
+
|
|
312
|
+
- The raw Pragya response nests URLs under `summaryData.data`; `getContainer` flattens it automatically, so code that re-reads `summaryData.data` after calling `getContainer` will find nothing there.
|
|
313
|
+
- `notesUrl` and `actionItemsUrl` may not be present in all API versions. Prefer `getSummary()`, which returns notes, short notes, and action items in one request.
|
|
314
|
+
- A per-response `keyUrl` overrides `containerInfo.encryptionKeyUrl`; code that decrypts with the container key alone can fail on content sealed under a different key.
|
|
315
|
+
- `getTranscriptUrl` is synchronous and returns a `string`, unlike every other public method — awaiting it yields the string, but treating it as a promise-returning API is a mistake.
|
|
316
|
+
- Summary availability is not guaranteed: `summaryData.status` must be `Active`, org-level AI features must be enabled, and the AI assistant must have been enabled during the call.
|
|
317
|
+
- Decryption requires a registered device and a working Mercury connection; calling the module on an unregistered SDK instance fails inside KMS key retrieval rather than at validation.
|
|
318
|
+
|
|
319
|
+
## Export stability
|
|
320
|
+
|
|
321
|
+
| Export or entry point | Consumer | Stability | Versioning and deprecation rule | Declaration or API report |
|
|
322
|
+
| --------------------- | ---------- | --------- | ------------------------------- | ------------------------- |
|
|
323
|
+
| default export `AISummary` | Webex SDK consumers | Internal | Does not strictly adhere to semantic versioning | `src/index.ts` |
|
|
324
|
+
| `webex.internal.aisummary` namespace | Webex SDK consumers | Internal | Registered on import; renaming is a breaking change | `src/index.ts` |
|
|
325
|
+
| Request and response interfaces | TypeScript consumers | Internal | Shipped as declarations built to `dist/` | `src/types.ts` |
|
|
326
|
+
|
|
327
|
+
## Host integration and theming
|
|
328
|
+
|
|
329
|
+
- Mount or entry contract: importing `@webex/internal-plugin-call-ai-summary` calls `registerInternalPlugin('aisummary', ...)`, making the module reachable at `webex.internal.aisummary`.
|
|
330
|
+
- Required providers, peers, or host versions: an authenticated Webex SDK instance with a registered device, plus `@webex/internal-plugin-encryption`, which the entry point imports for its side effects.
|
|
331
|
+
- Theme and design-token contract: N/A — the module renders nothing.
|
|
332
|
+
- Accessibility and lifecycle obligations: N/A — no UI surface; note that `note` and `shortNote` are HTML strings, so consumers that render them own their own sanitization and accessibility.
|
|
333
|
+
|
|
334
|
+
## Key design trade-off
|
|
335
|
+
|
|
336
|
+
| Chosen trade-off | Preserved invariant or benefit | Cost or limitation | Decision evidence |
|
|
337
|
+
| ---------------- | ------------------------------ | ------------------ | ----------------- |
|
|
338
|
+
| Flatten `summaryData.data` onto `summaryData` inside `getContainer` | Consumers access `summaryData.summaryUrl` directly and are insulated from the upstream nesting | The returned object no longer matches the raw Pragya wire body, so wire-level debugging must account for the transform | `docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md` |
|
|
339
|
+
| Prefer one `getSummary` request over three standalone calls | One round trip returns note, short note, and action items, and tolerates optional URLs being absent | Callers wanting only one content type still pay for the combined response | `docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md` |
|
|
340
|
+
| Self-contained plugin owning its own types and constants | Zero changes to existing packages; the plugin can ship independently | Type definitions overlap conceptually with neighbouring plugins and must be maintained in parallel | `src/types.ts` |
|
|
341
|
+
| No separate service discovery for content URLs | Region correctness comes from Pragya for free | The module trusts absolute URLs supplied by an upstream response | `src/ai-summary.ts` |
|
|
342
|
+
|
|
343
|
+
## Verification
|
|
344
|
+
|
|
345
|
+
| Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
|
|
346
|
+
| ------------------------ | ----------------------------- | ----------------- | ----------------------------- | ---------------------- |
|
|
347
|
+
| `MOD-001` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (already-flat body left untouched) | none |
|
|
348
|
+
| `MOD-002` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (decryption failure propagates) | none |
|
|
349
|
+
| `MOD-003` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (falls back when the response keyUrl is absent) | none |
|
|
350
|
+
| `MOD-004` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (empty, whitespace, undefined, non-string) | none |
|
|
351
|
+
| `MOD-005` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (unmapped failure is method-prefixed) | none |
|
|
352
|
+
| `MOD-006` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (throws when the transcriptUrl field is absent) | none |
|
|
353
|
+
| `MOD-007` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (all six methods present on a real WebexCore instance) | none |
|
|
354
|
+
|
|
355
|
+
The module has an automated unit suite at `test/unit/spec/ai-summary.ts`: **38 tests**, run with `yarn test:unit`
|
|
356
|
+
(`webex-legacy-tools test --unit --runner jest`). Upstream wire fixtures live in
|
|
357
|
+
`test/unit/fixture/responses.ts`, which is the repository's only record of the externally owned
|
|
358
|
+
Pragya and AI Bridge response shapes.
|
|
359
|
+
|
|
360
|
+
All six public methods are covered, along with the `summaryData.data` flattening transform, the
|
|
361
|
+
`keyUrl` precedence rule, every validation branch, and each normalized error mapping. Two manual
|
|
362
|
+
scripts under `src/` remain the only end-to-end verification against live services, and both require
|
|
363
|
+
a token and container ID.
|
|
364
|
+
|
|
365
|
+
Registration (`MOD-007`) is covered too: because `registerInternalPlugin` runs as an import side
|
|
366
|
+
effect, those three cases assert against the internal-core plugin registry. They deliberately do
|
|
367
|
+
**not** construct a live `WebexCore` — doing so boots the service catalog, which fires asynchronous
|
|
368
|
+
U2C requests that outlive the test and make the runner exit nonzero even though every assertion
|
|
369
|
+
passes. Every documented requirement now has unit evidence.
|
|
370
|
+
|
|
371
|
+
The suite requires only `engines.node >=16` and is verified on Node 24.10 as well as the monorepo's
|
|
372
|
+
pinned 22.14, so it can be run without `nvm`.
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* WEBEX_TOKEN='<token>' node src/manual-integration-test.js
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Standalone Node dev script: it loads the built package by name and uses CommonJS
|
|
16
|
+
// requires, so the module-graph rules below do not apply.
|
|
17
|
+
/* eslint-disable no-console, require-jsdoc, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
|
|
16
18
|
|
|
17
19
|
require('@webex/internal-plugin-call-ai-summary');
|
|
18
20
|
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* Or paste your token directly into WEBEX_TOKEN below.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Standalone Node dev script: it loads the built package by name and uses CommonJS
|
|
13
|
+
// requires, so the module-graph rules below do not apply.
|
|
14
|
+
/* eslint-disable no-console, require-jsdoc, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
|
|
13
15
|
|
|
14
16
|
require('@webex/internal-plugin-call-ai-summary');
|
|
15
17
|
|
package/src/types.ts
CHANGED
|
@@ -69,6 +69,18 @@ export interface GetSummaryContentOptions {
|
|
|
69
69
|
|
|
70
70
|
// --- Summary Response DTOs ---
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Single action item snippet.
|
|
74
|
+
*/
|
|
75
|
+
export interface ActionItemSnippet {
|
|
76
|
+
/** Unique identifier */
|
|
77
|
+
id: string;
|
|
78
|
+
/** User-edited version (if available) */
|
|
79
|
+
editedContent?: string;
|
|
80
|
+
/** Decrypted AI-generated content */
|
|
81
|
+
aiGeneratedContent: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
/**
|
|
73
85
|
* Decrypted AI-generated summary content.
|
|
74
86
|
* Contains all three content types returned by the summary API.
|
|
@@ -98,18 +110,6 @@ export interface SummaryNotes {
|
|
|
98
110
|
feedbackUrl?: string;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
/**
|
|
102
|
-
* Single action item snippet.
|
|
103
|
-
*/
|
|
104
|
-
export interface ActionItemSnippet {
|
|
105
|
-
/** Unique identifier */
|
|
106
|
-
id: string;
|
|
107
|
-
/** User-edited version (if available) */
|
|
108
|
-
editedContent?: string;
|
|
109
|
-
/** Decrypted AI-generated content */
|
|
110
|
-
aiGeneratedContent: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
113
|
/**
|
|
114
114
|
* Decrypted AI-generated action items.
|
|
115
115
|
*/
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mock Pragya and AI Bridge responses.
|
|
7
|
+
*
|
|
8
|
+
* These shapes are the upstream wire bodies as observed against the live Pragya
|
|
9
|
+
* and AI Bridge services. Both surfaces are externally owned, so this file is
|
|
10
|
+
* the repository's only record of them: `src/types.ts` declares only the subset
|
|
11
|
+
* the plugin consumes. Keep these fixtures faithful to the wire, not to the DTOs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Raw Pragya container response, with summary URLs still nested under `summaryData.data`. */
|
|
15
|
+
export const rawPragyaContainer = {
|
|
16
|
+
id: '34125120-13b5-11f1-9b36-adb685725098',
|
|
17
|
+
objectType: 'callingAIContainer',
|
|
18
|
+
summaryData: {
|
|
19
|
+
extensionId: 'extension-id',
|
|
20
|
+
objectType: 'extension',
|
|
21
|
+
extensionType: 'callingAISummary',
|
|
22
|
+
data: {
|
|
23
|
+
id: 'summary-id',
|
|
24
|
+
objectType: 'callingAISummary',
|
|
25
|
+
status: 'Active',
|
|
26
|
+
summaryUrl: 'https://aibridge-url/summaries/c635e870',
|
|
27
|
+
transcriptUrl: 'https://aibridge-url/summaries/c635e870/transcripts',
|
|
28
|
+
summarizeAfterCall: true,
|
|
29
|
+
aclUrl: 'https://acl-a.wbx2.com/acl/api/v1/acls/inner',
|
|
30
|
+
kmsResourceObjectUrl: 'kms://kms-cisco.wbx2.com/resources/inner',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
encryptionKeyUrl: 'kms://kms-cisco.wbx2.com/keys/897e4d2d',
|
|
34
|
+
kmsResourceObjectUrl: 'kms://kms-cisco.wbx2.com/resources/f7316435',
|
|
35
|
+
aclUrl: 'https://acl-a.wbx2.com/acl/api/v1/acls/78c4cd90',
|
|
36
|
+
forkSessionId: '123e4567-fork',
|
|
37
|
+
callSessionId: '123e4567-call',
|
|
38
|
+
ownerUserId: '123e4567-owner',
|
|
39
|
+
orgId: '123e4567-org',
|
|
40
|
+
start: '2023-10-01T12:00:00Z',
|
|
41
|
+
end: '2023-10-01T12:00:00Z',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A container as callers see it after `getContainer` flattens `summaryData.data`. */
|
|
45
|
+
export const flattenedContainer = {
|
|
46
|
+
summaryData: {
|
|
47
|
+
status: 'Active',
|
|
48
|
+
summaryUrl: 'https://aibridge-url/summaries/c635e870',
|
|
49
|
+
notesUrl: 'https://aibridge-url/summaries/c635e870/notes',
|
|
50
|
+
actionItemsUrl: 'https://aibridge-url/summaries/c635e870/action-items',
|
|
51
|
+
transcriptUrl: 'https://aibridge-url/summaries/c635e870/transcripts',
|
|
52
|
+
summarizeAfterCall: true,
|
|
53
|
+
},
|
|
54
|
+
encryptionKeyUrl: 'kms://kms-cisco.wbx2.com/keys/897e4d2d',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** AI Bridge response for `summaryUrl?fields=note,shortnote,actionitems`. */
|
|
58
|
+
export const summaryResponse = {
|
|
59
|
+
id: '10293-dk93-ddie-odir-did932j3kdde',
|
|
60
|
+
keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
|
|
61
|
+
note: {aiGeneratedContent: '<encrypted_note_content>'},
|
|
62
|
+
shortnote: {aiGeneratedContent: '<encrypted_short_note_content>'},
|
|
63
|
+
actionitems: {
|
|
64
|
+
snippets: [
|
|
65
|
+
{
|
|
66
|
+
id: '394r0087',
|
|
67
|
+
content: 'edited version',
|
|
68
|
+
aiGeneratedContent: '<encrypted_ai_generated_content>',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
links: [
|
|
73
|
+
{
|
|
74
|
+
rel: 'feedback',
|
|
75
|
+
href: 'https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/1',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** AI Bridge response for the standalone `notesUrl` endpoint. */
|
|
81
|
+
export const notesResponse = {
|
|
82
|
+
id: '10293-dk93-ddie-odir-did932j3kdde',
|
|
83
|
+
aiGeneratedContent: '<encrypted_content>',
|
|
84
|
+
feedbackUrl: 'https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/report/1',
|
|
85
|
+
keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** AI Bridge response for the standalone `actionItemsUrl` endpoint. Note the array wrapper. */
|
|
89
|
+
export const actionItemsResponse = [
|
|
90
|
+
{
|
|
91
|
+
id: '1234-dk93-ddie-odir-dk93dj33',
|
|
92
|
+
keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
|
|
93
|
+
snippets: [
|
|
94
|
+
{
|
|
95
|
+
id: '394r0087',
|
|
96
|
+
content: 'edited version',
|
|
97
|
+
aiGeneratedContent: '<encrypted_ai_generated_content>',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/** AI Bridge response for `transcriptUrl`. */
|
|
104
|
+
export const transcriptResponse = {
|
|
105
|
+
id: 'transcript-id',
|
|
106
|
+
totalCount: 2,
|
|
107
|
+
transcriptSnippetList: [
|
|
108
|
+
{
|
|
109
|
+
startTime: '1000',
|
|
110
|
+
endTime: '2000',
|
|
111
|
+
content: '<encrypted_snippet_1>',
|
|
112
|
+
audioCSI: 'csi-1',
|
|
113
|
+
speaker: {speakerName: 'Ada Lovelace', speakerId: 'speaker-1'},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
startTime: '2000',
|
|
117
|
+
endTime: '3000',
|
|
118
|
+
content: '<encrypted_snippet_2>',
|
|
119
|
+
audioCSI: 'csi-2',
|
|
120
|
+
speaker: {speakerName: 'Alan Turing', speakerId: 'speaker-2'},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|