Skip to content

Commit 61e8218

Browse files
authored
feat(ai-assistant): schedule meeting message handling (#4584)
1 parent 3d8e184 commit 61e8218

File tree

6 files changed

+180
-2
lines changed

6 files changed

+180
-2
lines changed

packages/@webex/internal-plugin-ai-assistant/src/ai-assistant.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ import {
2727
CONTEXT_RESOURCE_TYPES,
2828
RESPONSE_NAMES,
2929
} from './constants';
30-
import {decryptCitedAnswer, decryptMessage, decryptToolUse, decryptWorkspace} from './utils';
30+
import {
31+
decryptCitedAnswer,
32+
decryptMessage,
33+
decryptScheduleMeeting,
34+
decryptToolUse,
35+
decryptWorkspace,
36+
} from './utils';
3137

3238
const AIAssistant = WebexPlugin.extend({
3339
namespace: 'AIAssistant',
@@ -169,6 +175,10 @@ const AIAssistant = WebexPlugin.extend({
169175
await decryptCitedAnswer(responseContent, this.webex);
170176
break;
171177
}
178+
case RESPONSE_NAMES.SCHEDULE_MEETING: {
179+
await decryptScheduleMeeting(responseContent, this.webex);
180+
break;
181+
}
172182
case RESPONSE_NAMES.TOOL_RESULT: {
173183
// No encrypted content in tool_result
174184
break;
@@ -280,6 +290,7 @@ const AIAssistant = WebexPlugin.extend({
280290
* @param {Object} options.assistant optional parameter to specify the assistant to use
281291
* @param {Object} options.locale optional locale to use for the request, defaults to 'en_US'
282292
* @param {string} options.requestId optional request ID to use for this request, if not provided a new UUID will be generated
293+
* @param {string} options.entryPoint optional entryPoint to use for this request
283294
* @returns {Promise<Object>} Resolves with an object containing the requestId, sessionId and streamEventName
284295
* @public
285296
* @memberof AIAssistant
@@ -313,6 +324,7 @@ const AIAssistant = WebexPlugin.extend({
313324
async: 'chunked',
314325
locale: options.locale || 'en_US',
315326
content,
327+
...(options.entryPoint ? {entryPoint: options.entryPoint} : {}),
316328
...(options.assistant ? {assistant: options.assistant} : {}),
317329
},
318330
...(options.requestId ? {requestId: options.requestId} : {}),

packages/@webex/internal-plugin-ai-assistant/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ export enum RESPONSE_NAMES {
4343
MESSAGE = 'message',
4444
TOOL_RESULT = 'tool_result',
4545
WORKSPACE = 'workspace',
46+
SCHEDULE_MEETING = 'schedule_meeting',
4647
}

packages/@webex/internal-plugin-ai-assistant/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ export interface AiAssistantRequestOptions {
5151
assistant?: string;
5252
locale?: string;
5353
requestId?: string;
54+
entryPoint?: string;
5455
}

packages/@webex/internal-plugin-ai-assistant/src/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ export const decryptCitedAnswer = async (data, webex) => {
3131

3232
await decryptInPlace(data, 'value.value', 'encryptionKeyUrl', webex);
3333
};
34+
export const decryptScheduleMeeting = async (data, webex) => {
35+
// Decrypt commentary in parameters
36+
await decryptInPlace(data, 'parameters.commentary', 'encryptionKeyUrl', webex);
37+
38+
const meetingData = data.value?.results?.data;
39+
if (meetingData) {
40+
// Decrypt attendee emails
41+
if (meetingData.attendees) {
42+
await Promise.all(
43+
meetingData.attendees.map((attendee, index) => {
44+
return decryptInPlace(
45+
data,
46+
`value.results.data.attendees.${index}.email`,
47+
'encryptionKeyUrl',
48+
webex
49+
);
50+
})
51+
);
52+
}
53+
54+
// Decrypt other fields in the meeting data
55+
await Promise.all([
56+
decryptInPlace(data, 'value.results.data.title', 'encryptionKeyUrl', webex),
57+
decryptInPlace(data, 'value.results.data.inScopeReply', 'encryptionKeyUrl', webex),
58+
decryptInPlace(data, 'value.results.data.meetingLink', 'encryptionKeyUrl', webex),
59+
decryptInPlace(data, 'value.results.data.description', 'encryptionKeyUrl', webex),
60+
]);
61+
}
62+
};
3463

3564
export const decryptMessage = async (data, webex) => {
3665
await decryptInPlace(data, 'value', 'encryptionKeyUrl', webex);

packages/@webex/internal-plugin-ai-assistant/test/unit/data/messages.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,4 +656,65 @@ export const workspaceResponse = [
656656
},
657657
},
658658
},
659+
];
660+
661+
export const scheduleMeetingResponse = [
662+
{
663+
eventType: 'assistant-api.response',
664+
sequence: 1,
665+
finished: true,
666+
clientRequestId: 'test-request-id',
667+
responseId: 'b6893a00-c6cc-11f0-adb9-f9fe7ea2ec69',
668+
responseType: 'response',
669+
response: {
670+
sessionId: 'a64345a0-c6cc-11f0-8c21-a7bce84cd4be',
671+
sessionUrl:
672+
'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/a64345a0-c6cc-11f0-8c21-a7bce84cd4be',
673+
messageId: 'b688c4d0-c6cc-11f0-adb9-f9fe7ea2ec69',
674+
messageUrl:
675+
'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/a64345a0-c6cc-11f0-8c21-a7bce84cd4be/messages/b688c4d0-c6cc-11f0-adb9-f9fe7ea2ec69',
676+
responseId: 'b6893a00-c6cc-11f0-adb9-f9fe7ea2ec69',
677+
responseUrl:
678+
'https://assistant-api-a.wbx2.com:443/assistant-api/api/v1/sessions/a64345a0-c6cc-11f0-8c21-a7bce84cd4be/messages/b6893a00-c6cc-11f0-adb9-f9fe7ea2ec69',
679+
content: {
680+
name: 'schedule_meeting',
681+
type: 'json',
682+
encryptionKeyUrl: 'kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630',
683+
parameters: {
684+
commentary: 'schedule_meeting_encrypted_commentary',
685+
},
686+
value: {
687+
results: {
688+
category: 'schedule_meeting',
689+
data: {
690+
success: true,
691+
status: 'created',
692+
attendees: [
693+
{
694+
email: 'schedule_meeting_encrypted_email_0',
695+
status: 'available',
696+
},
697+
{
698+
email: 'schedule_meeting_encrypted_email_1',
699+
status: 'available',
700+
},
701+
],
702+
startTime: '2025-11-26T09:00:00Z',
703+
duration: '1800000',
704+
title: 'schedule_meeting_encrypted_title',
705+
description: 'schedule_meeting_encrypted_description',
706+
timeZone: 'Europe/London',
707+
inScopeReply: 'schedule_meeting_encrypted_inScopeReply',
708+
meetingLink: 'schedule_meeting_encrypted_meetingLink',
709+
schedulerUUID: '21e53358f00b46bda778d9e7124f6f1e',
710+
meetingUUID: '920eab08068b4382a31d26ddecc9ec8b',
711+
meetingId: '837dc916-2259-6c04-9b51-d4dcfc7c4557',
712+
},
713+
},
714+
},
715+
},
716+
createdAt: '2025-11-21T11:25:01.670071069Z',
717+
creator: {role: 'assistant'},
718+
},
719+
},
659720
];

packages/@webex/internal-plugin-ai-assistant/test/unit/spec/ai-assistant.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
AI_ASSISTANT_ERROR_CODES,
1717
AI_ASSISTANT_ERRORS,
1818
} from '@webex/internal-plugin-ai-assistant/src/constants';
19-
import {jsonResponse, messageResponse, workspaceResponse} from '../data/messages';
19+
import {jsonResponse, messageResponse, workspaceResponse, scheduleMeetingResponse} from '../data/messages';
2020

2121
const waitForAsync = () =>
2222
new Promise<void>((resolve) =>
@@ -606,6 +606,49 @@ describe('plugin-ai-assistant', () => {
606606
expect(triggerSpy.getCall(2).args[1]).to.deep.equal(expectedResult);
607607
});
608608

609+
it('handles a schedule meeting response', async () => {
610+
const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
611+
webex.internal.encryption.decryptText.callsFake(async (keyUrl, value) => {
612+
return `decrypted-with-${keyUrl}-${value}`;
613+
});
614+
615+
await webex.internal.aiAssistant._request({
616+
resource: 'test-resource',
617+
params: {param1: 'value1'},
618+
});
619+
620+
// Handle schedule meeting event with encrypted fields
621+
const event = cloneDeep(scheduleMeetingResponse[0]);
622+
event.clientRequestId = 'test-request-id';
623+
624+
await webex.internal.aiAssistant._handleEvent(event);
625+
626+
expect(triggerSpy.getCall(0).args[0]).to.equal(
627+
`aiassistant:result:test-request-id`
628+
);
629+
630+
await waitForAsync();
631+
632+
// Verify all encrypted fields were decrypted
633+
const expectedResult = cloneDeep(event);
634+
expectedResult.response.content.parameters.commentary =
635+
'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 =
637+
'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 =
639+
'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 =
641+
'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 =
643+
'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 =
645+
'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 =
647+
'decrypted-with-kms://kms-cisco.wbx2.com/keys/dd6053f0-a1b3-428d-8104-317527d73630-schedule_meeting_encrypted_meetingLink';
648+
649+
expect(triggerSpy.getCall(0).args[1]).to.deep.equal(expectedResult);
650+
});
651+
609652
it('decrypts and emits data when receiving event data', async () => {
610653
const triggerSpy = sinon.spy(webex.internal.aiAssistant, 'trigger');
611654

@@ -980,6 +1023,37 @@ describe('plugin-ai-assistant', () => {
9801023
expect(requestArgs.body.assistant).to.be.undefined;
9811024
});
9821025

1026+
it('includes entryPoint in request when provided', async () => {
1027+
const options = {
1028+
sessionId: 'test-session-id',
1029+
encryptionKeyUrl: 'test-key-url',
1030+
contextResources: [],
1031+
contentType: 'action' as const,
1032+
contentValue: 'test_action',
1033+
entryPoint: 'custom-entry-point',
1034+
};
1035+
1036+
await webex.internal.aiAssistant.makeAiAssistantRequest(options);
1037+
1038+
const requestArgs = webex.request.getCall(0).args[0];
1039+
expect(requestArgs.body.entryPoint).to.equal('custom-entry-point');
1040+
});
1041+
1042+
it('does not include entryPoint in request when not provided', async () => {
1043+
const options = {
1044+
sessionId: 'test-session-id',
1045+
encryptionKeyUrl: 'test-key-url',
1046+
contextResources: [],
1047+
contentType: 'action' as const,
1048+
contentValue: 'test_action',
1049+
};
1050+
1051+
await webex.internal.aiAssistant.makeAiAssistantRequest(options);
1052+
1053+
const requestArgs = webex.request.getCall(0).args[0];
1054+
expect(requestArgs.body.entryPoint).to.be.undefined;
1055+
});
1056+
9831057
it('handles request rejection', async () => {
9841058
webex.request.rejects(new Error('Network error'));
9851059

0 commit comments

Comments
 (0)