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; } } }
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); } } } } }
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"); } }
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); } } } }
public ServiceNowHandoffMiddleware(ConversationHandoffRecordMap conversationHandoffRecordMap, IServiceNowCredentialsProvider creds, BotFrameworkAuthentication botFrameworkAuth) : base(conversationHandoffRecordMap) { _conversationHandoffRecordMap = conversationHandoffRecordMap; _creds = creds; _botFrameworkAuth = botFrameworkAuth; }
public LivePersonHandoffMiddleware(ConversationHandoffRecordMap conversationHandoffRecordMap, ILivePersonCredentialsProvider creds) : base(conversationHandoffRecordMap) { _conversationHandoffRecordMap = conversationHandoffRecordMap; _creds = creds; }
public LivePersonHandoffController(BotAdapter adapter, IBot bot, ILivePersonCredentialsProvider credentials, ConversationHandoffRecordMap conversationHandoffRecordMap) : base(conversationHandoffRecordMap) { _credentials = credentials; _adapter = adapter; _bot = bot; }
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 }); }