Exemplo n.º 1
0
        private async Task HandleExConversationChangeNotification(string body)
        {
            var webhookData = JsonConvert.DeserializeObject <Models.ExConversationChangeNotification.WebhookData>(body);

            foreach (var change in webhookData.body.changes)
            {
                string state = change?.result?.conversationDetails?.state;
                switch (state)
                {
                case "CLOSE":
                {
                    // Agent has closed the conversation
                    var conversationId = change?.result?.convId;

                    if (await ConversationHandoffRecordMap.GetByRemoteConversationId(conversationId) is LivePersonHandoffRecord handoffRecord)
                    {
                        var eventActivity = EventFactory.CreateHandoffStatus(handoffRecord.ConversationReference.Conversation, "completed") as Activity;

                        //await _adapter.ContinueConversationAsync(_credentials.MsAppId, eventActivity, _bot.OnTurnAsync, default);

                        //TEMPORARY WORKAROUND UNTIL CLOUDADAPTER IS IN PLACE SO ABOVE LINE WILL WORK
                        await(_adapter).ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(eventActivity, cancellationToken), default);
                    }
                }

                break;

                case "OPEN":
                    break;
                }
            }
        }
        public async Task PostAsync()
        {
            using (var sr = new StreamReader(Request.Body))
            {
                var body = await sr.ReadToEndAsync();

                var responseMessage = JsonConvert.DeserializeObject <ServiceNowResponseMessage>(body);

                if (responseMessage != null)
                {
                    // Do we have a matching handoff record for the incoming ConversationID from ServiceNow?
                    var handoffRecord = await ConversationHandoffRecordMap.GetByRemoteConversationId(responseMessage.clientSessionId) as ServiceNowHandoffRecord;

                    if (handoffRecord != null)
                    {
                        await HandleContentEvent(responseMessage);

                        // If ServiceNow indicates it's completed handoff from it's perspective we end handoff and return control.
                        if (responseMessage.completed)
                        {
                            var eventActivity = EventFactory.CreateHandoffStatus(handoffRecord.ConversationReference.Conversation, "completed") as Activity;
                            await(_adapter).ContinueConversationAsync(
                                _credentials.MsAppId,
                                handoffRecord.ConversationReference,
                                (turnContext, cancellationToken) => turnContext.SendActivityAsync(eventActivity, cancellationToken), default);

                            var traceActivity = Activity.CreateTraceActivity(
                                "ServiceNowVirtualAgent",
                                label: "ServiceNowHandoff->Handoff completed");

                            await(_adapter).ContinueConversationAsync(
                                _credentials.MsAppId,
                                handoffRecord.ConversationReference,
                                (turnContext, cancellationToken) => turnContext.SendActivityAsync(traceActivity, cancellationToken), default);
                        }

                        Response.StatusCode = (int)HttpStatusCode.OK;
                    }
                    else
                    {
                        // No matching handoff record for the conversation referenced by ServiceNow
                        Response.StatusCode = (int)HttpStatusCode.NotFound;
                    }
                }
                else
                {
                    // Malformed response from ServiceNow
                    Response.StatusCode = (int)HttpStatusCode.BadRequest;
                }
            }
        }
Exemplo n.º 3
0
        private async Task HandleAcceptStatusEvent(string body)
        {
            var webhookData = JsonConvert.DeserializeObject <Models.AcceptStatusEvent.WebhookData>(body);

            foreach (var change in webhookData.body.changes)
            {
                if (change?.originatorMetadata?.role == "ASSIGNED_AGENT")
                {
                    // Agent has accepted the conversation
                    var convId = change?.conversationId;

                    if (await ConversationHandoffRecordMap.GetByRemoteConversationId(change.conversationId) is LivePersonHandoffRecord handoffRecord)
                    {
                        if (handoffRecord.ConversationRecord.IsAcknowledged || handoffRecord.ConversationRecord.IsClosed)
                        {
                            // Already acknowledged this one
                            break;
                        }

                        var conversationRecord = new LivePersonConversationRecord()
                        {
                            AppJWT         = handoffRecord.ConversationRecord.AppJWT,
                            ConsumerJWS    = handoffRecord.ConversationRecord.ConsumerJWS,
                            MessageDomain  = handoffRecord.ConversationRecord.MessageDomain,
                            ConversationId = handoffRecord.ConversationRecord.ConversationId,
                            IsClosed       = handoffRecord.ConversationRecord.IsClosed,
                            IsAcknowledged = true
                        };

                        var updatedHandoffRecord = new LivePersonHandoffRecord(handoffRecord.ConversationReference, conversationRecord);

                        // Update atomically -- only one will succeed
                        if (ConversationHandoffRecordMap.TryUpdate(convId, updatedHandoffRecord, handoffRecord))
                        {
                            var eventActivity = EventFactory.CreateHandoffStatus(
                                updatedHandoffRecord.ConversationReference.Conversation, "accepted") as Activity;

                            //await _adapter.ContinueConversationAsync(_credentials.MsAppId, eventActivity, _bot.OnTurnAsync, default);

                            //TEMPORARY WORKAROUND UNTIL CLOUDADAPTER IS IN PLACE SO ABOVE LINE WILL WORK
                            await(_adapter).ContinueConversationAsync(
                                _credentials.MsAppId,
                                handoffRecord.ConversationReference,
                                (turnContext, cancellationToken) => turnContext.SendActivityAsync(eventActivity, cancellationToken), default);
                        }
                    }
                }
            }
        }
