@webex/contact-center 3.11.0 → 3.12.0-llmrefactor.2
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/.sdd/manifest.json +882 -0
- package/AGENTS.md +94 -0
- package/ai-docs/ARCHITECTURE.md +168 -0
- package/ai-docs/CONTRACTS.md +46 -0
- package/ai-docs/GETTING_STARTED.md +168 -0
- package/ai-docs/GLOSSARY.md +43 -0
- package/ai-docs/README.md +138 -0
- package/ai-docs/REVIEW_CHECKLIST.md +41 -0
- package/ai-docs/RULES.md +444 -0
- package/ai-docs/SECURITY.md +52 -0
- package/ai-docs/SERVICE_STATE.md +48 -0
- package/ai-docs/SPEC_INDEX.md +65 -0
- package/ai-docs/adr/0001-spec-source-policy.md +55 -0
- package/ai-docs/adr/README.md +8 -0
- package/ai-docs/adr/_adr-template.md +31 -0
- package/ai-docs/contact-center-spec.md +341 -0
- package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
- package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
- package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
- package/ai-docs/patterns/event-driven-patterns.md +485 -0
- package/ai-docs/patterns/testing-patterns.md +480 -0
- package/ai-docs/patterns/typescript-patterns.md +365 -0
- package/ai-docs/templates/README.md +102 -0
- package/ai-docs/templates/documentation/create-agents-md.md +240 -0
- package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
- package/ai-docs/templates/existing-service/bug-fix.md +254 -0
- package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
- package/ai-docs/templates/new-method/00-master.md +80 -0
- package/ai-docs/templates/new-method/01-requirements.md +232 -0
- package/ai-docs/templates/new-method/02-implementation.md +295 -0
- package/ai-docs/templates/new-method/03-tests.md +201 -0
- package/ai-docs/templates/new-method/04-validation.md +141 -0
- package/ai-docs/templates/new-service/00-master.md +109 -0
- package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
- package/ai-docs/templates/new-service/02-code-generation.md +346 -0
- package/ai-docs/templates/new-service/03-integration.md +178 -0
- package/ai-docs/templates/new-service/04-test-generation.md +205 -0
- package/ai-docs/templates/new-service/05-validation.md +145 -0
- package/dist/cc.js +379 -50
- package/dist/cc.js.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +22 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js +27 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +114 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +32 -3
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +318 -0
- package/dist/services/ApiAiAssistant.js.map +1 -0
- package/dist/services/UserPreference.js +427 -0
- package/dist/services/UserPreference.js.map +1 -0
- package/dist/services/agent/types.js.map +1 -1
- package/dist/services/config/Util.js +8 -4
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +35 -2
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +41 -2
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +66 -8
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +27 -1
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +122 -25
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +92 -17
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +22 -6
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/core/websocket/connection-service.js +3 -1
- package/dist/services/core/websocket/connection-service.js.map +1 -1
- package/dist/services/core/websocket/types.js.map +1 -1
- package/dist/services/index.js +6 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +688 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +45 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +751 -457
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +220 -23
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +23 -2
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +129 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/digital/Digital.js +77 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +873 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +567 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +161 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +382 -0
- package/dist/services/task/state-machine/guards.js.map +1 -0
- package/dist/services/task/state-machine/index.js +53 -0
- package/dist/services/task/state-machine/index.js.map +1 -0
- package/dist/services/task/state-machine/types.js +54 -0
- package/dist/services/task/state-machine/types.js.map +1 -0
- package/dist/services/task/state-machine/uiControlsComputer.js +603 -0
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
- package/dist/services/task/taskDataNormalizer.js +99 -0
- package/dist/services/task/taskDataNormalizer.js.map +1 -0
- package/dist/services/task/types.js +227 -4
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1044 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +149 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- package/dist/types/cc.d.ts +894 -0
- package/dist/types/config.d.ts +72 -0
- package/dist/types/constants.d.ts +66 -0
- package/dist/types/index.d.ts +199 -0
- package/dist/types/logger-proxy.d.ts +71 -0
- package/dist/types/metrics/MetricsManager.d.ts +223 -0
- package/dist/types/metrics/behavioral-events.d.ts +29 -0
- package/dist/types/metrics/constants.d.ts +181 -0
- package/dist/types/services/AddressBook.d.ts +74 -0
- package/dist/types/services/ApiAiAssistant.d.ts +49 -0
- package/dist/types/services/EntryPoint.d.ts +67 -0
- package/dist/types/services/Queue.d.ts +76 -0
- package/dist/types/services/UserPreference.d.ts +118 -0
- package/dist/types/services/WebCallingService.d.ts +1 -0
- package/dist/types/services/agent/index.d.ts +46 -0
- package/dist/types/services/agent/types.d.ts +413 -0
- package/dist/types/services/config/Util.d.ts +20 -0
- package/dist/types/services/config/constants.d.ts +270 -0
- package/dist/types/services/config/index.d.ts +177 -0
- package/dist/types/services/config/types.d.ts +1368 -0
- package/dist/types/services/constants.d.ts +110 -0
- package/dist/types/services/core/Err.d.ts +125 -0
- package/dist/types/services/core/GlobalTypes.d.ts +58 -0
- package/dist/types/services/core/Utils.d.ts +121 -0
- package/dist/types/services/core/WebexRequest.d.ts +22 -0
- package/dist/types/services/core/aqm-reqs.d.ts +65 -0
- package/dist/types/services/core/constants.d.ts +99 -0
- package/dist/types/services/core/types.d.ts +47 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
- package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
- package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
- package/dist/types/services/core/websocket/types.d.ts +37 -0
- package/dist/types/services/index.d.ts +54 -0
- package/dist/types/services/task/AutoWrapup.d.ts +40 -0
- package/dist/types/services/task/Task.d.ts +157 -0
- package/dist/types/services/task/TaskFactory.d.ts +12 -0
- package/dist/types/services/task/TaskManager.d.ts +1 -0
- package/dist/types/services/task/TaskUtils.d.ts +138 -0
- package/dist/types/services/task/constants.d.ts +91 -0
- package/dist/types/services/task/contact.d.ts +69 -0
- package/dist/types/services/task/dialer.d.ts +73 -0
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1194 -0
- package/dist/types/services/task/state-machine/actions.d.ts +10 -0
- package/dist/types/services/task/state-machine/constants.d.ts +107 -0
- package/dist/types/services/task/state-machine/guards.d.ts +102 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +269 -0
- package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
- package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
- package/dist/types/services/task/types.d.ts +1856 -0
- package/dist/types/services/task/voice/Voice.d.ts +184 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- package/dist/types/types.d.ts +778 -0
- package/dist/types/utils/PageCache.d.ts +173 -0
- package/dist/types/webex-config.d.ts +53 -0
- package/dist/types/webex.d.ts +8 -0
- package/dist/types.js +130 -1
- package/dist/types.js.map +1 -1
- package/dist/webex.js +14 -2
- package/dist/webex.js.map +1 -1
- package/package.json +16 -12
- package/src/cc.ts +477 -51
- package/src/config.ts +6 -0
- package/src/constants.ts +21 -1
- package/src/index.ts +24 -5
- package/src/metrics/ai-docs/AGENTS.md +350 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
- package/src/metrics/ai-docs/metrics-spec.md +854 -0
- package/src/metrics/behavioral-events.ts +120 -0
- package/src/metrics/constants.ts +37 -3
- package/src/services/ApiAiAssistant.ts +412 -0
- package/src/services/UserPreference.ts +509 -0
- package/src/services/agent/ai-docs/AGENTS.md +240 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
- package/src/services/agent/ai-docs/agent-spec.md +504 -0
- package/src/services/agent/types.ts +1 -1
- package/src/services/ai-docs/AGENTS.md +386 -0
- package/src/services/ai-docs/services-spec.md +492 -0
- package/src/services/config/Util.ts +10 -2
- package/src/services/config/ai-docs/AGENTS.md +255 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
- package/src/services/config/ai-docs/config-spec.md +669 -0
- package/src/services/config/constants.ts +37 -1
- package/src/services/config/index.ts +45 -1
- package/src/services/config/types.ts +241 -11
- package/src/services/constants.ts +29 -0
- package/src/services/core/Err.ts +3 -0
- package/src/services/core/Utils.ts +143 -30
- package/src/services/core/ai-docs/AGENTS.md +381 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
- package/src/services/core/ai-docs/core-spec.md +783 -0
- package/src/services/core/aqm-reqs.ts +100 -22
- package/src/services/core/websocket/WebSocketManager.ts +23 -6
- package/src/services/core/websocket/connection-service.ts +5 -1
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +4 -0
- package/src/services/task/Task.ts +837 -0
- package/src/services/task/TaskFactory.ts +55 -0
- package/src/services/task/TaskManager.ts +793 -521
- package/src/services/task/TaskUtils.ts +314 -24
- package/src/services/task/ai-docs/AGENTS.md +457 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +594 -0
- package/src/services/task/ai-docs/task-spec.md +1319 -0
- package/src/services/task/constants.ts +23 -0
- package/src/services/task/dialer.ts +136 -1
- package/src/services/task/digital/Digital.ts +95 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +1166 -0
- package/src/services/task/state-machine/actions.ts +738 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +458 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1137 -0
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2177 -0
- package/src/services/task/state-machine/constants.ts +172 -0
- package/src/services/task/state-machine/guards.ts +445 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +243 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +961 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +734 -71
- package/src/services/task/voice/Voice.ts +1270 -0
- package/src/services/task/voice/WebRTC.ts +187 -0
- package/src/types.ts +205 -2
- package/src/utils/AGENTS.md +278 -0
- package/src/utils/ai-docs/utils-spec.md +381 -0
- package/src/webex.js +2 -0
- package/test/unit/spec/cc.ts +503 -43
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
- package/test/unit/spec/services/UserPreference.ts +401 -0
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/config/index.ts +85 -29
- package/test/unit/spec/services/core/Utils.ts +481 -2
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
- package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +477 -0
- package/test/unit/spec/services/task/TaskFactory.ts +62 -0
- package/test/unit/spec/services/task/TaskManager.ts +1001 -1003
- package/test/unit/spec/services/task/TaskUtils.ts +235 -0
- package/test/unit/spec/services/task/dialer.ts +372 -96
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +2651 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +637 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2663 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +649 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
- package/dist/services/task/index.js +0 -1525
- package/dist/services/task/index.js.map +0 -1
- package/src/services/task/index.ts +0 -1801
- package/test/unit/spec/services/task/index.ts +0 -2184
|
@@ -3,23 +3,38 @@ import {ICall, LINE_EVENTS} from '@webex/calling';
|
|
|
3
3
|
import {WebSocketManager} from '../core/websocket/WebSocketManager';
|
|
4
4
|
import routingContact from './contact';
|
|
5
5
|
import WebCallingService from '../WebCallingService';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MEDIA_CHANNEL,
|
|
8
|
+
TASK_EVENTS,
|
|
9
|
+
TaskData,
|
|
10
|
+
TaskId,
|
|
11
|
+
ITask,
|
|
12
|
+
WebSocketPayload,
|
|
13
|
+
WebSocketMessage,
|
|
14
|
+
TaskEventActions,
|
|
15
|
+
EventContext,
|
|
16
|
+
} from './types';
|
|
7
17
|
import {TASK_MANAGER_FILE} from '../../constants';
|
|
8
|
-
import {METHODS} from './constants';
|
|
9
|
-
import {CC_EVENTS,
|
|
10
|
-
import {LoginOption} from '../../types';
|
|
18
|
+
import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
|
|
19
|
+
import {CC_EVENTS, WrapupData} from '../config/types';
|
|
20
|
+
import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
|
|
11
21
|
import LoggerProxy from '../../logger-proxy';
|
|
12
|
-
import Task from '.';
|
|
13
|
-
import MetricsManager from '../../metrics/MetricsManager';
|
|
14
|
-
import {METRIC_EVENT_NAMES} from '../../metrics/constants';
|
|
15
22
|
import {
|
|
16
|
-
checkParticipantNotInInteraction,
|
|
17
23
|
getIsConferenceInProgress,
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
isCampaignPreviewTask,
|
|
25
|
+
isCampaignPreviewReservation,
|
|
20
26
|
isSecondaryEpDnAgent,
|
|
21
27
|
shouldAutoAnswerTask,
|
|
22
28
|
} from './TaskUtils';
|
|
29
|
+
import TaskFactory from './TaskFactory';
|
|
30
|
+
import WebRTC from './voice/WebRTC';
|
|
31
|
+
import {TaskEvent, type TaskEventPayload} from './state-machine';
|
|
32
|
+
import {normalizeTaskData} from './taskDataNormalizer';
|
|
33
|
+
import {ApiAIAssistant} from '../ApiAiAssistant';
|
|
34
|
+
|
|
35
|
+
const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
|
|
36
|
+
|
|
37
|
+
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
|
|
23
38
|
|
|
24
39
|
/** @internal */
|
|
25
40
|
export default class TaskManager extends EventEmitter {
|
|
@@ -33,35 +48,89 @@ export default class TaskManager extends EventEmitter {
|
|
|
33
48
|
private taskCollection: Record<TaskId, ITask>;
|
|
34
49
|
private webCallingService: WebCallingService;
|
|
35
50
|
private webSocketManager: WebSocketManager;
|
|
36
|
-
private
|
|
37
|
-
|
|
51
|
+
private rtdWebSocketManager: WebSocketManager;
|
|
52
|
+
// eslint-disable-next-line no-use-before-define
|
|
53
|
+
private static taskManager: TaskManager;
|
|
54
|
+
private configFlags?: ConfigFlags;
|
|
38
55
|
private wrapupData: WrapupData;
|
|
39
56
|
private agentId: string;
|
|
40
57
|
private webRtcEnabled: boolean;
|
|
58
|
+
private apiAIAssistant?: ApiAIAssistant;
|
|
41
59
|
/**
|
|
42
60
|
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
|
|
43
61
|
* @param webCallingService - Webrtc Service Layer
|
|
44
62
|
* @param webSocketManager - Websocket Manager to maintain websocket connection and keepalives
|
|
45
63
|
*/
|
|
46
64
|
constructor(
|
|
65
|
+
apiAIAssistant: ApiAIAssistant,
|
|
47
66
|
contact: ReturnType<typeof routingContact>,
|
|
48
67
|
webCallingService: WebCallingService,
|
|
49
|
-
webSocketManager: WebSocketManager
|
|
68
|
+
webSocketManager: WebSocketManager,
|
|
69
|
+
rtdWebSocketManager: WebSocketManager
|
|
50
70
|
) {
|
|
51
71
|
super();
|
|
72
|
+
this.apiAIAssistant = apiAIAssistant;
|
|
52
73
|
this.contact = contact;
|
|
53
|
-
this.taskCollection = {};
|
|
54
74
|
this.webCallingService = webCallingService;
|
|
55
75
|
this.webSocketManager = webSocketManager;
|
|
56
|
-
this.
|
|
76
|
+
this.rtdWebSocketManager = rtdWebSocketManager;
|
|
77
|
+
this.taskCollection = {};
|
|
78
|
+
this.webRtcEnabled = false;
|
|
79
|
+
|
|
57
80
|
this.registerTaskListeners();
|
|
58
81
|
this.registerIncomingCallEvent();
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
public handleRealtimeWebsocketEvent(event: string) {
|
|
85
|
+
try {
|
|
86
|
+
const payload = JSON.parse(event);
|
|
87
|
+
|
|
88
|
+
const interactionId = payload?.data?.data?.conversationId;
|
|
89
|
+
if (!interactionId) return;
|
|
90
|
+
|
|
91
|
+
const task = this.taskCollection[interactionId];
|
|
92
|
+
if (!task) {
|
|
93
|
+
LoggerProxy.info(`Realtime transcription task not found`, {
|
|
94
|
+
module: TASK_MANAGER_FILE,
|
|
95
|
+
method: METHODS.HANDLE_REAL_TIME_WEBSOCKET_EVENT,
|
|
96
|
+
interactionId,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (payload.type) {
|
|
103
|
+
case CC_EVENTS.REAL_TIME_TRANSCRIPTION:
|
|
104
|
+
case CC_EVENTS.SUGGESTED_RESPONSE:
|
|
105
|
+
task.emit(payload.type, payload.data);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
LoggerProxy.error('Failed to parse RTD WebSocket message', {
|
|
110
|
+
module: TASK_MANAGER_FILE,
|
|
111
|
+
method: METHODS.HANDLE_REAL_TIME_WEBSOCKET_EVENT,
|
|
112
|
+
error,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set config flags for task creation
|
|
119
|
+
*/
|
|
120
|
+
public setConfigFlags(configFlags: ConfigFlags) {
|
|
121
|
+
this.configFlags = configFlags;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set wrapup configuration data
|
|
126
|
+
*/
|
|
61
127
|
public setWrapupData(wrapupData: WrapupData) {
|
|
62
128
|
this.wrapupData = wrapupData;
|
|
63
129
|
}
|
|
64
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Set agent ID for task operations
|
|
133
|
+
*/
|
|
65
134
|
public setAgentId(agentId: string) {
|
|
66
135
|
this.agentId = agentId;
|
|
67
136
|
}
|
|
@@ -81,7 +150,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
81
150
|
|
|
82
151
|
private handleIncomingWebCall = (call: ICall) => {
|
|
83
152
|
const currentTask = Object.values(this.taskCollection).find(
|
|
84
|
-
(task) =>
|
|
153
|
+
(task) =>
|
|
154
|
+
task.data.interaction.mediaType === 'telephony' && !isCampaignPreviewReservation(task)
|
|
85
155
|
);
|
|
86
156
|
|
|
87
157
|
if (currentTask) {
|
|
@@ -91,7 +161,15 @@ export default class TaskManager extends EventEmitter {
|
|
|
91
161
|
method: METHODS.HANDLE_INCOMING_WEB_CALL,
|
|
92
162
|
interactionId: currentTask.data.interactionId,
|
|
93
163
|
});
|
|
94
|
-
|
|
164
|
+
|
|
165
|
+
// Send TASK_INCOMING to state machine - it will emit on the task object
|
|
166
|
+
const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
|
|
167
|
+
CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
168
|
+
currentTask.data
|
|
169
|
+
);
|
|
170
|
+
if (eventPayload && currentTask) {
|
|
171
|
+
currentTask.sendStateMachineEvent(eventPayload);
|
|
172
|
+
}
|
|
95
173
|
}
|
|
96
174
|
this.call = call;
|
|
97
175
|
};
|
|
@@ -104,471 +182,634 @@ export default class TaskManager extends EventEmitter {
|
|
|
104
182
|
this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
|
|
105
183
|
}
|
|
106
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Map WebSocket CC_EVENTS to state machine TaskEvent
|
|
187
|
+
* @param ccEvent - The CC_EVENT type from WebSocket
|
|
188
|
+
* @param payload - The event payload
|
|
189
|
+
* @param agentId - Optional agent ID for state detection (needed for HYDRATE)
|
|
190
|
+
* @returns TaskEventPayload for state machine or null if no mapping
|
|
191
|
+
*/
|
|
192
|
+
private static mapEventToTaskStateMachineEvent(
|
|
193
|
+
ccEvent: CC_EVENTS,
|
|
194
|
+
payload: WebSocketPayload,
|
|
195
|
+
agentId?: string
|
|
196
|
+
): TaskEventPayload | null {
|
|
197
|
+
const mediaResourceId =
|
|
198
|
+
payload.mediaResourceId ||
|
|
199
|
+
payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
|
|
200
|
+
|
|
201
|
+
switch (ccEvent) {
|
|
202
|
+
// CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
|
|
203
|
+
case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
|
|
204
|
+
if (isCampaignPreviewTask(payload)) {
|
|
205
|
+
return {
|
|
206
|
+
type: TaskEvent.TASK_INCOMING,
|
|
207
|
+
taskData: payload,
|
|
208
|
+
isCampaignReservationAccept: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {type: TaskEvent.TASK_INCOMING, taskData: payload};
|
|
213
|
+
|
|
214
|
+
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: // -> TASK_INCOMING (campaign branch via guard)
|
|
215
|
+
return {
|
|
216
|
+
type: TaskEvent.TASK_INCOMING,
|
|
217
|
+
taskData: payload,
|
|
218
|
+
isCampaignReservationAccept: true,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
|
|
222
|
+
return {type: TaskEvent.TASK_OFFERED, taskData: payload};
|
|
223
|
+
|
|
224
|
+
case CC_EVENTS.AGENT_CONTACT: // AgentContact -> HYDRATE
|
|
225
|
+
// Include agentId for state detection (e.g., checking isWrapUp in participant data)
|
|
226
|
+
return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
|
|
227
|
+
|
|
228
|
+
case CC_EVENTS.CONTACT_UPDATED:
|
|
229
|
+
return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
|
|
230
|
+
case CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
231
|
+
return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
|
|
232
|
+
|
|
233
|
+
case CC_EVENTS.AGENT_OFFER_CONSULT: // AgentOfferConsult -> OFFER_CONSULT
|
|
234
|
+
return {
|
|
235
|
+
type: TaskEvent.OFFER_CONSULT,
|
|
236
|
+
taskData: {...payload, isConsulted: true},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
case CC_EVENTS.AGENT_CONTACT_ASSIGNED: // AgentContactAssigned -> ASSIGN
|
|
240
|
+
return {type: TaskEvent.ASSIGN, taskData: payload};
|
|
241
|
+
|
|
242
|
+
case CC_EVENTS.AGENT_CONTACT_HELD:
|
|
243
|
+
return {
|
|
244
|
+
type: TaskEvent.HOLD_SUCCESS,
|
|
245
|
+
mediaResourceId: mediaResourceId || '',
|
|
246
|
+
taskData: payload,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
case CC_EVENTS.AGENT_CONTACT_UNHELD:
|
|
250
|
+
return {
|
|
251
|
+
type: TaskEvent.UNHOLD_SUCCESS,
|
|
252
|
+
mediaResourceId: mediaResourceId || '',
|
|
253
|
+
taskData: payload,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
case CC_EVENTS.AGENT_CONSULT_CREATED:
|
|
257
|
+
return {
|
|
258
|
+
type: TaskEvent.CONSULT_CREATED,
|
|
259
|
+
taskData: {...payload, isConsulted: false},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
case CC_EVENTS.AGENT_CONSULTING: // AgentConsulting -> CONSULTING_ACTIVE
|
|
263
|
+
// use context to figure out if it's the initiator or receiver using consultInitiator from context
|
|
264
|
+
return {
|
|
265
|
+
type: TaskEvent.CONSULTING_ACTIVE,
|
|
266
|
+
consultDestinationAgentJoined: true,
|
|
267
|
+
taskData: payload,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
case CC_EVENTS.AGENT_CONSULT_ENDED: // AgentConsultEnded -> CONSULT_END
|
|
271
|
+
return {type: TaskEvent.CONSULT_END, taskData: payload};
|
|
272
|
+
|
|
273
|
+
case CC_EVENTS.AGENT_CONSULT_FAILED:
|
|
274
|
+
case CC_EVENTS.AGENT_CTQ_FAILED:
|
|
275
|
+
return {type: TaskEvent.CONSULT_FAILED, reason: payload.reason, taskData: payload};
|
|
276
|
+
|
|
277
|
+
case CC_EVENTS.AGENT_CTQ_CANCELLED:
|
|
278
|
+
return {type: TaskEvent.CTQ_CANCEL, taskData: payload};
|
|
279
|
+
|
|
280
|
+
case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
|
|
281
|
+
return {type: TaskEvent.CTQ_CANCEL_FAILED, taskData: payload};
|
|
282
|
+
|
|
283
|
+
case CC_EVENTS.AGENT_BLIND_TRANSFERRED: // AgentBlindTransferred -> TRANSFER_SUCCESS
|
|
284
|
+
case CC_EVENTS.AGENT_CONSULT_TRANSFERRED: // AgentConsultTransferred -> TRANSFER_SUCCESS
|
|
285
|
+
case CC_EVENTS.AGENT_VTEAM_TRANSFERRED: // AgentVTeamTransferred -> TRANSFER_SUCCESS
|
|
286
|
+
return {
|
|
287
|
+
type: TaskEvent.TRANSFER_SUCCESS,
|
|
288
|
+
taskData: payload,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
case CC_EVENTS.AGENT_WRAPUP:
|
|
292
|
+
return {type: TaskEvent.TASK_WRAPUP, taskData: {...payload, wrapUpRequired: true}};
|
|
293
|
+
case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
|
|
294
|
+
return null; // Add WRAPUP if needed
|
|
295
|
+
|
|
296
|
+
case CC_EVENTS.AGENT_BLIND_TRANSFER_FAILED:
|
|
297
|
+
case CC_EVENTS.AGENT_VTEAM_TRANSFER_FAILED:
|
|
298
|
+
case CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED:
|
|
299
|
+
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
300
|
+
return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
|
|
301
|
+
|
|
302
|
+
case CC_EVENTS.CONTACT_ENDED:
|
|
303
|
+
return {
|
|
304
|
+
type: TaskEvent.CONTACT_ENDED,
|
|
305
|
+
taskData: {
|
|
306
|
+
...payload,
|
|
307
|
+
wrapUpRequired: isCampaignPreviewTask(payload)
|
|
308
|
+
? false
|
|
309
|
+
: payload.agentsPendingWrapUp?.includes(agentId || '') || false,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
case CC_EVENTS.AGENT_INVITE_FAILED:
|
|
314
|
+
return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
|
|
315
|
+
|
|
316
|
+
case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
|
|
317
|
+
return {type: TaskEvent.ASSIGN_FAILED, reason: payload.reason};
|
|
318
|
+
|
|
319
|
+
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
320
|
+
return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
|
|
321
|
+
|
|
322
|
+
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
|
|
323
|
+
return {type: TaskEvent.OUTBOUND_FAILED, taskData: payload, reason: payload.reason};
|
|
324
|
+
|
|
325
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
|
|
326
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
|
|
327
|
+
|
|
328
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
|
|
329
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED, taskData: payload};
|
|
330
|
+
|
|
331
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
|
|
332
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED, taskData: payload};
|
|
333
|
+
|
|
334
|
+
case CC_EVENTS.CONTACT_RECORDING_STARTED:
|
|
335
|
+
return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
|
|
336
|
+
|
|
337
|
+
case CC_EVENTS.CONTACT_RECORDING_PAUSED:
|
|
338
|
+
return {type: TaskEvent.PAUSE_RECORDING, taskData: payload};
|
|
339
|
+
|
|
340
|
+
case CC_EVENTS.CONTACT_RECORDING_RESUMED:
|
|
341
|
+
return {type: TaskEvent.RESUME_RECORDING, taskData: payload};
|
|
342
|
+
|
|
343
|
+
case CC_EVENTS.AGENT_WRAPPEDUP:
|
|
344
|
+
return {type: TaskEvent.WRAPUP_COMPLETE, taskData: payload};
|
|
345
|
+
|
|
346
|
+
// Conference events - these trigger state machine transition to CONFERENCING
|
|
347
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
|
|
348
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
|
|
349
|
+
case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE:
|
|
350
|
+
return {type: TaskEvent.CONFERENCE_START, taskData: payload};
|
|
351
|
+
|
|
352
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
|
|
353
|
+
return {type: TaskEvent.CONFERENCE_FAILED, reason: payload.reason, taskData: payload};
|
|
354
|
+
|
|
355
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
|
|
356
|
+
return {type: TaskEvent.CONFERENCE_END, taskData: payload};
|
|
357
|
+
|
|
358
|
+
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE:
|
|
359
|
+
return {
|
|
360
|
+
type: TaskEvent.PARTICIPANT_LEAVE,
|
|
361
|
+
taskData: payload,
|
|
362
|
+
participantId: payload?.participantId,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
|
|
366
|
+
return {type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: payload};
|
|
367
|
+
|
|
368
|
+
default:
|
|
369
|
+
// Not all events need state machine mapping
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Register WebSocket message listeners for task events
|
|
376
|
+
*
|
|
377
|
+
* Main entry point that orchestrates event processing through a clear pipeline:
|
|
378
|
+
* 1. Parse and validate incoming WebSocket messages
|
|
379
|
+
* 2. Prepare event context with task and state machine mappings
|
|
380
|
+
* 3. Handle task lifecycle (creation, updates, collection management)
|
|
381
|
+
* 4. Send events to state machine (task-level transitions/emissions)
|
|
382
|
+
* 5. Cleanup is triggered via task events emitted by the state machine
|
|
383
|
+
*
|
|
384
|
+
* This architecture separates concerns:
|
|
385
|
+
* - TaskManager: Manages task collection lifecycle and operational concerns
|
|
386
|
+
* - State Machine: Manages individual task state and event emissions
|
|
387
|
+
*/
|
|
107
388
|
private registerTaskListeners() {
|
|
108
389
|
this.webSocketManager.on('message', (event) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
141
|
-
payload.data,
|
|
142
|
-
this.agentId,
|
|
143
|
-
this.webCallingService.loginOption,
|
|
144
|
-
this.webRtcEnabled
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
task = new Task(
|
|
148
|
-
this.contact,
|
|
149
|
-
this.webCallingService,
|
|
150
|
-
{
|
|
151
|
-
...payload.data,
|
|
152
|
-
wrapUpRequired:
|
|
153
|
-
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
154
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
155
|
-
isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
|
|
156
|
-
},
|
|
157
|
-
this.wrapupData,
|
|
158
|
-
this.agentId
|
|
159
|
-
);
|
|
160
|
-
this.taskCollection[payload.data.interactionId] = task;
|
|
161
|
-
// Condition 1: The state is=new i.e it is a incoming task
|
|
162
|
-
if (payload.data.interaction.state === 'new') {
|
|
163
|
-
LoggerProxy.log(
|
|
164
|
-
`Got AGENT_CONTACT for a task with state=new, sending TASK_INCOMING event`,
|
|
165
|
-
{
|
|
166
|
-
module: TASK_MANAGER_FILE,
|
|
167
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
168
|
-
interactionId: payload.data.interactionId,
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
this.emit(TASK_EVENTS.TASK_INCOMING, task);
|
|
172
|
-
} else {
|
|
173
|
-
// Condition 2: The state is anything else i.e the task was connected
|
|
174
|
-
LoggerProxy.log(
|
|
175
|
-
`Got AGENT_CONTACT for a task with state=${payload.data.interaction.state}, sending TASK_HYDRATE event`,
|
|
176
|
-
{
|
|
177
|
-
module: TASK_MANAGER_FILE,
|
|
178
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
179
|
-
interactionId: payload.data.interactionId,
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
this.emit(TASK_EVENTS.TASK_HYDRATE, task);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
|
|
187
|
-
case CC_EVENTS.AGENT_CONTACT_RESERVED: {
|
|
188
|
-
// Check if auto-answer should happen for this task
|
|
189
|
-
const shouldAutoAnswerReserved = shouldAutoAnswerTask(
|
|
190
|
-
payload.data,
|
|
191
|
-
this.agentId,
|
|
192
|
-
this.webCallingService.loginOption,
|
|
193
|
-
this.webRtcEnabled
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
task = new Task(
|
|
197
|
-
this.contact,
|
|
198
|
-
this.webCallingService,
|
|
199
|
-
{
|
|
200
|
-
...payload.data,
|
|
201
|
-
isConsulted: false,
|
|
202
|
-
isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
|
|
203
|
-
},
|
|
204
|
-
this.wrapupData,
|
|
205
|
-
this.agentId
|
|
206
|
-
);
|
|
207
|
-
this.taskCollection[payload.data.interactionId] = task;
|
|
208
|
-
if (
|
|
209
|
-
this.webCallingService.loginOption !== LoginOption.BROWSER ||
|
|
210
|
-
task.data.interaction.mediaType !== MEDIA_CHANNEL.TELEPHONY // for digital channels
|
|
211
|
-
) {
|
|
212
|
-
this.emit(TASK_EVENTS.TASK_INCOMING, task);
|
|
213
|
-
} else if (this.call) {
|
|
214
|
-
this.emit(TASK_EVENTS.TASK_INCOMING, task);
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
case CC_EVENTS.AGENT_OFFER_CONTACT:
|
|
219
|
-
// We don't have to emit any event here since this will be result of promise.
|
|
220
|
-
task = this.updateTaskData(task, payload.data);
|
|
221
|
-
LoggerProxy.log(`Agent offer contact received for task`, {
|
|
222
|
-
module: TASK_MANAGER_FILE,
|
|
223
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
224
|
-
interactionId: payload.data?.interactionId,
|
|
225
|
-
});
|
|
226
|
-
this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);
|
|
227
|
-
|
|
228
|
-
// Handle auto-answer for offer contact
|
|
229
|
-
this.handleAutoAnswer(task);
|
|
230
|
-
break;
|
|
231
|
-
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
|
|
232
|
-
if (task) {
|
|
233
|
-
task = this.updateTaskData(task, payload.data);
|
|
234
|
-
this.metricsManager.trackEvent(
|
|
235
|
-
METRIC_EVENT_NAMES.TASK_OUTDIAL_FAILED,
|
|
236
|
-
{
|
|
237
|
-
...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
|
|
238
|
-
taskId: payload.data.interactionId,
|
|
239
|
-
reason: payload.data.reasonCode || payload.data.reason,
|
|
240
|
-
},
|
|
241
|
-
['behavioral', 'operational']
|
|
242
|
-
);
|
|
243
|
-
LoggerProxy.log(`Agent outbound failed for task`, {
|
|
244
|
-
module: TASK_MANAGER_FILE,
|
|
245
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
246
|
-
interactionId: payload.data.interactionId,
|
|
247
|
-
});
|
|
248
|
-
task.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, payload.data.reason ?? 'UNKNOWN_REASON');
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
|
|
252
|
-
task = this.updateTaskData(task, payload.data);
|
|
253
|
-
task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
|
|
254
|
-
break;
|
|
255
|
-
case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
|
|
256
|
-
task = this.updateTaskData(task, {
|
|
257
|
-
...payload.data,
|
|
258
|
-
wrapUpRequired: true,
|
|
259
|
-
});
|
|
260
|
-
task.emit(TASK_EVENTS.TASK_END, task);
|
|
261
|
-
break;
|
|
262
|
-
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
263
|
-
case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
|
|
264
|
-
case CC_EVENTS.AGENT_INVITE_FAILED: {
|
|
265
|
-
task = this.updateTaskData(task, payload.data);
|
|
266
|
-
|
|
267
|
-
const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
|
|
268
|
-
[CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED]: 'AGENT_CONTACT_ASSIGN_FAILED',
|
|
269
|
-
[CC_EVENTS.AGENT_INVITE_FAILED]: 'AGENT_INVITE_FAILED',
|
|
270
|
-
};
|
|
271
|
-
const metricEventName: keyof typeof METRIC_EVENT_NAMES =
|
|
272
|
-
eventTypeToMetricMap[payload.data.type] || 'AGENT_RONA';
|
|
273
|
-
|
|
274
|
-
this.metricsManager.trackEvent(
|
|
275
|
-
METRIC_EVENT_NAMES[metricEventName],
|
|
276
|
-
{
|
|
277
|
-
...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
|
|
278
|
-
taskId: payload.data.interactionId,
|
|
279
|
-
reason: payload.data.reason,
|
|
280
|
-
},
|
|
281
|
-
['behavioral', 'operational']
|
|
282
|
-
);
|
|
283
|
-
this.handleTaskCleanup(task);
|
|
284
|
-
task.emit(TASK_EVENTS.TASK_REJECT, payload.data.reason);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
case CC_EVENTS.CONTACT_ENDED:
|
|
288
|
-
// Update task data
|
|
289
|
-
if (task) {
|
|
290
|
-
task = this.updateTaskData(task, {
|
|
291
|
-
...payload.data,
|
|
292
|
-
wrapUpRequired:
|
|
293
|
-
payload.data.interaction.state !== 'new' &&
|
|
294
|
-
!isSecondaryEpDnAgent(payload.data.interaction),
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Handle cleanup based on whether task should be deleted
|
|
298
|
-
this.handleTaskCleanup(task);
|
|
299
|
-
|
|
300
|
-
task?.emit(TASK_EVENTS.TASK_END, task);
|
|
301
|
-
}
|
|
302
|
-
break;
|
|
303
|
-
case CC_EVENTS.CONTACT_MERGED:
|
|
304
|
-
task = this.handleContactMerged(task, payload.data);
|
|
305
|
-
break;
|
|
306
|
-
case CC_EVENTS.AGENT_CONTACT_HELD:
|
|
307
|
-
// As soon as the main interaction is held, we need to emit TASK_HOLD
|
|
308
|
-
task = this.updateTaskData(task, payload.data);
|
|
309
|
-
task.emit(TASK_EVENTS.TASK_HOLD, task);
|
|
310
|
-
break;
|
|
311
|
-
case CC_EVENTS.AGENT_CONTACT_UNHELD:
|
|
312
|
-
// As soon as the main interaction is unheld, we need to emit TASK_RESUME
|
|
313
|
-
task = this.updateTaskData(task, payload.data);
|
|
314
|
-
task.emit(TASK_EVENTS.TASK_RESUME, task);
|
|
315
|
-
break;
|
|
316
|
-
case CC_EVENTS.AGENT_VTEAM_TRANSFERRED:
|
|
317
|
-
task = this.updateTaskData(task, {
|
|
318
|
-
...payload.data,
|
|
319
|
-
wrapUpRequired: true,
|
|
320
|
-
});
|
|
321
|
-
task.emit(TASK_EVENTS.TASK_END, task);
|
|
322
|
-
break;
|
|
323
|
-
case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
|
|
324
|
-
task = this.updateTaskData(task, payload.data);
|
|
325
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, task);
|
|
326
|
-
break;
|
|
327
|
-
case CC_EVENTS.AGENT_CONSULT_CREATED:
|
|
328
|
-
// Received when self agent initiates a consult
|
|
329
|
-
task = this.updateTaskData(task, {
|
|
330
|
-
...payload.data,
|
|
331
|
-
isConsulted: false, // This ensures that the task consult status is always reset
|
|
332
|
-
});
|
|
333
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_CREATED, task);
|
|
334
|
-
break;
|
|
335
|
-
case CC_EVENTS.AGENT_OFFER_CONSULT:
|
|
336
|
-
// Received when other agent sends us a consult offer
|
|
337
|
-
task = this.updateTaskData(task, {
|
|
338
|
-
...payload.data,
|
|
339
|
-
isConsulted: true, // This ensures that the task is marked as us being requested for a consult
|
|
340
|
-
});
|
|
341
|
-
task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);
|
|
342
|
-
|
|
343
|
-
// Handle auto-answer for consult offer
|
|
344
|
-
this.handleAutoAnswer(task);
|
|
345
|
-
break;
|
|
346
|
-
case CC_EVENTS.AGENT_CONSULTING:
|
|
347
|
-
// Received when agent is in an active consult state
|
|
348
|
-
// TODO: Check if we can use backend consult state instead of isConsulted
|
|
349
|
-
task = this.updateTaskData(task, payload.data);
|
|
350
|
-
if (task.data.isConsulted) {
|
|
351
|
-
// Fire only if you are the agent who received the consult request
|
|
352
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
|
|
353
|
-
} else {
|
|
354
|
-
// Fire only if you are the agent who initiated the consult
|
|
355
|
-
task.emit(TASK_EVENTS.TASK_CONSULTING, task);
|
|
356
|
-
}
|
|
357
|
-
break;
|
|
358
|
-
case CC_EVENTS.AGENT_CONSULT_FAILED:
|
|
359
|
-
// This can only be received by the agent who initiated the consult.
|
|
360
|
-
// We need not emit any event here since this will be result of promise
|
|
361
|
-
task = this.updateTaskData(task, payload.data);
|
|
362
|
-
break;
|
|
363
|
-
case CC_EVENTS.AGENT_CONSULT_ENDED:
|
|
364
|
-
task = this.updateTaskData(task, payload.data);
|
|
365
|
-
if (task.data.isConsulted) {
|
|
366
|
-
// This will be the end state of the task as soon as we end the consult in case of
|
|
367
|
-
// us being offered a consult
|
|
368
|
-
this.removeTaskFromCollection(task);
|
|
369
|
-
}
|
|
370
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_END, task);
|
|
371
|
-
break;
|
|
372
|
-
case CC_EVENTS.AGENT_CTQ_CANCELLED:
|
|
373
|
-
// This event is received when the consult using queue is cancelled using API
|
|
374
|
-
task = this.updateTaskData(task, payload.data);
|
|
375
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, task);
|
|
376
|
-
break;
|
|
377
|
-
case CC_EVENTS.AGENT_WRAPUP:
|
|
378
|
-
task = this.updateTaskData(task, {...payload.data, wrapUpRequired: true});
|
|
379
|
-
task.emit(TASK_EVENTS.TASK_END, task);
|
|
380
|
-
break;
|
|
381
|
-
case CC_EVENTS.AGENT_WRAPPEDUP:
|
|
382
|
-
task.cancelAutoWrapupTimer();
|
|
383
|
-
this.removeTaskFromCollection(task);
|
|
384
|
-
task.emit(TASK_EVENTS.TASK_WRAPPEDUP, task);
|
|
385
|
-
break;
|
|
386
|
-
case CC_EVENTS.CONTACT_RECORDING_PAUSED:
|
|
387
|
-
task = this.updateTaskData(task, payload.data);
|
|
388
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_PAUSED, task);
|
|
389
|
-
break;
|
|
390
|
-
case CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED:
|
|
391
|
-
task = this.updateTaskData(task, payload.data);
|
|
392
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED, task);
|
|
393
|
-
break;
|
|
394
|
-
case CC_EVENTS.CONTACT_RECORDING_RESUMED:
|
|
395
|
-
task = this.updateTaskData(task, payload.data);
|
|
396
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_RESUMED, task);
|
|
397
|
-
break;
|
|
398
|
-
case CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED:
|
|
399
|
-
task = this.updateTaskData(task, payload.data);
|
|
400
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
|
|
401
|
-
break;
|
|
402
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
|
|
403
|
-
// Conference is being established - update task state and emit establishing event
|
|
404
|
-
task = this.updateTaskData(task, payload.data);
|
|
405
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
|
|
406
|
-
break;
|
|
407
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
|
|
408
|
-
// Conference started successfully - update task state and emit event
|
|
409
|
-
task = this.updateTaskData(task, payload.data);
|
|
410
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
|
|
411
|
-
break;
|
|
412
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
|
|
413
|
-
// Conference failed - update task state and emit failure event
|
|
414
|
-
task = this.updateTaskData(task, payload.data);
|
|
415
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
|
|
416
|
-
break;
|
|
417
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
|
|
418
|
-
// Conference ended - update task state and emit event
|
|
419
|
-
task = this.updateTaskData(task, payload.data);
|
|
420
|
-
if (
|
|
421
|
-
!task ||
|
|
422
|
-
isPrimary(task, this.agentId) ||
|
|
423
|
-
isParticipantInMainInteraction(task, this.agentId)
|
|
424
|
-
) {
|
|
425
|
-
LoggerProxy.log('Primary or main interaction participant leaving conference');
|
|
426
|
-
} else {
|
|
427
|
-
this.removeTaskFromCollection(task);
|
|
428
|
-
}
|
|
429
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
|
|
430
|
-
break;
|
|
431
|
-
case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
|
|
432
|
-
task = this.updateTaskData(task, {
|
|
433
|
-
...payload.data,
|
|
434
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
435
|
-
});
|
|
436
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
|
|
440
|
-
// Conference ended - update task state and emit event
|
|
441
|
-
|
|
442
|
-
task = this.updateTaskData(task, {
|
|
443
|
-
...payload.data,
|
|
444
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
445
|
-
});
|
|
446
|
-
if (checkParticipantNotInInteraction(task, this.agentId)) {
|
|
447
|
-
if (
|
|
448
|
-
isParticipantInMainInteraction(task, this.agentId) ||
|
|
449
|
-
isPrimary(task, this.agentId)
|
|
450
|
-
) {
|
|
451
|
-
LoggerProxy.log('Primary or main interaction participant leaving conference');
|
|
452
|
-
} else {
|
|
453
|
-
this.removeTaskFromCollection(task);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
|
|
460
|
-
// Conference exit failed - update task state and emit failure event
|
|
461
|
-
task = this.updateTaskData(task, payload.data);
|
|
462
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
|
|
463
|
-
break;
|
|
464
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
|
|
465
|
-
// Conference end failed - update task state with error details and emit failure event
|
|
466
|
-
task = this.updateTaskData(task, payload.data);
|
|
467
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
|
|
468
|
-
break;
|
|
469
|
-
case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
|
|
470
|
-
// Conference was transferred - update task state and emit transfer success event
|
|
471
|
-
// Note: Backend should provide hasLeft and wrapUpRequired status
|
|
472
|
-
task = this.updateTaskData(task, payload.data);
|
|
473
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
|
|
474
|
-
break;
|
|
475
|
-
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
476
|
-
// Conference transfer failed - update task state with error details and emit failure event
|
|
477
|
-
task = this.updateTaskData(task, payload.data);
|
|
478
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
|
|
479
|
-
break;
|
|
480
|
-
case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
|
|
481
|
-
// Post-call activity for participant - update task state with activity details
|
|
482
|
-
task = this.updateTaskData(task, payload.data);
|
|
483
|
-
task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
|
|
484
|
-
break;
|
|
485
|
-
default:
|
|
486
|
-
break;
|
|
487
|
-
}
|
|
488
|
-
if (task) {
|
|
489
|
-
task.emit(payload.data.type, payload.data);
|
|
490
|
-
}
|
|
390
|
+
// Step 1: Parse and validate the message
|
|
391
|
+
const message = TaskManager.parseWebSocketMessage(event);
|
|
392
|
+
if (!message) return;
|
|
393
|
+
|
|
394
|
+
// Step 2: Prepare event context
|
|
395
|
+
const eventContext = this.prepareEventContext(message);
|
|
396
|
+
if (!eventContext) return;
|
|
397
|
+
|
|
398
|
+
const actions = this.handleTaskLifecycleEvent(eventContext);
|
|
399
|
+
|
|
400
|
+
const {task} = actions;
|
|
401
|
+
if (!task) return;
|
|
402
|
+
|
|
403
|
+
const {payload, stateMachineEvent} = eventContext;
|
|
404
|
+
|
|
405
|
+
// Always keep task.data updated (even for mapped events) so consumers relying
|
|
406
|
+
// on TaskManager-managed task instances see the latest payload.
|
|
407
|
+
if (payload) {
|
|
408
|
+
this.updateTaskData(task, payload);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Send event to state machine - this will trigger all TASK_EVENTS emissions
|
|
412
|
+
// including TASK_INCOMING which is now handled via the state machine callbacks
|
|
413
|
+
if (stateMachineEvent) {
|
|
414
|
+
task.sendStateMachineEvent(stateMachineEvent);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Emit TASK_POST_CALL_ACTIVITY for ParticipantPostCallActivity events so
|
|
418
|
+
// consumers (Widgets) can detect the interaction state change to post_call.
|
|
419
|
+
if (eventContext.eventType === CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY) {
|
|
420
|
+
task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
|
|
491
421
|
}
|
|
422
|
+
|
|
423
|
+
// Send transcript start/stop events for relevant CC events
|
|
424
|
+
this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId);
|
|
492
425
|
});
|
|
493
426
|
}
|
|
494
427
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Parse and validate WebSocket message
|
|
430
|
+
* @returns Parsed message or null if invalid/keepalive
|
|
431
|
+
*/
|
|
432
|
+
private static parseWebSocketMessage(event: string): WebSocketMessage | null {
|
|
433
|
+
try {
|
|
434
|
+
const payload = JSON.parse(event) as WebSocketMessage;
|
|
499
435
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
});
|
|
505
|
-
}
|
|
436
|
+
// Filter out keepalive messages
|
|
437
|
+
if (payload?.keepalive === 'true' || payload?.keepalive === true) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
506
440
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
441
|
+
// Normalize task data if present
|
|
442
|
+
if (payload?.data?.interaction) {
|
|
443
|
+
payload.data = normalizeTaskData(payload.data);
|
|
444
|
+
}
|
|
510
445
|
|
|
511
|
-
return
|
|
446
|
+
return payload;
|
|
512
447
|
} catch (error) {
|
|
513
|
-
LoggerProxy.error(
|
|
448
|
+
LoggerProxy.error('Failed to parse WebSocket message', {
|
|
514
449
|
module: TASK_MANAGER_FILE,
|
|
515
|
-
method:
|
|
516
|
-
|
|
450
|
+
method: 'parseWebSocketMessage',
|
|
451
|
+
error,
|
|
517
452
|
});
|
|
518
453
|
|
|
519
|
-
return
|
|
454
|
+
return null;
|
|
520
455
|
}
|
|
521
456
|
}
|
|
522
457
|
|
|
523
458
|
/**
|
|
524
|
-
*
|
|
525
|
-
* @
|
|
526
|
-
* @param taskData - The task data from the event payload
|
|
527
|
-
* @returns Updated or newly created task
|
|
528
|
-
* @private
|
|
459
|
+
* Prepare context for event processing
|
|
460
|
+
* @returns Event context or null if event type is invalid
|
|
529
461
|
*/
|
|
530
|
-
private
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
462
|
+
private prepareEventContext(message: WebSocketMessage): EventContext | null {
|
|
463
|
+
const eventType = message.data?.type || message.type;
|
|
464
|
+
|
|
465
|
+
if (!eventType || !isCcEvent(eventType)) {
|
|
466
|
+
return null;
|
|
534
467
|
}
|
|
535
468
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
469
|
+
const interactionId = message.data.interactionId;
|
|
470
|
+
const reservationInteractionId = message.data.reservationInteractionId;
|
|
471
|
+
let task = this.taskCollection[interactionId];
|
|
472
|
+
|
|
473
|
+
// When a campaign preview contact is accepted, the assigned event may arrive
|
|
474
|
+
// with a new interactionId while the task is stored under the original
|
|
475
|
+
// reservationInteractionId. Fall back to that key so the task is found.
|
|
476
|
+
if (!task && reservationInteractionId) {
|
|
477
|
+
task = this.taskCollection[reservationInteractionId];
|
|
478
|
+
if (task) {
|
|
479
|
+
// Re-key the task under the new interaction ID and remove the old entry
|
|
480
|
+
delete this.taskCollection[reservationInteractionId];
|
|
481
|
+
this.taskCollection[interactionId] = task;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const wasConsultedTask = Boolean(task?.data?.isConsulted);
|
|
486
|
+
const computeWrapUpRequired = () => {
|
|
487
|
+
if (message.data.wrapUpRequired !== undefined) {
|
|
488
|
+
return message.data.wrapUpRequired;
|
|
489
|
+
}
|
|
490
|
+
if (message.data.isConsulted !== undefined) {
|
|
491
|
+
return !message.data.isConsulted;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return !wasConsultedTask;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const adjustedPayload =
|
|
498
|
+
eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
|
|
499
|
+
eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
|
|
500
|
+
eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
|
|
501
|
+
? {
|
|
502
|
+
...message.data,
|
|
503
|
+
wrapUpRequired: computeWrapUpRequired(),
|
|
504
|
+
}
|
|
505
|
+
: message.data;
|
|
506
|
+
|
|
507
|
+
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
|
|
508
|
+
eventType,
|
|
509
|
+
adjustedPayload,
|
|
510
|
+
this.agentId
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
LoggerProxy.info(`Handling task event ${eventType}`, {
|
|
514
|
+
module: TASK_MANAGER_FILE,
|
|
515
|
+
method: 'prepareEventContext',
|
|
516
|
+
interactionId,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
eventType,
|
|
521
|
+
payload: adjustedPayload,
|
|
522
|
+
task,
|
|
523
|
+
stateMachineEvent,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Handle task lifecycle events and determine required actions
|
|
529
|
+
*
|
|
530
|
+
* Delegates to specific event handlers based on event type. Each handler
|
|
531
|
+
* is responsible for TaskManager-level concerns:
|
|
532
|
+
* - Task creation and collection management
|
|
533
|
+
* - Metrics tracking
|
|
534
|
+
* - Resource cleanup decisions
|
|
535
|
+
*
|
|
536
|
+
* Note: Task-level state transitions and event emissions are handled by
|
|
537
|
+
* the task state machine via sendStateMachineEvent()
|
|
538
|
+
*/
|
|
539
|
+
private handleTaskLifecycleEvent(context: EventContext): TaskEventActions {
|
|
540
|
+
const {eventType} = context;
|
|
541
|
+
|
|
542
|
+
switch (eventType) {
|
|
543
|
+
case CC_EVENTS.AGENT_CONTACT_RESERVED:
|
|
544
|
+
return this.handleContactReserved(context);
|
|
545
|
+
|
|
546
|
+
case CC_EVENTS.AGENT_CONTACT:
|
|
547
|
+
return this.handleAgentContact(context);
|
|
548
|
+
|
|
549
|
+
case CC_EVENTS.CONTACT_MERGED:
|
|
550
|
+
return this.handleContactMergedEvent(context);
|
|
551
|
+
|
|
552
|
+
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
|
|
553
|
+
return this.handleCampaignPreviewReservation(context);
|
|
554
|
+
|
|
555
|
+
case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
|
|
556
|
+
return this.handleCampaignContactUpdated(context);
|
|
557
|
+
|
|
558
|
+
default:
|
|
559
|
+
return {task: context.task};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private handleCampaignContactUpdated(context: EventContext) {
|
|
564
|
+
const {payload} = context;
|
|
565
|
+
let {task} = context;
|
|
566
|
+
// CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
|
|
567
|
+
// Update the task data and emit an event so consumers can react to the updated contact.
|
|
568
|
+
// Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
|
|
569
|
+
if (task) {
|
|
570
|
+
// Carry forward campaign preview fields from existing task data since the updated
|
|
571
|
+
// contact payload may not include them, and reconcileData would delete them.
|
|
572
|
+
const existingCpd = task.data?.interaction?.callProcessingDetails;
|
|
573
|
+
const updatedData: TaskData = {...payload};
|
|
574
|
+
|
|
575
|
+
if (existingCpd) {
|
|
576
|
+
const campaignFields = {
|
|
577
|
+
...(existingCpd.campaignPreviewAutoAction && {
|
|
578
|
+
campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
|
|
579
|
+
}),
|
|
580
|
+
...(existingCpd.campaignPreviewOfferTimeout && {
|
|
581
|
+
campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
|
|
582
|
+
}),
|
|
583
|
+
...(existingCpd.campaignPreviewSkipDisabled && {
|
|
584
|
+
campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
|
|
585
|
+
}),
|
|
586
|
+
...(existingCpd.campaignPreviewRemoveDisabled && {
|
|
587
|
+
campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
|
|
588
|
+
}),
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (!updatedData.interaction) {
|
|
592
|
+
updatedData.interaction = {} as typeof updatedData.interaction;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
updatedData.interaction = {
|
|
596
|
+
...updatedData.interaction,
|
|
597
|
+
callProcessingDetails: {
|
|
598
|
+
...campaignFields,
|
|
599
|
+
...(updatedData.interaction.callProcessingDetails || {}),
|
|
600
|
+
} as typeof existingCpd,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
|
|
547
605
|
module: TASK_MANAGER_FILE,
|
|
548
606
|
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
549
|
-
interactionId:
|
|
607
|
+
interactionId: payload.interactionId,
|
|
608
|
+
data: {
|
|
609
|
+
hasCpd: !!updatedData.interaction?.callProcessingDetails,
|
|
610
|
+
autoAction: updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
|
|
611
|
+
skipDisabled: updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
|
|
612
|
+
removeDisabled:
|
|
613
|
+
updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
|
|
614
|
+
},
|
|
550
615
|
});
|
|
551
616
|
|
|
552
|
-
task =
|
|
617
|
+
task = this.updateTaskData(task, updatedData);
|
|
618
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return {task};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Creates or updates a task for campaign preview reservation.
|
|
626
|
+
* TASK_CAMPAIGN_PREVIEW_RESERVATION is emitted by the state machine (campaign TASK_INCOMING branch).
|
|
627
|
+
*/
|
|
628
|
+
private handleCampaignPreviewReservation(context: EventContext): TaskEventActions {
|
|
629
|
+
const {payload} = context;
|
|
630
|
+
let {task} = context;
|
|
631
|
+
|
|
632
|
+
LoggerProxy.log('Campaign preview reservation received', {
|
|
633
|
+
module: TASK_MANAGER_FILE,
|
|
634
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
635
|
+
interactionId: payload.interactionId,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!task) {
|
|
639
|
+
task = TaskFactory.createTask(
|
|
553
640
|
this.contact,
|
|
554
641
|
this.webCallingService,
|
|
555
642
|
{
|
|
556
|
-
...
|
|
557
|
-
wrapUpRequired:
|
|
558
|
-
isConferenceInProgress:
|
|
643
|
+
...payload,
|
|
644
|
+
wrapUpRequired: false,
|
|
645
|
+
isConferenceInProgress: false,
|
|
646
|
+
isAutoAnswering: false,
|
|
559
647
|
},
|
|
648
|
+
this.configFlags,
|
|
560
649
|
this.wrapupData,
|
|
561
650
|
this.agentId
|
|
562
651
|
);
|
|
563
|
-
this.
|
|
652
|
+
this.setupTaskListeners(task);
|
|
653
|
+
this.taskCollection[payload.interactionId] = task;
|
|
654
|
+
} else {
|
|
655
|
+
task = this.updateTaskData(task, payload);
|
|
564
656
|
}
|
|
565
657
|
|
|
566
|
-
|
|
658
|
+
return {task};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Handle AGENT_CONTACT_RESERVED event
|
|
663
|
+
* Creates a new task; state machine event is sent during processing
|
|
664
|
+
*/
|
|
665
|
+
private handleContactReserved(context: EventContext): TaskEventActions {
|
|
666
|
+
const {payload} = context;
|
|
667
|
+
const isConsultedTask =
|
|
668
|
+
payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
|
|
669
|
+
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
670
|
+
payload,
|
|
671
|
+
this.agentId,
|
|
672
|
+
this.webCallingService.loginOption,
|
|
673
|
+
this.webRtcEnabled
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const taskData: TaskData = {
|
|
677
|
+
...payload,
|
|
678
|
+
isConsulted: isConsultedTask,
|
|
679
|
+
isAutoAnswering: shouldAutoAnswer,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const task = TaskFactory.createTask(
|
|
683
|
+
this.contact,
|
|
684
|
+
this.webCallingService,
|
|
685
|
+
taskData,
|
|
686
|
+
this.configFlags,
|
|
687
|
+
this.wrapupData,
|
|
688
|
+
this.agentId
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
this.setupTaskListeners(task);
|
|
692
|
+
this.taskCollection[payload.interactionId] = task;
|
|
693
|
+
|
|
694
|
+
return {task};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Handle AGENT_CONTACT event
|
|
699
|
+
* Re-creates task if missing (multi-session scenario)
|
|
700
|
+
*/
|
|
701
|
+
private handleAgentContact(context: EventContext): TaskEventActions {
|
|
702
|
+
let {task} = context;
|
|
703
|
+
const {payload} = context;
|
|
704
|
+
|
|
705
|
+
if (!task) {
|
|
706
|
+
const isConsultedTask =
|
|
707
|
+
payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
|
|
708
|
+
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
709
|
+
payload,
|
|
710
|
+
this.agentId,
|
|
711
|
+
this.webCallingService.loginOption,
|
|
712
|
+
this.webRtcEnabled
|
|
713
|
+
);
|
|
714
|
+
const taskData: TaskData = {
|
|
715
|
+
...payload,
|
|
716
|
+
isConsulted: isConsultedTask,
|
|
717
|
+
wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
718
|
+
isConferenceInProgress: getIsConferenceInProgress(payload),
|
|
719
|
+
isAutoAnswering: shouldAutoAnswer,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
task = TaskFactory.createTask(
|
|
723
|
+
this.contact,
|
|
724
|
+
this.webCallingService,
|
|
725
|
+
taskData,
|
|
726
|
+
this.configFlags,
|
|
727
|
+
this.wrapupData,
|
|
728
|
+
this.agentId
|
|
729
|
+
);
|
|
730
|
+
this.setupTaskListeners(task);
|
|
731
|
+
this.taskCollection[payload.interactionId] = task;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {task};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private updateTaskData(task: ITask, taskData: TaskData): ITask {
|
|
738
|
+
if (!task) {
|
|
739
|
+
throw new Error('Task not found for update');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const snapshot = task.stateMachineService?.getSnapshot?.();
|
|
743
|
+
const isConsultingFlow =
|
|
744
|
+
snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting';
|
|
745
|
+
|
|
746
|
+
const updateTaskData = isConsultingFlow
|
|
747
|
+
? {
|
|
748
|
+
...taskData,
|
|
749
|
+
destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null,
|
|
750
|
+
destinationType:
|
|
751
|
+
taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null,
|
|
752
|
+
}
|
|
753
|
+
: taskData;
|
|
754
|
+
|
|
755
|
+
task.updateTaskData(updateTaskData);
|
|
756
|
+
this.taskCollection[taskData.interactionId] = task;
|
|
567
757
|
|
|
568
758
|
return task;
|
|
569
759
|
}
|
|
570
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Setup listeners for task events that need to be bubbled up to TaskManager
|
|
763
|
+
* This replaces the previous callback injection pattern
|
|
764
|
+
*/
|
|
765
|
+
private setupTaskListeners(task: ITask): void {
|
|
766
|
+
// Listen for TASK_INCOMING and re-emit so webex.cc can notify consumers
|
|
767
|
+
task.on(TASK_EVENTS.TASK_INCOMING, (t: ITask) => {
|
|
768
|
+
LoggerProxy.log(`Task incoming event received`, {
|
|
769
|
+
module: TASK_MANAGER_FILE,
|
|
770
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
771
|
+
interactionId: t.data?.interactionId,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
this.emit(TASK_EVENTS.TASK_INCOMING, t);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (t: ITask) => {
|
|
778
|
+
LoggerProxy.log(`Campaign preview reservation event received`, {
|
|
779
|
+
module: TASK_MANAGER_FILE,
|
|
780
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
781
|
+
interactionId: t.data?.interactionId,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, t);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Listen for TASK_HYDRATE on the task and re-emit on TaskManager
|
|
788
|
+
task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
|
|
789
|
+
// Task data is already updated by the task itself before emitting
|
|
790
|
+
this.emit(TASK_EVENTS.TASK_HYDRATE, t);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (t: ITask) => {
|
|
794
|
+
this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, t);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Listen for internal cleanup signal emitted by the state machine
|
|
798
|
+
task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
|
|
799
|
+
this.handleTaskCleanup(t);
|
|
800
|
+
if (options?.removeFromCollection) {
|
|
801
|
+
const interactionId = t?.data?.interactionId;
|
|
802
|
+
if (interactionId && this.taskCollection[interactionId]) {
|
|
803
|
+
this.removeTaskFromCollection(t);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
571
809
|
private removeTaskFromCollection(task: ITask) {
|
|
810
|
+
if (typeof task.cancelAutoWrapupTimer === 'function') {
|
|
811
|
+
task.cancelAutoWrapupTimer();
|
|
812
|
+
}
|
|
572
813
|
if (task?.data?.interactionId) {
|
|
573
814
|
delete this.taskCollection[task.data.interactionId];
|
|
574
815
|
LoggerProxy.info(`Task removed from collection`, {
|
|
@@ -580,69 +821,72 @@ export default class TaskManager extends EventEmitter {
|
|
|
580
821
|
}
|
|
581
822
|
|
|
582
823
|
/**
|
|
583
|
-
* Handles
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
* 2. Agent-initiated WebRTC outdial calls
|
|
588
|
-
* 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
|
|
589
|
-
*
|
|
590
|
-
* @param task - The task to auto-answer
|
|
824
|
+
* Handles CONTACT_MERGED event logic
|
|
825
|
+
* @param task - The task to process
|
|
826
|
+
* @param taskData - The task data from the event payload
|
|
827
|
+
* @returns Updated or newly created task
|
|
591
828
|
* @private
|
|
592
829
|
*/
|
|
593
|
-
private
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
830
|
+
private handleContactMergedEvent(context: EventContext): TaskEventActions {
|
|
831
|
+
const {payload} = context;
|
|
832
|
+
let task = context.task;
|
|
597
833
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
interactionId: task.data.interactionId,
|
|
602
|
-
});
|
|
834
|
+
if (payload.childInteractionId && this.taskCollection[payload.childInteractionId]) {
|
|
835
|
+
this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]);
|
|
836
|
+
}
|
|
603
837
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
LoggerProxy.info(`Task auto-answered successfully`, {
|
|
838
|
+
if (task) {
|
|
839
|
+
LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
|
|
607
840
|
module: TASK_MANAGER_FILE,
|
|
608
|
-
method:
|
|
609
|
-
interactionId:
|
|
841
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
842
|
+
interactionId: payload.interactionId,
|
|
610
843
|
});
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
taskId: task.data.interactionId,
|
|
617
|
-
mediaType: task.data.interaction.mediaType,
|
|
618
|
-
isAutoAnswered: true,
|
|
619
|
-
},
|
|
620
|
-
['behavioral', 'operational']
|
|
621
|
-
);
|
|
622
|
-
// Emit task:autoAnswered event for widgets/UI to react
|
|
623
|
-
task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
624
|
-
} catch (error) {
|
|
625
|
-
// Reset isAutoAnswering flag on failure
|
|
626
|
-
task.updateTaskData({...task.data, isAutoAnswering: false});
|
|
627
|
-
LoggerProxy.error(`Failed to auto-answer task`, {
|
|
844
|
+
// update the task data
|
|
845
|
+
this.updateTaskData(task, payload);
|
|
846
|
+
} else {
|
|
847
|
+
// Case2 : Task is not present in taskCollection
|
|
848
|
+
LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
|
|
628
849
|
module: TASK_MANAGER_FILE,
|
|
629
|
-
method:
|
|
630
|
-
interactionId:
|
|
631
|
-
error,
|
|
850
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
851
|
+
interactionId: payload.interactionId,
|
|
632
852
|
});
|
|
633
853
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
854
|
+
const taskData: TaskData = {
|
|
855
|
+
...payload,
|
|
856
|
+
wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
857
|
+
isConferenceInProgress: getIsConferenceInProgress(payload),
|
|
858
|
+
isConsulted: false,
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
task = TaskFactory.createTask(
|
|
862
|
+
this.contact,
|
|
863
|
+
this.webCallingService,
|
|
864
|
+
taskData,
|
|
865
|
+
this.configFlags,
|
|
866
|
+
this.wrapupData,
|
|
867
|
+
this.agentId
|
|
644
868
|
);
|
|
869
|
+
this.taskCollection[payload.interactionId] = task;
|
|
870
|
+
|
|
871
|
+
// Transition the new task out of IDLE immediately so UI controls are
|
|
872
|
+
// computed before TASK_MERGED is emitted. This handles the race where
|
|
873
|
+
// AgentContactAssigned arrives before ContactMerged and gets dropped.
|
|
874
|
+
// Send HYDRATE before setupTaskListeners so the emitTaskHydrate action
|
|
875
|
+
// doesn't bubble up to the Widget (avoids duplicate listener registration).
|
|
876
|
+
task.sendStateMachineEvent({
|
|
877
|
+
type: TaskEvent.HYDRATE,
|
|
878
|
+
taskData,
|
|
879
|
+
agentId: this.agentId,
|
|
880
|
+
} as TaskEventPayload);
|
|
881
|
+
|
|
882
|
+
this.setupTaskListeners(task);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (task) {
|
|
886
|
+
this.emit(TASK_EVENTS.TASK_MERGED, task);
|
|
645
887
|
}
|
|
888
|
+
|
|
889
|
+
return {task};
|
|
646
890
|
}
|
|
647
891
|
|
|
648
892
|
/**
|
|
@@ -654,7 +898,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
654
898
|
// Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
|
|
655
899
|
if (
|
|
656
900
|
this.webCallingService.loginOption === LoginOption.BROWSER &&
|
|
657
|
-
task.data.interaction.mediaType ===
|
|
901
|
+
task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
|
|
902
|
+
task instanceof WebRTC
|
|
658
903
|
) {
|
|
659
904
|
task.unregisterWebCallListeners();
|
|
660
905
|
this.webCallingService.cleanUpCall();
|
|
@@ -662,45 +907,72 @@ export default class TaskManager extends EventEmitter {
|
|
|
662
907
|
|
|
663
908
|
const isOutdial = task.data.interaction.outboundType === 'OUTDIAL';
|
|
664
909
|
const isNew = task.data.interaction.state === 'new';
|
|
665
|
-
const needsWrapUp = task.data.agentsPendingWrapUp?.
|
|
910
|
+
const needsWrapUp = task.data.agentsPendingWrapUp?.includes(this.agentId) ?? false;
|
|
666
911
|
|
|
667
912
|
// For OUTDIAL: only remove if NOT terminated (user-declined, no wrap-up follows)
|
|
668
|
-
// If terminated, keep task for wrap-up flow (CONTACT_ENDED → AGENT_WRAPUP)
|
|
669
913
|
// For non-OUTDIAL: remove if state is 'new'
|
|
670
914
|
// Always remove if secondary EpDn agent
|
|
671
|
-
if (
|
|
915
|
+
if (
|
|
916
|
+
(isNew && !(isOutdial && needsWrapUp)) ||
|
|
917
|
+
isSecondaryEpDnAgent(task.data.interaction) ||
|
|
918
|
+
(!needsWrapUp && isOutdial) // For outdial tasks, needs wrap-up is false and state is "WRAPUP". We need to just remove the task.
|
|
919
|
+
) {
|
|
672
920
|
this.removeTaskFromCollection(task);
|
|
673
921
|
}
|
|
674
922
|
}
|
|
675
923
|
|
|
676
924
|
/**
|
|
677
|
-
*
|
|
925
|
+
* Sends transcript start/stop event based on the CC event type.
|
|
926
|
+
* Fire-and-forget; errors are logged but do not interrupt event processing.
|
|
678
927
|
*/
|
|
679
|
-
|
|
928
|
+
private requestRealTimeTranscripts(eventType: string, interactionId: string): void {
|
|
929
|
+
const action = TRANSCRIPT_EVENT_MAP[eventType];
|
|
930
|
+
if (!action || !this.apiAIAssistant) return;
|
|
931
|
+
if (this.configFlags?.aiFeature?.realtimeTranscripts?.enable !== true) return;
|
|
932
|
+
|
|
933
|
+
this.apiAIAssistant
|
|
934
|
+
.sendEvent(
|
|
935
|
+
this.agentId,
|
|
936
|
+
interactionId,
|
|
937
|
+
AIAssistantEventType.CUSTOM_EVENT,
|
|
938
|
+
AIAssistantEventName.GET_TRANSCRIPTS,
|
|
939
|
+
{action}
|
|
940
|
+
)
|
|
941
|
+
.catch((error) => {
|
|
942
|
+
LoggerProxy.error(`Failed to send transcript ${action} event`, {
|
|
943
|
+
module: TASK_MANAGER_FILE,
|
|
944
|
+
method: METHODS.REQUEST_REAL_TIME_TRANSCRIPTS,
|
|
945
|
+
interactionId,
|
|
946
|
+
error,
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
public getTask(taskId: TaskId): ITask {
|
|
680
952
|
return this.taskCollection[taskId];
|
|
681
|
-
}
|
|
953
|
+
}
|
|
682
954
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
public getAllTasks = (): Record<TaskId, ITask> => {
|
|
687
|
-
return this.taskCollection;
|
|
688
|
-
};
|
|
955
|
+
public getAllTasks(): Record<TaskId, ITask> {
|
|
956
|
+
return {...this.taskCollection};
|
|
957
|
+
}
|
|
689
958
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
* @param webCallingService - Webrtc Service Layer
|
|
693
|
-
* @param webSocketManager - Websocket Manager to maintain websocket connection and keepalives
|
|
694
|
-
*/
|
|
695
|
-
public static getTaskManager = (
|
|
959
|
+
public static getTaskManager(
|
|
960
|
+
apiAIAssistant: ApiAIAssistant,
|
|
696
961
|
contact: ReturnType<typeof routingContact>,
|
|
697
962
|
webCallingService: WebCallingService,
|
|
698
|
-
webSocketManager: WebSocketManager
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
963
|
+
webSocketManager: WebSocketManager,
|
|
964
|
+
rtdWebSocketManager?: WebSocketManager
|
|
965
|
+
): TaskManager {
|
|
966
|
+
if (!TaskManager.taskManager) {
|
|
967
|
+
TaskManager.taskManager = new TaskManager(
|
|
968
|
+
apiAIAssistant,
|
|
969
|
+
contact,
|
|
970
|
+
webCallingService,
|
|
971
|
+
webSocketManager,
|
|
972
|
+
rtdWebSocketManager
|
|
973
|
+
);
|
|
702
974
|
}
|
|
703
975
|
|
|
704
|
-
return
|
|
705
|
-
}
|
|
976
|
+
return TaskManager.taskManager;
|
|
977
|
+
}
|
|
706
978
|
}
|