@webex/internal-plugin-ai-assistant 3.11.0 → 3.12.0

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.
@@ -16,7 +16,14 @@ import {
16
16
  AI_ASSISTANT_ERROR_CODES,
17
17
  AI_ASSISTANT_ERRORS,
18
18
  } from '@webex/internal-plugin-ai-assistant/src/constants';
19
- import {jsonResponse, messageResponse, workspaceResponse, scheduleMeetingResponse} from '../data/messages';
19
+ import {
20
+ jsonResponse,
21
+ messageResponse,
22
+ workspaceResponse,
23
+ scheduleMeetingResponse,
24
+ assistantActivity,
25
+ citedAnswerWithSourcesResponse,
26
+ } from '../data/messages';
20
27
 
21
28
  const waitForAsync = () =>
22
29
  new Promise<void>((resolve) =>
@@ -78,12 +85,15 @@ describe('plugin-ai-assistant', () => {
78
85
  it('registers correctly', async () => {
79
86
  await webex.internal.aiAssistant.register();
80
87
 
81
- assert.callCount(webex.internal.mercury.on, 1);
88
+ assert.callCount(webex.internal.mercury.on, 2);
82
89
 
83
- const callArgs = webex.internal.mercury.on.getCall(0).args;
90
+ const firstCallArgs = webex.internal.mercury.on.getCall(0).args;
91
+ expect(firstCallArgs[0]).to.equal('event:assistant-api.response');
92
+ expect(firstCallArgs[1]).to.be.a('function');
84
93
 
85
- expect(callArgs[0]).to.equal('event:assistant-api.response');
86
- expect(callArgs[1]).to.be.a('function');
94
+ const secondCallArgs = webex.internal.mercury.on.getCall(1).args;
95
+ expect(secondCallArgs[0]).to.equal('assistant-api.activity');
96
+ expect(secondCallArgs[1]).to.be.a('function');
87
97
 
88
98
  assert.equal(webex.internal.aiAssistant.registered, true);
89
99
  });
@@ -115,11 +125,13 @@ describe('plugin-ai-assistant', () => {
115
125
 
116
126
  await webex.internal.aiAssistant.unregister();
117
127
 
118
- assert.callCount(webex.internal.mercury.off, 1);
128
+ assert.callCount(webex.internal.mercury.off, 2);
119
129
 
120
- const callArgs = webex.internal.mercury.off.getCall(0).args;
130
+ const firstCallOrg = webex.internal.mercury.off.getCall(0).args;
131
+ expect(firstCallOrg[0]).to.equal('event:assistant-api.response');
121
132
 
122
- expect(callArgs[0]).to.equal('event:assistant-api.response');
133
+ const secondCallOrg = webex.internal.mercury.off.getCall(1).args;
134
+ expect(secondCallOrg[0]).to.equal('assistant-api.activity');
123
135
 
124
136
  assert.equal(webex.internal.aiAssistant.registered, false);
125
137
  });
@@ -130,7 +142,6 @@ describe('plugin-ai-assistant', () => {
130
142
  const result = await webex.internal.aiAssistant.unregister();
131
143
 
132
144
  expect(result).to.be.undefined;
133
- assert.callCount(webex.internal.mercury.disconnect, 0);
134
145
  assert.equal(webex.internal.aiAssistant.registered, false);
135
146
  });
136
147
  });
@@ -225,6 +236,47 @@ describe('plugin-ai-assistant', () => {
225
236
  clientRequestId: 'test-request-id',
226
237
  param1: 'value1',
227
238
  },
239
+ headers: undefined,
240
+ });
241
+
242
+ const result = await requestPromise;
243
+
244
+ expect(result).to.deep.equal({
245
+ id: 'test-message-id',
246
+ url: 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id/messages/test-message-id',
247
+ sessionId: 'test-session-id',
248
+ sessionUrl:
249
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id',
250
+ creatorId: 'test-creator-id',
251
+ createdAt: '2025-08-05T02:11:12.361Z',
252
+ requestId: 'test-request-id',
253
+ streamEventName: 'aiassistant:stream:test-request-id',
254
+ });
255
+ });
256
+
257
+ it('makes a request with additional headers', async () => {
258
+ const requestPromise = webex.internal.aiAssistant._request({
259
+ resource: 'test-resource',
260
+ params: {param1: 'value1'},
261
+ headers: {
262
+ 'X-Custom-Header': 'foo',
263
+ 'X-Another-Header': 'bar',
264
+ },
265
+ });
266
+
267
+ expect(webex.request.getCall(0).args[0]).to.deep.equal({
268
+ service: 'assistant-api',
269
+ resource: 'test-resource',
270
+ method: 'POST',
271
+ contentType: 'application/json',
272
+ body: {
273
+ clientRequestId: 'test-request-id',
274
+ param1: 'value1',
275
+ },
276
+ headers: {
277
+ 'X-Custom-Header': 'foo',
278
+ 'X-Another-Header': 'bar',
279
+ },
228
280
  });
229
281
 
230
282
  const result = await requestPromise;
@@ -282,6 +334,28 @@ describe('plugin-ai-assistant', () => {
282
334
  expect(triggerSpy.getCall(2).args[1]).to.deep.equal(expectedResult);
283
335
  });
284
336
 
337
+ it('handles an activity', async () => {
338
+ const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
339
+
340
+ webex.internal.encryption.decryptText.callsFake(async (keyUrl, value) => {
341
+ return `decrypted-with-${keyUrl}-${value}`;
342
+ });
343
+
344
+ // assume assistant event is received
345
+ await webex.internal.aiAssistant._handleAssistantActivity(cloneDeep(assistantActivity[0]));
346
+
347
+ await waitForAsync();
348
+
349
+ let expectedResult = set(
350
+ cloneDeep(assistantActivity[0]),
351
+ 'activity.content.value.message',
352
+ 'decrypted-with-kms://kms-cisco.wbx2.com/keys/9b838423-f31b-49d5-a7c7-182572340a37-message_encrypted_value_for_activity'
353
+ );
354
+
355
+ expect(triggerSpy.getCall(0).args[0]).to.deep.equal('aiassistant:activityReceived');
356
+ expect(triggerSpy.getCall(0).args[1]).to.deep.equal(expectedResult);
357
+ });
358
+
285
359
  it('decrypts a chunked json response', async () => {
286
360
  const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
287
361
  webex.internal.encryption.decryptText.callsFake(async (keyUrl, value) => {
@@ -332,9 +406,7 @@ describe('plugin-ai-assistant', () => {
332
406
  responseType: 'thought',
333
407
  };
334
408
  expect(triggerSpy.getCall(1).args[0]).to.deep.equal('aiassistant:stream:test-request-id');
335
- expect(triggerSpy.getCall(1).args[1]).to.deep.equal(
336
- expectedResult
337
- );
409
+ expect(triggerSpy.getCall(1).args[1]).to.deep.equal(expectedResult);
338
410
 
339
411
  triggerSpy.resetHistory();
340
412
 
@@ -443,7 +515,8 @@ describe('plugin-ai-assistant', () => {
443
515
  type: 'json',
444
516
  encryptionKeyUrl: 'kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e',
445
517
  value: {
446
- value: 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_3_encrypted_value',
518
+ value:
519
+ 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_3_encrypted_value',
447
520
  type: 'markdown',
448
521
  },
449
522
  },
@@ -574,12 +647,10 @@ describe('plugin-ai-assistant', () => {
574
647
  // Update the clientRequestId to match the test setup
575
648
  const firstEvent = cloneDeep(workspaceResponse[0]);
576
649
  firstEvent.clientRequestId = 'test-request-id';
577
-
650
+
578
651
  await webex.internal.aiAssistant._handleEvent(firstEvent);
579
652
 
580
- expect(triggerSpy.getCall(0).args[0]).to.equal(
581
- `aiassistant:result:test-request-id`
582
- );
653
+ expect(triggerSpy.getCall(0).args[0]).to.equal(`aiassistant:result:test-request-id`);
583
654
 
584
655
  await waitForAsync();
585
656
 
@@ -594,7 +665,7 @@ describe('plugin-ai-assistant', () => {
594
665
  // second event is another workspace chunk with an encrypted value
595
666
  const secondEvent = cloneDeep(workspaceResponse[1]);
596
667
  secondEvent.clientRequestId = 'test-request-id';
597
-
668
+
598
669
  await webex.internal.aiAssistant._handleEvent(secondEvent);
599
670
 
600
671
  expectedResult = set(
@@ -604,7 +675,7 @@ describe('plugin-ai-assistant', () => {
604
675
  );
605
676
 
606
677
  expect(triggerSpy.getCall(2).args[1]).to.deep.equal(expectedResult);
607
- });
678
+ });
608
679
 
609
680
  it('handles a schedule meeting response', async () => {
610
681
  const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
@@ -620,35 +691,130 @@ describe('plugin-ai-assistant', () => {
620
691
  // Handle schedule meeting event with encrypted fields
621
692
  const event = cloneDeep(scheduleMeetingResponse[0]);
622
693
  event.clientRequestId = 'test-request-id';
623
-
694
+
624
695
  await webex.internal.aiAssistant._handleEvent(event);
625
696
 
626
- expect(triggerSpy.getCall(0).args[0]).to.equal(
627
- `aiassistant:result:test-request-id`
628
- );
697
+ expect(triggerSpy.getCall(0).args[0]).to.equal(`aiassistant:result:test-request-id`);
629
698
 
630
699
  await waitForAsync();
631
700
 
632
701
  // Verify all encrypted fields were decrypted
633
702
  const expectedResult = cloneDeep(event);
634
- expectedResult.response.content.parameters.commentary =
703
+ expectedResult.response.content.parameters.commentary =
635
704
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_commentary';
636
- expectedResult.response.content.value.results.data.attendees[0].email =
705
+ expectedResult.response.content.value.results.data.attendees[0].email =
637
706
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_email_0';
638
- expectedResult.response.content.value.results.data.attendees[1].email =
707
+ expectedResult.response.content.value.results.data.attendees[1].email =
639
708
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_email_1';
640
- expectedResult.response.content.value.results.data.title =
709
+ expectedResult.response.content.value.results.data.title =
641
710
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_title';
642
- expectedResult.response.content.value.results.data.description =
711
+ expectedResult.response.content.value.results.data.description =
643
712
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_description';
644
- expectedResult.response.content.value.results.data.inScopeReply =
713
+ expectedResult.response.content.value.results.data.inScopeReply =
645
714
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_inScopeReply';
646
- expectedResult.response.content.value.results.data.meetingLink =
715
+ expectedResult.response.content.value.results.data.meetingLink =
647
716
  'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_meetingLink';
648
717
 
649
718
  expect(triggerSpy.getCall(0).args[1]).to.deep.equal(expectedResult);
650
719
  });
651
720
 
721
+ it('handles a cited answer with sources response', async () => {
722
+ const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
723
+ webex.internal.encryption.decryptText.callsFake(async (keyUrl, value) => {
724
+ return `decrypted-with-${keyUrl}-${value}`;
725
+ });
726
+
727
+ await webex.internal.aiAssistant._request({
728
+ resource: 'test-resource',
729
+ params: {param1: 'value1'},
730
+ });
731
+
732
+ const event = cloneDeep(citedAnswerWithSourcesResponse[0]);
733
+ event.clientRequestId = 'test-request-id';
734
+
735
+ await webex.internal.aiAssistant._handleEvent(event);
736
+
737
+ expect(triggerSpy.getCall(0).args[0]).to.equal(`aiassistant:result:test-request-id`);
738
+
739
+ await waitForAsync();
740
+
741
+ const expectedResult = {
742
+ sessionId: '3c1939c0-92fe-11f0-8e9f-1bafc66fbbc5',
743
+ sessionUrl:
744
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/3c1939c0-92fe-11f0-8e9f-1bafc66fbbc5',
745
+ messageId: '3c19fd10-92fe-11f0-8e9f-1bafc66fbbc5',
746
+ messageUrl:
747
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/3c1939c0-92fe-11f0-8e9f-1bafc66fbbc5/messages/3c19fd10-92fe-11f0-8e9f-1bafc66fbbc5',
748
+ responseId: '3c1a4b30-92fe-11f0-8e9f-1bafc66fbbc5',
749
+ responseUrl:
750
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/3c1939c0-92fe-11f0-8e9f-1bafc66fbbc5/messages/3c1a4b30-92fe-11f0-8e9f-1bafc66fbbc5',
751
+ content: {
752
+ name: 'cited_answer',
753
+ type: 'json',
754
+ encryptionKeyUrl: 'kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e',
755
+ value: {
756
+ value:
757
+ 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_1_encrypted_value',
758
+ type: 'markdown',
759
+ citations: [
760
+ {
761
+ id: '6ccc8286e2084e05a6b9a29faae77095',
762
+ index: 1,
763
+ name: 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_1_encrypted_citation_0',
764
+ url: 'https://co.webex.com/webappng/sites/co/recording/playback/6ccc8286e2084e05a6b9a29faae77095',
765
+ metadata: {
766
+ provider: 'webex',
767
+ type: 'meeting_recording',
768
+ },
769
+ },
770
+ ],
771
+ sources: [
772
+ {
773
+ id: '6ccc8286e2084e05a6b9a29faae77096',
774
+ index: 1,
775
+ type: 'post_meeting',
776
+ name: 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_1_encrypted_source_0',
777
+ metadata: {
778
+ meetingContainerId: 'mccc8286e2084e05a6b9a29faae77096',
779
+ },
780
+ },
781
+ {
782
+ id: '6ccc8286e2084e05a6b9a29faae77096',
783
+ index: 2,
784
+ type: 'post_call',
785
+ name: 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_1_encrypted_source_1',
786
+ metadata: {
787
+ callContainerId: 'mccc8286e2084e05a6b9a29faae77096',
788
+ },
789
+ },
790
+ {
791
+ id: '6ccc8286e2084e05a6b9a29faae77096',
792
+ index: 3,
793
+ type: 'message',
794
+ name: 'decrypted-with-kms://kms-us.wbx2.com/keys/9565506d-78b1-4742-b0fd-63719748282e-json_1_encrypted_source_2',
795
+ metadata: {
796
+ spaceId: 'mccc8286e2084e05a6b9a29faae77096',
797
+ },
798
+ },
799
+ ],
800
+ },
801
+ },
802
+ createdAt: '2025-09-16T13:08:30.594220705Z',
803
+ creator: {
804
+ role: 'assistant',
805
+ },
806
+ // the below fields are added by the SDK
807
+ errorCode: undefined,
808
+ errorMessage: undefined,
809
+ finished: true,
810
+ requestId: 'test-request-id',
811
+ responseType: 'response',
812
+ };
813
+
814
+ expect(triggerSpy.getCall(1).args[0]).to.deep.equal('aiassistant:stream:test-request-id');
815
+ expect(triggerSpy.getCall(1).args[1]).to.deep.equal(expectedResult);
816
+ });
817
+
652
818
  it('decrypts and emits data when receiving event data', async () => {
653
819
  const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
654
820
 
@@ -830,7 +996,8 @@ describe('plugin-ai-assistant', () => {
830
996
  id: 'test-message-id',
831
997
  url: 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id/messages/test-message-id',
832
998
  sessionId: 'test-session-id',
833
- sessionUrl: 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id',
999
+ sessionUrl:
1000
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id',
834
1001
  creatorId: 'test-creator-id',
835
1002
  createdAt: '2025-08-05T02:11:12.361Z',
836
1003
  },
@@ -867,7 +1034,7 @@ describe('plugin-ai-assistant', () => {
867
1034
  // Verify the request was made correctly
868
1035
  expect(webex.request.calledOnce).to.be.true;
869
1036
  const requestArgs = webex.request.getCall(0).args[0];
870
-
1037
+
871
1038
  expect(requestArgs.service).to.equal('assistant-api');
872
1039
  expect(requestArgs.resource).to.equal('sessions/test-session-id/messages');
873
1040
  expect(requestArgs.method).to.equal('POST');
@@ -901,7 +1068,8 @@ describe('plugin-ai-assistant', () => {
901
1068
  id: 'test-message-id',
902
1069
  url: 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id/messages/test-message-id',
903
1070
  sessionId: 'test-session-id',
904
- sessionUrl: 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id',
1071
+ sessionUrl:
1072
+ 'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/test-session-id',
905
1073
  creatorId: 'test-creator-id',
906
1074
  createdAt: '2025-08-05T02:11:12.361Z',
907
1075
  requestId: 'custom-request-id',
@@ -1003,7 +1171,7 @@ describe('plugin-ai-assistant', () => {
1003
1171
  // Should use the UUID stub
1004
1172
  expect(result.requestId).to.equal('test-request-id');
1005
1173
  expect(result.streamEventName).to.equal('aiassistant:stream:test-request-id');
1006
-
1174
+
1007
1175
  const requestArgs = webex.request.getCall(0).args[0];
1008
1176
  expect(requestArgs.body.clientRequestId).to.equal('test-request-id');
1009
1177
  });
@@ -1054,6 +1222,24 @@ describe('plugin-ai-assistant', () => {
1054
1222
  expect(requestArgs.body.entryPoint).to.be.undefined;
1055
1223
  });
1056
1224
 
1225
+ it('includes AI-Assistant-Render-Protocol in the request header when renderProtocolVersion is provided', async () => {
1226
+ const options = {
1227
+ sessionId: 'test-session-id',
1228
+ encryptionKeyUrl: 'test-key-url',
1229
+ contextResources: [],
1230
+ contentType: 'action' as const,
1231
+ contentValue: 'test_action',
1232
+ renderProtocolVersion: '1.0',
1233
+ };
1234
+
1235
+ await webex.internal.aiAssistant.makeAiAssistantRequest(options);
1236
+
1237
+ const requestArgs = webex.request.getCall(0).args[0];
1238
+ expect(requestArgs.headers).to.deep.equal({
1239
+ 'AI-Assistant-Render-Protocol': '1.0',
1240
+ });
1241
+ });
1242
+
1057
1243
  it('handles request rejection', async () => {
1058
1244
  webex.request.rejects(new Error('Network error'));
1059
1245
 
@@ -1065,9 +1251,9 @@ describe('plugin-ai-assistant', () => {
1065
1251
  contentValue: 'test_action',
1066
1252
  };
1067
1253
 
1068
- await expect(
1069
- webex.internal.aiAssistant.makeAiAssistantRequest(options)
1070
- ).to.be.rejectedWith('Network error');
1254
+ await expect(webex.internal.aiAssistant.makeAiAssistantRequest(options)).to.be.rejectedWith(
1255
+ 'Network error'
1256
+ );
1071
1257
  });
1072
1258
 
1073
1259
  it('starts timer when making a request', async () => {
@@ -1087,7 +1273,7 @@ describe('plugin-ai-assistant', () => {
1087
1273
 
1088
1274
  it('handles timeout when no streaming response comes back', async () => {
1089
1275
  const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
1090
-
1276
+
1091
1277
  const options = {
1092
1278
  sessionId: 'test-session-id',
1093
1279
  encryptionKeyUrl: 'test-key-url',
@@ -1105,10 +1291,13 @@ describe('plugin-ai-assistant', () => {
1105
1291
 
1106
1292
  // Should trigger timeout event on the stream
1107
1293
  expect(triggerSpy.calledWith('aiassistant:stream:test-request-id')).to.be.true;
1108
- const timeoutCall = triggerSpy.getCalls().find(call =>
1109
- call.args[0] === 'aiassistant:stream:test-request-id' &&
1110
- call.args[1].errorMessage === AI_ASSISTANT_ERRORS.AI_ASSISTANT_TIMEOUT
1111
- );
1294
+ const timeoutCall = triggerSpy
1295
+ .getCalls()
1296
+ .find(
1297
+ (call) =>
1298
+ call.args[0] === 'aiassistant:stream:test-request-id' &&
1299
+ call.args[1].errorMessage === AI_ASSISTANT_ERRORS.AI_ASSISTANT_TIMEOUT
1300
+ );
1112
1301
  expect(timeoutCall).to.exist;
1113
1302
  expect(timeoutCall.args[1]).to.deep.include({
1114
1303
  requestId: 'test-request-id',
@@ -1121,7 +1310,7 @@ describe('plugin-ai-assistant', () => {
1121
1310
  it('resets timer when streaming responses are received', async () => {
1122
1311
  const timerResetSpy = sinon.spy(Timer.prototype, 'reset');
1123
1312
  const timerCancelSpy = sinon.spy(Timer.prototype, 'cancel');
1124
-
1313
+
1125
1314
  const options = {
1126
1315
  sessionId: 'test-session-id',
1127
1316
  encryptionKeyUrl: 'test-key-url',