Exemplo n.º 4
0
        private async Task SendActivityToUser(Change change, Activity humanActivity)
        {
            if (await ConversationHandoffRecordMap.GetByRemoteConversationId(change.conversationId) is
                LivePersonHandoffRecord handoffRecord)
            {
                if (!handoffRecord.ConversationRecord.IsClosed)
                {
                    MicrosoftAppCredentials.TrustServiceUrl(handoffRecord.ConversationReference.ServiceUrl);

                    await(_adapter).ContinueConversationAsync(
                        _credentials.MsAppId,
                        handoffRecord.ConversationReference,
                        (turnContext, cancellationToken) =>
                        turnContext.SendActivityAsync(humanActivity, cancellationToken), default);
                }
            }
            else
            {
                // The bot has no record of this conversation, this should not happen
                throw new Exception("Cannot find conversation");
            }
        }
Exemplo n.º 5
0
        private async Task HandleChatStateEvent(string body)
        {
            var webhookData = JsonConvert.DeserializeObject <Models.ChatStateEvent.WebhookData>(body);

            foreach (var change in webhookData.body.changes)
            {
                if (change?.@event?.chatState == "COMPOSING")
                {
                    if (await ConversationHandoffRecordMap.GetByRemoteConversationId(change.conversationId) is LivePersonHandoffRecord handoffRecord)
                    {
                        var typingActivity = new Activity
                        {
                            Type = ActivityTypes.Typing
                        };

                        await _adapter.ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(typingActivity, cancellationToken), default);
                    }
                }
            }
        }
Exemplo n.º 6
0
 public ServiceNowHandoffMiddleware(ConversationHandoffRecordMap conversationHandoffRecordMap, IServiceNowCredentialsProvider creds, BotFrameworkAuthentication botFrameworkAuth) : base(conversationHandoffRecordMap)
 {
     _conversationHandoffRecordMap = conversationHandoffRecordMap;
     _creds            = creds;
     _botFrameworkAuth = botFrameworkAuth;
 }
Exemplo n.º 7
0
 public LivePersonHandoffMiddleware(ConversationHandoffRecordMap conversationHandoffRecordMap, ILivePersonCredentialsProvider creds) : base(conversationHandoffRecordMap)
 {
     _conversationHandoffRecordMap = conversationHandoffRecordMap;
     _creds = creds;
 }
Exemplo n.º 8
0
 public LivePersonHandoffController(BotAdapter adapter, IBot bot, ILivePersonCredentialsProvider credentials, ConversationHandoffRecordMap conversationHandoffRecordMap) : base(conversationHandoffRecordMap)
 {
     _credentials = credentials;
     _adapter     = adapter;
     _bot         = bot;
 }
