diff --git a/skills/src/csharp/Skills.sln b/skills/src/csharp/Skills.sln index 75a2a8cef7..a733c24260 100644 --- a/skills/src/csharp/Skills.sln +++ b/skills/src/csharp/Skills.sln @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HospitalitySkill", "experim EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicSkill", "experimental\musicskill\MusicSkill.csproj", "{A2ECB4BF-FD59-4746-B699-F1C326D561BB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSkill", "experimental\eventskill\EventSkill.csproj", "{5BF2293A-6E56-464A-8355-EDC8972F7E09}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU @@ -177,6 +179,14 @@ Global {A2ECB4BF-FD59-4746-B699-F1C326D561BB}.Documentation|Any CPU.Build.0 = Debug|Any CPU {A2ECB4BF-FD59-4746-B699-F1C326D561BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2ECB4BF-FD59-4746-B699-F1C326D561BB}.Release|Any CPU.Build.0 = Release|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BF2293A-6E56-464A-8355-EDC8972F7E09}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -198,6 +208,7 @@ Global {8CB101FA-B496-4507-80A2-151FAB7E8ED1} = {3665D242-1A88-4860-B148-BAB695B7B5E4} {0EFEA4F2-DC7E-436E-B951-E9B566AFF7F0} = {3665D242-1A88-4860-B148-BAB695B7B5E4} {A2ECB4BF-FD59-4746-B699-F1C326D561BB} = {3665D242-1A88-4860-B148-BAB695B7B5E4} + {5BF2293A-6E56-464A-8355-EDC8972F7E09} = {3665D242-1A88-4860-B148-BAB695B7B5E4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B849B7E-CCF5-4031-91F7-CA835433B457} diff --git a/skills/src/csharp/experimental/eventskill/.filenesting.json b/skills/src/csharp/experimental/eventskill/.filenesting.json new file mode 100644 index 0000000000..90c31b9e8a --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/.filenesting.json @@ -0,0 +1,15 @@ +{ + "help": "https://go.microsoft.com/fwlink/?linkid=866610", + "dependentFileProviders": { + "add": { + "pathSegment": { + "add": { + ".*": [ + ".json", + ".resx" + ] + } + } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Adapters/DefaultAdapter.cs b/skills/src/csharp/experimental/eventskill/Adapters/DefaultAdapter.cs new file mode 100644 index 0000000000..d46b799671 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Adapters/DefaultAdapter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Globalization; +using EventSkill.Responses.Shared; +using EventSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Solutions.Middleware; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; + +namespace EventSkill.Bots +{ + public class DefaultAdapter : BotFrameworkHttpAdapter + { + public DefaultAdapter( + BotSettings settings, + ICredentialProvider credentialProvider, + IBotTelemetryClient telemetryClient, + ResponseManager responseManager) + : base(credentialProvider) + { + OnTurnError = async (context, exception) => + { + CultureInfo.CurrentUICulture = new CultureInfo(context.Activity.Locale); + await context.SendActivityAsync(responseManager.GetResponse(SharedResponses.ErrorMessage)); + await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}")); + telemetryClient.TrackException(exception); + }; + + // Uncomment the following line for local development without Azure Storage + // Use(new TranscriptLoggerMiddleware(new MemoryTranscriptStore())); + Use(new TranscriptLoggerMiddleware(new AzureBlobTranscriptStore(settings.BlobStorage.ConnectionString, settings.BlobStorage.Container))); + Use(new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: true)); + Use(new ShowTypingMiddleware()); + Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us")); + Use(new EventDebuggerMiddleware()); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Adapters/EventSkillAdapter.cs b/skills/src/csharp/experimental/eventskill/Adapters/EventSkillAdapter.cs new file mode 100644 index 0000000000..dbd040ca88 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Adapters/EventSkillAdapter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Globalization; +using EventSkill.Responses.Shared; +using EventSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions.Middleware; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Schema; + +namespace EventSkill.Adapters +{ + public class EventSkillAdapter : SkillWebSocketBotAdapter + { + public EventSkillAdapter( + BotSettings settings, + UserState userState, + ConversationState conversationState, + ResponseManager responseManager, + IBotTelemetryClient telemetryClient) + { + OnTurnError = async (context, exception) => + { + CultureInfo.CurrentUICulture = new CultureInfo(context.Activity.Locale); + await context.SendActivityAsync(responseManager.GetResponse(SharedResponses.ErrorMessage)); + await context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"Skill Error: {exception.Message} | {exception.StackTrace}")); + telemetryClient.TrackException(exception); + }; + + // Uncomment the following line for local development without Azure Storage + // Use(new TranscriptLoggerMiddleware(new MemoryTranscriptStore())); + Use(new TranscriptLoggerMiddleware(new AzureBlobTranscriptStore(settings.BlobStorage.ConnectionString, settings.BlobStorage.Container))); + Use(new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: true)); + Use(new SetLocaleMiddleware(settings.DefaultLocale ?? "en-us")); + Use(new EventDebuggerMiddleware()); + Use(new SkillMiddleware(userState, conversationState, conversationState.CreateProperty(nameof(EventSkill)))); + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Bots/DialogBot.cs b/skills/src/csharp/experimental/eventskill/Bots/DialogBot.cs new file mode 100644 index 0000000000..5a1f9c9f38 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Bots/DialogBot.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.DependencyInjection; + +namespace EventSkill.Bots +{ + public class DialogBot : IBot + where T : Dialog + { + private readonly Dialog _dialog; + private readonly BotState _conversationState; + private readonly BotState _userState; + private readonly IBotTelemetryClient _telemetryClient; + + public DialogBot(IServiceProvider serviceProvider, T dialog) + { + _dialog = dialog; + _conversationState = serviceProvider.GetService(); + _userState = serviceProvider.GetService(); + _telemetryClient = serviceProvider.GetService(); + } + + public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) + { + // Client notifying this bot took to long to respond (timed out) + if (turnContext.Activity.Code == EndOfConversationCodes.BotTimedOut) + { + _telemetryClient.TrackTrace($"Timeout in {turnContext.Activity.ChannelId} channel: Bot took too long to respond.", Severity.Information, null); + return; + } + + await _dialog.RunAsync(turnContext, _conversationState.CreateProperty(nameof(DialogState)), cancellationToken); + + // Save any state changes that might have occured during the turn. + await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); + await _userState.SaveChangesAsync(turnContext, false, cancellationToken); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Connected Services/Application Insights/ConnectedService.json b/skills/src/csharp/experimental/eventskill/Connected Services/Application Insights/ConnectedService.json new file mode 100644 index 0000000000..d15936a838 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Connected Services/Application Insights/ConnectedService.json @@ -0,0 +1,7 @@ +{ + "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", + "Version": "8.13.10627.1", + "GettingStartedDocument": { + "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Content/EventCard.json b/skills/src/csharp/experimental/eventskill/Content/EventCard.json new file mode 100644 index 0000000000..19fc2f7563 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Content/EventCard.json @@ -0,0 +1,96 @@ +{ + "type": "AdaptiveCard", + "id": "EventCard", + "body": [ + { + "type": "Container", + "backgroundImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAADVCAIAAABlrTvmAAAACXBIWXMAABYlAAAWJQFJUiTwAAAG0mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMDMtMjJUMTc6MDM6NDAtMDc6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAzLTIyVDE3OjE2OjU2LTA3OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE5LTAzLTIyVDE3OjE2OjU2LTA3OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY0ZGY4YjdiLTM2MTYtNDRkNy04MTI3LTgyNzk4NmUyODk3ZSIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmExNDU0NmE3LWZhODMtMjc0Mi1hNWU0LWIxMzYzNjQ2NWU5NyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjhlMDlmYmE5LTE2ZjktNGZiNC05MzdhLThkOTYzMGMxOTQyNiI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6OGUwOWZiYTktMTZmOS00ZmI0LTkzN2EtOGQ5NjMwYzE5NDI2IiBzdEV2dDp3aGVuPSIyMDE5LTAzLTIyVDE3OjAzOjQwLTA3OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6Yzc5ODg2ZTQtZTg3Zi00MmMzLWI5ZjYtN2FkMDg3YTlkOTg4IiBzdEV2dDp3aGVuPSIyMDE5LTAzLTIyVDE3OjE2OjU2LTA3OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NjRkZjhiN2ItMzYxNi00NGQ3LTgxMjctODI3OTg2ZTI4OTdlIiBzdEV2dDp3aGVuPSIyMDE5LTAzLTIyVDE3OjE2OjU2LTA3OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7bJaRFAAAIbUlEQVR4nO3dy27bSAKGUZKWFCfuLKYH6N712/XzzTvNE8ykJzfHkmZRhpN2ZItkFVkXnrOUNj9AgB9IkVD/57/+3QHQuk9fvv33w6fcK6Z5ux/2N/3Fry5/uroh9wAAFtdYQcshogCNU9DliChAyxR0USIK0CwFXZqIArRJQVcgogANUtB1iChAaxR0NSIK0BQFXZOIArRDQVcmogCNUND1iShACxQ0CxEFqJ6C5iKiAHVT0IxEFKBiCpqXiALUSkGzE1GAKtVY0Nu2CtqJKECNKi3ooa2CdiIKUB0FLYeIAtREQYsiogDVUNDSiChAHRS0QCIKUAEFLZOIApROQYslogBFU9CSiShAuRS0cCIKUCgFLZ+IApTo/ttDjQXdDxsqaCeiAAU6nc//+fA594ppNljQTkQBCvTx0/3xeMq9YoJtFrQTUYACff5yn3vCBJstaCeiAKX59nCs6DJ0ywXtRBSgNF++PuSeMNbGC9qJKEBparkMVdBORAFKczxVEFEFDUQUgGkU9ImIAjCBgv5IRAHKctjvck94kYI+I6IAZdnvCj0zK+jPCj1UAJt1OJR4JaqgF4koQFmGvn97e8i94m8U9CUiClCc93dvck/4TkFfIaIAxbm5GX65u829ousU9BoRBSjR+7s3u91N3g0KepWIAhTqn/+4y9hRBR1DRAEKNfR9ro4q6EgiClCuLB1V0PFEFKBoK3dUQScRUYDSrdbR2/2wU9ApRBSgAit0VEFnEFGAOizaUQWdR0QBqrFQRxV0NhEFqEnyjnqSKIaIAlQmYUcVNJKIAtQnSUcVNJ6IAlQpsqMKmoSIAtRqdkcVNBURBajYjI4qaEIiClC3SR1V0LREFKB6IzvqfdDkRBSgBVc7qqBLEFGARrzSUQVdiIgCtONiRxV0OSIK0JRnHVXQRYkoQGueOqqgSxNRgAYNff/H7+/fHXa5hzRORAEadHcYbnfDb7/eHZb8H29EFKA1d4fhcNN3XTcMvY4uSkQBmvJU0EBHFyWiAO14VtBAR5cjogCNuFjQQEcXIqIALXi3f7GggY4uQUQBqvduP7zZXX8fVEeTE1GAuo0saKCjaYkoQMUmFTTQ0YREFKBWMwoa6GgqIgpQpdkFDXQ0CREFqE9kQQMdjSeiAJVJUtBARyOJKEBNrr4POpWOxhBRgGokL2igo7OJKEAdFipooKPziChABRYtaKCjM4goQOlWKGigo1OJKEDRVitooKOTiChAuVYuaKCj44koQKGyFDTQ0ZFEFKBEGQsa6OgYIgpQnOwFDXT0KhEFKEshBQ109HUiClCQogoa6OgrRBSgFAUWNNDRl4goQBGKLWigoxeJKEB+hRc00NGfiShAZlUUNNDRZ0QUIKeKChro6I9EFCCb6goa6OgTEQXIo9KCBjoaiChABlUXNNDRTkQB1tdAQQMdFVGAVTVT0GDjHRVRgPU0VtBgyx0VUYCVNFnQYLMdFVGANTRc0GCbHRVRgMU1X9Bggx0VUYBlbaSgwdY6KqIAC9pUQYNNdVREAZaywYIG2+moiAIsYrMFDYah/+3XX5rvqIgCpLfxggZb6KiIAiR2u+sVNGi+oyIKkNJu6G93Tq3ftd1RRxogpbd759XnGu6ogw2QzOGmdx/3olY7KqIAyewl9GVNdlREAdLo+24/iOhr2uuoiAKkcdMr6HWNdVREAdJwK3ekljoqogBpuBAdr5mOiigAGbTRUREFSON4zr2gNg10VEQB0jiJ6HS1d1REAdI4ns6n3BtqVHVHRRQgmQe3dGept6MiCpDMVxGdq9KOiihAMsfT+V5H56qxoyIKkNLnh5NfRmerrqMiCpDS+dx9vNfR+erqqIgCJHY8nXU0RkUdFVGA9HQ0Ui0dFVGARehopCo6KqIAS3nsqMd15yq/oyIKsKDj6fzxm47OV3hHRRRgWToaqeSOiijA4nQ0UrEdFVGANYSOyuhsZXZURAFWcjyd/3evo/MV2FERBViPjkYqraMiCrAqHY0UOrovo6MiCrA2HY00DP3vZXRURAEy0NFIhXRURAHy0NFIJXRURAGy0dFI2TsqogA56WikvB0VUYDMdDRSxo6KKEB+OhopV0dFFKAIOhopS0dFFKAUOhpp/Y6KKEBBdDTSyh0VUYCy6GikNTsqogDF0dFIq3VURAFKpKOR1umoiAIUKnQ094qKrdBREQUo1/F0/ktHIyzdUREFKJqORlq0oyIKUDodjfTY0X36joooQAV0NNJCHRVRgDroaKQlOiqiANXQ0UjJOyqiADUJHT17gXSutB0VUYDK6GikhB0VUYD66GikVB0VUYAq6Wik0NFDXEdFFKBWOhopvqMiClAxHY0U2VERBaibjkaK6aiIAlRPRyPNfs5IRAFaoKOR5nVURAEaoaORZnRURAHaoaORpv4+KqIATTmezn991dH5JnVURAFaczzraJTxHRVRgAbpaKSRHRVRgDbpaKQxHRVRgGbpaKSrHRVRgJbpaKTXOyqiAI3T0UivdFREAdqno5Fe6qiIAmyCjka62FERBdiK0NGTjs71c0dFFGBDdDTSs46KKMC26GikHzsqogCbo6ORnjoqogBbpKORQkdFFGCjdDTSMPQiCrBdOhpJRAE2zfujMUQUYOt0dDYRBUBHZxJRALpOR2cRUQAePXY094yKiCgA3+noJCIKwN88nHR0LBEF4DkdHUlEAbhAR8cQUQAu09GrRBSAF+no60QUgNfo6CtEFIArdPQlIgrAdTp6kYgCMIqO/kxEARhLR58RUQAm0NEfiSgA0+joExEFYDIdDUQUgDl0tBNRAGbTUREFYL6Nd1REAYiy5Y6KKACxNttREQUggW12VEQBSOOxo1sKqYgCkMzD6fxhSx39P2CJeSCLm9hfAAAAAElFTkSuQmCC", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "Image", + "id": "icon", + "horizontalAlignment": "Center", + "url": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCAxNiAxMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE2IDRDMTUuODU5NCA0IDE1LjcyOTIgNC4wMjYwNCAxNS42MDk0IDQuMDc4MTJDMTUuNDg5NiA0LjEzMDIxIDE1LjM4MjggNC4yMDMxMiAxNS4yODkxIDQuMjk2ODhDMTUuMjAwNSA0LjM4NTQyIDE1LjEzMDIgNC40ODk1OCAxNS4wNzgxIDQuNjA5MzhDMTUuMDI2IDQuNzI5MTcgMTUgNC44NTkzOCAxNSA1QzE1IDUuMTQwNjIgMTUuMDI2IDUuMjcwODMgMTUuMDc4MSA1LjM5MDYyQzE1LjEzMDIgNS41MTA0MiAxNS4yMDA1IDUuNjE3MTkgMTUuMjg5MSA1LjcxMDk0QzE1LjM4MjggNS43OTk0OCAxNS40ODk2IDUuODY5NzkgMTUuNjA5NCA1LjkyMTg4QzE1LjcyOTIgNS45NzM5NiAxNS44NTk0IDYgMTYgNlYxMEgwVjZDMC4xNDA2MjUgNiAwLjI3MDgzMyA1Ljk3Mzk2IDAuMzkwNjI1IDUuOTIxODhDMC41MTA0MTcgNS44Njk3OSAwLjYxNDU4MyA1Ljc5OTQ4IDAuNzAzMTI1IDUuNzEwOTRDMC43OTY4NzUgNS42MTcxOSAwLjg2OTc5MiA1LjUxMDQyIDAuOTIxODc1IDUuMzkwNjJDMC45NzM5NTggNS4yNzA4MyAxIDUuMTQwNjIgMSA1QzEgNC44NTkzOCAwLjk3Mzk1OCA0LjcyOTE3IDAuOTIxODc1IDQuNjA5MzhDMC44Njk3OTIgNC40ODk1OCAwLjc5Njg3NSA0LjM4NTQyIDAuNzAzMTI1IDQuMjk2ODhDMC42MTQ1ODMgNC4yMDMxMiAwLjUxMDQxNyA0LjEzMDIxIDAuMzkwNjI1IDQuMDc4MTJDMC4yNzA4MzMgNC4wMjYwNCAwLjE0MDYyNSA0IDAgNFYwSDE2VjRaTTE1IDFIMVYzLjI2NTYyQzEuMzEyNSAzLjQ0NzkyIDEuNTU3MjkgMy42OTI3MSAxLjczNDM4IDRDMS45MTE0NiA0LjMwNzI5IDIgNC42NDA2MiAyIDVDMiA1LjM1OTM4IDEuOTExNDYgNS42OTI3MSAxLjczNDM4IDZDMS41NTcyOSA2LjMwNzI5IDEuMzEyNSA2LjU1MjA4IDEgNi43MzQzOFY5SDE1VjYuNzM0MzhDMTQuNjg3NSA2LjU1MjA4IDE0LjQ0MjcgNi4zMDcyOSAxNC4yNjU2IDZDMTQuMDg4NSA1LjY5MjcxIDE0IDUuMzU5MzggMTQgNUMxNCA0LjY0MDYyIDE0LjA4ODUgNC4zMDcyOSAxNC4yNjU2IDRDMTQuNDQyNyAzLjY5MjcxIDE0LjY4NzUgMy40NDc5MiAxNSAzLjI2NTYyVjFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "size": "Small", + "width": "30px", + "height": "30px" + } + ], + "width": "auto" + }, + { + "type": "Column", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "TextBlock", + "id": "title", + "size": "Large", + "weight": "Bolder", + "color": "Light", + "text": "Local Event" + } + ], + "width": "stretch" + } + ] + } + ] + }, + { + "type": "Image", + "url": "{ImageUrl}" + }, + { + "type": "TextBlock", + "text": "**{Title}**", + "size": "Medium", + "weight": "Bolder" + }, + { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "text": "{StartDate}", + "isSubtle": true, + "spacing": "Small" + }, + { + "type": "TextBlock", + "text": "{Location}", + "isSubtle": true, + "spacing": "Small" + }, + { + "type": "TextBlock", + "text": "{Price}", + "isSubtle": true, + "spacing": "Small" + } + ] + }, + { + "type": "TextBlock", + "text": "Powered by **Eventbrite**", + "horizontalAlignment": "Right", + "size": "Small" + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get Tickets", + "url": "{Url}" + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "speak": "{Speak}" +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Controllers/BotController.cs b/skills/src/csharp/experimental/eventskill/Controllers/BotController.cs new file mode 100644 index 0000000000..a4ffdf4398 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Controllers/BotController.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions; + +namespace EventSkill.Controllers +{ + [ApiController] + public class BotController : SkillController + { + public BotController( + IBot bot, + BotSettingsBase botSettings, + IBotFrameworkHttpAdapter botFrameworkHttpAdapter, + SkillWebSocketAdapter skillWebSocketAdapter) + : base(bot, botSettings, botFrameworkHttpAdapter, skillWebSocketAdapter) + { + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/Event.lu b/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/Event.lu new file mode 100644 index 0000000000..19f216cf9d --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/Event.lu @@ -0,0 +1,51 @@ +> ! Automatically generated by [LUDown CLI](https://github.com/Microsoft/botbuilder-tools/tree/master/Ludown), Wed Aug 21 2019 14:37:19 GMT-0700 (Pacific Daylight Time) + +> ! Source LUIS JSON file: stdin + +> ! Source QnA TSV file: Not Specified + +> ! Source QnA Alterations file: Not Specified + + +> # Intent definitions + +## FindEvents +- are there any events near me +- can you recommend events in the area +- can you recommend things for me to do +- find an event in my area +- find events near me +- find events nearby +- find me something to do +- find me something to do nearby +- get local events +- i want to go to an event nearby +- recommend events in my area +- recommend events nearby +- recommend things to do nearby +- what events are happening nearby +- whats happening in my area +- what's happening nearby + + +## None +- goodbye +- hello +- hi +- logout + + +> # Entity definitions + + +> # PREBUILT Entity definitions + + +> # Phrase list definitions + + +> # List entities + +> # RegEx entities + + diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/General.lu b/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/General.lu new file mode 100644 index 0000000000..bdf02ee0a9 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Resources/LU/en/General.lu @@ -0,0 +1,536 @@ +> ! Automatically generated by [LUDown CLI](https://github.com/Microsoft/botbuilder-tools/tree/master/Ludown), Wed Aug 21 2019 14:37:29 GMT-0700 (Pacific Daylight Time) + +> ! Source LUIS JSON file: stdin + +> ! Source QnA TSV file: Not Specified + +> ! Source QnA Alterations file: Not Specified + + +> # Intent definitions + +## Cancel +- cancel +- cancel app +- cancel cancel +- cancel it +- cancel never mind +- cancel that +- canceled +- cancelled +- do nothing +- don't do that +- don't do that anymore +- forget about it +- go away +- just cancel +- just cancel it +- nerver mind +- never mind +- never mind cancel +- never mind cancel that +- no cancel +- no cancel cancel +- no cancel it +- no cancel that +- no never mind +- no no cancel +- no no cancel it +- nothing never mind +- nothing please +- oh cancel +- oh don't do that +- please do nothing +- quit +- sorry, don't do it + + +## Confirm +- confirm +- do it +- fine +- go for it +- great +- i'm sure +- it's fine +- no doubt +- of course +- oh yeah +- oh yes +- ok +- ok for now +- okay +- perfect +- perfect thank you +- right +- right yes +- sounds good +- sounds good thank you +- sounds good thanks +- sounds good to me +- sounds great +- sounds perfect +- sure +- sure does +- sure is +- sure thing +- sure yes +- thank you very much +- thank you yes +- that is correct +- that is right +- that right +- that sounds good +- that's fine +- this is good +- very good +- ya +- yeah +- yeah baby +- yeah bro +- yeah buddy +- yeah cool +- yeah go ahead +- yeah go for it +- yeah good +- yes + + +## Escalate +- can i talk to a person +- contact support +- contact the customer service +- customer service +- human service +- i need manual customer service +- i need real human help +- i need support +- i want to talk to a human +- i want to talk to a real human +- is there any person i can talk to +- is there any real human +- is there any real person +- talk to a human + + +## FinishTask +- all finished +- all set +- done +- finish +- finished +- finished with +- i am done +- i am finished +- it finished +- it's done +- it's finished +- just be +- submit +- submit it + + +## GoBack +- back +- back please +- back to +- back to last +- back to last step +- get back +- get back please +- get back to last +- go back +- go back on +- go back please +- go back to +- last step +- no go back to +- no no go back to +- please return +- return + + +## Help +- any help +- can you help +- can you help me +- give me some help +- help +- how can i get it +- how to do it +- i need help +- i need some assist +- i need some help +- is there any help +- open help +- please help +- some help +- who can help me + + +## Logout +- forget me +- log out +- logout +- sign out +- signout + + +## None +- all of them +- i want them all +- i want to all of them + + +## ReadAloud +- can you read it +- can you read that +- can you read that for me +- can you you read page aloud +- could you tell me what that says +- detail aloud what that says +- hey read that for me +- i need to hear this page +- i would like you to read that for me +- make a reading of this page +- please read +- please read it +- please read me the page +- please read my latest email +- please read this +- please read this out loud +- please read this page +- please read this page aloud +- please read this page out loud +- please read this to me +- read all on the screen to me +- read aloud +- read aloud the current text onscreen +- read file aloud +- read it +- read it aloud +- read it out loud +- read it outloud +- read it please +- read it to me +- read me this page +- read my to list +- read outloud +- read page +- read page aloud +- read page outloud +- read sentence out loud +- read text +- read text aloud +- read that +- read that out loud +- read the page +- read the page onscreen to me +- read the page out loud +- read the page to me +- read the text to me +- read the words on this page +- read this for me please +- read this page +- read this page aloud +- read this page out loud +- read this page to me +- read to me +- read what is currently on the screen to me +- speak of what is on this page +- start reading this +- tell me about the information on the screen +- tell me the current text on screen +- vocalize what s on the page +- what does the page say +- would you please read that for me +- would you read that out loud please +- would you read that to me please + + +## Reject +- i don't like it +- i reject +- negative +- never +- no +- no i don't want that +- no later +- no leave it +- no more no +- no no +- no no no +- no no thank you +- no not that one +- no reject it +- no thank you +- no thanks +- no way +- no wrong +- nope +- not +- not at all +- not even close +- not exactly +- not now +- not quite +- not right now +- not that +- nothing much +- oh no +- reject +- reject it + + +## Repeat +- again +- could you say it again +- i didn't hear repeat again +- i have not heard +- pardon +- repeat +- repeat please +- repeat that +- say again +- say again please +- say that again +- sorry +- what +- what did you say +- what was that again + + +## SelectAny +- any of it +- any one is ok +- anyone is fine +- anything +- choose anyone +- choose one of it randomly +- opt for a random one +- opt for any of it +- select a random choice +- select a random one +- select any +- select any choice +- select any of it + + +## SelectItem +- choose last +- choose last one +- choose no.2 +- choose the {DirectionalReference=bottom left} +- choose the first choice +- choose the fourth one +- choose the {DirectionalReference=upper left} choice +- choose the {DirectionalReference=upper right} one +- choose {DirectionalReference=top right} +- choose {DirectionalReference=top right} one +- i like {DirectionalReference=left} one +- i like second +- i like second one +- i like the {DirectionalReference=bottom} one +- i like the first one +- i like the third +- i like the third choice +- i like the {DirectionalReference=top right} one +- i like the {DirectionalReference=upper right} +- i like {DirectionalReference=upper right} +- i want {DirectionalReference=bottom} +- i want fourth +- i want {DirectionalReference=left} +- i want {DirectionalReference=right} one +- i want the first +- i want the fourth choice +- i want the {DirectionalReference=left} +- i want the {DirectionalReference=lower} choice +- i want the {DirectionalReference=right} one +- i want the second one +- i want third one +- i want to choose {DirectionalReference=bottom} one +- i want to choose {DirectionalReference=lower right} +- i want to choose {DirectionalReference=right} +- i want to choose second one +- i want to choose the first one +- i want to choose the fourth +- i want to choose the last choice +- i want to choose the {DirectionalReference=left} one +- i want to choose the {DirectionalReference=lower} choice +- i want to choose the {DirectionalReference=right} +- i want to choose third +- opt for first one +- opt for last +- opt for {DirectionalReference=left} +- opt for {DirectionalReference=right} one +- opt for the last one +- opt for the {DirectionalReference=left} one +- opt for the {DirectionalReference=lower} choice +- opt for the {DirectionalReference=right} +- opt for the second +- opt for the second choice +- select fourth one +- select {DirectionalReference=lower} one +- select no.5 +- select the {DirectionalReference=bottom} choice +- select the first +- select the last choice +- select the {DirectionalReference=lower} one +- select the {DirectionalReference=right} one +- select the third one +- select the {DirectionalReference=upper right} +- select third +- select {DirectionalReference=upper} +- what about the last +- what about the third one + + +## SelectNone +- i don't want to choose any one +- i don't want to select any one +- i want neither of them +- i want none of them +- neither +- neither of those +- neither one +- neither one of them +- neither thank you +- none +- none none +- none none of them +- none of them +- none of them thank you +- none of these +- none of those +- they look bad, can you give me other choices + + +## ShowNext +- and after that +- display more +- displays more +- give me more +- go forward +- go to the next one +- go to the next three items +- i need to go to the next one +- i want more +- more +- move to the next one +- next +- reveal more +- show me the next +- show more +- show the next 3 +- show the next 4 items +- show the next one +- show the next two options +- tell me more +- tell more +- what about next one +- what after that +- what's after that +- what's more +- what's next +- what's the next 2 +- what's up next + + +## ShowPrevious +- back to the last one +- bring the previous one +- display previously +- get back to the last one +- go back to last one +- go back to previous +- go back to the last one +- go previously +- go to the previous +- go to the previous one +- previous one +- previous one please +- return to the previous one +- reveal previous +- reveal previously +- show earlier +- show me the previous one +- show previous +- show the previous one +- what before that +- what is the previous +- what's before that + + +## StartOver +- clear and start again +- could you start it over +- please begin again +- restart +- restart it +- start again +- start it over +- start over +- start over it +- turn over a new leaf + + +## Stop +- baby just be quiet +- be quiet +- be quiet now +- come on stop +- dismiss +- end +- end it +- exit exit +- exit stop +- god shut up +- hey stop +- i love you to stop talking +- i mean stop listening +- i said stop +- just be quiet +- my god shut up +- never mind stop +- no be quiet +- no be quiet now +- no no no no stop talking +- no shut up +- no stop +- nobody cares stop talking +- nowhere just be quiet +- oh my god shut up +- ok stop stop +- quiet +- quiet now +- shut the fuck up +- shut up +- shut up be quiet +- shut up quiet +- shut your mouth +- stop please +- stop talking +- turn off +- turn off stop + + +> # Entity definitions + +$DirectionalReference:simple + + +> # PREBUILT Entity definitions + +$PREBUILT:number + +$PREBUILT:ordinal + + +> # Phrase list definitions + + +> # List entities + +> # RegEx entities + + diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Resources/parameters.template.json b/skills/src/csharp/experimental/eventskill/Deployment/Resources/parameters.template.json new file mode 100644 index 0000000000..95841eb589 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Resources/parameters.template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "useCosmosDb": { + "value": false + }, + "useStorage": { + "value": false + }, + "appServicePlanSku": { + "value": { + "tier": "Free", + "name": "F1" + } + }, + "botServiceSku": { + "value": "F0" + }, + "luisServiceSku": { + "value": "F0" + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Resources/template.json b/skills/src/csharp/experimental/eventskill/Deployment/Resources/template.json new file mode 100644 index 0000000000..5eb8a2cab3 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Resources/template.json @@ -0,0 +1,245 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "suffix": { + "type": "string", + "defaultValue": "[take(uniqueString(resourceGroup().id), 7)]" + }, + "microsoftAppId": { + "type": "string" + }, + "microsoftAppPassword": { + "type": "string" + }, + "useCosmosDb": { + "type": "bool", + "defaultValue": true + }, + "cosmosDbName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "useStorage": { + "type": "bool", + "defaultValue": true + }, + "storageAccountName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appServicePlanName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appServicePlanSku": { + "type": "object", + "defaultValue": { + "tier": "Standard", + "name": "S1" + } + }, + "appInsightsName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "appInsightsLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "botWebAppName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "botServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-', parameters('suffix'))]" + }, + "botServiceSku": { + "type": "string", + "defaultValue": "S1" + }, + "luisServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-luis-', parameters('suffix'))]" + }, + "luisServiceSku": { + "type": "string", + "defaultValue": "S0" + }, + "luisServiceLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + } + }, + "variables": { + "botWebAppName": "[replace(parameters('botWebAppName'), '_', '')]", + "storageAccountName": "[toLower(take(replace(replace(parameters('storageAccountName'), '-', ''), '_', ''), 24))]", + "cosmosDbAccountName": "[toLower(take(replace(parameters('cosmosDbName'), '_', ''), 31))]", + "botEndpoint": "[concat('https://', toLower(variables('botWebAppName')), '.azurewebsites.net/api/messages')]" + }, + "resources": [ + { + "apiVersion": "2018-02-01", + "name": "99ea37e6-a3e6-4102-a249-71c880607386", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + }, + { + "comments": "CosmosDB for bot state.", + "type": "Microsoft.DocumentDB/databaseAccounts", + "kind": "GlobalDocumentDB", + "apiVersion": "2015-04-08", + "name": "[variables('cosmosDbAccountName')]", + "location": "[parameters('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0 + } + ] + }, + "condition": "[parameters('useCosmosDb')]" + }, + { + "comments": "storage account", + "type": "Microsoft.Storage/storageAccounts", + "kind": "StorageV2", + "apiVersion": "2018-07-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "condition": "[parameters('useStorage')]" + }, + { + "comments": "app service plan", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2018-02-01", + "name": "[parameters('appServicePlanName')]", + "location": "[parameters('location')]", + "sku": "[parameters('appServicePlanSku')]", + "properties": {} + }, + { + "comments": "app insights", + "type": "Microsoft.Insights/components", + "kind": "web", + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('appInsightsLocation')]", + "properties": { + "Application_Type": "web" + } + }, + { + "comments": "bot web app", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-02-01", + "name": "[variables('botWebAppName')]", + "location": "[parameters('location')]", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "siteConfig": { + "webSocketsEnabled": true, + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('microsoftAppId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('microsoftAppPassword')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]" + ] + }, + { + "comments": "bot service", + "type": "Microsoft.BotService/botServices", + "kind": "sdk", + "apiVersion": "2018-07-12", + "name": "[parameters('botServiceName')]", + "location": "global", + "sku": { + "name": "[parameters('botServiceSku')]" + }, + "properties": { + "displayName": "[parameters('botServiceName')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('microsoftAppId')]", + "developerAppInsightKey": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).InstrumentationKey]", + "developerAppInsightsApplicationId": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).ApplicationId]" + } + }, + { + "comments": "Cognitive service key for all LUIS apps.", + "type": "Microsoft.CognitiveServices/accounts", + "kind": "LUIS", + "apiVersion": "2017-04-18", + "name": "[parameters('luisServiceName')]", + "location": "[parameters('luisServiceLocation')]", + "sku": { + "name": "[parameters('luisServiceSku')]" + } + } + ], + "outputs": { + "botWebAppName": { + "type": "string", + "value": "[variables('botWebAppName')]" + }, + "ApplicationInsights": { + "type": "object", + "value": { + "InstrumentationKey": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName'))).InstrumentationKey]" + } + }, + "blobStorage": { + "type": "object", + "value": { + "connectionString": "[if(parameters('useStorage'), concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2018-07-01').keys[0].value, ';EndpointSuffix=core.windows.net'), '')]", + "container": "transcripts" + } + }, + "cosmosDb": { + "type": "object", + "value": { + "cosmosDBEndpoint": "[if(parameters('useCosmosDb'), reference(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName'))).documentEndpoint, '')]", + "authKey": "[if(parameters('useCosmosDb'), listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbAccountName')), '2015-04-08').primaryMasterKey, '')]", + "databaseId": "botstate-db", + "collectionId": "botstate-collection" + } + }, + "luis": { + "type": "object", + "value": { + "accountName": "[parameters('luisServiceName')]", + "region": "[parameters('luisServiceLocation')]", + "key": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('luisServiceName')),'2017-04-18').key1]" + } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy.ps1 new file mode 100644 index 0000000000..2ec874cf98 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy.ps1 @@ -0,0 +1,245 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $resourceGroup, + [string] $location, + [string] $appId, + [string] $appPassword, + [string] $luisAuthoringKey, + [string] $luisAuthoringRegion, + [string] $parametersFile, + [string] $languages = "en-us", + [string] $projDir = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "deploy_log.txt") +) + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +if (-not (Test-Path (Join-Path $projDir 'appsettings.json'))) +{ + Write-Host "! Could not find an 'appsettings.json' file in the current directory." -ForegroundColor DarkRed + Write-Host "+ Please re-run this script from your project directory." -ForegroundColor Magenta + Break +} + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Bot Name (used as default name for resource group and deployed resources)" +} + +if (-not $resourceGroup) { + $resourceGroup = $name +} + +if (-not $location) { + $location = Read-Host "? Azure resource group region" +} + +if (-not $appPassword) { + $appPassword = Read-Host "? Password for MSA app registration (must be at least 16 characters long, contain at least 1 special character, and contain at least 1 numeric character)" +} + +if (-not $luisAuthoringRegion) { + $luisAuthoringRegion = Read-Host "? LUIS Authoring Region (westus, westeurope, or australiaeast)" +} + +if (-not $luisAuthoringKey) { + Switch ($luisAuthoringRegion) { + "westus" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://luis.ai/user/settings)" + Break + } + "westeurope" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://eu.luis.ai/user/settings)" + Break + } + "australiaeast" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://au.luis.ai/user/settings)" + Break + } + default { + Write-Host "! $($luisAuthoringRegion) is not a valid LUIS authoring region." -ForegroundColor DarkRed + Break + } + } + + if (-not $luisAuthoringKey) { + Break + } +} + +if (-not $appId) { + # Create app registration + $app = (az ad app create ` + --display-name $name ` + --password `"$($appPassword)`" ` + --available-to-other-tenants ` + --reply-urls 'https://token.botframework.com/.auth/web/redirect' ` + --output json) + + # Retrieve AppId + if ($app) { + $appId = ($app | ConvertFrom-Json) | Select-Object -ExpandProperty appId + } + + if(-not $appId) { + Write-Host "! Could not provision Microsoft App Registration automatically. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Provision an app manually in the Azure Portal, then try again providing the -appId and -appPassword arguments. See https://aka.ms/vamanualappcreation for more information." -ForegroundColor Magenta + Break + } +} + +# Get timestamp +$timestamp = Get-Date -f MMddyyyyHHmmss + +# Create resource group +Write-Host "> Creating resource group ..." +(az group create --name $resourceGroup --location $location --output json) 2>> $logFile | Out-Null + +# Deploy Azure services (deploys LUIS, QnA Maker, Content Moderator, CosmosDB) +if ($parametersFile) { + Write-Host "> Validating Azure deployment ..." + $validation = az group deployment validate ` + --resource-group $resourcegroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters "@$($parametersFile)" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + + if ($validation) { + $validation >> $logFile + $validation = $validation | ConvertFrom-Json + + if (-not $validation.error) { + Write-Host "> Deploying Azure services (this could take a while)..." -ForegroundColor Yellow + $deployment = az group deployment create ` + --name $timestamp ` + --resource-group $resourceGroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters "@$($parametersFile)" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + } + else { + Write-Host "! Template is not valid with provided parameters. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Error: $($validation.error.message)" -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break + } + } +} +else { + Write-Host "> Validating Azure deployment ..." + $validation = az group deployment validate ` + --resource-group $resourcegroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + + if ($validation) { + $validation >> $logFile + $validation = $validation | ConvertFrom-Json + + if (-not $validation.error) { + Write-Host "> Deploying Azure services (this could take a while)..." -ForegroundColor Yellow + $deployment = az group deployment create ` + --name $timestamp ` + --resource-group $resourceGroup ` + --template-file "$(Join-Path $PSScriptRoot '..' 'Resources' 'template.json')" ` + --parameters name=$name microsoftAppId=$appId microsoftAppPassword="`"$($appPassword)`"" ` + --output json + } + else { + Write-Host "! Template is not valid with provided parameters. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Error: $($validation.error.message)" -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break + } + } +} + +# Get deployment outputs +$outputs = (az group deployment show ` + --name $timestamp ` + --resource-group $resourceGroup ` + --query properties.outputs ` + --output json) 2>> $logFile + +# If it succeeded then we perform the remainder of the steps +if ($outputs) +{ + # Log and convert to JSON + $outputs >> $logFile + $outputs = $outputs | ConvertFrom-Json + $outputMap = @{} + $outputs.PSObject.Properties | Foreach-Object { $outputMap[$_.Name] = $_.Value } + + # Update appsettings.json + Write-Host "> Updating appsettings.json ..." + if (Test-Path $(Join-Path $projDir appsettings.json)) { + $settings = Get-Content $(Join-Path $projDir appsettings.json) | ConvertFrom-Json + } + else { + $settings = New-Object PSObject + } + + $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppId' -Value $appId + $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppPassword' -Value $appPassword + foreach ($key in $outputMap.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $outputMap[$key].value } + $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $projDir appsettings.json) + + if ($outputs.qnaMaker.value.key) { $qnaSubscriptionKey = $outputs.qnaMaker.value.key } + + # Delay to let QnA Maker finish setting up + Start-Sleep -s 30 + + # Deploy cognitive models + Invoke-Expression "& '$(Join-Path $PSScriptRoot 'deploy_cognitive_models.ps1')' -name $($name) -luisAuthoringRegion $($luisAuthoringRegion) -luisAuthoringKey $($luisAuthoringKey) -luisAccountName $($outputs.luis.value.accountName) -luisAccountRegion $($outputs.luis.value.region) -luisSubscriptionKey $($outputs.luis.value.key) -resourceGroup $($resourceGroup) -qnaSubscriptionKey '$($qnaSubscriptionKey)' -outFolder '$($projDir)' -languages '$($languages)'" + + # Publish bot + Invoke-Expression "& '$(Join-Path $PSScriptRoot 'publish.ps1')' -name $($outputs.botWebAppName.value) -resourceGroup $($resourceGroup) -projFolder '$($projDir)'" + + Write-Host "> Done." +} +else +{ + # Check for failed deployments + $operations = (az group deployment operation list -g $resourceGroup -n $timestamp --output json) 2>> $logFile | Out-Null + + if ($operations) { + $operations = $operations | ConvertFrom-Json + $failedOperations = $operations | Where { $_.properties.statusmessage.error -ne $null } + if ($failedOperations) { + foreach ($operation in $failedOperations) { + switch ($operation.properties.statusmessage.error.code) { + "MissingRegistrationForLocation" { + Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType). This resource is not avaliable in the location provided." -ForegroundColor DarkRed + Write-Host "+ Update the .\Deployment\Resources\parameters.template.json file with a valid region for this resource and provide the file path in the -parametersFile parameter." -ForegroundColor Magenta + } + default { + Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType)." + Write-Host "! Code: $($operation.properties.statusMessage.error.code)." + Write-Host "! Message: $($operation.properties.statusMessage.error.message)." + } + } + } + } + } + else { + Write-Host "! Deployment failed. Please refer to the log file for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + } + + Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta + Break +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy_cognitive_models.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy_cognitive_models.ps1 new file mode 100644 index 0000000000..361af45fa9 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/deploy_cognitive_models.ps1 @@ -0,0 +1,300 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $luisAuthoringRegion, + [string] $luisAuthoringKey, + [string] $luisAccountName, + [string] $luisAccountRegion, + [string] $luisSubscriptionKey, + [string] $qnaSubscriptionKey, + [string] $resourceGroup, + [switch] $useDispatch, + [string] $languages = "en-us", + [string] $outFolder = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "deploy_cognitive_models_log.txt") +) + +. $PSScriptRoot\luis_functions.ps1 +. $PSScriptRoot\qna_functions.ps1 + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Base name for Cognitive Models" +} + +if (-not $luisAuthoringRegion) { + $luisAuthoringRegion = Read-Host "? LUIS Authoring Region (westus, westeurope, or australiaeast)" +} + +if (-not $luisAuthoringKey) { + Switch ($luisAuthoringRegion) { + "westus" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://luis.ai/user/settings)" + Break + } + "westeurope" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://eu.luis.ai/user/settings)" + Break + } + "australiaeast" { + $luisAuthoringKey = Read-Host "? LUIS Authoring Key (found at https://au.luis.ai/user/settings)" + Break + } + default { + Write-Host "! $($luisAuthoringRegion) is not a valid LUIS authoring region." -ForegroundColor DarkRed + Break + } + } + + if (-not $luisAuthoringKey) { + Break + } +} + +if (-not $luisAccountName) { + $luisAccountName = Read-Host "? LUIS Service Name (exising service in Azure required)" +} + +if (-not $resourceGroup) { + $resourceGroup = $name + + $rgExists = az group exists -n $resourceGroup --output json + if ($rgExists -eq "false") + { + $resourceGroup = Read-Host "? LUIS Service Resource Group (exising service in Azure required)" + } +} + +if (-not $luisSubscriptionKey) { + $keys = az cognitiveservices account keys list --name $luisAccountName --resource-group $resourceGroup --output json | ConvertFrom-Json + + if ($keys) { + $luisSubscriptionKey = $keys.key1 + } + else { + Write-Host "! Could not retrieve LUIS Subscription Key." -ForgroundColor DarkRed + Write-Host "+ Verify the -luisAccountName and -resourceGroup parameters are correct." -ForegroundColor Magenta + Break + } +} + +if (-not $luisAccountRegion) { + $luisAccountRegion = Read-Host "? LUIS Service Location" +} + +if (-not $qnaSubscriptionKey) { + $useQna = $false +} +else { + $useQna = $true +} + +$azAccount = az account show --output json | ConvertFrom-Json +$azAccessToken = $(Invoke-Expression "az account get-access-token --output json") | ConvertFrom-Json + +# Get languages +$languageArr = $languages -split "," + +# Initialize settings obj +$settings = @{ defaultLocale = $languageArr[0]; cognitiveModels = New-Object PSObject } + +# Deploy localized resources +Write-Host "> Deploying cognitive models ..." +foreach ($language in $languageArr) +{ + $langCode = ($language -split "-")[0] + $config = New-Object PSObject + + if ($useDispatch) { + # Add dispatch to config + $config | Add-Member -MemberType NoteProperty -Name dispatchModel -Value $(New-Object PSObject) + + # Initialize Dispatch + Write-Host "> Initializing dispatch model ..." + $dispatchName = "$($name)$($langCode)_Dispatch" + $dataFolder = Join-Path $PSScriptRoot .. Resources Dispatch $langCode + (dispatch init ` + --name $dispatchName ` + --luisAuthoringKey $luisAuthoringKey ` + --luisAuthoringRegion $luisAuthoringRegion ` + --dataFolder $dataFolder) 2>> $logFile | Out-Null + } + + # Deploy LUIS apps + $luisFiles = Get-ChildItem "$(Join-Path $PSScriptRoot .. 'Resources' 'LU' $langCode)" | Where {$_.extension -eq ".lu"} + if ($luisFiles) { + $config | Add-Member -MemberType NoteProperty -Name languageModels -Value @() + + foreach ($lu in $luisFiles) + { + # Deploy LUIS model + $luisApp = DeployLUIS ` + -name $name ` + -lu_file $lu ` + -region $luisAuthoringRegion ` + -luisAuthoringKey $luisAuthoringKey ` + -language $language ` + -log $logFile + + Write-Host "> Setting LUIS subscription key ..." + if ($luisApp) { + # Setting subscription key + $addKeyResult = luis add appazureaccount ` + --appId $luisApp.id ` + --authoringKey $luisAuthoringKey ` + --region $luisAuthoringRegion ` + --accountName $luisAccountName ` + --azureSubscriptionId $azAccount.id ` + --resourceGroup $resourceGroup ` + --armToken "$($azAccessToken.accessToken)" 2>> $logFile + + if (-not $addKeyResult) { + $luisKeySet = $false + Write-Host "! Could not assign subscription key automatically. Review the log for more information. " -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Please assign your subscription key manually in the LUIS portal." -ForegroundColor Magenta + } + + if ($useDispatch) { + # Add luis app to dispatch + Write-Host "> Adding $($lu.BaseName) app to dispatch model ..." + (dispatch add ` + --type "luis" ` + --name $luisApp.name ` + --id $luisApp.id ` + --region $luisApp.region ` + --intentName "l_$($lu.BaseName)" ` + --dataFolder $dataFolder ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")") 2>> $logFile | Out-Null + } + + # Add to config + $config.languageModels += @{ + id = $lu.BaseName + name = $luisApp.name + appid = $luisApp.id + authoringkey = $luisAuthoringKey + authoringRegion = $luisAuthoringRegion + subscriptionkey = $luisSubscriptionKey + version = $luisApp.activeVersion + region = $luisAccountRegion + } + } + else { + Write-Host "! Could not create LUIS app. Skipping dispatch add." -ForegroundColor Cyan + } + } + } + + if ($useQna) { + if (Test-Path $(Join-Path $PSScriptRoot .. 'Resources' 'QnA' $langCode)) { + # Deploy QnA Maker KBs + $qnaFiles = Get-ChildItem "$(Join-Path $PSScriptRoot .. 'Resources' 'QnA' $langCode)" -Recurse | Where {$_.extension -eq ".lu"} + + if ($qnaFiles) { + $config | Add-Member -MemberType NoteProperty -Name knowledgebases -Value @() + + foreach ($lu in $qnaFiles) + { + # Deploy QnA Knowledgebase + $qnaKb = DeployKB -name $name -lu_file $lu -qnaSubscriptionKey $qnaSubscriptionKey -log $logFile + + if ($qnaKb) { + if ($useDispatch) { + Write-Host "> Adding $($lu.BaseName) kb to dispatch model ..." + (dispatch add ` + --type "qna" ` + --name $qnaKb.name ` + --id $qnaKb.id ` + --key $qnaSubscriptionKey ` + --intentName "q_$($lu.BaseName)" ` + --dataFolder $dataFolder ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")") 2>> $logFile | Out-Null + } + + # Add to config + $config.knowledgebases += @{ + id = $lu.BaseName + name = $qnaKb.name + kbId = $qnaKb.kbId + subscriptionKey = $qnaKb.subscriptionKey + endpointKey = $qnaKb.endpointKey + hostname = $qnaKb.hostname + } + } + else { + Write-Host "! Could not create knowledgebase. Skipping dispatch add." -ForegroundColor Cyan + } + } + } + } + else { + Write-Host "! No knowledgebases found. Skipping." -ForegroundColor Cyan + } + } + else { + Write-Host "! No QnA Maker Subscription Key provided. Skipping knowledgebases." -ForegroundColor Cyan + } + + if ($useDispatch) { + # Create dispatch model + Write-Host "> Creating dispatch model..." + $dispatch = (dispatch create ` + --dispatch "$(Join-Path $dataFolder "$($dispatchName).dispatch")" ` + --dataFolder $dataFolder ` + --culture $language) 2>> $logFile + + if (-not $dispatch) { + Write-Host "! Could not create Dispatch app. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Break + } + else { + $dispatchApp = $dispatch | ConvertFrom-Json + + # Setting subscription key + Write-Host "> Setting LUIS subscription key ..." + $addKeyResult = luis add appazureaccount ` + --appId $dispatchApp.appId ` + --accountName $luisAccountName ` + --authoringKey $luisAuthoringKey ` + --region $luisAuthoringRegion ` + --azureSubscriptionId $azAccount.id ` + --resourceGroup $resourceGroup ` + --armToken $azAccessToken.accessToken 2>> $logFile + + if (-not $addKeyResult) { + $luisKeySet = $false + Write-Host "! Could not assign subscription key automatically. Review the log for more information. " -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed + Write-Host "+ Please assign your subscription key manually in the LUIS portal." -ForegroundColor Magenta + } + + # Add to config + $config.dispatchModel = @{ + type = "dispatch" + name = $dispatchApp.name + appid = $dispatchApp.appId + authoringkey = $luisauthoringkey + authoringRegion = $luisAuthoringRegion + subscriptionkey = $luisSubscriptionKey + region = $luisAccountRegion + } + } + } + + # Add config to cognitivemodels dictionary + $settings.cognitiveModels | Add-Member -Type NoteProperty -Force -Name $langCode -Value $config +} + +# Write out config to file +$settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $outFolder "cognitivemodels.json" ) \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/luis_functions.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/luis_functions.ps1 new file mode 100644 index 0000000000..0da139daa8 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/luis_functions.ps1 @@ -0,0 +1,112 @@ +function DeployLUIS ($name, $lu_file, $region, $luisAuthoringKey, $language, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + $appName = "$($name)$($langCode)_$($id)" + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $language ` + --out_folder $outFolder ` + --out $outFile + + # Create LUIS app + Write-Host "> Deploying $($id) LUIS app ..." + $luisApp = (luis import application ` + --appName $appName ` + --authoringKey $luisAuthoringKey ` + --subscriptionKey $luisAuthoringKey ` + --region $region ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait) 2>> $log | ConvertFrom-Json + + if (-not $luisApp) { + Write-Host "! Could not deploy LUIS model. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($log)" -ForegroundColor DarkRed + Return $null + } + else { + # train and publish luis app + $(luis train version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait + & luis publish version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait) 2>> $log | Out-Null + + Return $luisApp + } +} + +function UpdateLUIS ($lu_file, $appId, $version, $region, $authoringKey, $subscriptionKey, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + + $luisApp = luis get application --appId $appId --region $region --authoringKey $authoringKey | ConvertFrom-Json + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $luisApp.culture ` + --out_folder $outFolder ` + --out $outFile + + Write-Host "> Getting current versions ..." + # Get list of current versions + $versions = luis list versions ` + --appId $appId ` + --region $region ` + --authoringKey $authoringKey | ConvertFrom-Json + + # If the current version exists + if ($versions | Where { $_.version -eq $version }) + { + # delete any old backups + if ($versions | Where { $_.version -eq "backup" }) + { + Write-Host "> Deleting old backup version ..." + luis delete version ` + --appId $appId ` + --versionId "backup" ` + --region $region ` + --authoringKey $authoringKey ` + --force --wait | Out-Null + } + + # rename the active version to backup + Write-Host "> Saving current version as backup ..." + luis rename version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --newVersionId backup ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --wait | Out-Null + } + + # import the new 0.1 version from the .luis file + Write-Host "> Importing new version ..." + luis import version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait | ConvertFrom-Json + + # train and publish luis app + $(luis train version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait + & luis publish version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait) 2>> $log | Out-Null +} + +function RunLuisGen($lu_file, $outName, $outFolder) { + $id = $lu_file.BaseName + $luisFolder = $lu_file.DirectoryName + $luisFile = Join-Path $luisFolder "$($id).luis" + + luisgen $luisFile -cs "$($outName)Luis" -o $outFolder +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/publish.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/publish.ps1 new file mode 100644 index 0000000000..9483ef1ce5 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/publish.ps1 @@ -0,0 +1,66 @@ +#Requires -Version 6 + +Param( + [string] $name, + [string] $resourceGroup, + [string] $projFolder = $(Get-Location), + [string] $logFile = $(Join-Path $PSScriptRoot .. "publish_log.txt") +) + +# Get mandatory parameters +if (-not $name) { + $name = Read-Host "? Bot Web App Name" +} + +if (-not $resourceGroup) { + $resourceGroup = Read-Host "? Bot Resource Group" +} + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +# Check for existing deployment files +if (-not (Test-Path (Join-Path $projFolder '.deployment'))) { + + # Get path to csproj file + $projFile = Get-ChildItem $projFolder ` + | Where-Object {$_.extension -eq ".csproj" } ` + | Select-Object -First 1 + + # Add needed deployment files for az + az bot prepare-deploy --lang Csharp --code-dir $projFolder --proj-file-path $projFile.name --output json | Out-Null +} + +# Delete src zip, if it exists +$zipPath = $(Join-Path $projFolder 'code.zip') +if (Test-Path $zipPath) { + Remove-Item $zipPath -Force | Out-Null +} + +# Perform dotnet publish step ahead of zipping up +$publishFolder = $(Join-Path $projFolder 'bin\Release\netcoreapp2.2') +dotnet publish -c release -o $publishFolder -v q > $logFile + +if($?) +{ + # Compress source code + Get-ChildItem -Path "$($publishFolder)" | Compress-Archive -DestinationPath "$($zipPath)" -Force | Out-Null + + # Publish zip to Azure + Write-Host "> Publishing to Azure ..." -ForegroundColor Green + (az webapp deployment source config-zip ` + --resource-group $resourceGroup ` + --name $name ` + --src $zipPath ` + --output json) 2>> $logFile | Out-Null +} +else +{ + Write-Host "! Could not deploy automatically to Azure. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/qna_functions.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/qna_functions.ps1 new file mode 100644 index 0000000000..0da139daa8 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/qna_functions.ps1 @@ -0,0 +1,112 @@ +function DeployLUIS ($name, $lu_file, $region, $luisAuthoringKey, $language, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + $appName = "$($name)$($langCode)_$($id)" + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $language ` + --out_folder $outFolder ` + --out $outFile + + # Create LUIS app + Write-Host "> Deploying $($id) LUIS app ..." + $luisApp = (luis import application ` + --appName $appName ` + --authoringKey $luisAuthoringKey ` + --subscriptionKey $luisAuthoringKey ` + --region $region ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait) 2>> $log | ConvertFrom-Json + + if (-not $luisApp) { + Write-Host "! Could not deploy LUIS model. Review the log for more information." -ForegroundColor DarkRed + Write-Host "! Log: $($log)" -ForegroundColor DarkRed + Return $null + } + else { + # train and publish luis app + $(luis train version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait + & luis publish version --appId $luisApp.id --region $region --authoringKey $luisAuthoringKey --versionId $luisApp.activeVersion --wait) 2>> $log | Out-Null + + Return $luisApp + } +} + +function UpdateLUIS ($lu_file, $appId, $version, $region, $authoringKey, $subscriptionKey, $log) +{ + $id = $lu_file.BaseName + $outFile = "$($id).luis" + $outFolder = $lu_file.DirectoryName + + $luisApp = luis get application --appId $appId --region $region --authoringKey $authoringKey | ConvertFrom-Json + + # Parse LU file + Write-Host "> Parsing $($id) LU file ..." + ludown parse toluis ` + --in $lu_file ` + --luis_culture $luisApp.culture ` + --out_folder $outFolder ` + --out $outFile + + Write-Host "> Getting current versions ..." + # Get list of current versions + $versions = luis list versions ` + --appId $appId ` + --region $region ` + --authoringKey $authoringKey | ConvertFrom-Json + + # If the current version exists + if ($versions | Where { $_.version -eq $version }) + { + # delete any old backups + if ($versions | Where { $_.version -eq "backup" }) + { + Write-Host "> Deleting old backup version ..." + luis delete version ` + --appId $appId ` + --versionId "backup" ` + --region $region ` + --authoringKey $authoringKey ` + --force --wait | Out-Null + } + + # rename the active version to backup + Write-Host "> Saving current version as backup ..." + luis rename version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --newVersionId backup ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --wait | Out-Null + } + + # import the new 0.1 version from the .luis file + Write-Host "> Importing new version ..." + luis import version ` + --appId $appId ` + --versionId $version ` + --region $region ` + --authoringKey $authoringKey ` + --subscriptionKey $subscriptionKey ` + --in "$(Join-Path $outFolder $outFile)" ` + --wait | ConvertFrom-Json + + # train and publish luis app + $(luis train version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait + & luis publish version --appId $appId --region $region --authoringKey $authoringKey --versionId $version --wait) 2>> $log | Out-Null +} + +function RunLuisGen($lu_file, $outName, $outFolder) { + $id = $lu_file.BaseName + $luisFolder = $lu_file.DirectoryName + $luisFile = Join-Path $luisFolder "$($id).luis" + + luisgen $luisFile -cs "$($outName)Luis" -o $outFolder +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Deployment/Scripts/update_cognitive_models.ps1 b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/update_cognitive_models.ps1 new file mode 100644 index 0000000000..d435017c30 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Deployment/Scripts/update_cognitive_models.ps1 @@ -0,0 +1,159 @@ +#Requires -Version 6 + +Param( + [switch] $RemoteToLocal, + [switch] $useLuisGen = $true, + [string] $configFile = $(Join-Path (Get-Location) 'cognitivemodels.json'), + [string] $dispatchFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'Dispatch'), + [string] $luisFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'LU'), + [string] $qnaFolder = $(Join-Path $PSScriptRoot '..' 'Resources' 'QnA'), + [string] $lgOutFolder = $(Join-Path (Get-Location) 'Services'), + [string] $logFile = $(Join-Path $PSScriptRoot .. "update_cognitive_models_log.txt") +) + +. $PSScriptRoot\luis_functions.ps1 +. $PSScriptRoot\qna_functions.ps1 + +# Reset log file +if (Test-Path $logFile) { + Clear-Content $logFile -Force | Out-Null +} +else { + New-Item -Path $logFile | Out-Null +} + +Write-Host "> Getting config file ..." +$languageMap = @{ } +$config = Get-Content -Raw -Path $configFile | ConvertFrom-Json +$config.cognitiveModels.PSObject.Properties | Foreach-Object { $languageMap[$_.Name] = $_.Value } + +foreach ($langCode in $languageMap.Keys) { + $models = $languageMap[$langCode] + $dispatch = $models.dispatchModel + + if ($RemoteToLocal) { + # Update local LU files based on hosted models + foreach ($luisApp in $models.languageModels) { + $culture = (luis get application ` + --appId $luisApp.appId ` + --authoringKey $luisApp.authoringKey ` + --subscriptionKey $luisApp.subscriptionKey ` + --region $luisApp.authoringRegion | ConvertFrom-Json).culture + + Write-Host "> Updating local $($luisApp.id).lu file ..." + luis export version ` + --appId $luisApp.appid ` + --versionId $luisApp.version ` + --region $luisApp.authoringRegion ` + --authoringKey $luisApp.authoringKey | ludown refresh ` + --stdin ` + -n "$($luisApp.id).lu" ` + -o $(Join-Path $luisFolder $langCode) + + # Parse LU file + $id = $luisApp.id + $outFile = "$($id).luis" + $outFolder = $(Join-Path $luisFolder $langCode) + $appName = "$($name)$($langCode)_$($id)" + + Write-Host "> Parsing $($luisApp.id) LU file ..." + ludown parse toluis ` + --in $(Join-Path $outFolder "$($luisApp.id).lu") ` + --luis_culture $culture ` + --out_folder $(Join-Path $luisFolder $langCode) ` + --out "$($luisApp.id).luis" + + if ($useLuisGen) { + Write-Host "> Running LuisGen for $($luisApp.id) app ..." + $luPath = $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + RunLuisGen -lu_file $(Get-Item $luPath) -outName "$($luisApp.id)" -outFolder $lgOutFolder + } + + # Add the LUIS application to the dispatch model. + # If the LUIS application id already exists within the model no action will be taken + if ($dispatch) { + Write-Host "> Adding $($luisApp.id) app to dispatch model ... " + (dispatch add ` + --type "luis" ` + --name $luisApp.name ` + --id $luisApp.appid ` + --region $luisApp.authoringRegion ` + --intentName "l_$($luisApp.id)" ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode)) 2>> $logFile | Out-Null + } + } + + # Update local LU files based on hosted QnA KBs + foreach ($kb in $models.knowledgebases) { + Write-Host "> Updating local $($kb.id).lu file ..." + qnamaker export kb ` + --environment Prod ` + --kbId $kb.kbId ` + --subscriptionKey $kb.subscriptionKey | ludown refresh ` + --stdin ` + -n "$($kb.id).lu" ` + -o $(Join-Path $qnaFolder $langCode) + + # Add the knowledge base to the dispatch model. + # If the knowledge base id already exists within the model no action will be taken + if ($dispatch) { + Write-Host "> Adding $($kb.id) kb to dispatch model ..." + (dispatch add ` + --type "qna" ` + --name $kb.name ` + --id $kb.kbId ` + --key $kb.subscriptionKey ` + --intentName "q_$($kb.id)" ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode)) 2>> $logFile | Out-Null + } + } + } + else { + # Update each luis model based on local LU files + foreach ($luisApp in $models.languageModels) { + Write-Host "> Updating hosted $($luisApp.id) app..." + $lu = Get-Item -Path $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + UpdateLUIS ` + -lu_file $lu ` + -appId $luisApp.appid ` + -version $luisApp.version ` + -region $luisApp.authoringRegion ` + -authoringKey $luisApp.authoringKey ` + -subscriptionKey $app.subscriptionKey + + if ($useLuisGen) { + Write-Host "> Running LuisGen for $($luisApp.id) app ..." + $luPath = $(Join-Path $luisFolder $langCode "$($luisApp.id).lu") + RunLuisGen -lu_file $(Get-Item $luPath) -outName "$($luisApp.id)" -outFolder $lgOutFolder + } + } + + # Update each knowledgebase based on local LU files + foreach ($kb in $models.knowledgebases) { + Write-Host "> Updating hosted $($kb.id) kb..." + $lu = Get-Item -Path $(Join-Path $qnaFolder $langCode "$($kb.id).lu") + UpdateKB ` + -lu_file $lu ` + -kbId $kb.kbId ` + -qnaSubscriptionKey $kb.subscriptionKey + } + } + + if ($dispatch) { + # Update dispatch model + Write-Host "> Updating dispatch model ..." + dispatch refresh ` + --dispatch $(Join-Path $dispatchFolder $langCode "$($dispatch.name).dispatch") ` + --dataFolder $(Join-Path $dispatchFolder $langCode) 2>> $logFile | Out-Null + + if ($useLuisGen) { + # Update dispatch.cs file + Write-Host "> Running LuisGen for Dispatch app..." + luisgen $(Join-Path $dispatchFolder $langCode "$($dispatch.name).json") -cs "DispatchLuis" -o $lgOutFolder 2>> $logFile | Out-Null + } + } +} + +Write-Host "> Done." \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Dialogs/EventDialogBase.cs b/skills/src/csharp/experimental/eventskill/Dialogs/EventDialogBase.cs new file mode 100644 index 0000000000..d753227d7d --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Dialogs/EventDialogBase.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using EventSkill.Models; +using EventSkill.Responses.Shared; +using EventSkill.Services; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions.Authentication; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Builder.Solutions.Util; +using Microsoft.Bot.Schema; + +namespace EventSkill.Dialogs +{ + public class EventDialogBase : ComponentDialog + { + public EventDialogBase( + string dialogId, + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + UserState userState, + IBotTelemetryClient telemetryClient) + : base(dialogId) + { + Services = services; + ResponseManager = responseManager; + StateAccessor = conversationState.CreateProperty(nameof(EventSkillState)); + UserAccessor = userState.CreateProperty(nameof(EventSkillUserState)); + TelemetryClient = telemetryClient; + + // NOTE: Uncomment the following if your skill requires authentication + // if (!settings.OAuthConnections.Any()) + // { + // throw new Exception("You must configure an authentication connection before using this component."); + // } + + // AddDialog(new MultiProviderAuthDialog(settings.OAuthConnections)); + } + + protected BotSettings Settings { get; set; } + + protected BotServices Services { get; set; } + + protected IStatePropertyAccessor StateAccessor { get; set; } + + protected IStatePropertyAccessor UserAccessor { get; set; } + + protected ResponseManager ResponseManager { get; set; } + + protected override async Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken)) + { + await GetLuisResult(dc); + return await base.OnBeginDialogAsync(dc, options, cancellationToken); + } + + protected override async Task OnContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + await GetLuisResult(dc); + return await base.OnContinueDialogAsync(dc, cancellationToken); + } + + protected async Task GetAuthToken(WaterfallStepContext sc, CancellationToken cancellationToken) + { + try + { + return await sc.PromptAsync(nameof(MultiProviderAuthDialog), new PromptOptions()); + } + catch (SkillException ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + catch (Exception ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + } + + protected async Task AfterGetAuthToken(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + // When the user authenticates interactively we pass on the tokens/Response event which surfaces as a JObject + // When the token is cached we get a TokenResponse object. + if (sc.Result is ProviderTokenResponse providerTokenResponse) + { + var state = await StateAccessor.GetAsync(sc.Context); + state.Token = providerTokenResponse.TokenResponse.Token; + } + + return await sc.NextAsync(); + } + catch (SkillException ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + catch (Exception ex) + { + await HandleDialogExceptions(sc, ex); + return new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs); + } + } + + // Validators + protected Task TokenResponseValidator(PromptValidatorContext pc, CancellationToken cancellationToken) + { + var activity = pc.Recognized.Value; + if (activity != null && activity.Type == ActivityTypes.Event) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(false); + } + } + + protected Task AuthPromptValidator(PromptValidatorContext promptContext, CancellationToken cancellationToken) + { + var token = promptContext.Recognized.Value; + if (token != null) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(false); + } + } + + // Helpers + protected async Task GetLuisResult(DialogContext dc) + { + if (dc.Context.Activity.Type == ActivityTypes.Message) + { + var state = await StateAccessor.GetAsync(dc.Context, () => new EventSkillState()); + + // Get luis service for current locale + var locale = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var localeConfig = Services.CognitiveModelSets[locale]; + var luisService = localeConfig.LuisServices["Event"]; + + // Get intent and entities for activity + var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None); + state.LuisResult = result; + } + } + + // This method is called by any waterfall step that throws an exception to ensure consistency + protected async Task HandleDialogExceptions(WaterfallStepContext sc, Exception ex) + { + // send trace back to emulator + var trace = new Activity(type: ActivityTypes.Trace, text: $"DialogException: {ex.Message}, StackTrace: {ex.StackTrace}"); + await sc.Context.SendActivityAsync(trace); + + // log exception + TelemetryClient.TrackException(ex, new Dictionary { { nameof(sc.ActiveDialog), sc.ActiveDialog?.Id } }); + + // send error message to bot user + await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ErrorMessage)); + + // clear state + var state = await StateAccessor.GetAsync(sc.Context); + state.Clear(); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Dialogs/FindEventsDialog.cs b/skills/src/csharp/experimental/eventskill/Dialogs/FindEventsDialog.cs new file mode 100644 index 0000000000..3f0c8a4ed8 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Dialogs/FindEventsDialog.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventSkill.Models; +using EventSkill.Models.Eventbrite; +using EventSkill.Responses.FindEvents; +using EventSkill.Services; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace EventSkill.Dialogs +{ + public class FindEventsDialog : EventDialogBase + { + private EventbriteService _eventbriteService; + + public FindEventsDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + ConversationState conversationState, + UserState userState, + IBotTelemetryClient telemetryClient) + : base(nameof(FindEventsDialog), settings, services, responseManager, conversationState, userState, telemetryClient) + { + var findEvents = new WaterfallStep[] + { + GetLocation, + FindEvents + }; + + _eventbriteService = new EventbriteService(settings); + + AddDialog(new WaterfallDialog(nameof(FindEventsDialog), findEvents)); + AddDialog(new TextPrompt(DialogIds.LocationPrompt, ValidateLocationPrompt)); + } + + private async Task GetLocation(WaterfallStepContext sc, CancellationToken cancellationToken) + { + var convState = await StateAccessor.GetAsync(sc.Context, () => new EventSkillState()); + var userState = await UserAccessor.GetAsync(sc.Context, () => new EventSkillUserState()); + + if (string.IsNullOrWhiteSpace(userState.Location)) + { + if (!string.IsNullOrWhiteSpace(convState.CurrentCoordinates)) + { + userState.Location = convState.CurrentCoordinates; + } + else + { + return await sc.PromptAsync(DialogIds.LocationPrompt, new PromptOptions() + { + Prompt = ResponseManager.GetResponse(FindEventsResponses.LocationPrompt), + RetryPrompt = ResponseManager.GetResponse(FindEventsResponses.RetryLocationPrompt) + }); + } + } + + return await sc.NextAsync(); + } + + private async Task ValidateLocationPrompt(PromptValidatorContext promptContext, CancellationToken cancellationToken) + { + var userState = await UserAccessor.GetAsync(promptContext.Context, () => new EventSkillUserState()); + if (promptContext.Recognized.Succeeded && !string.IsNullOrWhiteSpace(promptContext.Recognized.Value)) + { + userState.Location = promptContext.Recognized.Value; + return true; + } + + return false; + } + + private async Task FindEvents(WaterfallStepContext sc, CancellationToken cancellationToken) + { + var userState = await UserAccessor.GetAsync(sc.Context, () => new EventSkillUserState()); + + var location = userState.Location; + List events = await _eventbriteService.GetEventsAsync(location); + List cards = new List(); + + foreach (var item in events) + { + var eventCardData = new EventCardData() + { + Title = item.Name.Text, + ImageUrl = item?.Logo?.Url ?? " ", + StartDate = item.Start.Local.ToString("dddd, MMMM dd, h:mm tt"), + Location = GetVenueLocation(item), + Price = item.IsFree ? "Free" : "Starts at " + + Convert.ToDouble(item.TicketAvailability.MinTicketPrice.MajorValue) + .ToString("C", System.Globalization.CultureInfo.GetCultureInfo(item.Locale.Replace("_", "-"))), + Url = item.Url + }; + + cards.Add(new Card("EventCard", eventCardData)); + } + + await sc.Context.SendActivityAsync(ResponseManager.GetCardResponse(FindEventsResponses.FoundEvents, cards, null)); + + return await sc.EndDialogAsync(); + } + + // Get formatted location string based on data event has + private string GetVenueLocation(Event eventData) + { + string venueLocation = null; + if (string.IsNullOrEmpty(eventData.Venue?.Address?.LocalizedAreaDisplay)) + { + venueLocation = eventData.Venue.Name; + } + else if (string.IsNullOrEmpty(eventData.Venue?.Name)) + { + venueLocation = eventData.Venue.Address.LocalizedAreaDisplay; + } + else + { + venueLocation = string.Format("{0}, {1}", eventData.Venue.Name, eventData.Venue.Address.LocalizedAreaDisplay); + } + + return venueLocation; + } + + private class DialogIds + { + public const string LocationPrompt = "locationPrompt"; + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Dialogs/MainDialog.cs b/skills/src/csharp/experimental/eventskill/Dialogs/MainDialog.cs new file mode 100644 index 0000000000..7c715969ab --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Dialogs/MainDialog.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using EventSkill.Models; +using EventSkill.Responses.Main; +using EventSkill.Responses.Shared; +using EventSkill.Services; +using Luis; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions; +using Microsoft.Bot.Builder.Solutions.Dialogs; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Schema; + +namespace EventSkill.Dialogs +{ + public class MainDialog : RouterDialog + { + private BotSettings _settings; + private BotServices _services; + private ResponseManager _responseManager; + private IStatePropertyAccessor _stateAccessor; + private IStatePropertyAccessor _contextAccessor; + + public MainDialog( + BotSettings settings, + BotServices services, + ResponseManager responseManager, + UserState userState, + ConversationState conversationState, + FindEventsDialog findEventsDialog, + IBotTelemetryClient telemetryClient) + : base(nameof(MainDialog), telemetryClient) + { + _settings = settings; + _services = services; + _responseManager = responseManager; + TelemetryClient = telemetryClient; + + // Initialize state accessor + _stateAccessor = conversationState.CreateProperty(nameof(EventSkillState)); + _contextAccessor = userState.CreateProperty(nameof(SkillContext)); + + // Register dialogs + AddDialog(findEventsDialog ?? throw new ArgumentNullException(nameof(findEventsDialog))); + } + + protected override async Task OnStartAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + var locale = CultureInfo.CurrentUICulture; + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.WelcomeMessage)); + } + + protected override async Task RouteAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + // get current activity locale + var locale = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var localeConfig = _services.CognitiveModelSets[locale]; + + // Populate state from SemanticAction as required + await PopulateStateFromSemanticAction(dc.Context); + + // Get skill LUIS model from configuration + localeConfig.LuisServices.TryGetValue("Event", out var luisService); + + if (luisService == null) + { + throw new Exception("The specified LUIS Model could not be found in your Bot Services configuration."); + } + else + { + var turnResult = EndOfTurn; + var result = await luisService.RecognizeAsync(dc.Context, CancellationToken.None); + var intent = result?.TopIntent().intent; + + switch (intent) + { + case EventLuis.Intent.FindEvents: + { + // searching for local events + turnResult = await dc.BeginDialogAsync(nameof(FindEventsDialog)); + break; + } + + case EventLuis.Intent.None: + { + // No intent was identified, send confused message + await dc.Context.SendActivityAsync(_responseManager.GetResponse(SharedResponses.DidntUnderstandMessage)); + turnResult = new DialogTurnResult(DialogTurnStatus.Complete); + break; + } + + default: + { + // intent was identified but not yet implemented + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.FeatureNotAvailable)); + turnResult = new DialogTurnResult(DialogTurnStatus.Complete); + break; + } + } + + if (turnResult != EndOfTurn) + { + await CompleteAsync(dc); + } + } + } + + protected override async Task CompleteAsync(DialogContext dc, DialogTurnResult result = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var response = dc.Context.Activity.CreateReply(); + response.Type = ActivityTypes.EndOfConversation; + await dc.Context.SendActivityAsync(response); + await dc.EndDialogAsync(result); + } + + protected override async Task OnEventAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + switch (dc.Context.Activity.Name) + { + case TokenEvents.TokenResponseEventName: + { + // Auth dialog completion + var result = await dc.ContinueDialogAsync(); + + // If the dialog completed when we sent the token, end the skill conversation + if (result.Status != DialogTurnStatus.Waiting) + { + var response = dc.Context.Activity.CreateReply(); + response.Type = ActivityTypes.EndOfConversation; + + await dc.Context.SendActivityAsync(response); + } + + break; + } + } + } + + protected override async Task OnInterruptDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = InterruptionAction.NoAction; + + if (dc.Context.Activity.Type == ActivityTypes.Message) + { + // get current activity locale + var locale = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var localeConfig = _services.CognitiveModelSets[locale]; + + // check general luis intent + localeConfig.LuisServices.TryGetValue("General", out var luisService); + + if (luisService == null) + { + throw new Exception("The specified LUIS Model could not be found in your Skill configuration."); + } + else + { + var luisResult = await luisService.RecognizeAsync(dc.Context, cancellationToken); + var topIntent = luisResult.TopIntent(); + + if (topIntent.score > 0.5) + { + switch (topIntent.intent) + { + case GeneralLuis.Intent.Cancel: + { + result = await OnCancel(dc); + break; + } + + case GeneralLuis.Intent.Help: + { + result = await OnHelp(dc); + break; + } + + case GeneralLuis.Intent.Logout: + { + result = await OnLogout(dc); + break; + } + } + } + } + } + + return result; + } + + private async Task OnCancel(DialogContext dc) + { + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.CancelMessage)); + await CompleteAsync(dc); + await dc.CancelAllDialogsAsync(); + return InterruptionAction.StartedDialog; + } + + private async Task OnHelp(DialogContext dc) + { + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.HelpMessage)); + return InterruptionAction.MessageSentToUser; + } + + private async Task OnLogout(DialogContext dc) + { + BotFrameworkAdapter adapter; + var supported = dc.Context.Adapter is BotFrameworkAdapter; + if (!supported) + { + throw new InvalidOperationException("OAuthPrompt.SignOutUser(): not supported by the current adapter"); + } + else + { + adapter = (BotFrameworkAdapter)dc.Context.Adapter; + } + + await dc.CancelAllDialogsAsync(); + + // Sign out user + var tokens = await adapter.GetTokenStatusAsync(dc.Context, dc.Context.Activity.From.Id); + foreach (var token in tokens) + { + await adapter.SignOutUserAsync(dc.Context, token.ConnectionName); + } + + await dc.Context.SendActivityAsync(_responseManager.GetResponse(MainResponses.LogOut)); + + return InterruptionAction.StartedDialog; + } + + private async Task PopulateStateFromSemanticAction(ITurnContext context) + { + // Example of populating local state with data passed through semanticAction out of Activity + var activity = context.Activity; + var semanticAction = activity.SemanticAction; + if (semanticAction != null && semanticAction.Entities.ContainsKey("location")) + { + var location = semanticAction.Entities["location"]; + var locationObj = location.Properties["location"].ToString(); + var state = await _stateAccessor.GetAsync(context, () => new EventSkillState()); + state.CurrentCoordinates = locationObj; + } + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/EventSkill.csproj b/skills/src/csharp/experimental/eventskill/EventSkill.csproj new file mode 100644 index 0000000000..1c26cd793d --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/EventSkill.csproj @@ -0,0 +1,155 @@ + + + + netcoreapp2.2 + NU1701 + 12ed3ad7-66e5-4160-b86c-aff9da5cf4b6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + TextTemplatingFileGenerator + FindEventsResponses.cs + + + TextTemplatingFileGenerator + MainResponses.cs + + + TextTemplatingFileGenerator + SharedResponses.cs + + + + + + + + + + True + True + general.lu + + + True + True + skill.lu + + + True + True + deploy.ps1 + + + True + True + deploy_cognitive_models.ps1 + + + True + True + MyTemplate.vstemplate + + + True + True + readme.md + + + True + True + FindEventsResponses.tt + + + True + True + MainResponses.tt + + + True + True + ResponseIdCollection.t4 + + + True + True + SharedResponses.tt + + + True + True + __TemplateIcon.ico + + + + diff --git a/skills/src/csharp/experimental/eventskill/Models/EventCardData.cs b/skills/src/csharp/experimental/eventskill/Models/EventCardData.cs new file mode 100644 index 0000000000..2c72421cbe --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/EventCardData.cs @@ -0,0 +1,19 @@ +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace EventSkill.Models +{ + public class EventCardData : ICardData + { + public string Title { get; set; } + + public string ImageUrl { get; set; } + + public string StartDate { get; set; } + + public string Location { get; set; } + + public string Price { get; set; } + + public string Url { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/EventSkillState.cs b/skills/src/csharp/experimental/eventskill/Models/EventSkillState.cs new file mode 100644 index 0000000000..69e903fce3 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/EventSkillState.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Luis; + +namespace EventSkill.Models +{ + public class EventSkillState + { + public string Token { get; set; } + + public EventLuis LuisResult { get; set; } + + public string CurrentCoordinates { get; internal set; } + + public void Clear() + { + Token = null; + LuisResult = null; + CurrentCoordinates = null; + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/EventSkillUserState.cs b/skills/src/csharp/experimental/eventskill/Models/EventSkillUserState.cs new file mode 100644 index 0000000000..d26f3aa435 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/EventSkillUserState.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace EventSkill.Models +{ + public class EventSkillUserState + { + public string Location { get; set; } + + public void Clear() + { + Location = null; + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Address.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Address.cs new file mode 100644 index 0000000000..9d9fdfb575 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Address.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class Address + { + [JsonProperty("address_1")] + public string Address1 { get; set; } + + [JsonProperty("city")] + public string City { get; set; } + + [JsonProperty("region")] + public string Region { get; set; } + + [JsonProperty("postal_code")] + public string PostalCode { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + + [JsonProperty("localized_address_display")] + public string LocalizedAddressDisplay { get; set; } + + [JsonProperty("localized_area_display")] + public string LocalizedAreaDisplay { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/AugmentedLocation.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/AugmentedLocation.cs new file mode 100644 index 0000000000..914118d3f3 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/AugmentedLocation.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models +{ + public class AugmentedLocation + { + [JsonProperty("city")] + public string City { get; set; } + + [JsonProperty("region")] + public string Region { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/DateTimeTZ.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/DateTimeTZ.cs new file mode 100644 index 0000000000..dcb44ca27b --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/DateTimeTZ.cs @@ -0,0 +1,17 @@ +using System; +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class DateTimeTZ + { + [JsonProperty("timezone")] + public string Timezone { get; set; } + + [JsonProperty("local")] + public DateTime Local { get; set; } + + [JsonProperty("utc")] + public DateTime Utc { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Event.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Event.cs new file mode 100644 index 0000000000..61167e0888 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Event.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class Event + { + [JsonProperty("name")] + public MultipartText Name { get; set; } + + [JsonProperty("description")] + public MultipartText Description { get; set; } + + [JsonProperty("summary")] + public string Summary { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("start")] + public DateTimeTZ Start { get; set; } + + [JsonProperty("end")] + public DateTimeTZ End { get; set; } + + [JsonProperty("locale")] + public string Locale { get; set; } + + [JsonProperty("is_free")] + public bool IsFree { get; set; } + + [JsonProperty("venue")] + public Venue Venue { get; set; } + + [JsonProperty("ticket_availability")] + public TicketAvailability TicketAvailability { get; set; } + + [JsonProperty("logo")] + public Logo Logo { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/EventSearchResult.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/EventSearchResult.cs new file mode 100644 index 0000000000..e86e545f54 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/EventSearchResult.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class EventSearchResult + { + [JsonProperty("pagination")] + public Pagination Pagination { get; set; } + + [JsonProperty("events")] + public List Events { get; set; } + + [JsonProperty("location")] + public Location Location { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Location.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Location.cs new file mode 100644 index 0000000000..cbad835442 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Location.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models +{ + public class Location + { + [JsonProperty("latitude")] + public string Latitude { get; set; } + + [JsonProperty("augmented_location")] + public AugmentedLocation AugmentedLocation { get; set; } + + [JsonProperty("within")] + public string Within { get; set; } + + [JsonProperty("longitude")] + public string Longitude { get; set; } + + [JsonProperty("address")] + public string Address { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Logo.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Logo.cs new file mode 100644 index 0000000000..a2e14f958e --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Logo.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class Logo + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/MultipartText.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/MultipartText.cs new file mode 100644 index 0000000000..9bef0e4005 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/MultipartText.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class MultipartText + { + [JsonProperty("text")] + public string Text { get; set; } + + [JsonProperty("html")] + public string Html { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Pagination.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Pagination.cs new file mode 100644 index 0000000000..bbe326a7de --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Pagination.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class Pagination + { + [JsonProperty("object_count")] + public int ObjectCount { get; set; } + + [JsonProperty("page_number")] + public int PageNumber { get; set; } + + [JsonProperty("page_size")] + public int PageSize { get; set; } + + [JsonProperty("page_count")] + public int PageCount { get; set; } + + [JsonProperty("has_more_items")] + public bool HasMoreItems { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketAvailability.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketAvailability.cs new file mode 100644 index 0000000000..580d8949c2 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketAvailability.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class TicketAvailability + { + [JsonProperty("has_available_tickets")] + public bool HasAvailableTickets { get; set; } + + [JsonProperty("minimum_ticket_price")] + public TicketPrice MinTicketPrice { get; set; } + + [JsonProperty("maximum_ticket_price")] + public TicketPrice MaxTicketPrice { get; set; } + + [JsonProperty("is_sold_out")] + public bool IsSoldOut { get; set; } + + [JsonProperty("start_sales_date")] + public DateTimeTZ StartSalesDate { get; set; } + + [JsonProperty("waitlist_available")] + public bool WaitlistAvailable { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketPrice.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketPrice.cs new file mode 100644 index 0000000000..d94baa1af6 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/TicketPrice.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class TicketPrice + { + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("major_value")] + public string MajorValue { get; set; } + + [JsonProperty("display")] + public string Display { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Venue.cs b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Venue.cs new file mode 100644 index 0000000000..46f58dea33 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Models/Eventbrite/Venue.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace EventSkill.Models.Eventbrite +{ + public class Venue + { + [JsonProperty("address")] + public Address Address { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Pipeline/EventSkill.yml b/skills/src/csharp/experimental/eventskill/Pipeline/EventSkill.yml new file mode 100644 index 0000000000..07158db945 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Pipeline/EventSkill.yml @@ -0,0 +1,61 @@ +# specific branch build +trigger: + branches: + include: + - master + - feature/* + +pool: + name: Hosted VS2017 + demands: + - msbuild + - visualstudio + +variables: + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- task: DotNetCoreInstaller@0 + displayName: 'Use .NET Core sdk 2.2.100' + inputs: + version: 2.2.100 + continueOnError: true + +- task: NuGetToolInstaller@0 + displayName: 'Use NuGet 4.9.1' + inputs: + versionSpec: 4.9.1 + +- task: NuGetCommand@2 + displayName: 'NuGet restore' + inputs: + # if your working directory is not root, you may change the following path + restoreSolution: 'EventSkill.sln' + +- task: VSBuild@1 + displayName: 'Build solution EventSkill.sln' + inputs: + # if your working directory is not root, you may change the following path + solution: EventSkill.sln + vsVersion: '16.0' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: 'test results' + inputs: + command: test + # if your working directory is not root, you may change the following path + projects: '$(System.DefaultWorkingDirectory)\EventSkill.Tests.csproj' + arguments: '-v n --configuration $(buildConfiguration) --no-build --no-restore --filter TestCategory!=IgnoreInAutomatedBuild /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura' + # if your working directory is not root, you may change the following path + workingDirectory: 'EventSkill.Tests' + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage' + inputs: + codeCoverageTool: Cobertura + # if your working directory is not root, you may change the following path + summaryFileLocation: '$(Build.SourcesDirectory)\EventSkill.Tests\coverage.cobertura.xml' + reportDirectory: '$(Build.SourcesDirectory)\EventSkill.Tests' \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Program.cs b/skills/src/csharp/experimental/eventskill/Program.cs new file mode 100644 index 0000000000..b39c536c59 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Program.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace EventSkill +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() // Note: Application Insights is added in Startup. Disabling is also handled there. + .Build(); + } +} diff --git a/skills/src/csharp/experimental/eventskill/Properties/launchSettings.json b/skills/src/csharp/experimental/eventskill/Properties/launchSettings.json new file mode 100644 index 0000000000..c156a2bafb --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:4000/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "EventSkill": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:1205/" + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.cs b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.cs new file mode 100644 index 0000000000..4dcf021ee8 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.cs @@ -0,0 +1,19 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace EventSkill.Responses.FindEvents +{ + /// + /// Contains bot responses. + /// + public class FindEventsResponses : IResponseIdCollection + { + // Generated accessors + public const string LocationPrompt = "LocationPrompt"; + public const string RetryLocationPrompt = "RetryLocationPrompt"; + public const string FoundEvents = "FoundEvents"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.json b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.json new file mode 100644 index 0000000000..156a489488 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.json @@ -0,0 +1,29 @@ +{ + "LocationPrompt": { + "replies": [ + { + "text": "Where are you?", + "speak": "Where are you?" + } + ], + "inputHint": "expectingInput" + }, + "RetryLocationPrompt": { + "replies": [ + { + "text": "Please provide your location.", + "speak": "Please provide your location." + } + ], + "inputHint": "expectingInput" + }, + "FoundEvents": { + "replies": [ + { + "text": "Here's what I found happening this week:", + "speak": "Here's what I found happening this week:" + } + ], + "inputHint": "ignoringInput" + } +} diff --git a/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.tt b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/FindEvents/FindEventsResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.cs b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.cs new file mode 100644 index 0000000000..d5bc028fcb --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.cs @@ -0,0 +1,23 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace EventSkill.Responses.Main +{ + /// + /// Contains bot responses. + /// + public class MainResponses : IResponseIdCollection + { + // Generated accessors + public const string WelcomeMessage = "WelcomeMessage"; + public const string HelpMessage = "HelpMessage"; + public const string GreetingMessage = "GreetingMessage"; + public const string GoodbyeMessage = "GoodbyeMessage"; + public const string LogOut = "LogOut"; + public const string FeatureNotAvailable = "FeatureNotAvailable"; + public const string CancelMessage = "CancelMessage"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.de.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.de.json new file mode 100644 index 0000000000..ef03ebc662 Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.de.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.es.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.es.json new file mode 100644 index 0000000000..27a60042db Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.es.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.fr.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.fr.json new file mode 100644 index 0000000000..ab07ecc7fd Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.fr.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.it.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.it.json new file mode 100644 index 0000000000..830683e337 Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.it.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.json new file mode 100644 index 0000000000..3570193c69 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.json @@ -0,0 +1,83 @@ +{ + "WelcomeMessage": { + "replies": [ + { + "text": "Welcome to the Event Skill!", + "speak": "Welcome to the Event Skill!" + } + ], + "suggestedActions": [], + "inputHint": "acceptingInput" + }, + "HelpMessage": { + "replies": [ + { + "text": "[Enter your help message here]", + "speak": "[Enter your help message here]" + } + ], + "suggestedActions": [], + "inputHint": "acceptingInput" + }, + "GreetingMessage": { + "replies": [ + { + "text": "Hi!", + "speak": "Hi!" + }, + { + "text": "Hi there!", + "speak": "Hi there!" + }, + { + "text": "Hello!", + "speak": "Hello!" + } + ], + "inputHint": "acceptingInput" + }, + "GoodbyeMessage": { + "replies": [ + { + "text": "Goodbye!", + "speak": "Goodbye!" + } + ], + "inputHint": "acceptingInput" + }, + "LogOut": { + "replies": [ + { + "text": "Your sign out was successful.", + "speak": "Your sign out was successful." + }, + { + "text": "You have successfully signed out.", + "speak": "You have successfully signed out." + }, + { + "text": "You have been logged out.", + "speak": "You have been logged out." + } + ], + "inputHint": "acceptingInput" + }, + "FeatureNotAvailable": { + "replies": [ + { + "text": "This feature is not yet available in this skill. Please try asking something else.", + "speak": "This feature is not yet available in thiskill. Please try asking something else." + } + ], + "inputHint": "acceptingInput" + }, + "CancelMessage": { + "replies": [ + { + "text": "Ok, let's start over.", + "speak": "Ok, let's start over." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.tt b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.zh.json b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.zh.json new file mode 100644 index 0000000000..647d005144 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Main/MainResponses.zh.json @@ -0,0 +1,82 @@ +{ + "WelcomeMessage": { + "replies": [ + { + "text": "[在此输入您的简介信息]", + "speak": "[在此输入您的简介信息]" + } + ], + "inputHint": "acceptingInput" + }, + "HelpMessage": { + "replies": [ + { + "text": "[在此处输入您的帮助信息]", + "speak": "[在此处输入您的帮助信息]" + } + ], + "suggestedActions": [], + "inputHint": "acceptingInput" + }, + "GreetingMessage": { + "replies": [ + { + "text": "嗨!", + "speak": "嗨!" + }, + { + "text": "我在这!", + "speak": "我在这!" + }, + { + "text": "你好!", + "speak": "你好!" + }, + { + "text": "您好!", + "speak": "您好!" + } + ], + "inputHint": "acceptingInput" + }, + "GoodbyeMessage": { + "replies": [ + { + "text": "再见!", + "speak": "再见!" + } + ], + "inputHint": "acceptingInput" + }, + "LogOut": { + "replies": [ + { + "text": "您已成功退出。", + "speak": "您已成功退出。" + }, + { + "text": "您已退出。", + "speak": "您已退出。" + } + ], + "inputHint": "acceptingInput" + }, + "FeatureNotAvailable": { + "replies": [ + { + "text": "此功能在此技能中尚不可用。", + "speak": "此功能在此技能中尚不可用。" + } + ], + "inputHint": "acceptingInput" + }, + "CancelMessage": { + "replies": [ + { + "text": "好的, 我们从头再来。", + "speak": "好的, 我们从头再来。" + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/ResponseIdCollection.t4 b/skills/src/csharp/experimental/eventskill/Responses/Shared/ResponseIdCollection.t4 new file mode 100644 index 0000000000..d6c0d7cc3e --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Shared/ResponseIdCollection.t4 @@ -0,0 +1,31 @@ +<#@ assembly name="Newtonsoft.Json.dll" #> +<# + var className = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile); + var namespaceName = System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint"); + string myFile = System.IO.File.ReadAllText(this.Host.ResolvePath(className + ".json")); + var json = Newtonsoft.Json.JsonConvert.DeserializeObject>(myFile); + var responses = string.Empty; + var cards = string.Empty; +#> +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace <#= namespaceName #> +{ + /// + /// Contains bot responses. + /// + public class <#= className #> : IResponseIdCollection + { + // Generated accessors +<# +// This code runs in the text json: +foreach (var propertyName in json) { +#> + public const string <#= propertyName.Key.Substring(0, 1).ToUpperInvariant() + propertyName.Key.Substring(1) #> = "<#= propertyName.Key #>"; +<# } #> + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.cs b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.cs new file mode 100644 index 0000000000..1abffee9b1 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.cs @@ -0,0 +1,22 @@ +// https://docs.microsoft.com/en-us/visualstudio/modeling/t4-include-directive?view=vs-2017 +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions.Responses; + +namespace EventSkill.Responses.Shared +{ + /// + /// Contains bot responses. + /// + public class SharedResponses : IResponseIdCollection + { + // Generated accessors + public const string DidntUnderstandMessage = "DidntUnderstandMessage"; + public const string CancellingMessage = "CancellingMessage"; + public const string NoAuth = "NoAuth"; + public const string AuthFailed = "AuthFailed"; + public const string ActionEnded = "ActionEnded"; + public const string ErrorMessage = "ErrorMessage"; + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.de.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.de.json new file mode 100644 index 0000000000..7573b415f8 Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.de.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.es.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.es.json new file mode 100644 index 0000000000..8839f0125a Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.es.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.fr.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.fr.json new file mode 100644 index 0000000000..18875ffabb Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.fr.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.it.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.it.json new file mode 100644 index 0000000000..5316e5df39 Binary files /dev/null and b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.it.json differ diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.json new file mode 100644 index 0000000000..f29bd17fe5 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.json @@ -0,0 +1,148 @@ +{ + "DidntUnderstandMessage": { + "replies": [ + { + "text": "Sorry, I didn't understand what you meant.", + "speak": "Sorry, I didn't understand what you meant." + }, + { + "text": "I didn't understand, perhaps try again in a different way.", + "speak": "I didn't understand, perhaps try again in a different way." + }, + { + "text": "Can you try to ask in a different way?", + "speak": "Can you try to ask in a different way?" + }, + { + "text": "I didn't get what you mean, can you try in a different way?", + "speak": "I didn't get what you mean, can you try in a different way?" + }, + { + "text": "Could you elaborate?", + "speak": "Could you elaborate?" + }, + { + "text": "Please say that again in a different way.", + "speak": "Please say that again in a different way." + }, + { + "text": "I didn't quite get that.", + "speak": "I didn't quite get that." + }, + { + "text": "Can you say that in a different way?", + "speak": "Can you say that in a different way?" + }, + { + "text": "Can you try to ask me again? I didn't get what you mean.", + "speak": "Can you try to ask me again? I didn't get what you mean." + } + ], + "inputHint": "acceptingInput" + }, + "CancellingMessage": { + "replies": [ + { + "text": "Sure, we can do this later.", + "speak": "Sure, we can do this later." + }, + { + "text": "Sure, we can start this later.", + "speak": "Sure, we can start this later." + }, + { + "text": "No problem, you can try again at another time.", + "speak": "No problem, you can try again at another time." + }, + { + "text": "Alright, let me know when you need my help.", + "speak": "Alright, let me know when you need my help." + }, + { + "text": "Sure, I'm here if you need me.", + "speak": "Sure, I'm here if you need me." + } + ], + "inputHint": "acceptingInput" + }, + "NoAuth": { + "replies": [ + { + "text": "Please log in before taking further action.", + "speak": "Please log in before taking further action." + }, + { + "text": "Please log in so I can take further action.", + "speak": "Please log in so I can take further action." + }, + { + "text": "Please log in so I can proceed with your request.", + "speak": "Please log in so I can proceed with your request." + }, + { + "text": "Can you log in so I can help you out further?", + "speak": "Can you log in so I can help you out further?" + }, + { + "text": "You need to log in so I can take further action.", + "speak": "You need to log in so I can take further action." + } + ], + "inputHint": "expectingInput" + }, + "AuthFailed": { + "replies": [ + { + "text": "Authentication failed. Please try again", + "speak": "Authentication failed. Please try again." + }, + { + "text": "You failed to log in. Please try again later.", + "speak": "You failed to log in. please try again later." + }, + { + "text": "Your log in failed. Let's try this again.", + "speak": "Your log in failed. Let's try this again." + } + ], + "inputHint": "acceptingInput" + }, + "ActionEnded": { + "replies": [ + { + "text": "Let me know if you need my help with something else.", + "speak": "Let me know if you need my help with something else." + }, + { + "text": "I'm here if you need me.", + "speak": "I'm here if you need me." + } + ], + "inputHint": "acceptingInput" + }, + "ErrorMessage": { + "replies": [ + { + "text": "Sorry, it looks like something went wrong!", + "speak": "Sorry, it looks like something went wrong!" + }, + { + "text": "An error occurred, please try again later.", + "speak": "An error occurred, please try again later." + }, + { + "text": "Something went wrong, sorry!", + "speak": "Something went wrong, sorry!" + }, + { + "text": "It seems like something went wrong. Can you try again later?", + "speak": "It seems like something went wrong. Can you try again later?" + }, + { + "text": "Sorry I can't help right now. Please try again later.", + "speak": "Sorry I can't help right now. Please try again later." + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.tt b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.tt new file mode 100644 index 0000000000..f204f0981b --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.tt @@ -0,0 +1,3 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ output extension=".cs" #> +<#@ include file="..\Shared\ResponseIdCollection.t4"#> \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.zh.json b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.zh.json new file mode 100644 index 0000000000..cc6a668c56 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Responses/Shared/SharedResponses.zh.json @@ -0,0 +1,100 @@ +{ + "DidntUnderstandMessage": { + "replies": [ + { + "text": "对不起,没有明白你的意思。", + "speak": "对不起,没有明白你的意思。" + }, + { + "text": "我没有明白你的意思,你能以不同的方式尝试吗?", + "speak": "我没有明白你的意思,你能以不同的方式尝试吗?" + }, + { + "text": "你能以不同的方式说出来吗?", + "speak": "你能以不同的方式说出来吗?" + }, + { + "text": "你能再试一次问我,我没听懂你的意思。", + "speak": "你能再试一次问我,我没听懂你的意思。" + } + ], + "inputHint": "acceptingInput" + }, + "CancellingMessage": { + "replies": [ + { + "text": "当然,我们可以稍后再做。", + "speak": "当然,我们可以稍后再做。" + } + ], + "inputHint": "acceptingInput" + }, + "NoAuth": { + "replies": [ + { + "text": "请登录,以便我采取进一步行动。", + "speak": "请登录,以便我采取进一步行动。" + } + ], + "inputHint": "expectingInput" + }, + "AuthFailed": { + "replies": [ + { + "text": "您的登录失败,请稍后再试。", + "speak": "您的登录失败,请稍后再试。" + } + ], + "inputHint": "acceptingInput" + }, + "ActionEnded": { + "replies": [ + { + "text": "还有什么需要我的帮助吗?", + "speak": "还有什么需要我的帮助吗?" + }, + { + "text": "还有什么我可以帮你的吗?", + "speak": "还有什么我可以帮你的吗?" + }, + { + "text": "还有什么想要采取行动吗?", + "speak": "还有什么想要采取行动吗?" + }, + { + "text": "如果您需要我的帮助,请告诉我。", + "speak": "如果您需要我的帮助,请告诉我。" + }, + { + "text": "如果你需要我的帮助,我就在这里。", + "speak": "如果你需要我的帮助,我就在这里。" + } + ], + "inputHint": "acceptingInput" + }, + "ErrorMessage": { + "replies": [ + { + "text": "对不起,看起来出了问题!", + "speak": "对不起,看起来出了问题!" + }, + { + "text": "发生错误,给我一些时间,稍后再试。", + "speak": "发生错误,给我一些时间,稍后再试。" + }, + { + "text": "出了点问题,对不起!", + "speak": "出了点问题,对不起!" + }, + { + "text": "我相信出了点问题。你稍后可以再试一次吗?", + "speak": "我相信出了点问题。你稍后可以再试一次吗?" + }, + { + "text": "抱歉,我现在找不到你想要的东西。请稍后再试。", + "speak": "抱歉,我现在找不到你想要的东西。请稍后再试。" + } + ], + "inputHint": "acceptingInput" + } +} diff --git a/skills/src/csharp/experimental/eventskill/Services/BotServices.cs b/skills/src/csharp/experimental/eventskill/Services/BotServices.cs new file mode 100644 index 0000000000..d15e4c70e9 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Services/BotServices.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +using Microsoft.Bot.Builder.AI.QnA; +using Microsoft.Bot.Builder.Solutions; + +namespace EventSkill.Services +{ + public class BotServices + { + public BotServices() + { + } + + public BotServices(BotSettings settings, IBotTelemetryClient client) + { + foreach (var pair in settings.CognitiveModels) + { + var set = new CognitiveModelSet(); + var language = pair.Key; + var config = pair.Value; + + var telemetryClient = client; + var luisOptions = new LuisPredictionOptions() + { + TelemetryClient = telemetryClient, + LogPersonalInformation = true, + }; + + if (config.DispatchModel != null) + { + var dispatchApp = new LuisApplication(config.DispatchModel.AppId, config.DispatchModel.SubscriptionKey, config.DispatchModel.GetEndpoint()); + set.DispatchService = new LuisRecognizer(dispatchApp); + } + + if (config.LanguageModels != null) + { + foreach (var model in config.LanguageModels) + { + var luisApp = new LuisApplication(model.AppId, model.SubscriptionKey, model.GetEndpoint()); + set.LuisServices.Add(model.Id, new LuisRecognizer(luisApp)); + } + } + + if (config.Knowledgebases != null) + { + foreach (var kb in config.Knowledgebases) + { + var qnaEndpoint = new QnAMakerEndpoint() + { + KnowledgeBaseId = kb.KbId, + EndpointKey = kb.EndpointKey, + Host = kb.Hostname, + }; + var qnaMaker = new QnAMaker(qnaEndpoint); + set.QnAServices.Add(kb.Id, qnaMaker); + } + } + + CognitiveModelSets.Add(language, set); + } + } + + public Dictionary CognitiveModelSets { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Services/BotSettings.cs b/skills/src/csharp/experimental/eventskill/Services/BotSettings.cs new file mode 100644 index 0000000000..fbc7b9db47 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Services/BotSettings.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder.Solutions; + +namespace EventSkill.Services +{ + public class BotSettings : BotSettingsBase + { + public string EventbriteApiKey { get; set; } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/Services/EventLuis.cs b/skills/src/csharp/experimental/eventskill/Services/EventLuis.cs new file mode 100644 index 0000000000..92fb28c136 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Services/EventLuis.cs @@ -0,0 +1,63 @@ +// +// Code generated by LUISGen +// Tool github: https://github.com/microsoft/botbuilder-tools +// Changes may cause incorrect behavior and will be lost if the code is +// regenerated. +// +using Newtonsoft.Json; +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +namespace Luis +{ + public partial class EventLuis: IRecognizerConvert + { + public string Text; + public string AlteredText; + public enum Intent { + FindEvents, + None + }; + public Dictionary Intents; + + public class _Entities + { + + // Instance + public class _Instance + { + } + [JsonProperty("$instance")] + public _Instance _instance; + } + public _Entities Entities; + + [JsonExtensionData(ReadData = true, WriteData = true)] + public IDictionary Properties {get; set; } + + public void Convert(dynamic result) + { + var app = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + Text = app.Text; + AlteredText = app.AlteredText; + Intents = app.Intents; + Entities = app.Entities; + Properties = app.Properties; + } + + public (Intent intent, double score) TopIntent() + { + Intent maxIntent = Intent.None; + var max = 0.0; + foreach (var entry in Intents) + { + if (entry.Value.Score > max) + { + maxIntent = entry.Key; + max = entry.Value.Score.Value; + } + } + return (maxIntent, max); + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Services/EventbriteService.cs b/skills/src/csharp/experimental/eventskill/Services/EventbriteService.cs new file mode 100644 index 0000000000..c8d2503836 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Services/EventbriteService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using EventSkill.Models.Eventbrite; +using Newtonsoft.Json; + +namespace EventSkill.Services +{ + public sealed class EventbriteService + { + private const string LocationAndDateApiUrl = "https://www.eventbriteapi.com/v3/events/search/?location.address={0}&location.within={1}&start_date.keyword={2}&expand=venue,ticket_availability&token={3}"; + private static string _apiKey; + private static HttpClient _httpClient; + + public EventbriteService(BotSettings settings) + { + _apiKey = settings.EventbriteApiKey ?? throw new Exception("The EventbriteKey must be provided to use this dialog."); + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + public async Task> GetEventsAsync(string location) + { + var url = string.Format(LocationAndDateApiUrl, location, "10mi", "this_week", _apiKey); + var response = await _httpClient.GetStringAsync(url); + var apiResponse = JsonConvert.DeserializeObject(response); + + // limit number of events returned + return apiResponse.Events.GetRange(0, 10); + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Services/GeneralLuis.cs b/skills/src/csharp/experimental/eventskill/Services/GeneralLuis.cs new file mode 100644 index 0000000000..bb0b784046 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Services/GeneralLuis.cs @@ -0,0 +1,88 @@ +// +// Code generated by LUISGen +// Tool github: https://github.com/microsoft/botbuilder-tools +// Changes may cause incorrect behavior and will be lost if the code is +// regenerated. +// +using Newtonsoft.Json; +using System.Collections.Generic; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.AI.Luis; +namespace Luis +{ + public partial class GeneralLuis: IRecognizerConvert + { + public string Text; + public string AlteredText; + public enum Intent { + Cancel, + Confirm, + Escalate, + FinishTask, + GoBack, + Help, + Logout, + None, + ReadAloud, + Reject, + Repeat, + SelectAny, + SelectItem, + SelectNone, + ShowNext, + ShowPrevious, + StartOver, + Stop + }; + public Dictionary Intents; + + public class _Entities + { + // Simple entities + public string[] DirectionalReference; + + // Built-in entities + public double[] number; + public double[] ordinal; + + // Instance + public class _Instance + { + public InstanceData[] DirectionalReference; + public InstanceData[] number; + public InstanceData[] ordinal; + } + [JsonProperty("$instance")] + public _Instance _instance; + } + public _Entities Entities; + + [JsonExtensionData(ReadData = true, WriteData = true)] + public IDictionary Properties {get; set; } + + public void Convert(dynamic result) + { + var app = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + Text = app.Text; + AlteredText = app.AlteredText; + Intents = app.Intents; + Entities = app.Entities; + Properties = app.Properties; + } + + public (Intent intent, double score) TopIntent() + { + Intent maxIntent = Intent.None; + var max = 0.0; + foreach (var entry in Intents) + { + if (entry.Value.Score > max) + { + maxIntent = entry.Key; + max = entry.Value.Score.Value; + } + } + return (maxIntent, max); + } + } +} diff --git a/skills/src/csharp/experimental/eventskill/Startup.cs b/skills/src/csharp/experimental/eventskill/Startup.cs new file mode 100644 index 0000000000..7f25db18e1 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/Startup.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using EventSkill.Adapters; +using EventSkill.Bots; +using EventSkill.Dialogs; +using EventSkill.Responses.FindEvents; +using EventSkill.Responses.Main; +using EventSkill.Responses.Shared; +using EventSkill.Services; +using Microsoft.ApplicationInsights; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.ApplicationInsights; +using Microsoft.Bot.Builder.Azure; +using Microsoft.Bot.Builder.BotFramework; +using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Builder.Solutions; +using Microsoft.Bot.Builder.Solutions.Responses; +using Microsoft.Bot.Builder.Solutions.TaskExtensions; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace EventSkill +{ + public class Startup + { + public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("cognitivemodels.json", optional: true) + .AddJsonFile($"cognitivemodels.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("skills.json", optional: true) + .AddJsonFile($"skills.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + + var provider = services.BuildServiceProvider(); + + // Load settings + var settings = new BotSettings(); + Configuration.Bind(settings); + services.AddSingleton(settings); + services.AddSingleton(settings); + + // Configure credentials + services.AddSingleton(); + services.AddSingleton(new MicrosoftAppCredentials(settings.MicrosoftAppId, settings.MicrosoftAppPassword)); + + // Configure telemetry + services.AddApplicationInsightsTelemetry(); + var telemetryClient = new BotTelemetryClient(new TelemetryClient()); + services.AddSingleton(telemetryClient); + services.AddBotApplicationInsights(telemetryClient); + + // Configure bot services + services.AddSingleton(); + + // Configure storage + // Uncomment the following line for local development without Cosmos Db + // services.AddSingleton(); + services.AddSingleton(new CosmosDbStorage(settings.CosmosDb)); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + { + var userState = sp.GetService(); + var conversationState = sp.GetService(); + return new BotStateSet(userState, conversationState); + }); + + // Configure proactive + services.AddSingleton(); + services.AddHostedService(); + + // Configure responses + services.AddSingleton(sp => new ResponseManager( + settings.CognitiveModels.Select(l => l.Key).ToArray(), + new MainResponses(), + new SharedResponses(), + new FindEventsResponses())); + + // Register dialogs + services.AddTransient(); + services.AddTransient(); + + // Configure adapters + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configure bot + services.AddTransient>(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseBotApplicationInsights() + .UseDefaultFiles() + .UseStaticFiles() + .UseWebSockets() + .UseMvc(); + } + } +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/appsettings.json b/skills/src/csharp/experimental/eventskill/appsettings.json new file mode 100644 index 0000000000..ea22216b80 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/appsettings.json @@ -0,0 +1,20 @@ +{ + "microsoftAppId": "", + "microsoftAppPassword": "", + "oauthConnections": [], + "ApplicationInsights": { + "InstrumentationKey": "" + }, + "blobStorage": { + "connectionString": "", + "container": "transcripts" + }, + "cosmosDb": { + "authkey": "", + "cosmosDBEndpoint": "", + "collectionId": "botstate-collection", + "databaseId": "botstate-db" + }, + "properties": {}, + "eventbriteKey": "" +} diff --git a/skills/src/csharp/experimental/eventskill/cognitivemodels.json b/skills/src/csharp/experimental/eventskill/cognitivemodels.json new file mode 100644 index 0000000000..2199d5f57a --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/cognitivemodels.json @@ -0,0 +1,29 @@ +{ + "cognitiveModels": { + "en": { + "languageModels": [ + { + "id": "General", + "name": "", + "appid": "", + "version": "0.1", + "region": "", + "authoringkey": "", + "authoringRegion": "", + "subscriptionkey": "" + }, + { + "id": "Event", + "name": "", + "appid": "", + "version": "0.1", + "region": "", + "authoringkey": "", + "authoringRegion": "", + "subscriptionkey": "" + } + ] + } + }, + "defaultLocale": "en-us" +} diff --git a/skills/src/csharp/experimental/eventskill/manifestTemplate.json b/skills/src/csharp/experimental/eventskill/manifestTemplate.json new file mode 100644 index 0000000000..51cdb4160c --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/manifestTemplate.json @@ -0,0 +1,33 @@ +{ + "id": "eventSkill", + "name": "Event Skill", + "description": "The Event experimental skill uses the Eventbrite API to retrieve local event information.", + "iconUrl": "", + "authenticationConnections": [], + "actions": [ + { + "id": "EventSkill_getEvents", + "definition": { + "description": "Finds events happening in the area today.", + "slots": [ + { + "name": "location", + "types": [ + "string" + ] + } + ], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "Event#GetEvents" + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/skills/src/csharp/experimental/eventskill/readme.md b/skills/src/csharp/experimental/eventskill/readme.md new file mode 100644 index 0000000000..0afaeb3437 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/readme.md @@ -0,0 +1 @@ +Please refer to the [Skill Template documentation](http://aka.ms/virtualassistantdocs) for deployment and customization instructions. diff --git a/skills/src/csharp/experimental/eventskill/wwwroot/default.htm b/skills/src/csharp/experimental/eventskill/wwwroot/default.htm new file mode 100644 index 0000000000..17a9350195 --- /dev/null +++ b/skills/src/csharp/experimental/eventskill/wwwroot/default.htm @@ -0,0 +1,426 @@ + + + + + + + Event Skill + + + + + +
+
+
+
Event Skill
+
+
+
+
+
Your Skill is ready!
+
+ You can test your Skill in the Bot Framework Emulator
+ by opening the Emulator and providing the Endpoint shown below along with the Microsoft AppId and Password which you can find in appsettings.json.
+
+ +
+ Your Skill's endpoint URL typically looks + like this: +
+
https://your_bots_hostname/api/messages
+
+
In addition the Skill manifest endpoint can be found here: +
+
https://your_bots_hostname/api/skill/manifest
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/solutions/HospitalitySample/Content/NewUserGreeting.json b/solutions/HospitalitySample/Content/NewUserGreeting.json index 1ebffe0758..12c41afb01 100644 --- a/solutions/HospitalitySample/Content/NewUserGreeting.json +++ b/solutions/HospitalitySample/Content/NewUserGreeting.json @@ -25,7 +25,7 @@ "size": "Large", "weight": "Bolder", "color": "Light", - "text": "Hi, I'm **your** Virtual Assistant", + "text": "Hi, I'm **your** Hospitality Assistant", "wrap": true }, { diff --git a/solutions/HospitalitySample/HospitalitySample.csproj b/solutions/HospitalitySample/HospitalitySample.csproj index b93fc574b8..a685a4482e 100644 --- a/solutions/HospitalitySample/HospitalitySample.csproj +++ b/solutions/HospitalitySample/HospitalitySample.csproj @@ -3,6 +3,7 @@ netcoreapp2.2 NU1701 + 96a10d5f-5616-41d3-a131-a310c90072ff diff --git a/solutions/HospitalitySample/Services/DispatchLuis.cs b/solutions/HospitalitySample/Services/DispatchLuis.cs index 81ce1dfe50..12f69a647b 100644 --- a/solutions/HospitalitySample/Services/DispatchLuis.cs +++ b/solutions/HospitalitySample/Services/DispatchLuis.cs @@ -1,6 +1,5 @@ // -// Code generated by LUISGen C:\Users\t-luitof\source\repos\botframework-solutions\solutions\HospitalitySample\Deployment\Resources\Dispatch\en\LunaHospitalityVAen_Dispatch.json -cs Luis.DispatchLuis -o C:\Users\t-luitof\source\repos\botframework-solutions\solutions\HospitalitySample\Services -// Tool github: https://github.com/microsoft/botbuilder-tools +// Code generated by LUISGen // Changes may cause incorrect behavior and will be lost if the code is // regenerated. // @@ -19,11 +18,11 @@ public enum Intent { q_Chitchat, q_Faq, q_hotel_FAQ, - hospitalitySkill, restaurantBookingSkill, - newsSkill, pointOfInterestSkill, + hospitalitySkill, WeatherSkill, + newsSkill, None }; public Dictionary Intents; @@ -31,20 +30,20 @@ public enum Intent { public class _Entities { // Simple entities + public string[] KEYWORD; + public string[] ADDRESS; public string[] Item; public string[] topic; public string[] site; - public string[] KEYWORD; - public string[] ADDRESS; // Instance public class _Instance { + public InstanceData[] KEYWORD; + public InstanceData[] ADDRESS; public InstanceData[] Item; public InstanceData[] topic; public InstanceData[] site; - public InstanceData[] KEYWORD; - public InstanceData[] ADDRESS; } [JsonProperty("$instance")] public _Instance _instance; diff --git a/solutions/HospitalitySample/skills.json b/solutions/HospitalitySample/skills.json index 9f24956d26..ea80988b04 100644 --- a/solutions/HospitalitySample/skills.json +++ b/solutions/HospitalitySample/skills.json @@ -92,6 +92,23 @@ ] } } + }, + { + "id": "hospitalitySkill_roomService", + "definition": { + "description": "Order room service.", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "Hospitality#RoomService" + ] + } + ] + } + } } ] }, @@ -148,7 +165,7 @@ { "id": "newsSkill_findArticles", "definition": { - "description": "Find News articles", + "description": "Find News articles.", "slots": [], "triggers": { "utteranceSources": [ @@ -161,6 +178,57 @@ ] } } + }, + { + "id": "newsSkill_trendingArticles", + "definition": { + "description": "Show articles currently trending on social media.", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "news#TrendingArticles" + ] + } + ] + } + } + }, + { + "id": "newsSkill_setFavoriteTopics", + "definition": { + "description": "Set the user's favorite news topic.", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "news#SetFavoriteTopics" + ] + } + ] + } + } + }, + { + "id": "newsSkill_showFavoriteTopics", + "definition": { + "description": "Show news articles of the user's currently set favorite topic.", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "news#ShowFavoriteTopics" + ] + } + ] + } + } } ] }, @@ -276,20 +344,20 @@ "msaAppId": "", "name": "WeatherSkill", "endpoint": "", - "description": "", + "description": "The weather skill provides an example of displaying the current weather using AccuWeather.", "authenticationConnections": [], "actions": [ { - "id": "WeatherSkill_Sample", + "id": "WeatherSkill_getForecast", "definition": { - "description": "Sample Skill action with no slots", + "description": "Showing the weather forecast.", "slots": [], "triggers": { "utteranceSources": [ { "locale": "en", "source": [ - "WeatherSkill#Sample" + "WeatherSkill#GetForecast" ] } ]