@webex/contact-center 3.12.0-llmrefactor.3 → 3.12.0-llmrefactor.4
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 +1 -1
- package/ai-docs/CONTRACTS.md +1 -1
- package/dist/services/task/TaskManager.js +185 -9
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +9 -9
- package/src/services/task/TaskManager.ts +298 -31
- package/src/services/task/ai-docs/task-spec.md +36 -1
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +11 -3
- package/test/unit/spec/services/task/TaskManager.ts +641 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
package/.sdd/manifest.json
CHANGED
|
@@ -309,7 +309,7 @@
|
|
|
309
309
|
"canonical_spec": "src/services/task/ai-docs/task-spec.md",
|
|
310
310
|
"contracts": {
|
|
311
311
|
"provides": [
|
|
312
|
-
"Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
|
|
312
|
+
"Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager lifecycle correlation, narrowly scoped backend-authoritative ContactOwnerChanged recovery, and task-event publication, including routing owner-changing ContactUpdated through the existing task:hydrate event; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
|
|
313
313
|
],
|
|
314
314
|
"requires": [
|
|
315
315
|
"Contact/dialer AqmReqs; WebSocket managers; @webex/calling; MetricsManager; XState task-state module"
|
package/ai-docs/CONTRACTS.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
| Contract ID | Owner module | Event / topic | Direction | Payload schema link | Delivery guarantees | Compatibility / deprecation | Defined at |
|
|
23
23
|
|---|---|---|---|---|---|---|---|
|
|
24
24
|
| `agent.events` | Agent | `agent:*` | publish to application | agent spec/types | realtime; backend ordering/correlation | additive constants | `src/services/agent/types.ts` |
|
|
25
|
-
| `task.events` | Task | `task
|
|
25
|
+
| `task.events` | Task | `task:*`, including existing `task:hydrate` | consume/map backend task lifecycle events, then publish to application/task | task spec `TASK-R-010` / types | `ContactOwnerChanged` and owner-changing `ContactUpdated` route through `CONTACT_OWNER_CHANGED`; a complete promoted-current-Agent owner change may narrowly recover a missing task; consumers receive one `task:hydrate` and no synthetic `task:incoming` | compatible semantic extension of an existing event; no new event constant; backend `interaction.owner` remains authoritative | `src/services/task/TaskManager.ts`, `src/services/task/Task.ts`, `src/services/task/types.ts` |
|
|
26
26
|
| `task.wxapp-mute.events` | Task | `task:wxapp-mute-state-updated` | publish to application/task | task spec/types | event-driven | additive | `src/services/task/types.ts`, `src/services/task/voice/Voice.ts` |
|
|
27
27
|
| `cc.events` | Config/Core | backend CC_EVENTS | consume/map | config/core specs | remote WebSocket delivery; AQM correlation where configured | backend contract | `src/services/config/types.ts` |
|
|
28
28
|
| `rtd.events` | Task | realtime transcription/suggestion | consume then publish per task | task spec | websocket best-effort according to remote service | additive payloads | `src/services/task/TaskManager.ts` |
|
|
@@ -17,6 +17,7 @@ var _TaskFactory = _interopRequireDefault(require("./TaskFactory"));
|
|
|
17
17
|
var _WebexCallingUtils = require("./WebexCallingUtils");
|
|
18
18
|
var _WebRTC = _interopRequireDefault(require("./voice/WebRTC"));
|
|
19
19
|
var _stateMachine = require("./state-machine");
|
|
20
|
+
var _constants3 = require("./state-machine/constants");
|
|
20
21
|
var _taskDataNormalizer = require("./taskDataNormalizer");
|
|
21
22
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
22
23
|
const CC_EVENT_SET = new Set(Object.values(_types2.CC_EVENTS));
|
|
@@ -186,6 +187,12 @@ class TaskManager extends _events.default {
|
|
|
186
187
|
agentId
|
|
187
188
|
};
|
|
188
189
|
case _types2.CC_EVENTS.CONTACT_UPDATED:
|
|
190
|
+
if (task && typeof payload.interaction?.owner === 'string' && payload.interaction.owner.trim().length > 0 && payload.interaction.owner !== task.data?.interaction?.owner) {
|
|
191
|
+
return {
|
|
192
|
+
type: _stateMachine.TaskEvent.CONTACT_OWNER_CHANGED,
|
|
193
|
+
taskData: payload
|
|
194
|
+
};
|
|
195
|
+
}
|
|
189
196
|
return {
|
|
190
197
|
type: _stateMachine.TaskEvent.CONTACT_UPDATED,
|
|
191
198
|
taskData: payload
|
|
@@ -561,6 +568,9 @@ class TaskManager extends _events.default {
|
|
|
561
568
|
this.taskCollection[interactionId] = task;
|
|
562
569
|
}
|
|
563
570
|
}
|
|
571
|
+
if (!task && eventType === _types2.CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
572
|
+
task = this.findTaskForContactOwnerChange(message.data);
|
|
573
|
+
}
|
|
564
574
|
if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
|
|
565
575
|
task = this.findUniqueTaskByRelatedInteraction(message.data);
|
|
566
576
|
}
|
|
@@ -574,10 +584,16 @@ class TaskManager extends _events.default {
|
|
|
574
584
|
}
|
|
575
585
|
return !wasConsultedTask;
|
|
576
586
|
};
|
|
577
|
-
|
|
587
|
+
let adjustedPayload = eventType === _types2.CC_EVENTS.AGENT_CONSULT_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_BLIND_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_VTEAM_TRANSFERRED ? {
|
|
578
588
|
...message.data,
|
|
579
589
|
wrapUpRequired: computeWrapUpRequired()
|
|
580
590
|
} : message.data;
|
|
591
|
+
if (task && eventType === _types2.CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
592
|
+
adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
|
|
593
|
+
}
|
|
594
|
+
if (task && eventType === _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
595
|
+
adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
|
|
596
|
+
}
|
|
581
597
|
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(eventType, adjustedPayload, this.agentId, task, this.configFlags?.enableWxBetterTogether);
|
|
582
598
|
_loggerProxy.default.info(`Handling task event ${eventType}`, {
|
|
583
599
|
module: _constants.TASK_MANAGER_FILE,
|
|
@@ -592,20 +608,108 @@ class TaskManager extends _events.default {
|
|
|
592
608
|
};
|
|
593
609
|
}
|
|
594
610
|
|
|
611
|
+
/**
|
|
612
|
+
* ContactOwnerChanged can identify the promoted agent's child interaction while
|
|
613
|
+
* the surviving task is stored under the main interaction. Accept one related
|
|
614
|
+
* task when its existing snapshot or the authoritative incoming promotion proves
|
|
615
|
+
* current-agent main-call membership, while excluding non-promoted and ambiguous matches.
|
|
616
|
+
*/
|
|
617
|
+
findTaskForContactOwnerChange(payload) {
|
|
618
|
+
const relatedCandidates = this.findRelatedTasks(payload, {
|
|
619
|
+
includeCollectionKeys: true,
|
|
620
|
+
includeMainCallMediaKeys: true
|
|
621
|
+
});
|
|
622
|
+
const incomingShowsCurrentAgentPromotion = this.isCurrentAgentPromotedOnIncomingMainCall(payload);
|
|
623
|
+
const eligibleCandidates = relatedCandidates.filter(candidate => this.isCurrentAgentActiveOnMainInteraction(candidate.data) || incomingShowsCurrentAgentPromotion);
|
|
624
|
+
if (eligibleCandidates.length === 1) {
|
|
625
|
+
return eligibleCandidates[0];
|
|
626
|
+
}
|
|
627
|
+
if (eligibleCandidates.length > 1) {
|
|
628
|
+
_loggerProxy.default.warn('Unable to correlate task event to a unique main interaction task', {
|
|
629
|
+
module: _constants.TASK_MANAGER_FILE,
|
|
630
|
+
method: 'findTaskForContactOwnerChange',
|
|
631
|
+
interactionId: payload.interaction?.mainInteractionId || payload.interaction?.parentInteractionId || payload.interaction?.callProcessingDetails?.parentInteractionId || payload.interactionId
|
|
632
|
+
});
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
isCurrentAgentActiveOnMainInteraction(taskData) {
|
|
638
|
+
const currentAgentId = this.agentId || taskData?.agentId;
|
|
639
|
+
if (!currentAgentId) return false;
|
|
640
|
+
return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
|
|
641
|
+
}
|
|
642
|
+
static isParticipantActiveOnMainInteraction(taskData, participantId) {
|
|
643
|
+
const interaction = taskData?.interaction;
|
|
644
|
+
const participant = interaction?.participants?.[participantId];
|
|
645
|
+
if (!participant || participant.hasLeft === true) return false;
|
|
646
|
+
return Object.values(interaction?.media ?? {}).some(media => media?.mType === _constants3.MEDIA_TYPE_MAIN_CALL && media.participants?.includes(participantId));
|
|
647
|
+
}
|
|
648
|
+
isCurrentAgentPromotedOnIncomingMainCall(payload) {
|
|
649
|
+
return Boolean(this.agentId && payload.interaction?.owner === this.agentId && TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId));
|
|
650
|
+
}
|
|
651
|
+
static getMainCallMediaId(interaction) {
|
|
652
|
+
return Object.entries(interaction?.media ?? {}).find(([, media]) => media?.mType === _constants3.MEDIA_TYPE_MAIN_CALL)?.[0];
|
|
653
|
+
}
|
|
654
|
+
static getStableMainInteractionId(payload) {
|
|
655
|
+
return payload.interaction?.mainInteractionId || TaskManager.getMainCallMediaId(payload.interaction) || payload.interaction?.interactionId || payload.interactionId;
|
|
656
|
+
}
|
|
657
|
+
canRecoverTaskFromContactOwnerChanged(payload) {
|
|
658
|
+
const interaction = payload.interaction;
|
|
659
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
660
|
+
return Boolean(this.agentId && stableInteractionId && interaction?.mediaType === _types.MEDIA_CHANNEL.TELEPHONY && interaction.isTerminated !== true && interaction.owner === this.agentId && TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
|
|
664
|
+
static preserveMainTaskIdentity(task, payload) {
|
|
665
|
+
const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
|
|
666
|
+
const payloadMainInteractionId = payload.interaction?.mainInteractionId;
|
|
667
|
+
const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
|
|
668
|
+
const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
|
|
669
|
+
const stableInteractionId = currentMainInteractionId || payloadMainInteractionId || currentMainMediaId || payloadMainMediaId || payload.interaction?.interactionId || task.data?.interaction?.interactionId || task.data?.interactionId;
|
|
670
|
+
if (!stableInteractionId || stableInteractionId === payload.interactionId) {
|
|
671
|
+
return payload;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
...payload,
|
|
675
|
+
interactionId: stableInteractionId
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* ParticipantLeftConference may arrive after an owner update while still carrying
|
|
681
|
+
* the departed owner. Keep the confirmed owner only when the new roster proves that
|
|
682
|
+
* owner is active on mainCall and identifies the incoming owner as departed.
|
|
683
|
+
*/
|
|
684
|
+
static preserveConfirmedOwner(task, payload) {
|
|
685
|
+
const confirmedOwner = task.data?.interaction?.owner;
|
|
686
|
+
const incomingOwner = payload.interaction?.owner;
|
|
687
|
+
if (!confirmedOwner || !incomingOwner || confirmedOwner === incomingOwner) {
|
|
688
|
+
return payload;
|
|
689
|
+
}
|
|
690
|
+
const confirmedOwnerIsActive = TaskManager.isParticipantActiveOnMainInteraction(payload, confirmedOwner);
|
|
691
|
+
const incomingOwnerIsExplicitlyDeparted = payload.participantId === incomingOwner || payload.interaction?.participants?.[incomingOwner]?.hasLeft === true;
|
|
692
|
+
if (!confirmedOwnerIsActive || !incomingOwnerIsExplicitlyDeparted) {
|
|
693
|
+
return payload;
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
...payload,
|
|
697
|
+
owner: confirmedOwner,
|
|
698
|
+
interaction: {
|
|
699
|
+
...payload.interaction,
|
|
700
|
+
owner: confirmedOwner
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
595
705
|
/**
|
|
596
706
|
* Resolve lifecycle events that are emitted for the main interaction while the
|
|
597
707
|
* local task can still be indexed by a child consult interaction. Exact task
|
|
598
708
|
* keys are handled before this fallback. Multiple aliases of the same task are
|
|
599
709
|
* treated as one candidate; genuinely ambiguous matches are intentionally ignored.
|
|
600
710
|
*/
|
|
601
|
-
findUniqueTaskByRelatedInteraction(payload) {
|
|
602
|
-
const
|
|
603
|
-
if (payloadInteractionIds.size === 0) return undefined;
|
|
604
|
-
const candidates = [...new Set(Object.values(this.taskCollection).filter(candidate => {
|
|
605
|
-
const taskInteraction = candidate?.data?.interaction;
|
|
606
|
-
const candidateInteractionIds = [candidate?.data?.interactionId, taskInteraction?.interactionId, taskInteraction?.mainInteractionId, taskInteraction?.parentInteractionId, taskInteraction?.callProcessingDetails?.parentInteractionId];
|
|
607
|
-
return candidateInteractionIds.some(interactionId => Boolean(interactionId) && payloadInteractionIds.has(interactionId));
|
|
608
|
-
}))];
|
|
711
|
+
findUniqueTaskByRelatedInteraction(payload, candidateFilter = () => true) {
|
|
712
|
+
const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
|
|
609
713
|
if (candidates.length === 1) {
|
|
610
714
|
return candidates[0];
|
|
611
715
|
}
|
|
@@ -618,6 +722,19 @@ class TaskManager extends _events.default {
|
|
|
618
722
|
}
|
|
619
723
|
return undefined;
|
|
620
724
|
}
|
|
725
|
+
findRelatedTasks(payload, options = {}) {
|
|
726
|
+
const {
|
|
727
|
+
includeCollectionKeys = false,
|
|
728
|
+
includeMainCallMediaKeys = false
|
|
729
|
+
} = options;
|
|
730
|
+
const payloadInteractionIds = new Set([payload.interactionId, payload.interaction?.interactionId, payload.interaction?.mainInteractionId, payload.interaction?.parentInteractionId, payload.interaction?.callProcessingDetails?.parentInteractionId, ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : [])].filter(interactionId => Boolean(interactionId)));
|
|
731
|
+
if (payloadInteractionIds.size === 0) return [];
|
|
732
|
+
return [...new Set(Object.entries(this.taskCollection).filter(([taskId, candidate]) => {
|
|
733
|
+
const taskInteraction = candidate?.data?.interaction;
|
|
734
|
+
const candidateInteractionIds = [...(includeCollectionKeys ? [taskId] : []), candidate?.data?.interactionId, taskInteraction?.interactionId, taskInteraction?.mainInteractionId, taskInteraction?.parentInteractionId, taskInteraction?.callProcessingDetails?.parentInteractionId, ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(taskInteraction)] : [])];
|
|
735
|
+
return candidateInteractionIds.some(interactionId => Boolean(interactionId) && payloadInteractionIds.has(interactionId));
|
|
736
|
+
}).map(([, candidate]) => candidate))];
|
|
737
|
+
}
|
|
621
738
|
|
|
622
739
|
/**
|
|
623
740
|
* Handle task lifecycle events and determine required actions
|
|
@@ -642,6 +759,8 @@ class TaskManager extends _events.default {
|
|
|
642
759
|
return this.handleAgentContact(context);
|
|
643
760
|
case _types2.CC_EVENTS.CONTACT_MERGED:
|
|
644
761
|
return this.handleContactMergedEvent(context);
|
|
762
|
+
case _types2.CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
763
|
+
return this.handleContactOwnerChanged(context);
|
|
645
764
|
case _types2.CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
|
|
646
765
|
return this.handleCampaignPreviewReservation(context);
|
|
647
766
|
case _types2.CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
|
|
@@ -652,6 +771,63 @@ class TaskManager extends _events.default {
|
|
|
652
771
|
};
|
|
653
772
|
}
|
|
654
773
|
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Recover the promoted agent's active voice task when local task state was lost.
|
|
777
|
+
* Existing related tasks are never replaced: an unresolved or ambiguous relation
|
|
778
|
+
* is safer to ignore than to create a duplicate task.
|
|
779
|
+
*/
|
|
780
|
+
handleContactOwnerChanged(context) {
|
|
781
|
+
if (context.task) {
|
|
782
|
+
return {
|
|
783
|
+
task: context.task
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const {
|
|
787
|
+
payload
|
|
788
|
+
} = context;
|
|
789
|
+
if (this.findRelatedTasks(payload, {
|
|
790
|
+
includeCollectionKeys: true,
|
|
791
|
+
includeMainCallMediaKeys: true
|
|
792
|
+
}).length > 0 || !this.canRecoverTaskFromContactOwnerChanged(payload)) {
|
|
793
|
+
return {};
|
|
794
|
+
}
|
|
795
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
796
|
+
if (!stableInteractionId || this.taskCollection[stableInteractionId]) {
|
|
797
|
+
return {};
|
|
798
|
+
}
|
|
799
|
+
const normalizedPayload = payload.interactionId === stableInteractionId ? payload : {
|
|
800
|
+
...payload,
|
|
801
|
+
interactionId: stableInteractionId
|
|
802
|
+
};
|
|
803
|
+
const taskData = {
|
|
804
|
+
...normalizedPayload,
|
|
805
|
+
owner: normalizedPayload.interaction.owner,
|
|
806
|
+
wrapUpRequired: normalizedPayload.interaction.participants?.[this.agentId]?.isWrapUp || false,
|
|
807
|
+
isConferenceInProgress: (0, _TaskUtils.getIsConferenceInProgress)(normalizedPayload),
|
|
808
|
+
isConsulted: false,
|
|
809
|
+
isAutoAnswering: false
|
|
810
|
+
};
|
|
811
|
+
const task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
|
|
812
|
+
this.taskCollection[stableInteractionId] = task;
|
|
813
|
+
|
|
814
|
+
// Restore the actor before installing listeners so the internal hydrate does
|
|
815
|
+
// not leak as a second public hydrate or as an incoming-task notification.
|
|
816
|
+
task.sendStateMachineEvent({
|
|
817
|
+
type: _stateMachine.TaskEvent.HYDRATE,
|
|
818
|
+
taskData,
|
|
819
|
+
agentId: this.agentId
|
|
820
|
+
});
|
|
821
|
+
this.setupTaskListeners(task);
|
|
822
|
+
context.payload = taskData;
|
|
823
|
+
context.stateMachineEvent = {
|
|
824
|
+
type: _stateMachine.TaskEvent.CONTACT_OWNER_CHANGED,
|
|
825
|
+
taskData
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
task
|
|
829
|
+
};
|
|
830
|
+
}
|
|
655
831
|
handleCampaignContactUpdated(context) {
|
|
656
832
|
const {
|
|
657
833
|
payload
|