Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2158,6 +2158,7 @@ function renderTaskList(taskList) {
const isNew = isIncomingTask(task, agentId);
const isTelephony = task.data.interaction.mediaType === 'telephony';
const isBrowserPhone = agentDeviceType === 'BROWSER';
const isAutoAnswering = task.data.isAutoAnswering || false;

// Determine which buttons to show
const showAcceptButton = isNew && (isBrowserPhone || !isTelephony);
Expand All @@ -2167,8 +2168,8 @@ function renderTaskList(taskList) {
taskElement.innerHTML = `
<div class="task-item-content">
<p>${callerDisplay}</p>
${showAcceptButton ? `<button class="accept-task" data-task-id="${taskId}">Accept</button>` : ''}
${showDeclineButton ? `<button class="decline-task" data-task-id="${taskId}">Decline</button>` : ''}
${showAcceptButton ? `<button class="accept-task" data-task-id="${taskId}" ${isAutoAnswering ? 'disabled' : ''}>Accept</button>` : ''}
${showDeclineButton ? `<button class="decline-task" data-task-id="${taskId}" ${isAutoAnswering ? 'disabled' : ''}>Decline</button>` : ''}
</div>
<hr class="task-separator">
`;
Expand Down Expand Up @@ -2239,24 +2240,40 @@ function renderTaskList(taskList) {
function enableAnswerDeclineButtons(task) {
const callerDisplay = task.data.interaction?.callAssociatedDetails?.ani;
const isNew = isIncomingTask(task, agentId);
const chatAndSocial = ['chat', 'social'];
const isAutoAnswering = task.data.isAutoAnswering || false;
const chatAndSocial = ['chat', 'social'];

if (task.data.interaction.mediaType === 'telephony') {
if (agentDeviceType === 'BROWSER') {
answerElm.disabled = !isNew;
declineElm.disabled = !isNew;
// Disable buttons if auto-answering or not new
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = !isNew || isAutoAnswering;

incomingDetailsElm.innerText = `Call from ${callerDisplay}`;

// Log auto-answer status for debugging
if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
} else {
incomingDetailsElm.innerText = `Call from ${callerDisplay}...please answer on the endpoint where the agent's extension is registered`;
}
} else if (chatAndSocial.includes(task.data.interaction.mediaType)) {
answerElm.disabled = !isNew;
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = true;
incomingDetailsElm.innerText = `Chat from ${callerDisplay}`;

if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
} else if (task.data.interaction.mediaType === 'email') {
answerElm.disabled = !isNew;
answerElm.disabled = !isNew || isAutoAnswering;
declineElm.disabled = true;
incomingDetailsElm.innerText = `Email from ${callerDisplay}`;

if (isAutoAnswering) {
console.log('✅ Auto-answer in progress for task:', task.data.interactionId);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/@webex/contact-center/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
// TODO: Make profile a singleton to make it available throughout app/sdk so we dont need to inject info everywhere
this.taskManager.setWrapupData(this.agentConfig.wrapUpData);
this.taskManager.setAgentId(this.agentConfig.agentId);
this.taskManager.setWebRtcEnabled(this.agentConfig.webRtcEnabled);

if (
this.agentConfig.webRtcEnabled &&
Expand Down
12 changes: 12 additions & 0 deletions packages/@webex/contact-center/src/metrics/behavioral-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,18 @@ const eventTaxonomyMap: Record<string, BehavioralEventTaxonomy> = {
target: 'task_accept_consult',
verb: 'fail',
},
[METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS]: {
product,
agent: 'user',
target: 'task_auto_answer',
verb: 'complete',
},
[METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED]: {
product,
agent: 'user',
target: 'task_auto_answer',
verb: 'fail',
},
[METRIC_EVENT_NAMES.TASK_OUTDIAL_SUCCESS]: {
product,
agent: 'user',
Expand Down
4 changes: 4 additions & 0 deletions packages/@webex/contact-center/src/metrics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type Enum<T extends Record<string, unknown>> = T[keyof T];
* @property {string} TASK_PAUSE_RECORDING_FAILED - Event name for failed pause of recording.
* @property {string} TASK_ACCEPT_CONSULT_SUCCESS - Event name for successful consult acceptance.
* @property {string} TASK_ACCEPT_CONSULT_FAILED - Event name for failed consult acceptance.
* @property {string} TASK_AUTO_ANSWER_SUCCESS - Event name for successful auto-answer.
* @property {string} TASK_AUTO_ANSWER_FAILED - Event name for failed auto-answer.
*
* @property {string} TASK_CONFERENCE_START_SUCCESS - Event name for successful conference start.
* @property {string} TASK_CONFERENCE_START_FAILED - Event name for failed conference start.
Expand Down Expand Up @@ -117,6 +119,8 @@ export const METRIC_EVENT_NAMES = {
TASK_PAUSE_RECORDING_FAILED: 'Task Pause Recording Failed',
TASK_ACCEPT_CONSULT_SUCCESS: 'Task Accept Consult Success',
TASK_ACCEPT_CONSULT_FAILED: 'Task Accept Consult Failed',
TASK_AUTO_ANSWER_SUCCESS: 'Task Auto Answer Success',
TASK_AUTO_ANSWER_FAILED: 'Task Auto Answer Failed',

// Conference Tasks
TASK_CONFERENCE_START_SUCCESS: 'Task Conference Start Success',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isParticipantInMainInteraction,
isPrimary,
isSecondaryEpDnAgent,
shouldAutoAnswerTask,
} from './TaskUtils';

/** @internal */
Expand All @@ -36,6 +37,7 @@ export default class TaskManager extends EventEmitter {
private static taskManager;
private wrapupData: WrapupData;
private agentId: string;
private webRtcEnabled: boolean;
/**
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
* @param webCallingService - Webrtc Service Layer
Expand Down Expand Up @@ -73,6 +75,10 @@ export default class TaskManager extends EventEmitter {
return this.agentId;
}

public setWebRtcEnabled(webRtcEnabled: boolean) {
this.webRtcEnabled = webRtcEnabled;
}

private handleIncomingWebCall = (call: ICall) => {
const currentTask = Object.values(this.taskCollection).find(
(task) => task.data.interaction.mediaType === 'telephony'
Expand Down Expand Up @@ -130,6 +136,14 @@ export default class TaskManager extends EventEmitter {
interactionId: payload.data.interactionId,
});

// Check if auto-answer should happen for this task
const shouldAutoAnswer = shouldAutoAnswerTask(
payload.data,
this.agentId,
this.webCallingService.loginOption,
this.webRtcEnabled
);

task = new Task(
this.contact,
this.webCallingService,
Expand All @@ -138,6 +152,7 @@ export default class TaskManager extends EventEmitter {
wrapUpRequired:
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
isConferenceInProgress: getIsConferenceInProgress(payload.data),
isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
},
this.wrapupData,
this.agentId
Expand Down Expand Up @@ -169,13 +184,22 @@ export default class TaskManager extends EventEmitter {
}
break;

case CC_EVENTS.AGENT_CONTACT_RESERVED:
case CC_EVENTS.AGENT_CONTACT_RESERVED: {
// Check if auto-answer should happen for this task
const shouldAutoAnswerReserved = shouldAutoAnswerTask(
payload.data,
this.agentId,
this.webCallingService.loginOption,
this.webRtcEnabled
);

task = new Task(
this.contact,
this.webCallingService,
{
...payload.data,
isConsulted: false,
isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
},
this.wrapupData,
this.agentId
Expand All @@ -190,6 +214,7 @@ export default class TaskManager extends EventEmitter {
this.emit(TASK_EVENTS.TASK_INCOMING, task);
}
break;
}
case CC_EVENTS.AGENT_OFFER_CONTACT:
// We don't have to emit any event here since this will be result of promise.
task = this.updateTaskData(task, payload.data);
Expand All @@ -199,6 +224,9 @@ export default class TaskManager extends EventEmitter {
interactionId: payload.data?.interactionId,
});
this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);

// Handle auto-answer for offer contact
this.handleAutoAnswer(task);
break;
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
task = this.updateTaskData(task, payload.data);
Expand Down Expand Up @@ -308,6 +336,9 @@ export default class TaskManager extends EventEmitter {
isConsulted: true, // This ensures that the task is marked as us being requested for a consult
});
task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);

// Handle auto-answer for consult offer
this.handleAutoAnswer(task);
break;
case CC_EVENTS.AGENT_CONSULTING:
// Received when agent is in an active consult state
Expand Down Expand Up @@ -545,6 +576,70 @@ export default class TaskManager extends EventEmitter {
}
}

/**
* Handles auto-answer logic for incoming tasks
* Automatically accepts tasks when isAutoAnswering flag is set
* The flag is set during task creation based on:
* 1. WebRTC calls with auto-answer enabled in agent profile
* 2. Agent-initiated WebRTC outdial calls
* 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
*
* @param task - The task to auto-answer
* @private
*/
private async handleAutoAnswer(task: ITask): Promise<void> {
if (!task || !task.data || !task.data.isAutoAnswering) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of returning, we throw an error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as return — this guard clause handles the normal operation where most tasks don’t have auto-answer enabled. Throwing an error here would treat the expected path as exceptional and introduce unnecessary error handling in the callers.

}

LoggerProxy.info(`Auto-answering task`, {
module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
});

try {
await task.accept();
LoggerProxy.info(`Task auto-answered successfully`, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add metrics for this - it would be good to know if the task was auto answer or not.
We can re-use the accept metrics itself but add another field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
});

// Track successful auto-answer
this.metricsManager.trackEvent(
METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS,
{
taskId: task.data.interactionId,
mediaType: task.data.interaction.mediaType,
isAutoAnswered: true,
},
['behavioral', 'operational']
);
} catch (error) {
// Reset isAutoAnswering flag on failure
task.updateTaskData({...task.data, isAutoAnswering: false});
LoggerProxy.error(`Failed to auto-answer task`, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here.. have metrics for error for auto answer also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

module: TASK_MANAGER_FILE,
method: 'handleAutoAnswer',
interactionId: task.data.interactionId,
error,
});

// Track auto-answer failure
this.metricsManager.trackEvent(
METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED,
{
taskId: task.data.interactionId,
mediaType: task.data.interaction.mediaType,
error: error?.message || 'Unknown error',
isAutoAnswered: false,
},
['behavioral', 'operational']
);
}
}

/**
* Handles cleanup of task resources including Desktop/WebRTC call cleanup and task removal
* @param task - The task to clean up
Expand Down
Loading
Loading