@webex/contact-center 3.12.0-llmrefactor.12 → 3.12.0-llmrefactor.13
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 +5 -5
- package/ai-docs/CONTRACTS.md +2 -0
- package/ai-docs/SERVICE_STATE.md +4 -0
- package/ai-docs/contact-center-spec.md +26 -3
- package/dist/cc.js +163 -2
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +25 -10
- package/dist/index.js.map +1 -1
- package/dist/metrics/constants.js +5 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +116 -1
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +5 -1
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -1
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +51 -0
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +4 -3
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Utils.js +5 -1
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/types/cc.d.ts +21 -1
- package/dist/types/constants.d.ts +4 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/metrics/constants.d.ts +5 -0
- package/dist/types/services/ApiAiAssistant.d.ts +35 -2
- package/dist/types/services/config/constants.d.ts +5 -0
- package/dist/types/services/config/types.d.ts +13 -2
- package/dist/types/services/core/Utils.d.ts +2 -0
- package/dist/types/types.d.ts +56 -1
- package/dist/types.js +39 -2
- package/dist/types.js.map +1 -1
- package/dist/webex.js +9 -1
- package/dist/webex.js.map +1 -1
- package/package.json +9 -9
- package/src/cc.ts +251 -18
- package/src/constants.ts +4 -0
- package/src/index.ts +11 -0
- package/src/metrics/ai-docs/metrics-spec.md +19 -20
- package/src/metrics/constants.ts +5 -0
- package/src/services/ApiAiAssistant.ts +153 -1
- package/src/services/ai-docs/services-spec.md +11 -3
- package/src/services/config/Util.ts +6 -0
- package/src/services/config/ai-docs/config-spec.md +14 -3
- package/src/services/config/constants.ts +12 -0
- package/src/services/config/index.ts +65 -0
- package/src/services/config/types.ts +11 -2
- package/src/services/core/Utils.ts +4 -0
- package/src/types.ts +63 -0
- package/src/webex.js +8 -0
- package/test/unit/spec/cc.ts +268 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
- package/test/unit/spec/services/config/Util.ts +44 -0
- package/test/unit/spec/services/config/index.ts +68 -0
- package/test/unit/spec/wellnessSampleUtils.ts +288 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const {
|
|
2
|
+
areAllTasksSafe,
|
|
3
|
+
createRecoveryMarker,
|
|
4
|
+
getLegacyExternalTransitionDecision,
|
|
5
|
+
getRecoveryDecision,
|
|
6
|
+
getSelectableIdleCodes,
|
|
7
|
+
getWellnessTransition,
|
|
8
|
+
parseRecoveryMarker,
|
|
9
|
+
shouldResetForSessionEvent,
|
|
10
|
+
shouldResetSampleAfterDeregister,
|
|
11
|
+
} = require('../../../../../../docs/samples/contact-center/wellness-utils');
|
|
12
|
+
const {readFileSync} = require('fs');
|
|
13
|
+
const {resolve} = require('path');
|
|
14
|
+
|
|
15
|
+
describe('Contact Center wellness sample utilities', () => {
|
|
16
|
+
describe('safe task detection', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
['new', false, true],
|
|
19
|
+
['reserved', false, true],
|
|
20
|
+
['connected', false, true],
|
|
21
|
+
['consulting', false, true],
|
|
22
|
+
['conference', false, true],
|
|
23
|
+
['wrapup', false, true],
|
|
24
|
+
['campaignPreview', false, true],
|
|
25
|
+
['terminated', false, false],
|
|
26
|
+
['completed', false, false],
|
|
27
|
+
['completed', true, true],
|
|
28
|
+
])(
|
|
29
|
+
'classifies task state %s with wrapUpRequired=%s',
|
|
30
|
+
(state, wrapUpRequired, expectedBlocking) => {
|
|
31
|
+
const taskList = {
|
|
32
|
+
task: {
|
|
33
|
+
data: {
|
|
34
|
+
wrapUpRequired,
|
|
35
|
+
interaction: {state},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
expect(areAllTasksSafe(taskList)).toBe(!expectedBlocking);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it('requires every task to be terminal', () => {
|
|
45
|
+
expect(
|
|
46
|
+
areAllTasksSafe({
|
|
47
|
+
completed: {data: {interaction: {state: 'completed'}}},
|
|
48
|
+
active: {data: {interaction: {state: 'engaged'}}},
|
|
49
|
+
})
|
|
50
|
+
).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('treats an incomplete task shape as blocking until authoritative state arrives', () => {
|
|
54
|
+
expect(areAllTasksSafe({task: {}})).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('external state synchronization', () => {
|
|
59
|
+
it.each([
|
|
60
|
+
['OnBreak', 'user-idle', 'complete'],
|
|
61
|
+
['OnBreak', 'rona-code', 'ignore'],
|
|
62
|
+
['WaitingForSafeState', 'rona-code', 'cancel'],
|
|
63
|
+
['Ready', '0', 'ignore'],
|
|
64
|
+
])(
|
|
65
|
+
'returns %s / %s as %s',
|
|
66
|
+
(lifecycle, nextAuxCodeId, expected) => {
|
|
67
|
+
expect(
|
|
68
|
+
getLegacyExternalTransitionDecision({
|
|
69
|
+
lifecycle,
|
|
70
|
+
nextAuxCodeId,
|
|
71
|
+
wellnessAuxCodeId: 'wellness',
|
|
72
|
+
validIdleCodeIds: ['user-idle'],
|
|
73
|
+
})
|
|
74
|
+
).toBe(expected);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('ordinary status selector', () => {
|
|
80
|
+
it('excludes WellbeingBreak and every other system code', () => {
|
|
81
|
+
expect(
|
|
82
|
+
getSelectableIdleCodes([
|
|
83
|
+
{id: '0', name: 'Available', isSystem: false},
|
|
84
|
+
{id: 'training', name: 'Training', isSystem: false},
|
|
85
|
+
{id: 'wellness', name: 'WellbeingBreak', isSystem: true},
|
|
86
|
+
{id: 'rona', name: 'RONA', isSystem: true},
|
|
87
|
+
])
|
|
88
|
+
).toEqual([
|
|
89
|
+
{id: '0', name: 'Available', isSystem: false},
|
|
90
|
+
{id: 'training', name: 'Training', isSystem: false},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('sample public surface guard', () => {
|
|
96
|
+
it('attaches the wellness constants consumed by the standalone browser sample', () => {
|
|
97
|
+
const webexSource = readFileSync(
|
|
98
|
+
resolve(__dirname, '../../../src/webex.js'),
|
|
99
|
+
'utf8'
|
|
100
|
+
);
|
|
101
|
+
const appSource = readFileSync(
|
|
102
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/app.js'),
|
|
103
|
+
'utf8'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
['WELLNESS_BREAK_NOTIFICATION_ACTIONS', 'WELLNESS_BREAK_USER_ACTIONS'].forEach(
|
|
107
|
+
(surface) => {
|
|
108
|
+
expect(webexSource).toContain(`Webex.${surface} = ${surface};`);
|
|
109
|
+
expect(appSource).toContain(surface);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('keeps the intake v0.4 US English wellness copy in the sample surfaces', () => {
|
|
115
|
+
const appSource = readFileSync(
|
|
116
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/app.js'),
|
|
117
|
+
'utf8'
|
|
118
|
+
);
|
|
119
|
+
const htmlSource = readFileSync(
|
|
120
|
+
resolve(__dirname, '../../../../../../docs/samples/contact-center/index.html'),
|
|
121
|
+
'utf8'
|
|
122
|
+
);
|
|
123
|
+
const sampleSource = `${appSource}\n${htmlSource}`.replace(/\\'/g, "'");
|
|
124
|
+
|
|
125
|
+
[
|
|
126
|
+
'Well-being break scheduled',
|
|
127
|
+
'This break is pre-approved by your organization for your well-being. You deserve it.',
|
|
128
|
+
'Take wellbeing break',
|
|
129
|
+
'Take a break',
|
|
130
|
+
'Later',
|
|
131
|
+
'Great. Your well-being break will begin shortly.',
|
|
132
|
+
'Great. Your well-being break starts right after this call.',
|
|
133
|
+
"It's great to see your dedication. But remember, taking breaks can boost your productivity and your health.",
|
|
134
|
+
"I'm sorry, you've reached your well-being break limit today. Continue with your tasks, but remember to take care of yourself.",
|
|
135
|
+
"Looks like you're busy. I didn't get a response, so I'll check back with you shortly.",
|
|
136
|
+
'Relax',
|
|
137
|
+
'Your 1 minute well-being break is starting in',
|
|
138
|
+
'This moment is yours.',
|
|
139
|
+
"In a few moments, you'll return to your day.",
|
|
140
|
+
'Transitioning back to work mode in',
|
|
141
|
+
'Well-being break completed',
|
|
142
|
+
"I hope you're feeling recharged after that well-being break. See you in your next break!",
|
|
143
|
+
"We couldn't start your well-being break due to a system issue. Please continue with your tasks and we will see you in your next well-being break.",
|
|
144
|
+
'We encountered an issue setting your status to Available.',
|
|
145
|
+
"We couldn't start your well-being break due to a system issue.",
|
|
146
|
+
].forEach((copy) => expect(sampleSource).toContain(copy));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('lifecycle transitions', () => {
|
|
152
|
+
it('revokes manual request eligibility after a not-allowed decision', () => {
|
|
153
|
+
expect(
|
|
154
|
+
getWellnessTransition(
|
|
155
|
+
{lifecycle: 'RequestPending', hasOffer: false, isReady: true},
|
|
156
|
+
{type: 'NOT_ALLOWED'}
|
|
157
|
+
)
|
|
158
|
+
).toEqual({
|
|
159
|
+
lifecycle: 'Ready',
|
|
160
|
+
canRequest: false,
|
|
161
|
+
clearOffer: true,
|
|
162
|
+
clearManualRequest: true,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps an offer retryable when response delivery fails', () => {
|
|
167
|
+
expect(
|
|
168
|
+
getWellnessTransition(
|
|
169
|
+
{lifecycle: 'OfferPending', hasOffer: true, isReady: true},
|
|
170
|
+
{type: 'OFFER_RESPONSE_STARTED'}
|
|
171
|
+
)
|
|
172
|
+
).toEqual({lifecycle: 'OfferResponding', allowed: true});
|
|
173
|
+
expect(
|
|
174
|
+
getWellnessTransition(
|
|
175
|
+
{lifecycle: 'OfferResponding', hasOffer: true, isReady: true},
|
|
176
|
+
{type: 'OFFER_RESPONSE_FAILED'}
|
|
177
|
+
)
|
|
178
|
+
).toEqual({lifecycle: 'OfferPending', rearmOffer: true});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('clears an offer only after its response succeeds', () => {
|
|
182
|
+
expect(
|
|
183
|
+
getWellnessTransition(
|
|
184
|
+
{lifecycle: 'OfferResponding', hasOffer: true, isReady: true},
|
|
185
|
+
{type: 'OFFER_RESPONSE_SUCCEEDED'}
|
|
186
|
+
)
|
|
187
|
+
).toEqual({lifecycle: 'Ready', clearOffer: true});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it.each([
|
|
191
|
+
[undefined, 'session-1', true],
|
|
192
|
+
['session-1', 'session-1', true],
|
|
193
|
+
['session-2', 'session-1', false],
|
|
194
|
+
])(
|
|
195
|
+
'scopes session cleanup for event session %s',
|
|
196
|
+
(eventSessionId, activeSessionId, expected) => {
|
|
197
|
+
expect(shouldResetForSessionEvent(eventSessionId, activeSessionId)).toBe(expected);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
it.each([
|
|
202
|
+
[true, true, true],
|
|
203
|
+
[false, false, true],
|
|
204
|
+
[false, true, false],
|
|
205
|
+
])(
|
|
206
|
+
'decides deregistration cleanup for success=%s config=%s',
|
|
207
|
+
(succeeded, hasAgentConfig, expected) => {
|
|
208
|
+
expect(shouldResetSampleAfterDeregister({succeeded, hasAgentConfig})).toBe(expected);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('browser recovery marker', () => {
|
|
214
|
+
it('round-trips only legacy session ownership', () => {
|
|
215
|
+
const marker = createRecoveryMarker({
|
|
216
|
+
agentSessionId: 'session-1',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(parseRecoveryMarker(JSON.stringify(marker))).toEqual({
|
|
220
|
+
version: 1,
|
|
221
|
+
agentSessionId: 'session-1',
|
|
222
|
+
stateModel: 'legacy',
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it.each([
|
|
227
|
+
{
|
|
228
|
+
name: 'discards another session',
|
|
229
|
+
input: {
|
|
230
|
+
marker: createRecoveryMarker({
|
|
231
|
+
agentSessionId: 'session-1',
|
|
232
|
+
}),
|
|
233
|
+
agentSessionId: 'session-2',
|
|
234
|
+
},
|
|
235
|
+
expected: 'discard',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'waits for an authoritative legacy snapshot',
|
|
239
|
+
input: {
|
|
240
|
+
marker: createRecoveryMarker({
|
|
241
|
+
agentSessionId: 'session-1',
|
|
242
|
+
}),
|
|
243
|
+
agentSessionId: 'session-1',
|
|
244
|
+
legacyStateKnown: false,
|
|
245
|
+
},
|
|
246
|
+
expected: 'wait',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'restores an owned legacy wellness state',
|
|
250
|
+
input: {
|
|
251
|
+
marker: createRecoveryMarker({
|
|
252
|
+
agentSessionId: 'session-1',
|
|
253
|
+
}),
|
|
254
|
+
agentSessionId: 'session-1',
|
|
255
|
+
legacyStateKnown: true,
|
|
256
|
+
legacyAuxCodeId: 'wellness',
|
|
257
|
+
wellnessAuxCodeId: 'wellness',
|
|
258
|
+
},
|
|
259
|
+
expected: 'restore',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'clears a legacy marker when the session already left wellness',
|
|
263
|
+
input: {
|
|
264
|
+
marker: createRecoveryMarker({
|
|
265
|
+
agentSessionId: 'session-1',
|
|
266
|
+
}),
|
|
267
|
+
agentSessionId: 'session-1',
|
|
268
|
+
legacyStateKnown: true,
|
|
269
|
+
legacyAuxCodeId: '0',
|
|
270
|
+
wellnessAuxCodeId: 'wellness',
|
|
271
|
+
},
|
|
272
|
+
expected: 'clear',
|
|
273
|
+
},
|
|
274
|
+
])('$name', ({input, expected}) => {
|
|
275
|
+
expect(getRecoveryDecision(input)).toBe(expected);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('rejects malformed or unsupported recovery markers', () => {
|
|
279
|
+
expect(parseRecoveryMarker('{')).toBeUndefined();
|
|
280
|
+
expect(parseRecoveryMarker(JSON.stringify({version: 2}))).toBeUndefined();
|
|
281
|
+
expect(
|
|
282
|
+
parseRecoveryMarker(
|
|
283
|
+
JSON.stringify({version: 1, agentSessionId: 'session-1', stateModel: 'agent-state-control'})
|
|
284
|
+
)
|
|
285
|
+
).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|