Exemplo n.º 9
0
        public static async Task <LivePersonConversationRecord> EscalateToAgentAsync(ITurnContext turnContext, IEventActivity handoffEvent, string account, string clientId, string clientSecret, ConversationHandoffRecordMap conversationHandoffRecordMap)
        {
            var sentinelDomain = await GetDomainAsync(account, "sentinel").ConfigureAwait(false);

            var appJWT = await GetAppJWTAsync(account, sentinelDomain, clientId, clientSecret).ConfigureAwait(false);

            var consumer = new ConsumerId {
                ext_consumer_id = turnContext.Activity.From.Id
            };

            var idpDomain = await GetDomainAsync(account, "idp").ConfigureAwait(false);

            var consumerJWS = await GetConsumerJWSAsync(account, idpDomain, appJWT, consumer).ConfigureAwait(false);

            var context = handoffEvent.Value as JObject;

            // This can be null:
            var skill = context?.Value <string>("Skill");
            var engagementAttributes = (context["EngagementAttributes"] as JArray)?.ToObject <EngagementAttribute[]>();

            var msgDomain = await GetDomainAsync(account, "asyncMessagingEnt").ConfigureAwait(false);

            var conversations = new Conversation[] {
                new Conversation {
                    kind = "req",
                    id   = "1,",
                    type = "userprofile.SetUserProfile",
                    body = new Body {
                        authenticatedData = new Authenticateddata {
                            lp_sdes = engagementAttributes?.Select(ea => ea.ToLivePersonEngagementAttribute()).ToArray()
                        }
                    }
                },
                new Conversation {
                    kind    = "req",
                    id      = "2,",
                    type    = "cm.ConsumerRequestConversation",
                    skillId = skill,
                    body    = new Body {
                        brandId = account
                    }
                },
            };

            var conversationId = await StartConversationAsync(account, msgDomain, appJWT, consumerJWS, conversations).ConfigureAwait(false);

            System.Diagnostics.Debug.WriteLine($"Started LP conversation id {conversationId}");

            var messageId = 1;

            // First, play out the transcript
            var handoffActivity = handoffEvent as Activity;

            if (handoffActivity.Attachments != null)
            {
                foreach (var attachment in handoffActivity.Attachments)
                {
                    if (attachment.Name == "Transcript")
                    {
                        var transcript = attachment.Content as Transcript;

                        if (transcript.Activities != null)
                        {
                            foreach (var activity in transcript.Activities)
                            {
                                var message2 = MakeLivePersonMessage(messageId++,
                                                                     conversationId,
                                                                     $"{activity.From.Name}: {activity.Text}");
                                await SendMessageToConversationAsync(account, msgDomain, appJWT, consumerJWS, message2)
                                .ConfigureAwait(false);
                            }
                        }
                    }
                }
            }

            return(new LivePersonConversationRecord {
                ConversationId = conversationId, AppJWT = appJWT, ConsumerJWS = consumerJWS, MessageDomain = msgDomain
            });
        }
        // A sample mapping from ServiceNow response types to Bot Framework concepts, in many cases this will need to be adapted to a
        // Bot owners specific requirements and styling.
        private async Task HandleContentEvent(ServiceNowResponseMessage responseMessage)
        {
            foreach (var item in responseMessage.body)
            {
                IMessageActivity responseActivity;

                // Map ServiceNow UX controls to Bot Framework concepts. This will need refinement as broader experiences are used but this covers a broad range of out-of-box ServiceNow response types.
                switch (item.uiType)
                {
                case "TopicPickerControl":
                case "ItemPicker":
                case "Picker":

                    // Map the picker concept to a basic HeroCard with buttons
                    List <CardAction> cardActions = new List <CardAction>();
                    foreach (var option in item.options)
                    {
                        cardActions.Add(new CardAction("imBack", option.description ?? option.label, value: option.label));
                    }

                    var pickerHeroCard = new HeroCard(buttons: cardActions);
                    responseActivity = MessageFactory.Attachment(pickerHeroCard.ToAttachment());

                    responseActivity.AsMessageActivity().Text = item.promptMsg ?? item.label;
                    break;

                case "DefaultPicker":

                    // Map the picker concept to a basic HeroCard with buttons
                    List <CardAction> defaultPickerActions = new List <CardAction>();
                    foreach (var option in item.options)
                    {
                        defaultPickerActions.Add(new CardAction("imBack", option.description ?? option.label, value: option.label));
                    }

                    var defaultPickerCard = new HeroCard(buttons: defaultPickerActions);
                    responseActivity = MessageFactory.Attachment(defaultPickerCard.ToAttachment());
                    break;

                case "GroupedPartsOutputControl":
                    responseActivity = MessageFactory.Text(item.header);
                    responseActivity.AttachmentLayout = "carousel";
                    responseActivity.Attachments      = new List <Microsoft.Bot.Schema.Attachment>();
                    foreach (var action in item.values)
                    {
                        var cardAction = new CardAction("openUrl", action.label, value: action.action);
                        var card       = new HeroCard(action.label, null, action.description, null, null, cardAction);
                        responseActivity.Attachments.Add(card.ToAttachment());
                    }
                    break;

                case "OutputHtml":

                    // We can't render HTML inside of conversations.
                    responseActivity = MessageFactory.Text(StripTags(item.value));

                    break;

                case "Boolean":
                    List <CardAction> booleanCardActions = new List <CardAction>();

                    booleanCardActions.Add(new CardAction("imBack", title: "Yes", displayText: "Yes", value: "true"));
                    booleanCardActions.Add(new CardAction("imBack", title: "No", displayText: "Yes", value: "false"));
                    var booleanHeroCard = new HeroCard(buttons: booleanCardActions);

                    responseActivity = MessageFactory.Attachment(booleanHeroCard.ToAttachment());
                    responseActivity.AsMessageActivity().Text = item.promptMsg ?? item.label;
                    break;

                case "OutputText":
                    responseActivity = MessageFactory.Text(item.value ?? item.label);
                    break;

                case "OutputImage":

                    var cardImages = new List <CardImage>();
                    cardImages.Add(new CardImage(url: item.value));

                    var imageHeroCard = new HeroCard(images: cardImages);
                    responseActivity = MessageFactory.Attachment(imageHeroCard.ToAttachment());

                    break;

                case "OutputLink":

                    var linkHeroCard = new HeroCard(buttons: new List <CardAction> {
                        new CardAction("openUrl", item.value)
                    });
                    responseActivity = MessageFactory.Attachment(linkHeroCard.ToAttachment());
                    responseActivity.AsMessageActivity().Text = item.promptMsg ?? item.label;

                    break;

                default:
                    responseActivity = MessageFactory.Text(item.value ?? item.label);
                    break;
                }

                if (await ConversationHandoffRecordMap.GetByRemoteConversationId(responseMessage.clientSessionId) is ServiceNowHandoffRecord handoffRecord)
                {
                    if (!handoffRecord.ConversationRecord.IsClosed)
                    {
                        MicrosoftAppCredentials.TrustServiceUrl(handoffRecord.ConversationReference.ServiceUrl);

                        await(_adapter).ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(responseActivity, cancellationToken), default);

                        var traceActivity = Activity.CreateTraceActivity(
                            "ServiceNowVirtualAgent",
                            $"Response from ServiceNow Virtual Agent received (Id:{responseMessage.requestId})",
                            label: "ServiceNowHandoff->Response from ServiceNow Virtual Agent");

                        await(_adapter).ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(traceActivity, cancellationToken), default);
                    }
                }
                else
                {
                    // The bot has no record of this conversation, this should not happen
                    throw new Exception("Cannot find conversation");
                }
            }
        }
        // A sample mapping from ServiceNow response types to Bot Framework concepts, in many cases this will need to be adapted to a
        // Bot owners specific requirements and styling.
        private async Task HandleContentEvent(ServiceNowResponseMessage responseMessage)
        {
            int VAOptionsCount = 0;

            foreach (var item in responseMessage.body)
            {
                IMessageActivity responseActivity;

                // Map ServiceNow UX controls to Bot Framework concepts. This will need refinement as broader experiences are used but this covers a broad range of out-of-box ServiceNow response types.
                switch (item.uiType)
                {
                case "TopicPickerControl":
                case "ItemPicker":
                case "Picker":
                    if (item.options.All(o => !string.IsNullOrEmpty(o.attachment)))
                    {
                        // Map the picker concept to a HeroCard carousel
                        responseActivity = MessageFactory.Text(item.label);
                        responseActivity.AttachmentLayout = "carousel";
                        responseActivity.Attachments      = new List <Microsoft.Bot.Schema.Attachment>();
                        foreach (var option in item.options)
                        {
                            var card = new HeroCard(
                                subtitle: option.description,
                                images: new List <CardImage>()
                            {
                                new CardImage($"https://{_credentials.ServiceNowTenant}/{option.attachment}")
                            },
                                buttons: new List <CardAction>
                            {
                                new CardAction("imBack", option.label, value: option.label)
                            });
                            responseActivity.Attachments.Add(card.ToAttachment());
                        }
                    }
                    else
                    {
                        List <CardAction> cardActions = new List <CardAction>();
                        foreach (var option in item.options)
                        {
                            VAOptionsCount = VAOptionsCount + 1;
                            cardActions.Add(new CardAction("imBack", option.description ?? option.label, value: option.label));
                            if (VAOptionsCount <= item.options.Count())
                            {
                                if (VAOptionsCount == 50)
                                {
                                    break;
                                }
                            }
                        }

                        var pickerHeroCard = new HeroCard(text: item.promptMsg ?? item.label, buttons: cardActions);
                        responseActivity = MessageFactory.Attachment(pickerHeroCard.ToAttachment());
                    }

                    break;

                case "DefaultPicker":

                    // Map the picker concept to a basic HeroCard with buttons
                    List <CardAction> defaultPickerActions = new List <CardAction>();
                    foreach (var option in item.options)
                    {
                        VAOptionsCount = VAOptionsCount + 1;
                        defaultPickerActions.Add(new CardAction("imBack", option.description ?? option.label, value: option.label));
                        if (VAOptionsCount <= item.options.Count())
                        {
                            if (VAOptionsCount == 50)
                            {
                                break;
                            }
                        }
                    }

                    var defaultPickerCard = new HeroCard(buttons: defaultPickerActions);
                    responseActivity = MessageFactory.Attachment(defaultPickerCard.ToAttachment());
                    break;

                case "GroupedPartsOutputControl":
                    responseActivity = MessageFactory.Text(item.header);
                    responseActivity.AttachmentLayout = "carousel";
                    responseActivity.Attachments      = new List <Microsoft.Bot.Schema.Attachment>();
                    foreach (var action in item.values)
                    {
                        var card = new HeroCard(subtitle: action.description, buttons: new List <CardAction>
                        {
                            new CardAction("openUrl", action.label ?? action.action, value: action.action)
                        });
                        if (VAOptionsCount <= item.values.Count())
                        {
                            if (VAOptionsCount == 11)
                            {
                                break;
                            }
                            responseActivity.Attachments.Add(card.ToAttachment());
                        }
                    }
                    break;

                case "Boolean":
                    List <CardAction> booleanCardActions = new List <CardAction>();

                    booleanCardActions.Add(new CardAction("imBack", title: "Yes", displayText: "Yes", value: "true"));
                    booleanCardActions.Add(new CardAction("imBack", title: "No", displayText: "Yes", value: "false"));
                    var booleanHeroCard = new HeroCard(text: item.promptMsg ?? item.label, buttons: booleanCardActions);

                    responseActivity = MessageFactory.Attachment(booleanHeroCard.ToAttachment());

                    break;

                case "OutputText":
                    responseActivity = MessageFactory.Text(item.value?.ToString() ?? item.label);
                    break;

                case "OutputImage":

                    var cardImages = new List <CardImage>();
                    cardImages.Add(new CardImage(url: $"{_credentials.ServiceNowTenant}{item.value}"));
                    var imageHeroCard = new HeroCard(images: cardImages);
                    responseActivity = MessageFactory.Attachment(imageHeroCard.ToAttachment());

                    break;

                case "OutputLink":

                    var bodyValue = item.value as BodyValue;

                    var linkHeroCard = new HeroCard(text: item.header, buttons: new List <CardAction>
                    {
                        new CardAction("openUrl", item.label, value: bodyValue.action)
                    });
                    responseActivity = MessageFactory.Attachment(linkHeroCard.ToAttachment());

                    break;

                case "OutputCard":

                    var cardData = JObject.Parse(item.data.ToString());

                    var items = new List <AdaptiveColumnSet>();

                    var titleColumnSet = new AdaptiveColumnSet
                    {
                        Columns = new List <AdaptiveColumn>()
                        {
                            new AdaptiveColumn
                            {
                                Items = new List <AdaptiveElement>()
                                {
                                    new AdaptiveTextBlock(cardData["title"]?.ToString())
                                    {
                                        Size   = AdaptiveTextSize.Medium,
                                        Weight = AdaptiveTextWeight.Bolder
                                    }
                                }
                            },
                            new AdaptiveColumn
                            {
                                Items = new List <AdaptiveElement>()
                                {
                                    new AdaptiveTextBlock(cardData["subtitle"]?.ToString())
                                    {
                                        Size = AdaptiveTextSize.Medium
                                    }
                                }
                            }
                        }
                    };

                    items.Add(titleColumnSet);

                    foreach (var field in cardData["fields"])
                    {
                        var columnSet = new AdaptiveColumnSet
                        {
                            Columns = new List <AdaptiveColumn>()
                            {
                                new AdaptiveColumn
                                {
                                    Items = new List <AdaptiveElement>()
                                    {
                                        new AdaptiveTextBlock(field["fieldLabel"]?.ToString())
                                        {
                                            Weight = AdaptiveTextWeight.Bolder
                                        }
                                    }
                                },
                                new AdaptiveColumn
                                {
                                    Items = new List <AdaptiveElement>()
                                    {
                                        new AdaptiveTextBlock(field["fieldValue"]?.ToString())
                                        {
                                            Wrap = true
                                        }
                                    }
                                }
                            }
                        };

                        items.Add(columnSet);
                    }

                    var adaptiveCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 2))
                    {
                        Body = new List <AdaptiveElement>()
                    };

                    adaptiveCard.Body.AddRange(items);

                    responseActivity = MessageFactory.Attachment(new Attachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content     = JObject.FromObject(adaptiveCard),
                    });

                    break;

                case "DateTime":
                    var dateTimeCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 2))
                    {
                        Body = new List <AdaptiveElement>()
                        {
                            new AdaptiveContainer()
                            {
                                Items = new List <AdaptiveElement>()
                                {
                                    new AdaptiveTextBlock()
                                    {
                                        Text = item.label,
                                        Wrap = true
                                    },
                                    new AdaptiveDateInput()
                                    {
                                        Id    = "dateVal",
                                        Value = DateTime.UtcNow.ToString("MM-dd-yyyy")
                                    },
                                    new AdaptiveTimeInput()
                                    {
                                        Id = "timeVal"
                                    }
                                }
                            }
                        },
                        Actions = new List <AdaptiveAction>()
                        {
                            // Using a SubmitAction with an msteams messageBack object to show display text
                            // from the user while sending the date and time inputs as data
                            new AdaptiveSubmitAction()
                            {
                                Title    = "Submit",
                                DataJson = "{\r\n\"msteams\": {\r\n\"type\": \"messageBack\",\r\n\"displayText\": \"Datetime submitted\"\r\n}\r\n}"
                            }
                        }
                    };

                    responseActivity = MessageFactory.Attachment(new Attachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content     = JObject.FromObject(dateTimeCard),
                    });

                    break;

                default:
                    responseActivity = MessageFactory.Text(item.value?.ToString() ?? item.label);
                    break;
                }

                if (await ConversationHandoffRecordMap.GetByRemoteConversationId(responseMessage.clientSessionId) is ServiceNowHandoffRecord handoffRecord)
                {
                    if (!handoffRecord.ConversationRecord.IsClosed)
                    {
                        MicrosoftAppCredentials.TrustServiceUrl(handoffRecord.ConversationReference.ServiceUrl);

                        await(_adapter).ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(responseActivity, cancellationToken), default);

                        var traceActivity = Activity.CreateTraceActivity(
                            "ServiceNowVirtualAgent",
                            $"Response from ServiceNow Virtual Agent received (Id:{responseMessage.requestId})",
                            label: "ServiceNowHandoff->Response from ServiceNow Virtual Agent");

                        await(_adapter).ContinueConversationAsync(
                            _credentials.MsAppId,
                            handoffRecord.ConversationReference,
                            (turnContext, cancellationToken) => turnContext.SendActivityAsync(traceActivity, cancellationToken), default);
                    }
                }
                else
                {
                    // The bot has no record of this conversation, this should not happen
                    throw new Exception("Cannot find conversation");
                }
            }
        }
        public static async Task <ServiceNowConversationRecord> EscalateToAgentAsync(ITurnContext turnContext, IEventActivity handoffEvent, string serviceNowTenant, string serviceNowAuthConnectionName, ConversationHandoffRecordMap conversationHandoffRecordMap)
        {
            var context = handoffEvent.Value as JObject;

            // These can be null, it may not be necessary to pass these if ServiceNow can infer from the logger in user (SSO) but shows the concept
            // of how a Bot can pass information as part of the handoff event for use by ServiceNow.
            var timeZone = context?.Value <string>("timeZone");
            var userId   = context?.Value <string>("userId");
            var emailId  = context?.Value <string>("emailId");

            return(new ServiceNowConversationRecord {
                ConversationId = turnContext.Activity.Conversation.Id, ServiceNowTenant = serviceNowTenant, ServiceNowAuthConnectionName = serviceNowAuthConnectionName, Timezone = timeZone, UserId = userId, EmailId = emailId
            });
        }