-
Notifications
You must be signed in to change notification settings - Fork 377
fix(ivr-transcript): implement-ivr-transcript #4448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughAdds IVR transcript support across the contact-center sample and plugin-cc Task service. The sample UI (HTML/JS) introduces controls to fetch and display IVR transcripts, with state management and rendering logic. The Task service exposes a new fetchIvrTranscript(orgId, interactionId, timeoutMins) API, backed by a new IvrTranscriptService that retrieves transcript metadata and conversations via Webex requests. New IVR transcript types are defined. Metrics constants gain success/failure events, and Task records corresponding metrics. Unit tests cover success, error, empty, and multi-transcript cases, including service mocking and metrics assertions. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Nitpick comments (11)
packages/@webex/plugin-cc/src/services/task/types.ts (1)
970-1069
: IVR transcript type model looks sound; minor typing nitOverall structure is clear and matches the service shape. Consider avoiding
any
for parameters to improve type safety.Apply this small tightening:
- /** Parameters extracted from the conversation */ - parameters?: Record<string, any>; + /** Parameters extracted from the conversation */ + parameters?: Record<string, unknown>;docs/samples/contact-center/index.html (1)
248-261
: Minor accessibility improvements for IVR Transcript section
- Make the action button explicit type to avoid accidental form submits.
- Add an accessible label for the timeout input.
- Announce status updates to assistive tech.
- <button id="fetch-ivr-transcript" class="btn--blue" disabled>Fetch IVR Transcript</button> - <input id="timeout-mins" placeholder="Timeout (mins)" value="5" type="number" min="1" max="60" style="width: 120px; margin-left: 10px;"> + <button id="fetch-ivr-transcript" type="button" class="btn--blue" disabled>Fetch IVR Transcript</button> + <input id="timeout-mins" aria-label="Timeout in minutes" placeholder="Timeout (mins)" value="5" type="number" min="1" max="60" style="width: 120px; margin-left: 10px;"> ... - <p id="ivr-transcript-status" class="status-par">No transcript fetched</p> + <p id="ivr-transcript-status" class="status-par" aria-live="polite">No transcript fetched</p>packages/@webex/plugin-cc/src/services/task/index.ts (2)
169-190
: Avoidany
cast to accesswebex
on WebexRequestAccessing
(webexRequest as any).webex
is fragile and bypasses type safety. Prefer exposing a typed accessor (e.g.,WebexRequest.getInstance().getWebex()
), or typing the instance to a known interface.If you want, I can propose a small change in WebexRequest to expose a
getWebex()
method and update this call site accordingly.
169-190
: Parameterize service base URL in IvrTranscriptService (follow-up)The service currently hardcodes the mediastorage base URL and region (see IvrTranscriptService.getIvrTranscriptMetadata in the related file). Consider sourcing this from configuration/service-catalog to support multi-region and non-prod environments.
Would you like a quick proposal for config-driven base URL selection for the IVR transcript service?
packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts (3)
26-26
: Unused import of METRICS_CONSTANTSIt looks unused in this file; you’re already using METRIC_EVENT_NAMES. Consider removing to avoid lint warnings.
-import * as METRICS_CONSTANTS from '../../../../../src/metrics/constants';
1491-1498
: Unnecessary global.fetch mockThe IVR flow uses webex.request through IvrTranscriptService, not global fetch. This mock isn’t used and can be removed.
- // Mock global fetch - global.fetch = jest.fn();
1609-1643
: Test name vs behavior mismatchYou’re asserting a success path that returns [] but the test title says “handle error when fetching transcript content from S3”. The behavior is fine (service continues on per-transcript error and returns aggregated results), but the title should reflect it returns an empty array and records success.
Consider renaming the test to: “should return empty array and still record success metric when transcript content fetch yields no data”.
docs/samples/contact-center/app.js (2)
1845-1860
: Duplicate state refresh callshandleTaskSelect calls updateIvrTranscriptButtonState twice; one call is sufficient.
currentTask = task - console.log('Updating IVR button state for selected task...'); - updateIvrTranscriptButtonState(); // Update IVR button state when task is selected @@ updateCallControlUI(task); // Enable/disable transfer controls - updateIvrTranscriptButtonState(); // Update IVR button state when task is selected + updateIvrTranscriptButtonState(); // Update IVR button state when task is selected
1243-1249
: Event wiring is fine; consider null-guardIf the sample HTML is edited, fetchIvrTranscriptBtn could be null. A defensive guard would avoid a runtime error.
-console.log('Setting up IVR transcript fetch button event listener...'); -fetchIvrTranscriptBtn.addEventListener('click', fetchIvrTranscript); +console.log('Setting up IVR transcript fetch button event listener...'); +if (fetchIvrTranscriptBtn) { + fetchIvrTranscriptBtn.addEventListener('click', fetchIvrTranscript); +}packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts (2)
55-55
: Hard-coded media storage base URLEmbedding region-specific hostnames (produs1) can cause breakage in other regions/environments. Prefer deriving this from the Webex SDK service catalog/config, or a configurable base.
I can propose a small helper to read from webex.config.services if available and fall back to a default. Want me to draft that?
256-285
: Sequential per-transcript fetch can be slow for many transcriptsYou’re matching “agent desktop logic,” but if latency becomes an issue, consider parallelizing with Promise.allSettled and preserving order when concatenating.
Here’s a sketch while preserving per-item error tolerance:
const results = await Promise.allSettled( transcriptMetaDataList.map((t) => this.fetchIvrConversation(t.transcriptPath).then((conv) => this.parseConversations(conv, t.botName)) ) ); let transcriptConversations: IvrTranscriptResponse = []; results.forEach((r, i) => { if (r.status === 'fulfilled') transcriptConversations = transcriptConversations.concat(r.value); else LoggerProxy.warn(`Failed to process transcript ${transcriptMetaDataList[i].transcriptId}: ${r.reason}`, { module: 'IvrTranscriptService', method: 'fetchIVRTranscript', interactionId }); });
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
docs/samples/contact-center/app.js
(7 hunks)docs/samples/contact-center/index.html
(1 hunks)packages/@webex/plugin-cc/src/metrics/constants.ts
(2 hunks)packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts
(1 hunks)packages/@webex/plugin-cc/src/services/task/constants.ts
(1 hunks)packages/@webex/plugin-cc/src/services/task/index.ts
(5 hunks)packages/@webex/plugin-cc/src/services/task/types.ts
(2 hunks)packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts
(7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
docs/samples/contact-center/app.js (2)
packages/@webex/plugin-cc/src/services/task/index.ts (1)
fetchIvrTranscript
(1081-1159)packages/@webex/plugin-cc/src/logger-proxy.ts (1)
error
(85-89)
packages/@webex/plugin-cc/src/services/task/index.ts (4)
packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts (1)
IvrTranscriptService
(10-306)packages/@webex/plugin-cc/src/services/task/types.ts (1)
IvrTranscriptResponse
(1068-1068)packages/@webex/plugin-cc/src/metrics/MetricsManager.ts (1)
MetricsManager
(38-512)packages/@webex/plugin-cc/src/services/core/Utils.ts (1)
getErrorDetails
(77-116)
packages/@webex/plugin-cc/src/services/task/types.ts (1)
docs/labs/contact-center/task-manager.js (1)
interactionId
(288-288)
packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts (4)
docs/samples/contact-center/app.js (2)
webex
(2-2)transcript
(662-662)docs/labs/contact-center/task-manager.js (1)
interactionId
(288-288)packages/@webex/plugin-cc/src/services/task/types.ts (3)
IvrTranscriptMetaDataResponse
(993-1002)IvrTranscriptResponse
(1068-1068)IvrTranscriptData
(1059-1062)packages/@webex/plugin-cc/src/logger-proxy.ts (1)
LoggerProxy
(9-110)
packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts (3)
packages/@webex/plugin-cc/src/services/task/index.ts (1)
Task
(132-1522)packages/@webex/plugin-cc/src/services/task/types.ts (2)
MEDIA_CHANNEL
(69-84)MEDIA_CHANNEL
(91-91)packages/@webex/plugin-cc/src/metrics/constants.ts (2)
METRIC_EVENT_NAMES
(70-129)METRIC_EVENT_NAMES
(142-142)
🔇 Additional comments (18)
packages/@webex/plugin-cc/src/services/task/constants.ts (1)
35-35
: LGTM: New Task method identifier added correctlyFETCH_IVR_TRANSCRIPT aligns with the new Task API and matches usage in Task.fetchIvrTranscript.
packages/@webex/plugin-cc/src/metrics/constants.ts (2)
54-56
: JSDoc extended for IVR transcript metrics — looks goodThe added documentation entries match the new constants and follow existing style.
115-118
: LGTM: New IVR transcript metric names addedNames are consistent with existing naming conventions and are used by Task.fetchIvrTranscript.
packages/@webex/plugin-cc/src/services/task/index.ts (1)
9-11
: Imports and type surface extension look goodIvrTranscriptService, WebexRequest, and IvrTranscriptResponse are wired correctly for the new API.
Also applies to: 27-27
packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts (8)
10-10
: Good addition: test harness now imports the new IvrTranscriptServiceThis aligns the tests with the new Task.fetchIvrTranscript path.
21-21
: MEDIA_CHANNEL import is appropriateConsistent with the type surface used in IVR-related tests.
32-40
: Solid mocking of IvrTranscriptServiceMocking the default export to a simple object with fetchIVRTranscript keeps test setup clean and focused.
104-110
: Good: WebexRequest mock includes webexTask.getIvrTranscriptService validates WebexRequest.getInstance().webex; exposing it on the mock avoids false negatives.
159-159
: Constructor usage updatedPassing { wrapUpProps: null } matches the new Task constructor signature and avoids unintended AutoWrapup initialization.
1500-1562
: Happy path IVR test looks goodOverriding getIvrTranscriptService() and asserting conversationTurns + metrics matches the implementation.
1564-1583
: Validation test for missing orgId is appropriateCovers the Task-side guard and failure metric.
1679-1738
: Happy path for multiple transcripts looks correctOverrides service, validates output content and success metric with conversationTurns: 4.
docs/samples/contact-center/app.js (3)
82-91
: UI element wiring is fineInitialization logging helps diagnose missing DOM nodes in the sample app.
744-761
: IVR button state refresh on task:end is goodKeeps UI controls consistent when the task completes.
1478-1487
: Enable IVR button post-acceptThis hook is in the right place; aligns with the “accepted + telephony” gating.
packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts (3)
99-139
: Conversation fetch logic is reasonableGraceful parsing handles both {conversation: [...] } and raw arrays; logs and errors are appropriate.
152-176
: Parameter flattening matches intent; note array overwrite behaviorIf arrays contain multiple primitives, the same key will be overwritten (last value wins). Confirm this mirrors agent desktop behavior.
Do we expect multi-valued arrays to be joined (e.g., comma-separated) instead of overwritten? If so, we can adjust getFlatParams accordingly.
296-304
: Error handling is consistentLogs via LoggerProxy and rethrow preserve context for Task.getErrorDetails.
docs/samples/contact-center/app.js
Outdated
// Function to update IVR transcript button state | ||
function updateIvrTranscriptButtonState() { | ||
console.log('Updating IVR transcript button state...'); | ||
|
||
if (!currentTask) { | ||
console.log('IVR Button State: No active task found'); | ||
fetchIvrTranscriptBtn.disabled = true; | ||
ivrStatusElm.textContent = 'No active task'; | ||
return; | ||
} | ||
console.log('IVR current task ', currentTask); | ||
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`); | ||
|
||
if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') { | ||
console.log('IVR Button State: Enabling button - task accepted and telephony channel'); | ||
fetchIvrTranscriptBtn.disabled = false; | ||
ivrStatusElm.textContent = 'Ready to fetch IVR transcript'; | ||
} else if (currentTask.data.interaction.state !== 'connected') { | ||
console.log('IVR Button State: Disabling button - task not accepted'); | ||
fetchIvrTranscriptBtn.disabled = true; | ||
ivrStatusElm.textContent = 'Task must be accepted first'; | ||
} else if (currentTask.data.mediaType !== 'telephony') { | ||
console.log('IVR Button State: Disabling button - not telephony channel'); | ||
fetchIvrTranscriptBtn.disabled = true; | ||
ivrStatusElm.textContent = 'IVR transcript only available for telephony'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: using currentTask.data.mediaType (should be currentTask.data.interaction.mediaType)
Task data stores media type under data.interaction.mediaType throughout the sample. Using data.mediaType will mis-detect channel and disable the button incorrectly.
Apply this diff:
- console.log('IVR current task ', currentTask);
- console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`);
+ console.log('IVR current task ', currentTask);
+ console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.interaction.mediaType}`);
- if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') {
+ if (currentTask.data.interaction.state === 'connected' && currentTask.data.interaction.mediaType === 'telephony') {
console.log('IVR Button State: Enabling button - task accepted and telephony channel');
fetchIvrTranscriptBtn.disabled = false;
ivrStatusElm.textContent = 'Ready to fetch IVR transcript';
} else if (currentTask.data.interaction.state !== 'connected') {
console.log('IVR Button State: Disabling button - task not accepted');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'Task must be accepted first';
- } else if (currentTask.data.mediaType !== 'telephony') {
+ } else if (currentTask.data.interaction.mediaType !== 'telephony') {
console.log('IVR Button State: Disabling button - not telephony channel');
fetchIvrTranscriptBtn.disabled = true;
ivrStatusElm.textContent = 'IVR transcript only available for telephony';
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Function to update IVR transcript button state | |
function updateIvrTranscriptButtonState() { | |
console.log('Updating IVR transcript button state...'); | |
if (!currentTask) { | |
console.log('IVR Button State: No active task found'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'No active task'; | |
return; | |
} | |
console.log('IVR current task ', currentTask); | |
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.mediaType}`); | |
if (currentTask.data.interaction.state === 'connected' && currentTask.data.mediaType === 'telephony') { | |
console.log('IVR Button State: Enabling button - task accepted and telephony channel'); | |
fetchIvrTranscriptBtn.disabled = false; | |
ivrStatusElm.textContent = 'Ready to fetch IVR transcript'; | |
} else if (currentTask.data.interaction.state !== 'connected') { | |
console.log('IVR Button State: Disabling button - task not accepted'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'Task must be accepted first'; | |
} else if (currentTask.data.mediaType !== 'telephony') { | |
console.log('IVR Button State: Disabling button - not telephony channel'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'IVR transcript only available for telephony'; | |
} | |
} | |
// Function to update IVR transcript button state | |
function updateIvrTranscriptButtonState() { | |
console.log('Updating IVR transcript button state...'); | |
if (!currentTask) { | |
console.log('IVR Button State: No active task found'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'No active task'; | |
return; | |
} | |
console.log('IVR current task ', currentTask); | |
console.log(`IVR Button State: Task status: ${currentTask.data.interaction.state}, Media channel: ${currentTask.data.interaction.mediaType}`); | |
if (currentTask.data.interaction.state === 'connected' && currentTask.data.interaction.mediaType === 'telephony') { | |
console.log('IVR Button State: Enabling button - task accepted and telephony channel'); | |
fetchIvrTranscriptBtn.disabled = false; | |
ivrStatusElm.textContent = 'Ready to fetch IVR transcript'; | |
} else if (currentTask.data.interaction.state !== 'connected') { | |
console.log('IVR Button State: Disabling button - task not accepted'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'Task must be accepted first'; | |
} else if (currentTask.data.interaction.mediaType !== 'telephony') { | |
console.log('IVR Button State: Disabling button - not telephony channel'); | |
fetchIvrTranscriptBtn.disabled = true; | |
ivrStatusElm.textContent = 'IVR transcript only available for telephony'; | |
} | |
} |
🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 597 to 623, the code
incorrectly reads currentTask.data.mediaType (which is not where media type is
stored) causing wrong button state; change all uses to
currentTask.data.interaction.mediaType (including the console.log that reports
media channel and the conditional that checks for 'telephony') so the
media-channel detection and enabling/disabling logic use
currentTask.data.interaction.mediaType consistently.
docs/samples/contact-center/app.js
Outdated
// Function to fetch IVR transcript | ||
async function fetchIvrTranscript() { | ||
console.log('=== Starting IVR Transcript Fetch ==='); | ||
|
||
// Check if task is accepted and media type is telephony | ||
if (!currentTask) { | ||
console.error('IVR Fetch: No active task found'); | ||
ivrStatusElm.textContent = 'No active task found'; | ||
return; | ||
} | ||
|
||
console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`); | ||
|
||
if (currentTask.data.interaction.state !== 'connected') { | ||
console.warn('IVR Fetch: Task is not accepted'); | ||
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript'; | ||
return; | ||
} | ||
|
||
if (currentTask.data.mediaType !== 'telephony') { | ||
console.warn('IVR Fetch: Media channel is not telephony'); | ||
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks'; | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: mediaType path again
Same issue in fetchIvrTranscript preconditions; fix to reference interaction.mediaType.
- console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`);
+ console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.interaction.mediaType}`);
@@
- if (currentTask.data.mediaType !== 'telephony') {
+ if (currentTask.data.interaction.mediaType !== 'telephony') {
console.warn('IVR Fetch: Media channel is not telephony');
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks';
return;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Function to fetch IVR transcript | |
async function fetchIvrTranscript() { | |
console.log('=== Starting IVR Transcript Fetch ==='); | |
// Check if task is accepted and media type is telephony | |
if (!currentTask) { | |
console.error('IVR Fetch: No active task found'); | |
ivrStatusElm.textContent = 'No active task found'; | |
return; | |
} | |
console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.mediaType}`); | |
if (currentTask.data.interaction.state !== 'connected') { | |
console.warn('IVR Fetch: Task is not accepted'); | |
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript'; | |
return; | |
} | |
if (currentTask.data.mediaType !== 'telephony') { | |
console.warn('IVR Fetch: Media channel is not telephony'); | |
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks'; | |
return; | |
// Function to fetch IVR transcript | |
async function fetchIvrTranscript() { | |
console.log('=== Starting IVR Transcript Fetch ==='); | |
// Check if task is accepted and media type is telephony | |
if (!currentTask) { | |
console.error('IVR Fetch: No active task found'); | |
ivrStatusElm.textContent = 'No active task found'; | |
return; | |
} | |
console.log(`IVR Fetch: Current task status: ${currentTask.data.interaction.state}, media channel: ${currentTask.data.interaction.mediaType}`); | |
if (currentTask.data.interaction.state !== 'connected') { | |
console.warn('IVR Fetch: Task is not accepted'); | |
ivrStatusElm.textContent = 'Task must be accepted to fetch IVR transcript'; | |
return; | |
} | |
if (currentTask.data.interaction.mediaType !== 'telephony') { | |
console.warn('IVR Fetch: Media channel is not telephony'); | |
ivrStatusElm.textContent = 'IVR transcript is only available for telephony tasks'; | |
return; | |
} | |
// …rest of function… | |
} |
🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 625 to 647, the precondition
checks for media type are incorrectly reading currentTask.data.mediaType instead
of the mediaType nested under interaction; update the code to reference
currentTask.data.interaction.mediaType everywhere in this function (including
the console.log and the if-check that validates 'telephony') so the
media-channel checks use interaction.mediaType consistently and
ivrStatus/messages remain accurate.
docs/samples/contact-center/app.js
Outdated
// Display the conversations | ||
let html = ''; | ||
transcript.forEach((conversation, index) => { | ||
console.log(`IVR Fetch: Processing conversation ${index + 1}:`, conversation); | ||
html += ` | ||
<div style="margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px;"> | ||
<h4>Conversation ${index + 1}</h4> | ||
<p><strong>Call ID:</strong> ${conversation.callId || 'N/A'}</p> | ||
<p><strong>Duration:</strong> ${conversation.duration || 'N/A'}</p> | ||
<p><strong>Start Time:</strong> ${conversation.startTime || 'N/A'}</p> | ||
<p><strong>End Time:</strong> ${conversation.endTime || 'N/A'}</p> | ||
${conversation.segments && conversation.segments.length > 0 ? ` | ||
<div style="margin-top: 10px;"> | ||
<strong>Segments:</strong> | ||
<ul style="margin-top: 5px;"> | ||
${conversation.segments.map(segment => ` | ||
<li style="margin-bottom: 5px;"> | ||
<strong>${segment.speaker || 'Unknown'}:</strong> ${segment.text || 'N/A'} | ||
<br><small>Duration: ${segment.duration || 'N/A'}, Offset: ${segment.offset || 'N/A'}</small> | ||
</li> | ||
`).join('')} | ||
</ul> | ||
</div> | ||
` : '<p><em>No segments available</em></p>'} | ||
</div> | ||
`; | ||
}); | ||
|
||
ivrResultsContentElm.innerHTML = html; | ||
console.log('IVR Fetch: UI updated with conversation data'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XSS risk: unescaped user text inserted via innerHTML
Transcript fields (e.g., customer query) originate from user speech-to-text and must be treated as untrusted. Direct innerHTML interpolation is vulnerable to XSS. Escape values or assign via textContent/DOM nodes.
Apply minimal escaping in this block:
- <p><strong>Call ID:</strong> ${conversation.callId || 'N/A'}</p>
- <p><strong>Duration:</strong> ${conversation.duration || 'N/A'}</p>
- <p><strong>Start Time:</strong> ${conversation.startTime || 'N/A'}</p>
- <p><strong>End Time:</strong> ${conversation.endTime || 'N/A'}</p>
+ <p><strong>Call ID:</strong> ${escapeHtml(conversation.callId || 'N/A')}</p>
+ <p><strong>Duration:</strong> ${escapeHtml(conversation.duration || 'N/A')}</p>
+ <p><strong>Start Time:</strong> ${escapeHtml(conversation.startTime || 'N/A')}</p>
+ <p><strong>End Time:</strong> ${escapeHtml(conversation.endTime || 'N/A')}</p>
@@
- ${conversation.segments.map(segment => `
+ ${conversation.segments.map(segment => `
<li style="margin-bottom: 5px;">
- <strong>${segment.speaker || 'Unknown'}:</strong> ${segment.text || 'N/A'}
- <br><small>Duration: ${segment.duration || 'N/A'}, Offset: ${segment.offset || 'N/A'}</small>
+ <strong>${escapeHtml(segment.speaker || 'Unknown')}:</strong> ${escapeHtml(segment.text || 'N/A')}
+ <br><small>Duration: ${escapeHtml(segment.duration || 'N/A')}, Offset: ${escapeHtml(segment.offset || 'N/A')}</small>
</li>
`).join('')}
Place this helper near the top of the file (or before usage):
// Escapes HTML special characters to prevent XSS in sample UI
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Note: The service returns IvrConversations as an array of turns ({customer?}/{bot?}). Consider rendering those turns directly in the UI instead of assuming conversation.segments/callId/duration, which may be undefined.
🤖 Prompt for AI Agents
In docs/samples/contact-center/app.js around lines 676-705, the code injects
transcript data directly into innerHTML leading to XSS risk; add an escapeHtml
helper near the top of the file (before usage) that replaces &, <, >, ", ' with
their HTML entities, then update this block to avoid raw innerHTML interpolation
by either building DOM nodes and setting textContent for all user-derived fields
(callId, duration, startTime, endTime, segment.speaker, segment.text, etc.) or,
if continuing to build a string, pass every user-derived value through
escapeHtml before inserting it; also handle missing fields safely and prefer
rendering turns (customer/bot) if available instead of assuming
segments/callId/duration exist.
* task.data.interactionId, | ||
* 5 | ||
* ); | ||
* console.log('IVR transcript fetched successfully'); | ||
* | ||
* // Process conversation data | ||
* transcript.forEach((turn, index) => { | ||
* console.log(`Turn ${index + 1}:`); | ||
* if (turn.customer) { | ||
* console.log(` Customer: ${turn.customer.query}`); | ||
* } | ||
* if (turn.bot) { | ||
* console.log(` Bot: ${turn.bot.reply} (Bot: ${turn.bot.botName})`); | ||
* } | ||
* }); | ||
* } catch (error) { | ||
* console.error('Failed to fetch IVR transcript:', error); | ||
* } | ||
* }); | ||
* ``` | ||
*/ | ||
public async fetchIvrTranscript( | ||
orgId: string, | ||
interactionId: string, | ||
timeoutMins: number | ||
): Promise<IvrTranscriptResponse> { | ||
try { | ||
LoggerProxy.info(`Fetching IVR transcript using direct IvrTranscriptService`, { | ||
module: TASK_FILE, | ||
method: METHODS.FETCH_IVR_TRANSCRIPT, | ||
interactionId, | ||
}); | ||
|
||
// Start metrics tracking | ||
this.metricsManager.timeEvent([ | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
]); | ||
|
||
// Validate required fields | ||
if (!orgId || !interactionId) { | ||
throw new Error('Organization ID or Interaction ID is missing'); | ||
} | ||
|
||
// Use IvrTranscriptService directly (matching agent desktop fetchIVRTranscript) | ||
const transcriptConversations = await this.getIvrTranscriptService().fetchIVRTranscript( | ||
orgId, | ||
interactionId, | ||
timeoutMins | ||
); | ||
|
||
// Track success metrics | ||
this.metricsManager.trackEvent( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | ||
{ | ||
taskId: interactionId, | ||
orgId, | ||
type: 'direct-service', | ||
conversationTurns: transcriptConversations.length, | ||
...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data), | ||
}, | ||
['operational', 'behavioral', 'business'] | ||
); | ||
|
||
LoggerProxy.log('IVR transcript fetched successfully using direct service', { | ||
module: TASK_FILE, | ||
method: METHODS.FETCH_IVR_TRANSCRIPT, | ||
interactionId, | ||
}); | ||
|
||
return transcriptConversations; | ||
} catch (error) { | ||
const {error: detailedError} = getErrorDetails( | ||
error, | ||
METHODS.FETCH_IVR_TRANSCRIPT, | ||
TASK_FILE | ||
); | ||
|
||
// Track failure metrics | ||
this.metricsManager.trackEvent( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
{ | ||
taskId: interactionId, | ||
orgId, | ||
type: 'direct-service', | ||
error: detailedError.message, | ||
...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data), | ||
}, | ||
['operational', 'behavioral', 'business'] | ||
); | ||
|
||
LoggerProxy.error(`IVR transcript fetch failed: ${detailedError.message}`, { | ||
module: TASK_FILE, | ||
method: METHODS.FETCH_IVR_TRANSCRIPT, | ||
interactionId, | ||
}); | ||
|
||
throw detailedError; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add media-type guard and fix failure metrics payload mapping
- Add a precondition: IVR transcripts should only be fetched for telephony tasks to avoid unnecessary network calls and clearer error messaging.
- For failure metrics, use
getCommonTrackingFieldForAQMResponseFailed(...)
to align with the established pattern elsewhere in Task.
public async fetchIvrTranscript(
orgId: string,
interactionId: string,
timeoutMins: number
): Promise<IvrTranscriptResponse> {
try {
@@
// Validate required fields
if (!orgId || !interactionId) {
throw new Error('Organization ID or Interaction ID is missing');
}
+ // Enforce voice-only usage for IVR transcript
+ if (this.data?.interaction?.mediaType !== MEDIA_CHANNEL.TELEPHONY) {
+ throw new Error('IVR transcript is only available for voice (telephony) tasks');
+ }
+
// Use IvrTranscriptService directly (matching agent desktop fetchIVRTranscript)
const transcriptConversations = await this.getIvrTranscriptService().fetchIVRTranscript(
orgId,
interactionId,
timeoutMins
);
@@
// Track failure metrics
this.metricsManager.trackEvent(
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED,
{
taskId: interactionId,
orgId,
type: 'direct-service',
error: detailedError.message,
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data),
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed((error as any)?.details || {}),
},
['operational', 'behavioral', 'business']
);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
/** | |
* Fetches the IVR transcript for the specified task. | |
* This method retrieves the Interactive Voice Response transcript that was recorded | |
* during the customer's interaction with the IVR system before being connected to an agent. | |
* Available for tasks that have IVR interactions. | |
* | |
* @param orgId - Organization ID (required) | |
* @param interactionId - Interaction ID for the task (required) | |
* @param timeoutMins - Timeout in minutes for the transcript URL (required) | |
* @returns Promise<IvrTranscriptResponse> The IVR transcript conversation data | |
* @throws Error if no IVR transcript is available, or the fetch operation fails | |
* @example | |
* ```typescript | |
* // Fetch IVR transcript after accepting a task | |
* task.on(TASK_EVENTS.TASK_ASSIGNED, async () => { | |
* try { | |
* const transcript = await task.fetchIvrTranscript( | |
* task.data.orgId, | |
* task.data.interactionId, | |
* 5 | |
* ); | |
* console.log('IVR transcript fetched successfully'); | |
* | |
* // Process conversation data | |
* transcript.forEach((turn, index) => { | |
* console.log(`Turn ${index + 1}:`); | |
* if (turn.customer) { | |
* console.log(` Customer: ${turn.customer.query}`); | |
* } | |
* if (turn.bot) { | |
* console.log(` Bot: ${turn.bot.reply} (Bot: ${turn.bot.botName})`); | |
* } | |
* }); | |
* } catch (error) { | |
* console.error('Failed to fetch IVR transcript:', error); | |
* } | |
* }); | |
* ``` | |
*/ | |
public async fetchIvrTranscript( | |
orgId: string, | |
interactionId: string, | |
timeoutMins: number | |
): Promise<IvrTranscriptResponse> { | |
try { | |
LoggerProxy.info(`Fetching IVR transcript using direct IvrTranscriptService`, { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
// Start metrics tracking | |
this.metricsManager.timeEvent([ | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
]); | |
// Validate required fields | |
if (!orgId || !interactionId) { | |
throw new Error('Organization ID or Interaction ID is missing'); | |
} | |
// Use IvrTranscriptService directly (matching agent desktop fetchIVRTranscript) | |
const transcriptConversations = await this.getIvrTranscriptService().fetchIVRTranscript( | |
orgId, | |
interactionId, | |
timeoutMins | |
); | |
// Track success metrics | |
this.metricsManager.trackEvent( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | |
{ | |
taskId: interactionId, | |
orgId, | |
type: 'direct-service', | |
conversationTurns: transcriptConversations.length, | |
...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data), | |
}, | |
['operational', 'behavioral', 'business'] | |
); | |
LoggerProxy.log('IVR transcript fetched successfully using direct service', { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
return transcriptConversations; | |
} catch (error) { | |
const {error: detailedError} = getErrorDetails( | |
error, | |
METHODS.FETCH_IVR_TRANSCRIPT, | |
TASK_FILE | |
); | |
// Track failure metrics | |
this.metricsManager.trackEvent( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
{ | |
taskId: interactionId, | |
orgId, | |
type: 'direct-service', | |
error: detailedError.message, | |
...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data), | |
}, | |
['operational', 'behavioral', 'business'] | |
); | |
LoggerProxy.error(`IVR transcript fetch failed: ${detailedError.message}`, { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
throw detailedError; | |
} | |
} | |
public async fetchIvrTranscript( | |
orgId: string, | |
interactionId: string, | |
timeoutMins: number | |
): Promise<IvrTranscriptResponse> { | |
try { | |
LoggerProxy.info(`Fetching IVR transcript using direct IvrTranscriptService`, { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
// Start metrics tracking | |
this.metricsManager.timeEvent([ | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
]); | |
// Validate required fields | |
if (!orgId || !interactionId) { | |
throw new Error('Organization ID or Interaction ID is missing'); | |
} | |
// Enforce voice-only usage for IVR transcript | |
if (this.data?.interaction?.mediaType !== MEDIA_CHANNEL.TELEPHONY) { | |
throw new Error('IVR transcript is only available for voice (telephony) tasks'); | |
} | |
// Use IvrTranscriptService directly (matching agent desktop fetchIVRTranscript) | |
const transcriptConversations = await this.getIvrTranscriptService().fetchIVRTranscript( | |
orgId, | |
interactionId, | |
timeoutMins | |
); | |
// Track success metrics | |
this.metricsManager.trackEvent( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | |
{ | |
taskId: interactionId, | |
orgId, | |
type: 'direct-service', | |
conversationTurns: transcriptConversations.length, | |
...MetricsManager.getCommonTrackingFieldForAQMResponse(this.data), | |
}, | |
['operational', 'behavioral', 'business'] | |
); | |
LoggerProxy.log('IVR transcript fetched successfully using direct service', { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
return transcriptConversations; | |
} catch (error) { | |
const {error: detailedError} = getErrorDetails( | |
error, | |
METHODS.FETCH_IVR_TRANSCRIPT, | |
TASK_FILE | |
); | |
// Track failure metrics | |
this.metricsManager.trackEvent( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
{ | |
taskId: interactionId, | |
orgId, | |
type: 'direct-service', | |
error: detailedError.message, | |
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed((error as any)?.details || {}), | |
}, | |
['operational', 'behavioral', 'business'] | |
); | |
LoggerProxy.error(`IVR transcript fetch failed: ${detailedError.message}`, { | |
module: TASK_FILE, | |
method: METHODS.FETCH_IVR_TRANSCRIPT, | |
interactionId, | |
}); | |
throw detailedError; | |
} | |
} |
🤖 Prompt for AI Agents
In packages/@webex/plugin-cc/src/services/task/index.ts around lines 1042 to
1159, add a precondition to only attempt fetching IVR transcripts for telephony
tasks: check this.data.mediaType (or equivalent media-type field) and throw a
descriptive Error if the task is not telephony to avoid unnecessary network
calls; then, in the catch block replace
MetricsManager.getCommonTrackingFieldForAQMResponse(this.data) with
MetricsManager.getCommonTrackingFieldForAQMResponseFailed(this.data) when
building the failure metrics payload so it follows the established failure
pattern.
uri: `https://mediastorage.produs1.ciscoccservice.com/media/organization/${orgId}/interaction/${interactionId}/ivrtranscript?timeOutMins=${timeOutMins}`, | ||
method: HTTP_METHODS.GET, | ||
headers: { | ||
'cisco-no-http-redirect': null, | ||
'spark-user-agent': null, | ||
Authorization: await this.webex.credentials.getUserToken(), | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Headers: avoid null header values; verify Authorization format
- Setting headers to null will send invalid header values on some transports. Remove those keys instead of setting null.
- Authorization: confirm what webex.credentials.getUserToken() returns (raw token vs “Bearer ”). If interceptors already attach Authorization, you can omit it entirely.
Apply this safer header construction:
- const requestPayload: WebexRequestPayload = {
+ const requestPayload: WebexRequestPayload = {
uri: `https://mediastorage.produs1.ciscoccservice.com/media/organization/${orgId}/interaction/${interactionId}/ivrtranscript?timeOutMins=${timeOutMins}`,
method: HTTP_METHODS.GET,
- headers: {
- 'cisco-no-http-redirect': null,
- 'spark-user-agent': null,
- Authorization: await this.webex.credentials.getUserToken(),
- },
+ headers: {
+ // Prefer relying on Webex interceptors; if needed, ensure this is 'Bearer <token>'
+ Authorization: await this.webex.credentials.getUserToken(),
+ },
};
Additionally, please verify the token format:
🌐 Web query:
In the Webex JS SDK, does webex.credentials.getUserToken() return a string already prefixed with 'Bearer ', or should callers prepend 'Bearer ' to the returned access token when setting the Authorization header?
💡 Result:
The webex.credentials.getUserToken()
method returns the raw access token string only—it does not include the 'Bearer '
prefix[3][9]. When setting the Authorization header for Webex REST API calls, callers must prepend 'Bearer '
to the returned access token[2][4].
Essential context and supporting details:
- Webex REST API requests require the access token to be formatted as
Authorization: Bearer <ACCESS_TOKEN>
[2][4]. - SDK documentation and sample applications demonstrate initializing with
{access_token: "<YOUR-ACCESS-TOKEN-HERE>"}
but always show API usage with the explicit'Bearer '
prefix when constructing HTTP headers[3][9]. - The prefix
'Bearer '
is required by OAuth 2.0 specifications to distinguish bearer tokens from other authentication schemes[4]. - There is no documentation or usage pattern indicating that the SDK's access token is returned with the prefix, nor do sample codes show using the token directly in the header without manual prefixing[3][9].
Example:
const token = await webex.credentials.getUserToken();
const headers = {
Authorization: `Bearer ${token}`
};
Relevant information:
- This pattern aligns with both official Webex API documentation and general OAuth practices[2][4].
- Personal access tokens or tokens returned by the SDK should be treated securely and only presented in HTTP headers as specified[2].
Summary Table:
SDK Method | Token Format Returned | Header Usage Required |
---|---|---|
webex.credentials.getUserToken() |
Access token (no prefix) | Authorization: Bearer <TOKEN> |
Conclusion:
Always manually prepend 'Bearer '
to the raw token returned from webex.credentials.getUserToken()
when setting the Authorization header for Webex REST API calls[2][4][3][9].
Citations:
- 1: http://web-sdk.webex.com/api/
- 2: https://developer.webex.com/docs/getting-your-personal-access-token
- 3: https://developer.webex.com/blog/how-to-receive-real-time-meeting-transcription-with-the-webex-javascript-sdk
- 4: https://developer.webex.com/docs/api/guides/integrations-and-authorization
- 5: https://github.com/WebexSamples/webex-meetings-quick-start
- 6: https://developer.webex.com/docs/login-with-webex
- 7: https://developer.webex.com/docs/sdks/browser
- 8: https://developer-usgov.webex.com/docs/sdks/browser
- 9: https://developer.webex.com/docs/browser-sdk-messaging-tutorial
- 10: https://developer.webex.com/blog/using-websockets-with-the-webex-javascript-sdk
Avoid null header values and prepend “Bearer ” to the Authorization token
- Remove the
cisco-no-http-redirect
andspark-user-agent
keys rather than setting them tonull
. webex.credentials.getUserToken()
returns only the raw token; you must prepend"Bearer "
when constructing the header.- If Core interceptors already inject these headers (and Authorization), you can drop the entire
headers
block—otherwise explicitly set only what’s needed.
Apply this patch:
const requestPayload: WebexRequestPayload = {
uri: `https://mediastorage.produs1.ciscoccservice.com/media/organization/${orgId}/interaction/${interactionId}/ivrtranscript?timeOutMins=${timeOutMins}`,
method: HTTP_METHODS.GET,
- headers: {
- 'cisco-no-http-redirect': null,
- 'spark-user-agent': null,
- Authorization: await this.webex.credentials.getUserToken(),
- },
+ headers: {
+ Authorization: `Bearer ${await this.webex.credentials.getUserToken()}`,
+ },
};
🤖 Prompt for AI Agents
In packages/@webex/plugin-cc/src/services/task/IvrTranscriptService.ts around
lines 55 to 62, the request headers set null-valued keys and pass a raw token;
remove the 'cisco-no-http-redirect' and 'spark-user-agent' entries instead of
setting them to null, and set Authorization to "Bearer " + (await
this.webex.credentials.getUserToken()); alternatively, if Core interceptors
already inject these headers (including Authorization), remove the entire
headers block—otherwise include only the necessary header(s) with the
Bearer-prefixed token.
/** | ||
* Fetches the IVR transcript for the current voice task | ||
* This method retrieves the Interactive Voice Response transcript that was recorded | ||
* during the customer's interaction with the IVR system before being connected to an agent. | ||
* Only available for voice tasks that have IVR interactions. | ||
* | ||
* @param orgId - Organization ID (required) | ||
* @param interactionId - Interaction ID for the task (required) | ||
* @param timeoutMins - Timeout in minutes for the transcript URL (required) | ||
* @param includeConversation - Whether to fetch the actual conversation data | ||
* @returns Promise<IvrTranscriptResponse> The IVR transcript data including metadata and content | ||
* @throws Error if the task is not a voice task, no IVR transcript is available, or the fetch operation fails | ||
* @example | ||
* ```typescript | ||
* // Fetch IVR transcript after accepting a voice task | ||
* try { | ||
* const transcript = await task.fetchIvrTranscript('org123', 'interaction456', 10); | ||
* console.log('IVR transcript:', transcript.content.transcripts); | ||
* | ||
* // Display transcript to agent | ||
* transcript.content.transcripts.forEach(entry => { | ||
* console.log(`${entry.timestamp}: ${entry.utterance}`); | ||
* }); | ||
* } catch (error) { | ||
* console.error('Failed to fetch IVR transcript:', error); | ||
* // Handle case where no transcript is available | ||
* } | ||
* ``` | ||
*/ | ||
fetchIvrTranscript( | ||
orgId: string, | ||
interactionId: string, | ||
timeoutMins: number | ||
): Promise<IvrTranscriptResponse>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix JSDoc: signature and example do not match the actual return type
- The doc lists an extra parameter includeConversation that the method does not accept.
- The example treats the return as
{ content: { transcripts: ... } }
, but the method returnsIvrTranscriptResponse
(an array of conversation turns).
This will mislead SDK consumers.
Suggested fix:
/**
* Fetches the IVR transcript for the current voice task
* This method retrieves the Interactive Voice Response transcript that was recorded
* during the customer's interaction with the IVR system before being connected to an agent.
* Only available for voice tasks that have IVR interactions.
*
* @param orgId - Organization ID (required)
* @param interactionId - Interaction ID for the task (required)
* @param timeoutMins - Timeout in minutes for the transcript URL (required)
- * @param includeConversation - Whether to fetch the actual conversation data
- * @returns Promise<IvrTranscriptResponse> The IVR transcript data including metadata and content
+ * @returns Promise<IvrTranscriptResponse> The IVR conversation turns array
* @throws Error if the task is not a voice task, no IVR transcript is available, or the fetch operation fails
* @example
* ```typescript
* // Fetch IVR transcript after accepting a voice task
* try {
- * const transcript = await task.fetchIvrTranscript('org123', 'interaction456', 10);
- * console.log('IVR transcript:', transcript.content.transcripts);
- *
- * // Display transcript to agent
- * transcript.content.transcripts.forEach(entry => {
- * console.log(`${entry.timestamp}: ${entry.utterance}`);
- * });
+ * const transcript = await task.fetchIvrTranscript('org123', 'interaction456', 10);
+ * console.log('Turns:', transcript.length);
+ * transcript.forEach((turn) => {
+ * if (turn.customer) {
+ * console.log(`${turn.customer.timestamp}: Customer — ${turn.customer.query}`);
+ * }
+ * if (turn.bot) {
+ * console.log(`${turn.bot.timestamp}: Bot (${turn.bot.botName}) — ${turn.bot.reply}`);
+ * }
+ * });
* } catch (error) {
* console.error('Failed to fetch IVR transcript:', error);
* // Handle case where no transcript is available
* }
* ```
*/
🤖 Prompt for AI Agents
In packages/@webex/plugin-cc/src/services/task/types.ts around lines 1214 to
1248, the JSDoc for fetchIvrTranscript incorrectly documents an extra parameter
includeConversation and shows an example that treats the return as { content: {
transcripts: ... } } while the method signature accepts only (orgId,
interactionId, timeoutMins) and returns IvrTranscriptResponse (an array of
conversation turns); update the JSDoc by removing the includeConversation
@param, correct the @returns to state it returns Promise<IvrTranscriptResponse>
(array of turns), and replace the example with one that treats the result as an
array (e.g., log transcript.length and iterate turns, checking turn.customer and
turn.bot fields and printing timestamps/queries/replies accordingly).
getIvrTranscriptUrl: jest.fn().mockResolvedValue({}), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Stale contactMock API for IVR
Task.fetchIvrTranscript delegates directly to IvrTranscriptService (via getIvrTranscriptService), it doesn’t use contact.getIvrTranscriptUrl anymore. Tests that rely on contactMock.getIvrTranscriptUrl to simulate IVR failures or empties won’t be exercising the real path and are brittle.
As part of the IVR tests below, replace usages of contactMock.getIvrTranscriptUrl with either:
- overriding (task as any).getIvrTranscriptService() to return a mock with fetchIVRTranscript (resolve/reject), or
- mocking WebexRequest.getInstance().request to return metadata responses.
I’ve proposed concrete diffs in those tests.
🤖 Prompt for AI Agents
In packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts around lines
89 to 90, the tests use the stale contactMock.getIvrTranscriptUrl API but
Task.fetchIvrTranscript now delegates to IvrTranscriptService via
getIvrTranscriptService; update the tests to stop stubbing
contact.getIvrTranscriptUrl and instead either override (task as
any).getIvrTranscriptService() to return a mock object that provides
fetchIVRTranscript which you can resolve or reject as needed, or mock
WebexRequest.getInstance().request to return the appropriate metadata responses
for the IVR flow so the tests exercise the real delegation path.
it('should handle API error when fetching transcript URL', async () => { | ||
const taskDataMockVoice = { | ||
...taskDataMock, | ||
mediaChannel: MEDIA_CHANNEL.TELEPHONY, | ||
orgId: 'test-org-123', | ||
taskId: 'test-task-456', | ||
}; | ||
|
||
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | ||
|
||
const apiError = new Error('API Error'); | ||
contactMock.getIvrTranscriptUrl.mockRejectedValue(apiError); | ||
|
||
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); | ||
|
||
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
expect.objectContaining({ | ||
error: expect.any(String) | ||
}), | ||
['operational', 'behavioral', 'business'] | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test incorrectly mocks contact API for IVR failure; update to mock IvrTranscriptService
Task no longer calls contact.getIvrTranscriptUrl. This test passes for the wrong reason (likely due to undefined metadata), making it brittle and misleading.
Apply this diff to correctly simulate the IVR service failure:
- const apiError = new Error('API Error');
- contactMock.getIvrTranscriptUrl.mockRejectedValue(apiError);
+ const apiError = new Error('API Error');
+ // Override the private IVR service to reject
+ (task as any).getIvrTranscriptService = jest.fn().mockReturnValue({
+ fetchIVRTranscript: jest.fn().mockRejectedValue(apiError),
+ });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
it('should handle API error when fetching transcript URL', async () => { | |
const taskDataMockVoice = { | |
...taskDataMock, | |
mediaChannel: MEDIA_CHANNEL.TELEPHONY, | |
orgId: 'test-org-123', | |
taskId: 'test-task-456', | |
}; | |
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | |
const apiError = new Error('API Error'); | |
contactMock.getIvrTranscriptUrl.mockRejectedValue(apiError); | |
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); | |
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
expect.objectContaining({ | |
error: expect.any(String) | |
}), | |
['operational', 'behavioral', 'business'] | |
); | |
}); | |
it('should handle API error when fetching transcript URL', async () => { | |
const taskDataMockVoice = { | |
...taskDataMock, | |
mediaChannel: MEDIA_CHANNEL.TELEPHONY, | |
orgId: 'test-org-123', | |
taskId: 'test-task-456', | |
}; | |
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | |
const apiError = new Error('API Error'); | |
// Override the private IVR service to reject | |
(task as any).getIvrTranscriptService = jest.fn().mockReturnValue({ | |
fetchIVRTranscript: jest.fn().mockRejectedValue(apiError), | |
}); | |
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); | |
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
expect.objectContaining({ | |
error: expect.any(String) | |
}), | |
['operational', 'behavioral', 'business'] | |
); | |
}); |
it('should handle empty transcripts array', async () => { | ||
const taskDataMockVoice = { | ||
...taskDataMock, | ||
mediaChannel: MEDIA_CHANNEL.TELEPHONY, | ||
orgId: 'test-org-123', | ||
taskId: 'test-task-456', | ||
}; | ||
|
||
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | ||
|
||
const mockTranscriptUrlResponse = { | ||
body: { | ||
orgId: 'test-org-123', | ||
interactionId: 'test-interaction-456', | ||
timeOutMins: 5, | ||
transcripts: [] | ||
} | ||
}; | ||
|
||
contactMock.getIvrTranscriptUrl.mockResolvedValue(mockTranscriptUrlResponse); | ||
|
||
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); | ||
|
||
expect(global.fetch).not.toHaveBeenCalled(); | ||
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
expect.objectContaining({ | ||
taskId: 'test-interaction-456', | ||
error: expect.any(String) | ||
}), | ||
['operational', 'behavioral', 'business'] | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong expectation: empty transcript metadata should not throw
IvrTranscriptService returns [] when meta.transcripts is empty; Task tracks success and returns []. This test currently expects a throw and relies on contactMock.getIvrTranscriptUrl, which Task does not use.
Update the test to mock IvrTranscriptService and assert a non-throwing empty result + success metric:
- it('should handle empty transcripts array', async () => {
- const taskDataMockVoice = {
- ...taskDataMock,
- mediaChannel: MEDIA_CHANNEL.TELEPHONY,
- orgId: 'test-org-123',
- taskId: 'test-task-456',
- };
-
- const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null });
-
- const mockTranscriptUrlResponse = {
- body: {
- orgId: 'test-org-123',
- interactionId: 'test-interaction-456',
- timeOutMins: 5,
- transcripts: []
- }
- };
-
- contactMock.getIvrTranscriptUrl.mockResolvedValue(mockTranscriptUrlResponse);
-
- await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow();
-
- expect(global.fetch).not.toHaveBeenCalled();
- expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
- METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED,
- expect.objectContaining({
- taskId: 'test-interaction-456',
- error: expect.any(String)
- }),
- ['operational', 'behavioral', 'business']
- );
- });
+ it('should return empty array (and record success metric) when no transcripts are available', async () => {
+ const taskDataMockVoice = {
+ ...taskDataMock,
+ interaction: {
+ ...taskDataMock.interaction,
+ mediaType: MEDIA_CHANNEL.TELEPHONY
+ },
+ orgId: 'test-org-123',
+ interactionId: 'test-interaction-456',
+ };
+
+ const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null });
+ // Service returns no conversations when metadata has no transcripts
+ (task as any).getIvrTranscriptService = jest.fn().mockReturnValue({
+ fetchIVRTranscript: jest.fn().mockResolvedValue([]),
+ });
+
+ const result = await task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5);
+ expect(result).toEqual([]);
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
+ METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS,
+ expect.objectContaining({
+ taskId: 'test-interaction-456',
+ orgId: 'test-org-123',
+ conversationTurns: 0
+ }),
+ ['operational', 'behavioral', 'business']
+ );
+ });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
it('should handle empty transcripts array', async () => { | |
const taskDataMockVoice = { | |
...taskDataMock, | |
mediaChannel: MEDIA_CHANNEL.TELEPHONY, | |
orgId: 'test-org-123', | |
taskId: 'test-task-456', | |
}; | |
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | |
const mockTranscriptUrlResponse = { | |
body: { | |
orgId: 'test-org-123', | |
interactionId: 'test-interaction-456', | |
timeOutMins: 5, | |
transcripts: [] | |
} | |
}; | |
contactMock.getIvrTranscriptUrl.mockResolvedValue(mockTranscriptUrlResponse); | |
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); | |
expect(global.fetch).not.toHaveBeenCalled(); | |
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | |
expect.objectContaining({ | |
taskId: 'test-interaction-456', | |
error: expect.any(String) | |
}), | |
['operational', 'behavioral', 'business'] | |
); | |
}); | |
it('should return empty array (and record success metric) when no transcripts are available', async () => { | |
const taskDataMockVoice = { | |
...taskDataMock, | |
interaction: { | |
...taskDataMock.interaction, | |
mediaType: MEDIA_CHANNEL.TELEPHONY | |
}, | |
orgId: 'test-org-123', | |
interactionId: 'test-interaction-456', | |
}; | |
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | |
// Service returns no conversations when metadata has no transcripts | |
(task as any).getIvrTranscriptService = jest.fn().mockReturnValue({ | |
fetchIVRTranscript: jest.fn().mockResolvedValue([]), | |
}); | |
const result = await task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5); | |
expect(result).toEqual([]); | |
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | |
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | |
expect.objectContaining({ | |
taskId: 'test-interaction-456', | |
orgId: 'test-org-123', | |
conversationTurns: 0 | |
}), | |
['operational', 'behavioral', 'business'] | |
); | |
}); |
🤖 Prompt for AI Agents
In packages/@webex/plugin-cc/test/unit/spec/services/task/index.ts around lines
1645-1677, the test currently expects fetchIvrTranscript to throw and mocks
contactMock.getIvrTranscriptUrl, but Task uses IvrTranscriptService and should
return an empty array and track success. Update the test to mock
IvrTranscriptService to return an empty transcripts array (or resolve to []),
change the assertion to await
expect(task.fetchIvrTranscript(...)).resolves.toEqual([]) (no throw), keep the
check that global.fetch was not called, and assert that
mockMetricsManager.trackEvent was called with the success metric (e.g.,
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCHED) and an object containing the
task/interaction id and appropriate success info.
/** | ||
* Service for retrieving IVR (Interactive Voice Response) transcript data | ||
* Handles fetching transcript metadata and conversation content from storage endpoints | ||
* Provides structured access to bot conversation history and customer interactions | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please improve this
// Start metrics tracking | ||
this.metricsManager.timeEvent([ | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_SUCCESS, | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-visit for metrics
const transcriptConversations = await this.getIvrTranscriptService().fetchIVRTranscript( | ||
orgId, | ||
interactionId, | ||
timeoutMins | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let break this into two steps
const transcriptService = this.getIvrTranscriptService();
const transcriptConversations = await fetchIVRTranscript(....);
{ | ||
taskId: interactionId, | ||
orgId, | ||
type: 'direct-service', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's confirm this value
this.metricsManager.trackEvent( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
{ | ||
taskId: interactionId, | ||
orgId, | ||
type: 'direct-service', | ||
error: detailedError.message, | ||
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}), | ||
}, | ||
['operational', 'behavioral', 'business'] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upload log should happen for the error case and metric should contain the feedbackId
docs/samples/contact-center/app.js
Outdated
console.log('IVR Transcript UI elements initialized:', { | ||
fetchButton: !!fetchIvrTranscriptBtn, | ||
timeoutInput: !!ivrTimeoutInput, | ||
statusElement: !!ivrStatusElm, | ||
resultsElement: !!ivrResultsContentElm | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check all the console logs and keep only relevant ones
docs/samples/contact-center/app.js
Outdated
console.log('IVR Button State: Enabling button - task accepted and telephony channel'); | ||
fetchIvrTranscriptBtn.disabled = false; | ||
// Don't override status if auto-fetch is in progress | ||
if (!ivrStatusElm.textContent.includes('Auto-fetching') && !ivrStatusElm.textContent.includes('Auto-fetched')) { | ||
ivrStatusElm.textContent = 'Ready to fetch IVR transcript'; | ||
} | ||
} else if (currentTask.data.interaction.state !== 'connected') { | ||
console.log('IVR Button State: Disabling button - task not accepted'); | ||
fetchIvrTranscriptBtn.disabled = true; | ||
ivrStatusElm.textContent = 'Task must be accepted first'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discuss the buttons' visibility with Ravi and use the control visibility properties coming with the task object.
docs/samples/contact-center/app.js
Outdated
// Auto-fetch IVR transcript for telephony tasks that are assigned (accepted) | ||
if (task.data.interaction.mediaType === 'telephony' && task.data.interaction.state === 'connected') { | ||
console.log('Auto-fetching IVR transcript for assigned telephony task...'); | ||
await autoFetchIvrTranscript(task, 'task assigned'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's ensure we are making the API call at only one place. We are calling at answer method as well, let's make it conditional to cover different type of station login
// Auto-fetch IVR transcript for telephony consult acceptance | ||
if (task.data.interaction.mediaType === 'telephony') { | ||
console.log('Auto-fetching IVR transcript for consulted telephony task...'); | ||
await autoFetchIvrTranscript(task, 'consult accepted'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, make sure we are calling it only once
docs/samples/contact-center/app.js
Outdated
// Example usage after fetching transcript: | ||
// const html = renderIVRTranscript(conversation); | ||
// document.getElementById('ivr-transcript-container').innerHTML = html; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove this comment
const mockOrgId = 'org123'; | ||
const mockInteractionId = 'interaction456'; | ||
const mockTimeOutMins = 5; | ||
const mockTranscriptPath = 'https://mediastorage.produs1.ciscoccservice.com/transcript.json'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's create a separate file for the mock transcript (fixture) and add the response there. Based on the file path, we should expect the response matching the data in that file.
Example of the fixture - packages/@webex/plugin-meetings/test/unit/spec/fixture/locus.js
|
||
(webex.request as jest.Mock) | ||
.mockResolvedValueOnce(mockResponse) | ||
.mockResolvedValueOnce(mockConversationResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the 2nd mock as we are hardcoding the response from the file
|
||
expect(webex.request).toHaveBeenCalledTimes(2); | ||
expect(webex.request).toHaveBeenCalledWith({ | ||
uri: `https://mediastorage.produs1.ciscoccservice.com/media/organization/${mockOrgId}/interaction/${mockInteractionId}/ivrtranscript?timeOutMins=${mockTimeOutMins}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the base path would be the file path defined
], | ||
}; | ||
|
||
const mockResponse = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mockMetaDataResponse
transcripts: [ | ||
mockTranscriptMetadata, | ||
{ | ||
...mockTranscriptMetadata, | ||
transcriptId: 'trans2', | ||
transcriptPath: 'https://example.com/transcript2.json', | ||
botName: 'Bot2', | ||
}, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make the array contain 3 transcripts. Fail the 2nd one and compare the result with 1st and 3rd ones
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
expect.objectContaining({ | ||
error: expect.any(String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add proper error message
const apiError = new Error('API Error'); | ||
mockIvrTranscriptService.fetchIVRTranscript.mockRejectedValue(apiError); | ||
|
||
await expect(task.fetchIvrTranscript('test-org-123', 'test-interaction-456', 5)).rejects.toThrow(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add the error details in the toThrow()
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith( | ||
METRIC_EVENT_NAMES.TASK_IVR_TRANSCRIPT_FETCH_FAILED, | ||
expect.objectContaining({ | ||
error: expect.any(String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, we can add error details
const task = new Task(contactMock, webCallingService, taskDataMockVoice, { wrapUpProps: null }); | ||
|
||
// Mock the fetchIVRTranscript method to return empty array (simulating S3 failure) | ||
mockIvrTranscriptService.fetchIVRTranscript.mockResolvedValue([]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't handle the S3 api call. We will need to mock throw error from API invocation of the transcript path
); | ||
}); | ||
|
||
it('should handle multiple transcripts', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change the description as we are not handling multiple transcripts here. Instead, we are having single transcript with data containing multiple conversation interaction.
COMPLETES #https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6767
Vidcast link: https://app.vidcast.io/share/6a0c6741-f865-460f-9b47-5bf960dc1853
This pull request addresses
Implemented the IVR transcript feature in the CC SDK.
To improve customer experience, Virtual Agents handle most initial queries. When escalation to a human agent is required, the full transcript of the customer’s interaction with the Virtual Agent (the IVR transcript) is shared with the agent. This prevents customers from having to repeat themselves and ensures a smooth handoff.
Added support for retrieving IVR transcripts via two REST API calls:
by making the following changes
Created a new service file called IvrTranscriptService, where we make API requests to retrieve the IVR transcript response.
We then instantiate this service in the Services class.
Through the Services class, we access the fetchIvrTranscript method from the IvrTranscriptService in the task component, making it part of the currentTask.
Agent Desktop Code References: https://sqbu-github.cisco.com/CBABU/wxcc-desktop/blob/2635b235b0fdb78c4174b483f63cba62947cbc5e/packages/agentx-mfe-wc-based/src/components/IvrTranscripts/IvrTranscripts.ts#L76
https://sqbu-github.cisco.com/CBABU/wxcc-desktop/blob/2635b235b0fdb78c4174b483f63cba62947cbc5e/packages/agentx-services/src/services/mediaStorage/media-storage-service.ts#L74
Metrics

Upload logs

Change Type
The following scenarios were tested
< ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >
The GAI Coding Policy And Copyright Annotation Best Practices
I certified that
Make sure to have followed the contributing guidelines before submitting.