diff --git a/experimental/language-generation/README.md b/experimental/language-generation/README.md index 331ab9841c..e90a617654 100644 --- a/experimental/language-generation/README.md +++ b/experimental/language-generation/README.md @@ -1,6 +1,6 @@ # Language Generation ***_[PREVIEW]_*** -> See [here](#Change-Log) for what's new in **4.6 PREVIEW 2** release. +> See [here](#Change-Log) for what's new in **4.7.0 PREVIEW** release. Learning from our customers experiences and bringing together capabilities first implemented by Cortana and Cognition teams, we are introducing Language Generation; which allows the developer to extract the embedded strings from their code and resource files and manage them through a Language Generation runtime and file format. Language Generation enable customers to define multiple variations on a phrase, execute simple expressions based on context, refer to conversational memory, and over time will enable us to bring additional capabilities all leading to a more natural conversational experience. @@ -52,10 +52,10 @@ For NodeJS ```typescript // multi lg files - let lgEngine = new TemplateEngine.addFiles(filePaths, importResolver?); + let lgEngine = new TemplateEngine().addFiles(filePaths, importResolver?); // single lg file - let lgEngine = new TemplateEngine.addFile(filePath, importResolver?); + let lgEngine = new TemplateEngine().addFile(filePath, importResolver?); ``` When you need template expansion, call the templateEngine and pass in the relevant template name @@ -69,7 +69,7 @@ For C# For NodeJS ```typescript - await turnContext.sendActivity(lgEngine.evaluateTemplate("", entitiesCollection)); + await turnContext.sendActivity(ActivityFactory.createActivity(lgEngine.evaluateTemplate("", entitiesCollection))); ``` If your template needs specific entity values to be passed for resolution/ expansion, you can pass them in on the call to `evaluateTemplate` @@ -83,8 +83,8 @@ For C# For NodeJS -```node - await turnContext.sendActivity(lgEngine.evaluateTemplate("WordGameReply", { GameName = "MarcoPolo" } )); +```typescript + await turnContext.sendActivity(ActivityFactory.createActivity(lgEngine.evaluateTemplate("WordGameReply", { GameName = "MarcoPolo" } ))); ``` ## Multi-lingual generation and language fallback policy @@ -95,10 +95,31 @@ Quite often your bot might target more than one spoken/ display language. To do The current library does not include any capabilities for grammar check or correction. ## Packages +Latest preview packages are available here +- C# -> [NuGet][14] +- JS -> [npm][15] -Packages for C# are available under the [BotBuilder MyGet feed][12] +Nightly packages for C# are available here +- C# -> [BotBuilder MyGet feed][12] +- JS -> [BotBuilder MyGet feed][13] ## Change Log +### 4.7 PREVIEW +- \[**BREAKING CHANGES**\]: + - Old way to refer to a template via `[TemplateName]` notation is deprecated in favor of `@{TemplateName()}` notation. There are no changes to how structured response templates are defined. + - All expressions must now be enclosed within `@{}`. The old notation `{}` is no longer supported. + - `ActivityBuilder` has been deprecated and removed in favor of `ActivityFactory`. Note that by stable release, functionality offered by `ActivityFactory` is likely to move into `MessageFactory`. + + | Old | New | + |-------|-----| + | # myTemplate
- I have {user.name} as your name | # myTemplate
- I have @{user.name} as your name | + | # myTemplate
- [ackPhrase]

# ackPhrase
- hi
- hello | # myTemplate
- @{ackPhrase()}

# ackPhrase
- hi
- hello | + +- \[**NEW**\]: + - Language generation preview is now available for JavaScript as well. Checkout packages [here][15]. Samples are [here][26] + - New `ActivityFactory` class that helps transform structured response template output from LG into a Bot framework activity. + - Bug fixes and stability improvements. + ### 4.6 PREVIEW 2 - \[**BREAKING CHANGES**\]: - Old `display || speak` notation is deprecated in favor of structured template support. See below for more details on structured template. @@ -130,9 +151,12 @@ Packages for C# are available under the [BotBuilder MyGet feed][12] [10]:https://github.com/Microsoft/botbuilder-tools/tree/master/packages/Chatdown#message-cards [11]:https://github.com/Microsoft/botbuilder-tools/tree/master/packages/Chatdown#message-attachments [12]:https://botbuilder.myget.org/F/botbuilder-v4-dotnet-daily/api/v3/index.json +[13]:https://botbuilder.myget.org/gallery/botbuilder-v4-js-daily +[14]:https://www.nuget.org/packages/Microsoft.Bot.Builder.LanguageGeneration/4.7.0-preview +[15]:https://www.npmjs.com/package/botbuilder-lg [20]:./docs/lg-file-format.md#Switch..Case [21]:./docs/lg-file-format.md#Importing-external-references [22]:https://aka.ms/lg-vscode-extension [23]:https://github.com/microsoft/botbuilder-tools/tree/V.Future/packages/MSLG [25]:./csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/ - +[26]:./javascript_nodejs/ diff --git a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/AdapterWithErrorHandler.cs b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/AdapterWithErrorHandler.cs index f7adb3a9ce..56c52b641c 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/AdapterWithErrorHandler.cs +++ b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/AdapterWithErrorHandler.cs @@ -6,9 +6,7 @@ using Microsoft.Bot.Builder; using Microsoft.Extensions.Logging; using Microsoft.Bot.Connector.Authentication; -using Microsoft.Bot.Builder.LanguageGeneration; using Microsoft.Bot.Builder.Integration.AspNet.Core; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; using System.Collections.Generic; namespace Microsoft.BotBuilderSamples diff --git a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/Dialogs/UserProfileDialog.cs b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/Dialogs/UserProfileDialog.cs index bc9b993074..76cd3ac1ae 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/Dialogs/UserProfileDialog.cs +++ b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/Dialogs/UserProfileDialog.cs @@ -8,8 +8,7 @@ using System.Collections.Generic; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; - +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { public class UserProfileDialog : ComponentDialog @@ -117,7 +116,7 @@ private async Task ConfirmStepAsync(WaterfallStepContext stepC }, stepContext); // We can send messages to the user at any point in the WaterfallStep. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(msg), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(msg.ToString()), cancellationToken); // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog. return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { @@ -138,7 +137,7 @@ private async Task SummaryStepAsync(WaterfallStepContext stepC var msg = _lgGenerator.GenerateActivity("SummaryReadout", userProfile, stepContext); - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(msg), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(msg.ToString()), cancellationToken); } else { diff --git a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiLingualTemplateEngine.cs b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiLingualTemplateEngine.cs index 2353053a1d..caeb897578 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiLingualTemplateEngine.cs +++ b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiLingualTemplateEngine.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using Microsoft.Bot.Schema; using Microsoft.Bot.Builder.LanguageGeneration; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; using System; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs.Adaptive; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { @@ -105,7 +105,7 @@ private Activity InternalGenerateActivity(string templateName, object data, stri if (TemplateEnginesPerLocale.ContainsKey(iLocale)) { - return ActivityBuilder.GenerateFromLG(TemplateEnginesPerLocale[locale].EvaluateTemplate(templateName, data)); + return ActivityFactory.CreateActivity(TemplateEnginesPerLocale[locale].EvaluateTemplate(templateName, data).ToString()); } var locales = new string[] { string.Empty }; if (!LangFallBackPolicy.TryGetValue(iLocale, out locales)) @@ -120,7 +120,7 @@ private Activity InternalGenerateActivity(string templateName, object data, stri { if (TemplateEnginesPerLocale.ContainsKey(fallBackLocale)) { - return ActivityBuilder.GenerateFromLG(TemplateEnginesPerLocale[fallBackLocale].EvaluateTemplate(templateName, data)); + return ActivityFactory.CreateActivity(TemplateEnginesPerLocale[fallBackLocale].EvaluateTemplate(templateName, data).ToString()); } } return new Activity(); diff --git a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiTurnMultiLingualPromptBot.csproj b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiTurnMultiLingualPromptBot.csproj index 97d59f7f73..267a9afea0 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiTurnMultiLingualPromptBot.csproj +++ b/experimental/language-generation/csharp_dotnetcore/05.a.multi-turn-prompt-with-language-fallback/MultiTurnMultiLingualPromptBot.csproj @@ -18,11 +18,11 @@ true - - - - - + + + + + diff --git a/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/AdapterWithErrorHandler.cs b/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/AdapterWithErrorHandler.cs index b661f5cd38..6e06f9f91a 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/AdapterWithErrorHandler.cs +++ b/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/AdapterWithErrorHandler.cs @@ -8,7 +8,7 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Builder.LanguageGeneration; using Microsoft.Bot.Builder.Integration.AspNet.Core; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { @@ -29,7 +29,7 @@ public AdapterWithErrorHandler(ICredentialProvider credentialProvider, ILogger TransportStepAsync(WaterfallStepCont new PromptOptions { - Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("ModeOfTransportPrompt")), + Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("ModeOfTransportPrompt").ToString()), Choices = ChoiceFactory.ToChoices(new List { "Car", "Bus", "Bicycle" }), }, cancellationToken); } @@ -64,7 +64,7 @@ private static async Task NameStepAsync(WaterfallStepContext s { stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value; - return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AskForName")) }, cancellationToken); + return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AskForName").ToString()) }, cancellationToken); } private async Task NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) @@ -72,12 +72,12 @@ private async Task NameConfirmStepAsync(WaterfallStepContext s stepContext.Values["name"] = (string)stepContext.Result; // We can send messages to the user at any point in the WaterfallStep. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AckName", new { + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AckName", new { Result = stepContext.Result - })), cancellationToken); + }).ToString()), cancellationToken); // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. - return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AgeConfirmPrompt")) }, cancellationToken); + return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AgeConfirmPrompt").ToString()) }, cancellationToken); } private async Task AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) @@ -88,8 +88,8 @@ private async Task AgeStepAsync(WaterfallStepContext stepConte // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog. var promptOptions = new PromptOptions { - Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AskForAge")), - RetryPrompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AskForAge.reprompt")), + Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AskForAge").ToString()), + RetryPrompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AskForAge.reprompt").ToString()), }; return await stepContext.PromptAsync(nameof(NumberPrompt), promptOptions, cancellationToken); @@ -111,10 +111,10 @@ private async Task ConfirmStepAsync(WaterfallStepContext stepC }); // We can send messages to the user at any point in the WaterfallStep. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(msg), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(msg.ToString()), cancellationToken); // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog. - return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("ConfirmPrompt")) }, cancellationToken); + return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("ConfirmPrompt").ToString()) }, cancellationToken); } private async Task SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) @@ -130,11 +130,11 @@ private async Task SummaryStepAsync(WaterfallStepContext stepC var msg = _lgEngine.EvaluateTemplate("SummaryReadout", userProfile); - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(msg), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(msg.ToString()), cancellationToken); } else { - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("NoProfileReadBack")), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("NoProfileReadBack").ToString()), cancellationToken); } // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end. diff --git a/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/MultiTurnPromptBot.csproj b/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/MultiTurnPromptBot.csproj index 97d59f7f73..267a9afea0 100644 --- a/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/MultiTurnPromptBot.csproj +++ b/experimental/language-generation/csharp_dotnetcore/05.multi-turn-prompt/MultiTurnPromptBot.csproj @@ -18,11 +18,11 @@ true - - - - - + + + + + diff --git a/experimental/language-generation/csharp_dotnetcore/06.using-cards/AdapterWithErrorHandler.cs b/experimental/language-generation/csharp_dotnetcore/06.using-cards/AdapterWithErrorHandler.cs index b661f5cd38..53bad94474 100644 --- a/experimental/language-generation/csharp_dotnetcore/06.using-cards/AdapterWithErrorHandler.cs +++ b/experimental/language-generation/csharp_dotnetcore/06.using-cards/AdapterWithErrorHandler.cs @@ -8,8 +8,7 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Builder.LanguageGeneration; using Microsoft.Bot.Builder.Integration.AspNet.Core; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; - +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { public class AdapterWithErrorHandler : BotFrameworkHttpAdapter @@ -29,7 +28,7 @@ public AdapterWithErrorHandler(ICredentialProvider credentialProvider, ILogger { new Fact("Order Number", "1234"), new Fact("Payment Method", "VISA 5555-****") }, - Items = new List - { - new ReceiptItem( - "Data Transfer", - price: "$ 38.45", - quantity: "368", - image: new CardImage(url: "https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png")), - new ReceiptItem( - "App Service", - price: "$ 45.00", - quantity: "720", - image: new CardImage(url: "https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png")), - }, - Tax = "$ 7.50", - Total = "$ 90.95", - Buttons = new List - { - new CardAction( - ActionTypes.ImBack, - "More information", - "https://account.windowsazure.com/content/6.10.1.38-.8225.160809-1618/aux-pre/images/offer-icon-freetrial.png", - "https://azure.microsoft.com/en-us/pricing/"), - }, - }; - - return receiptCard; - } - } -} diff --git a/experimental/language-generation/csharp_dotnetcore/06.using-cards/CardsBot.csproj b/experimental/language-generation/csharp_dotnetcore/06.using-cards/CardsBot.csproj index 8265d2b8a8..cbfb51e869 100644 --- a/experimental/language-generation/csharp_dotnetcore/06.using-cards/CardsBot.csproj +++ b/experimental/language-generation/csharp_dotnetcore/06.using-cards/CardsBot.csproj @@ -10,11 +10,11 @@ true - - - - - + + + + + diff --git a/experimental/language-generation/csharp_dotnetcore/06.using-cards/Dialogs/MainDialog.cs b/experimental/language-generation/csharp_dotnetcore/06.using-cards/Dialogs/MainDialog.cs index 7a405c47ce..56b3d6e66e 100644 --- a/experimental/language-generation/csharp_dotnetcore/06.using-cards/Dialogs/MainDialog.cs +++ b/experimental/language-generation/csharp_dotnetcore/06.using-cards/Dialogs/MainDialog.cs @@ -4,15 +4,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Dialogs.Choices; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; using Microsoft.Bot.Builder.LanguageGeneration; using System.IO; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; +using Newtonsoft.Json.Linq; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { @@ -54,7 +53,7 @@ private async Task ChoiceCardStepAsync(WaterfallStepContext st // Create options for the prompt var options = new PromptOptions() { - Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("CardChoice")), + Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("CardChoice").ToString()), Choices = new List(), }; @@ -90,52 +89,65 @@ private async Task ShowCardStepAsync(WaterfallStepContext step if (text.StartsWith("hero")) { // Display a HeroCard. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("HeroCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("HeroCard").ToString())); } else if (text.StartsWith("thumb")) { // Display a ThumbnailCard. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("ThumbnailCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("ThumbnailCard").ToString())); } else if (text.StartsWith("sign")) { // Display a SignInCard. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("SigninCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("SigninCard").ToString())); } else if (text.StartsWith("animation")) { // Display an AnimationCard. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AnimationCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AnimationCard").ToString())); } else if (text.StartsWith("video")) { // Display a VideoCard - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("VideoCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("VideoCard").ToString())); } else if (text.StartsWith("audio")) { // Display an AudioCard - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AudioCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AudioCard").ToString())); } else if (text.StartsWith("receipt")) { - // Display a ReceiptCard. - reply.Attachments.Add(Cards.GetReceiptCard().ToAttachment()); - // Send the card(s) to the user as an attachment to the activity - await stepContext.Context.SendActivityAsync(reply, cancellationToken); + var data = new JObject + { + ["receiptItems"] = JToken.FromObject(new List + { + new ReceiptItem( + "Data Transfer", + price: "$ 38.45", + quantity: "368", + image: new CardImage(url: "https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png")), + new ReceiptItem( + "App Service", + price: "$ 45.00", + quantity: "720", + image: new CardImage(url: "https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png")), + }) + }; + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("ReceiptCard", data).ToString())); } else if (text.StartsWith("adaptive")) { - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AdaptiveCard"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AdaptiveCard").ToString())); } else { // Display a carousel of all the rich card types. - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("AllCards"))); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("AllCards").ToString())); } // Give the user instructions about what to do next - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("CardStartOverResponse")), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("CardStartOverResponse").ToString()), cancellationToken); return await stepContext.EndDialogAsync(); } diff --git a/experimental/language-generation/csharp_dotnetcore/06.using-cards/Resources/Cards.LG b/experimental/language-generation/csharp_dotnetcore/06.using-cards/Resources/Cards.LG index debef23adc..bff3c45bad 100644 --- a/experimental/language-generation/csharp_dotnetcore/06.using-cards/Resources/Cards.LG +++ b/experimental/language-generation/csharp_dotnetcore/06.using-cards/Resources/Cards.LG @@ -6,10 +6,42 @@ # AllCards [Activity - Attachments = @{HeroCard()} | @{ThumbnailCard()} | @{SigninCard()} | @{AnimationCard()} | @{VideoCard()} | @{AudioCard()} | @{AdaptiveCard()} + Attachments = @{HeroCard()} | @{ThumbnailCard()} | @{SigninCard()} | @{AnimationCard()} | @{VideoCard()} | @{AudioCard()} | @{json(adaptivecardjson())} AttachmentLayout = @{AttachmentLayoutType()} ] +# ReceiptCard +[ReceiptCard + Title = John Doe + Tax = $ 7.50 + Total = $ 90.95 + buttons = @{ReceiptButton()} + Facts = @{json(ReceiptFacts())} + items = @{receiptItems} +] + +# ReceiptButton +[CardAction + Title = More information + Image = https://account.windowsazure.com/content/6.10.1.38-.8225.160809-1618/aux-pre/images/offer-icon-freetrial.png + Value = https://azure.microsoft.com/en-us/pricing/ + type = openUrl +] + +# ReceiptFacts +- ``` +[ + { + "key": "Order Number", + "value": "1234" + }, + { + "key": "Payment Method", + "value": "VISA 5555-****" + } +] +``` + # HeroCard [HeroCard title = BotFramework Hero Card @@ -31,7 +63,14 @@ # SigninCard [SigninCard text = BotFramework Sign-in Card - buttons = Sign-in + buttons = @{signinButton()} +] + +# signinButton +[CardAction + title = Sign-in + type = signin + value = http://login.microsoft.com ] # AnimationCard diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/AdapterWithErrorHandler.cs b/experimental/language-generation/csharp_dotnetcore/13.core-bot/AdapterWithErrorHandler.cs index b661f5cd38..53bad94474 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/AdapterWithErrorHandler.cs +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/AdapterWithErrorHandler.cs @@ -8,8 +8,7 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Builder.LanguageGeneration; using Microsoft.Bot.Builder.Integration.AspNet.Core; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; - +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples { public class AdapterWithErrorHandler : BotFrameworkHttpAdapter @@ -29,7 +28,7 @@ public AdapterWithErrorHandler(ICredentialProvider credentialProvider, ILogger membersA // To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. if (member.Id != turnContext.Activity.Recipient.Id) { - await turnContext.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("WelcomeCard", actions))); + await turnContext.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("WelcomeCard", actions).ToString())); } } } diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/CoreBot.csproj b/experimental/language-generation/csharp_dotnetcore/13.core-bot/CoreBot.csproj index 6ad78d5658..4a4148de51 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/CoreBot.csproj +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/CoreBot.csproj @@ -9,13 +9,13 @@ all true - - - - - - - + + + + + + + diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/BookingDialog.cs b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/BookingDialog.cs index b0791631e0..2cd22dbb5e 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/BookingDialog.cs +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/BookingDialog.cs @@ -8,8 +8,7 @@ using Microsoft.Recognizers.Text.DataTypes.TimexExpression; using Microsoft.Bot.Builder.LanguageGeneration; using System.IO; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; - +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples.Dialogs { public class BookingDialog : CancelAndHelpDialog @@ -46,7 +45,7 @@ private async Task DestinationStepAsync(WaterfallStepContext s if (bookingDetails.Destination == null) { - return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails)) }, cancellationToken); + return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails).ToString()) }, cancellationToken); } else { @@ -62,7 +61,7 @@ private async Task OriginStepAsync(WaterfallStepContext stepCo if (bookingDetails.Origin == null) { - return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails)) }, cancellationToken); + return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails).ToString()) }, cancellationToken); } else { @@ -91,7 +90,7 @@ private async Task ConfirmStepAsync(WaterfallStepContext stepC bookingDetails.TravelDate = (string)stepContext.Result; - return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails)) }, cancellationToken); + return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("PromptForMissingInformation", bookingDetails).ToString()) }, cancellationToken); } private async Task FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/DateResolverDialog.cs b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/DateResolverDialog.cs index 2e36f26be4..93f45eb688 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/DateResolverDialog.cs +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/DateResolverDialog.cs @@ -4,12 +4,11 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Recognizers.Text.DataTypes.TimexExpression; using Microsoft.Bot.Builder.LanguageGeneration; using System.IO; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples.Dialogs { @@ -50,8 +49,8 @@ private async Task InitialStepAsync(WaterfallStepContext stepC return await stepContext.PromptAsync(nameof(DateTimePrompt), new PromptOptions { - Prompt = ActivityBuilder.GenerateFromLG(promptMsg), - RetryPrompt = ActivityBuilder.GenerateFromLG(repromptMsg) + Prompt = ActivityFactory.CreateActivity(promptMsg.ToString()), + RetryPrompt = ActivityFactory.CreateActivity(repromptMsg.ToString()) }, cancellationToken); } else @@ -64,7 +63,7 @@ private async Task InitialStepAsync(WaterfallStepContext stepC return await stepContext.PromptAsync(nameof(DateTimePrompt), new PromptOptions { - Prompt = ActivityBuilder.GenerateFromLG(repromptMsg) + Prompt = ActivityFactory.CreateActivity(repromptMsg.ToString()) }, cancellationToken); } else diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/MainDialog.cs b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/MainDialog.cs index f597f114a4..5bdfc0527c 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/MainDialog.cs +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/Dialogs/MainDialog.cs @@ -11,10 +11,7 @@ using Microsoft.Recognizers.Text.DataTypes.TimexExpression; using Microsoft.Bot.Builder.LanguageGeneration; using System.IO; -using System.Collections.Generic; -using Microsoft.Bot.Schema; -using ActivityBuilder = Microsoft.Bot.Builder.Dialogs.Adaptive.Generators.ActivityGenerator; - +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.BotBuilderSamples.Dialogs { @@ -59,7 +56,7 @@ await stepContext.Context.SendActivityAsync( } else { - return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("IntroPrompt")) }, cancellationToken); + return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("IntroPrompt").ToString()) }, cancellationToken); } } @@ -92,7 +89,7 @@ private async Task FinalStepAsync(WaterfallStepContext stepCon var timeProperty = new TimexProperty(result.TravelDate); result.travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now); - await stepContext.Context.SendActivityAsync(ActivityBuilder.GenerateFromLG(_lgEngine.EvaluateTemplate("BookingConfirmation", result)), cancellationToken); + await stepContext.Context.SendActivityAsync(ActivityFactory.CreateActivity(_lgEngine.EvaluateTemplate("BookingConfirmation", result).ToString()), cancellationToken); } else { diff --git a/experimental/language-generation/csharp_dotnetcore/13.core-bot/LuisHelper.cs b/experimental/language-generation/csharp_dotnetcore/13.core-bot/LuisHelper.cs index 61ec233f2d..79c91b2a97 100644 --- a/experimental/language-generation/csharp_dotnetcore/13.core-bot/LuisHelper.cs +++ b/experimental/language-generation/csharp_dotnetcore/13.core-bot/LuisHelper.cs @@ -20,6 +20,7 @@ public static async Task ExecuteLuisQuery(IConfiguration configu try { + // Create the LUIS settings from configuration. var luisApplication = new LuisApplication( configuration["LuisAppId"], @@ -27,7 +28,9 @@ public static async Task ExecuteLuisQuery(IConfiguration configu "https://" + configuration["LuisAPIHostName"] ); - var recognizer = new LuisRecognizer(luisApplication); + var luisRecognizerOptions = new LuisRecognizerOptionsV2(luisApplication); + + var recognizer = new LuisRecognizer(luisRecognizerOptions); // The actual call to LUIS var recognizerResult = await recognizer.RecognizeAsync(turnContext, cancellationToken); diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.env b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.env new file mode 100644 index 0000000000..a695b3bf05 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.eslintrc.js b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.eslintrc.js new file mode 100644 index 0000000000..4efa0c5f84 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + "extends": "standard", + "rules": { + "semi": [2, "always"], + "indent": [2, 4], + "no-return-await": 0, + "space-before-function-paren": [2, { + "named": "never", + "anonymous": "never", + "asyncArrow": "always" + }], + "template-curly-spacing": [2, "always"] + } +}; \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/README.md b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/README.md new file mode 100644 index 0000000000..d1bf541c36 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/README.md @@ -0,0 +1,87 @@ +# multi turn prompt sample + +Bot Framework v4 multi-turn prompt bot sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use the prompts classes included in `botbuilder-dialogs`. This bot will ask for the user's name and age, then store the responses. It demonstrates a multi-turn dialog flow using a text prompt, a number prompt, and state accessors to store and retrieve values. + +In this sample, we will demonstrate use of [Language Generation][41] to generate all responses from the bot. + +## Prerequisites + +- [Node.js](https://nodejs.org) version 10.14 or higher + + ```bash + # determine node version + node --version + ``` + +## To try this sample + +- Clone the repository + + ```bash + git clone https://github.com/microsoft/botbuilder-samples.git + ``` + +- In a terminal, navigate to `samples/javascript_nodejs/05.multi-turn-prompt` + + ```bash + cd samples/javascript_nodejs/05.multi-turn-prompt + ``` + +- Install modules + + ```bash + npm install + ``` + +- Start the bot + + ```bash + npm start + ``` + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + +## Prompts + +A conversation between a bot and a user often involves asking (prompting) the user for information, parsing the user's response, +and then acting on that information. This sample demonstrates how to prompt users for information using the different prompt types +included in the [botbuilder-dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) library +and supported by the SDK. + +The `botbuilder-dialogs` library includes a variety of pre-built prompt classes, including text, number, and datetime types. This +sample demonstrates using a text prompt to collect the user's name, then using a number prompt to collect an age. + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Restify](https://www.npmjs.com/package/restify) +- [dotenv](https://www.npmjs.com/package/dotenv) + + +[41]:../../README.md diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/AdapterWithErrorHandler.LG b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/AdapterWithErrorHandler.LG new file mode 100644 index 0000000000..80515b045a --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/AdapterWithErrorHandler.LG @@ -0,0 +1,15 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# SomethingWentWrong +- @{ErrorPrefix()}, @{ErrorSuffix()}\nError:@{message} + +# ErrorSuffix +- it looks like something went wrong. +- I seem to have run into a snag. We need to start over. +- something is not right. We need to start over. + +# ErrorPrefix +- Oops +- Sorry +- I apologize \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/UserProfileDialog.LG b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/UserProfileDialog.LG new file mode 100644 index 0000000000..69a95aef6b --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/Resources/UserProfileDialog.LG @@ -0,0 +1,41 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# ModeOfTransportPrompt +- Please enter your mode of transport. + +# AskForName +- Please enter your name. + +# AckName +- Thanks, @{CapitalizeFirstLetter(name)}. + +# CapitalizeFirstLetter (arg) +- @{concat(toUpper(substring(arg, 0, 1)), substring(arg, 1))} + +# AgeConfirmPrompt +- Would you like to give your age? + +# AskForAge +- Please enter your age. + +# AskForAge.reprompt +- The value entered must be greater than 0 and less than 150. + +> This template uses inline expressions. Expressions are defined using the common expression language. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language to learn more. + +# AgeReadBack (userAge) +- IF: @{userAge == null} + - And, No age given. +- ELSE: + - And, I have your age as @{userAge}. + +# ConfirmPrompt +- Is this ok? + +# SummaryReadout +- I have your mode of transport as @{transport} and your name as @{CapitalizeFirstLetter(name)}. @{AgeReadBack(age)} + +# NoProfileReadBack +- Thanks. Your profile will not be kept. diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/bots/dialogBot.js b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/bots/dialogBot.js new file mode 100644 index 0000000000..aa758ab38b --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/bots/dialogBot.js @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { ActivityHandler } = require('botbuilder'); + +class DialogBot extends ActivityHandler { + /** + * + * @param {ConversationState} conversationState + * @param {UserState} userState + * @param {Dialog} dialog + */ + constructor(conversationState, userState, dialog) { + super(); + if (!conversationState) throw new Error('[DialogBot]: Missing parameter. conversationState is required'); + if (!userState) throw new Error('[DialogBot]: Missing parameter. userState is required'); + if (!dialog) throw new Error('[DialogBot]: Missing parameter. dialog is required'); + + this.conversationState = conversationState; + this.userState = userState; + this.dialog = dialog; + this.dialogState = this.conversationState.createProperty('DialogState'); + + this.onMessage(async (context, next) => { + console.log('Running dialog with Message Activity.'); + + // Run the Dialog with the new message Activity. + await this.dialog.run(context, this.dialogState); + + await next(); + }); + + this.onDialog(async (context, next) => { + // Save any state changes. The load happened during the execution of the Dialog. + await this.conversationState.saveChanges(context, false); + await this.userState.saveChanges(context, false); + await next(); + }); + } +} + +module.exports.DialogBot = DialogBot; diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json new file mode 100644 index 0000000000..ead3390932 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/new-rg-parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "value": "" + }, + "groupName": { + "value": "" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json new file mode 100644 index 0000000000..b6f5114fcc --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/preexisting-rg-parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appServicePlanLocation": { + "value": "" + }, + "existingAppServicePlan": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 0000000000..06b8284158 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 0000000000..43943b6581 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('servicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/dialogs/userProfileDialog.js b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/dialogs/userProfileDialog.js new file mode 100644 index 0000000000..6768af2d54 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/dialogs/userProfileDialog.js @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { + ChoiceFactory, + ChoicePrompt, + ComponentDialog, + ConfirmPrompt, + DialogSet, + DialogTurnStatus, + NumberPrompt, + TextPrompt, + WaterfallDialog +} = require('botbuilder-dialogs'); + +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); + +const { UserProfile } = require('../userProfile'); + +const CHOICE_PROMPT = 'CHOICE_PROMPT'; +const CONFIRM_PROMPT = 'CONFIRM_PROMPT'; +const NAME_PROMPT = 'NAME_PROMPT'; +const NUMBER_PROMPT = 'NUMBER_PROMPT'; +const USER_PROFILE = 'USER_PROFILE'; +const WATERFALL_DIALOG = 'WATERFALL_DIALOG'; + +class UserProfileDialog extends ComponentDialog { + constructor(userState) { + super('userProfileDialog'); + + this.userProfile = userState.createProperty(USER_PROFILE); + + this.addDialog(new TextPrompt(NAME_PROMPT)); + this.addDialog(new ChoicePrompt(CHOICE_PROMPT)); + this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT)); + this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator)); + + this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [ + this.transportStep.bind(this), + this.nameStep.bind(this), + this.nameConfirmStep.bind(this), + this.ageStep.bind(this), + this.confirmStep.bind(this), + this.summaryStep.bind(this) + ])); + + this.initialDialogId = WATERFALL_DIALOG; + + this.templateEngine = new TemplateEngine().addFile('./Resources/UserProfileDialog.LG'); + } + + /** + * The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system. + * If no dialog is active, it will start the default dialog. + * @param {*} turnContext + * @param {*} accessor + */ + async run(turnContext, accessor) { + const dialogSet = new DialogSet(accessor); + dialogSet.add(this); + + const dialogContext = await dialogSet.createContext(turnContext); + const results = await dialogContext.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog(this.id); + } + } + + async transportStep(step) { + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + // Running a prompt here means the next WaterfallStep will be run when the users response is received. + return await step.prompt(CHOICE_PROMPT, { + prompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('ModeOfTransportPrompt')), + choices: ChoiceFactory.toChoices(['Car', 'Bus', 'Bicycle']) + }); + } + + async nameStep(step) { + step.values.transport = step.result.value; + return await step.prompt(NAME_PROMPT, ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AskForName'))); + } + + async nameConfirmStep(step) { + step.values.name = step.result; + + // We can send messages to the user at any point in the WaterfallStep. + await step.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AckName', step.values))); + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + return await step.prompt(CONFIRM_PROMPT, ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AgeConfirmPrompt')), ['yes', 'no']); + } + + async ageStep(step) { + if (step.result) { + // User said "yes" so we will be prompting for the age. + // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog. + const promptOptions = { + prompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AskForAge')), + retryPrompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AskForAge.reprompt')) + }; + + return await step.prompt(NUMBER_PROMPT, promptOptions); + } else { + // User said "no" so we will skip the next step. Give -1 as the age. + return await step.next(-1); + } + } + + async confirmStep(step) { + step.values.age = step.result; + + // We can send messages to the user at any point in the WaterfallStep. + await step.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AgeReadBack', { + userAge:step.values.age + }))); + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog. + return await step.prompt(CONFIRM_PROMPT, { prompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('ConfirmPrompt')) }); + } + + async summaryStep(step) { + if (step.result) { + // Get the current profile object from user state. + const userProfile = await this.userProfile.get(step.context, new UserProfile()); + + userProfile.transport = step.values.transport; + userProfile.name = step.values.name; + userProfile.age = step.values.age; + + await step.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('SummaryReadout', userProfile))); + } else { + await step.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('NoProfileReadBack'))); + } + + // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end. + return await step.endDialog(); + } + + async agePromptValidator(promptContext) { + // This condition is our validation rule. You can also change the value at this point. + return promptContext.recognized.succeeded && promptContext.recognized.value > 0 && promptContext.recognized.value < 150; + } +} + +module.exports.UserProfileDialog = UserProfileDialog; diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/index.js b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/index.js new file mode 100644 index 0000000000..65df53d2db --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/index.js @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const restify = require('restify'); +const path = require('path'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder'); + +// Import our custom bot class that provides a turn handling function. +const { DialogBot } = require('./bots/dialogBot'); +const { UserProfileDialog } = require('./dialogs/userProfileDialog'); + +// Read environment variables from .env file +const ENV_FILE = path.join(__dirname, '.env'); +require('dotenv').config({ path: ENV_FILE }); + +// Create the adapter. See https://aka.ms/about-bot-adapter to learn more about using information from +// the .bot file when configuring your adapter. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Create template engine for language generation. +const templateEngine = new TemplateEngine().addFile('./Resources/AdapterWithErrorHandler.LG'); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. + console.error(templateEngine.evaluateTemplate('SomethingWentWrong', { + message : `${error}` + })); + + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + 'OnTurnError Trace', + `${ error }`, + 'https://www.botframework.com/schemas/error', + 'TurnError' + ); + + // Send a message to the user + await context.sendActivity('The bot encounted an error or bug.'); + await context.sendActivity('To continue to run this bot, please fix the bot source code.'); + // Clear out state + await conversationState.delete(context); +}; + +// Define the state store for your bot. +// See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state storage system to persist the dialog and user state between messages. +const memoryStorage = new MemoryStorage(); + +// Create conversation state with in-memory storage provider. +const conversationState = new ConversationState(memoryStorage); +const userState = new UserState(memoryStorage); + +// Create the main dialog. +const dialog = new UserProfileDialog(userState); +const bot = new DialogBot(conversationState, userState, dialog); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, function() { + console.log(`\n${ server.name } listening to ${ server.url }.`); + console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator'); + console.log('\nTo talk to your bot, open the emulator select "Open Bot"'); +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route the message to the bot's main handler. + await bot.run(context); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/package.json b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/package.json new file mode 100644 index 0000000000..4be0e0e30f --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/package.json @@ -0,0 +1,35 @@ +{ + "name": "multiturn-prompts-bot", + "version": "1.0.0", + "description": "Bot Builder v4 multiturn prompts sample", + "author": "Microsoft", + "license": "MIT", + "main": "index.js", + "scripts": { + "start": "node ./index.js", + "watch": "nodemon ./index.js", + "lint": "eslint .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "botbuilder": "~4.7.0", + "botbuilder-dialogs": "~4.7.0", + "botbuilder-lg": "4.7.0-preview", + "dotenv": "^8.2.0", + "path": "^0.12.7", + "restify": "~8.4.0" + }, + "devDependencies": { + "eslint": "^6.6.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^10.0.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "nodemon": "~1.19.4" + } +} diff --git a/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/userProfile.js b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/userProfile.js new file mode 100644 index 0000000000..05ea7cbcd5 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/05.multi-turn-prompt/userProfile.js @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +class UserProfile { + constructor(transport, name, age) { + this.transport = transport; + this.name = name; + this.age = age; + } +} + +module.exports.UserProfile = UserProfile; diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/.env b/experimental/language-generation/javascript_nodejs/06.using-cards/.env new file mode 100644 index 0000000000..660828e3e8 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/.env @@ -0,0 +1,2 @@ +MicrosoftAppId= +MicrosoftAppPassword= \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/.eslintrc.js b/experimental/language-generation/javascript_nodejs/06.using-cards/.eslintrc.js new file mode 100644 index 0000000000..4efa0c5f84 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + "extends": "standard", + "rules": { + "semi": [2, "always"], + "indent": [2, 4], + "no-return-await": 0, + "space-before-function-paren": [2, { + "named": "never", + "anonymous": "never", + "asyncArrow": "always" + }], + "template-curly-spacing": [2, "always"] + } +}; \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/README.md b/experimental/language-generation/javascript_nodejs/06.using-cards/README.md new file mode 100644 index 0000000000..dabfaebcac --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/README.md @@ -0,0 +1,92 @@ +# Using-Cards + +Bot Framework v4 using cards bot sample + +This bot has been created using [Microsoft Bot Framework][1], it shows how to create a bot that uses rich cards to enhance your bot design. + +In this sample, we will demonstrate use of [Language Generation][41] to generate all responses from the bot. + + +## Prerequisites + +- [Node.js](https://nodejs.org) version 10.14 or higher + + ```bash + # determine node version + node -v + ``` + +## To try this sample + +- Clone the repository + + ```bash + git clone https://github.com/microsoft/botbuilder-samples.git + ``` + +- In a terminal, navigate to `samples/javascript_nodejs/06.using-cards` + + ```bash + cd samples/javascript_nodejs/06.using-cards + ``` + +- Install modules + + ```bash + npm install + ``` + +- Start the bot + + ```bash + npm start + ``` + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + +## Rich Cards + +Most channels support rich content. In this sample we explore the different types of rich cards your bot may use. A key to good bot design is to send interactive media, such as Rich Cards. There are several different types of Rich Cards, which are as follows: + +- Animation Card +- Audio Card +- Hero Card +- Receipt Card +- Sign In Card +- Thumbnail Card +- Video Card + +When [designing the user experience](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-design-user-experience?view=azure-bot-service-4.0#cards) developers should consider adding visual elements such as Rich Cards. + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Design the user experience](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-design-user-experience?view=azure-bot-service-4.0#cards) +- [Add media to messages](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-media-attachments?view=azure-bot-service-4.0) +- [Rich card types](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-add-rich-cards?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Restify](https://www.npmjs.com/package/restify) +- [dotenv](https://www.npmjs.com/package/dotenv) + +[41]:../../README.md \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/bots/dialogBot.js b/experimental/language-generation/javascript_nodejs/06.using-cards/bots/dialogBot.js new file mode 100644 index 0000000000..e4a03dd363 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/bots/dialogBot.js @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { ActivityHandler } = require('botbuilder'); + +/** + * This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots + * to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types + * each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity. + * The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation, + * and the requirement is that all BotState objects are saved at the end of a turn. + */ +class DialogBot extends ActivityHandler { + /** + * + * @param {ConversationState} conversationState + * @param {UserState} userState + * @param {Dialog} dialog + */ + constructor(conversationState, userState, dialog) { + super(); + if (!conversationState) throw new Error('[DialogBot]: Missing parameter. conversationState is required'); + if (!userState) throw new Error('[DialogBot]: Missing parameter. userState is required'); + if (!dialog) throw new Error('[DialogBot]: Missing parameter. dialog is required'); + + this.conversationState = conversationState; + this.userState = userState; + this.dialog = dialog; + this.dialogState = this.conversationState.createProperty('DialogState'); + + this.onMessage(async (context, next) => { + console.log('Running dialog with Message Activity.'); + + // Run the Dialog with the new message Activity. + await this.dialog.run(context, this.dialogState); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onDialog(async (context, next) => { + // Save any state changes. The load happened during the execution of the Dialog. + await this.conversationState.saveChanges(context, false); + await this.userState.saveChanges(context, false); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} + +module.exports.DialogBot = DialogBot; diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/bots/richCardsBot.js b/experimental/language-generation/javascript_nodejs/06.using-cards/bots/richCardsBot.js new file mode 100644 index 0000000000..6004657a05 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/bots/richCardsBot.js @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { MessageFactory } = require('botbuilder'); +const { DialogBot } = require('./dialogBot'); + +/** + * RichCardsBot prompts a user to select a Rich Card and then returns the card + * that matches the user's selection. + */ +class RichCardsBot extends DialogBot { + constructor(conversationState, userState, dialog) { + super(conversationState, userState, dialog); + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id !== context.activity.recipient.id) { + const reply = MessageFactory.text('Welcome to CardBot. ' + + 'This bot will show you different types of Rich Cards. ' + + 'Please type anything to get started.'); + await context.sendActivity(reply); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} + +module.exports.RichCardsBot = RichCardsBot; diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/new-rg-parameters.json b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/new-rg-parameters.json new file mode 100644 index 0000000000..ead3390932 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/new-rg-parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "value": "" + }, + "groupName": { + "value": "" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/preexisting-rg-parameters.json b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/preexisting-rg-parameters.json new file mode 100644 index 0000000000..b6f5114fcc --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/preexisting-rg-parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appServicePlanLocation": { + "value": "" + }, + "existingAppServicePlan": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-new-rg.json b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 0000000000..06b8284158 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-preexisting-rg.json b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 0000000000..43943b6581 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('servicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/dialogs/mainDialog.js b/experimental/language-generation/javascript_nodejs/06.using-cards/dialogs/mainDialog.js new file mode 100644 index 0000000000..dccbd832d9 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/dialogs/mainDialog.js @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { AttachmentLayoutTypes, CardFactory } = require('botbuilder'); +const { ChoicePrompt, ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog } = require('botbuilder-dialogs'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); + +const MAIN_WATERFALL_DIALOG = 'mainWaterfallDialog'; + +class MainDialog extends ComponentDialog { + constructor() { + super('MainDialog'); + + // Define the main dialog and its related components. + this.addDialog(new ChoicePrompt('cardPrompt')); + this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [ + this.choiceCardStep.bind(this), + this.showCardStep.bind(this) + ])); + + // The initial child Dialog to run. + this.initialDialogId = MAIN_WATERFALL_DIALOG; + + this.templateEngine = new TemplateEngine().addFiles([ + "./resources/MainDialog.LG", + "./resources/Cards.lg" + ]); + } + + /** + * The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system. + * If no dialog is active, it will start the default dialog. + * @param {*} turnContext + * @param {*} accessor + */ + async run(turnContext, accessor) { + const dialogSet = new DialogSet(accessor); + dialogSet.add(this); + + const dialogContext = await dialogSet.createContext(turnContext); + const results = await dialogContext.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog(this.id); + } + } + + /** + * 1. Prompts the user if the user is not in the middle of a dialog. + * 2. Re-prompts the user when an invalid input is received. + * + * @param {WaterfallStepContext} stepContext + */ + async choiceCardStep(stepContext) { + console.log('MainDialog.choiceCardStep'); + + // Create the PromptOptions which contain the prompt and re-prompt messages. + // PromptOptions also contains the list of choices available to the user. + const options = { + prompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('CardChoice')), + retryPrompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('CardChoice.Invalid.Prompt')), + choices: this.getChoices() + }; + + // Prompt the user with the configured PromptOptions. + return await stepContext.prompt('cardPrompt', options); + } + + /** + * Send a Rich Card response to the user based on their choice. + * This method is only called when a valid prompt response is parsed from the user's response to the ChoicePrompt. + * @param {WaterfallStepContext} stepContext + */ + async showCardStep(stepContext) { + console.log('MainDialog.showCardStep'); + + switch (stepContext.result.value) { + case 'Adaptive Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AdaptiveCard'))); + break; + case 'Animation Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AnimationCard'))); + break; + case 'Audio Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AudioCard'))); + break; + case 'Hero Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('HeroCard'))); + break; + case 'Receipt Card': + var data = { + receiptItems : [ + { + title: 'Data Transfer', + price: '$38.45', + quantity: 368, + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png' } + }, + { + title: 'App Service', + price: '$45.00', + quantity: 720, + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png' } + } + ] + }; + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate("ReceiptCard", data))); + break; + case 'Signin Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('SigninCard'))); + break; + case 'Thumbnail Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('ThumbnailCard'))); + break; + case 'Video Card': + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('VideoCard'))); + break; + default: + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('AllCards'))); + break; + } + + // Give the user instructions about what to do next + await stepContext.context.sendActivity('Type anything to see another card.'); + + return await stepContext.endDialog(); + } + + /** + * Create the choices with synonyms to render for the user during the ChoicePrompt. + * (Indexes and upper/lower-case variants do not need to be added as synonyms) + */ + getChoices() { + const cardOptions = [ + { + value: 'Adaptive Card', + synonyms: ['adaptive'] + }, + { + value: 'Animation Card', + synonyms: ['animation'] + }, + { + value: 'Audio Card', + synonyms: ['audio'] + }, + { + value: 'Hero Card', + synonyms: ['hero'] + }, + { + value: 'Receipt Card', + synonyms: ['receipt'] + }, + { + value: 'Signin Card', + synonyms: ['signin'] + }, + { + value: 'Thumbnail Card', + synonyms: ['thumbnail', 'thumb'] + }, + { + value: 'Video Card', + synonyms: ['video'] + }, + { + value: 'All Cards', + synonyms: ['all'] + } + ]; + + return cardOptions; + } +} + +module.exports.MainDialog = MainDialog; diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/index.js b/experimental/language-generation/javascript_nodejs/06.using-cards/index.js new file mode 100644 index 0000000000..870ec603e2 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/index.js @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// index.js is used to setup and configure your bot + +// Import required packages +const path = require('path'); +const restify = require('restify'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { BotFrameworkAdapter, MemoryStorage, ConversationState, UserState } = require('botbuilder'); + +// This bot's main dialog. +const { RichCardsBot } = require('./bots/richCardsBot'); +const { MainDialog } = require('./dialogs/mainDialog'); + +const ENV_FILE = path.join(__dirname, '.env'); +require('dotenv').config({ path: ENV_FILE }); + +// Create adapter. See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Create template engine for language generation. +const templateEngine = new TemplateEngine().addFile('./resources/AdapterWithErrorHandler.lg'); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. + console.error(templateEngine.evaluateTemplate('SomethingWentWrong', { + message : `${error}` + })); + + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + 'OnTurnError Trace', + `${ error }`, + 'https://www.botframework.com/schemas/error', + 'TurnError' + ); + + // Send a message to the user + await context.sendActivity('The bot encounted an error or bug.'); + await context.sendActivity('To continue to run this bot, please fix the bot source code.'); + // Clear out state + await conversationState.delete(context); +}; + +// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state store to persist the dialog and user state between messages. + +// For local development, in-memory storage is used. +// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot +// is restarted, anything stored in memory will be gone. +const memoryStorage = new MemoryStorage(); +const conversationState = new ConversationState(memoryStorage); +const userState = new UserState(memoryStorage); + +// Create the main dialog. +const dialog = new MainDialog(); +const bot = new RichCardsBot(conversationState, userState, dialog); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, function() { + console.log(`\n${ server.name } listening to ${ server.url }.`); + console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator'); + console.log('\nTo talk to your bot, open the emulator select "Open Bot"'); +}); + +// Listen for incoming activities and route them to your bot main dialog. +server.post('/api/messages', (req, res) => { + // Route received a request to adapter for processing + adapter.processActivity(req, res, async (turnContext) => { + // route to bot activity handler. + await bot.run(turnContext); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/package.json b/experimental/language-generation/javascript_nodejs/06.using-cards/package.json new file mode 100644 index 0000000000..de004b5b64 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/package.json @@ -0,0 +1,34 @@ +{ + "name": "using-cards", + "version": "1.0.0", + "description": "Bot Builder v4 using cards sample", + "author": "Microsoft", + "license": "MIT", + "main": "index.js", + "scripts": { + "start": "node ./index.js", + "watch": "nodemon ./index.js", + "lint": "eslint .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "botbuilder": "~4.7.0", + "botbuilder-dialogs": "~4.7.0", + "botbuilder-lg": "4.7.0-preview", + "dotenv": "^8.2.0", + "restify": "~8.4.0" + }, + "devDependencies": { + "eslint": "^6.6.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^10.0.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "nodemon": "~1.19.4" + } +} diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/resources/AdapterWithErrorHandler.lg b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/AdapterWithErrorHandler.lg new file mode 100644 index 0000000000..02e179d5b9 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/AdapterWithErrorHandler.lg @@ -0,0 +1,15 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# SomethingWentWrong +- @{ErrorPrefix()}, @{ErrorSuffix()}\nError:@{Message} + +# ErrorSuffix +- it looks like something went wrong. +- I seem to have run into a snag. We need to start over. +- something is not right. We need to start over. + +# ErrorPrefix +- Oops +- Sorry +- I apologize \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/resources/Cards.lg b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/Cards.lg new file mode 100644 index 0000000000..54268cab96 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/Cards.lg @@ -0,0 +1,315 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more +> All cards use the structured LG notation +> Multi-line text are enclosed in ``` +> Multi-line text can include inline expressions enclosed in @{expression}. + +# AllCards +[Activity + Attachments = @{HeroCard()} | @{ThumbnailCard()} | @{SigninCard()} | @{AnimationCard()} | @{VideoCard()} | @{AudioCard()} | @{json(adaptivecardjson()) + AttachmentLayout = @{AttachmentLayoutType()} +] + +# ReceiptCard +[ReceiptCard + Title = John Doe + Tax = $ 7.50 + Total = $ 90.95 + buttons = @{ReceiptButton()} + Facts = @{json(ReceiptFacts())} + items = @{receiptItems} +] + +# ReceiptButton +[CardAction + Title = More information + Image = https://account.windowsazure.com/content/6.10.1.38-.8225.160809-1618/aux-pre/images/offer-icon-freetrial.png + Value = https://azure.microsoft.com/en-us/pricing/ + type = openUrl +] + +# ReceiptFacts +- ``` +[ + { + "key": "Order Number", + "value": "1234" + }, + { + "key": "Payment Method", + "value": "VISA 5555-****" + } +] +``` + +# HeroCard +[HeroCard + title = BotFramework Hero Card + subtitle = Microsoft Bot Framework + text = Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services. + image = https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg + buttons = Show more cards +] + +# ThumbnailCard +[ThumbnailCard + title = BotFramework Thumbnail Card + subtitle = Microsoft Bot Framework + text = Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services. + image = https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg + buttons = Get Started +] + +# SigninCard +[SigninCard + text = BotFramework Sign-in Card + buttons = @{signinButton()} +] + +# signinButton +[CardAction + title = Sign-in + type = signin + value = http://login.microsoft.com +] + +# AnimationCard +[AnimationCard + title = Microsoft Bot Framework + subtitle = Animation Card + image = https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png + media = http://i.giphy.com/Ki55RUbOV5njy.gif +] + +# VideoCard +[VideoCard + title = Big Buck Bunny + subtitle = by the Blender Institute + text = Big Buck Bunny (code-named Peach) is a short computer-animated comedy film by the Blender Institute + image = https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Big_buck_bunny_poster_big.jpg/220px-Big_buck_bunny_poster_big.jpg + media = http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4 + buttons = Learn More +] + +# AudioCard +[AudioCard + title = I am your father + subtitle = Star Wars: Episode V - The Empire Strikes Back + text = The Empire Strikes Back (also known as Star Wars: Episode V – The Empire Strikes Back) + image = https://upload.wikimedia.org/wikipedia/en/3/3c/SW_-_Empire_Strikes_Back.jpg + media = http://www.wavlist.com/movies/004/father.wav + buttons = Read More +] + +# AdaptiveCard +[Activity + Attachments = @{json(adaptivecardjson())} +] + +# PassengerName +- Vishwac +- Tom +- Chris +- Yochay + +# AttachmentLayoutType +- carousel +- list + +# adaptivecardjson +- ``` +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "type": "AdaptiveCard", + "speak": "Your flight is confirmed for you and 3 other passengers from San Francisco to Amsterdam on Friday, October 10 8:30 AM", + "body": [ + { + "type": "TextBlock", + "text": "Passengers", + "weight": "bolder", + "isSubtle": false + }, + { + "type": "TextBlock", + "text": "@{PassengerName()}", + "separator": true + }, + { + "type": "TextBlock", + "text": "2 Stops", + "weight": "bolder", + "spacing": "medium" + }, + { + "type": "TextBlock", + "text": "Fri, October 10 8:30 AM", + "weight": "bolder", + "spacing": "none" + }, + { + "type": "ColumnSet", + "separator": true, + "columns": [ + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "text": "San Francisco", + "isSubtle": true + }, + { + "type": "TextBlock", + "size": "extraLarge", + "color": "accent", + "text": "SFO", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": " " + }, + { + "type": "Image", + "url": "http://adaptivecards.io/content/airplane.png", + "size": "small", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "Amsterdam", + "isSubtle": true + }, + { + "type": "TextBlock", + "horizontalAlignment": "right", + "size": "extraLarge", + "color": "accent", + "text": "AMS", + "spacing": "none" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "Non-Stop", + "weight": "bolder", + "spacing": "medium" + }, + { + "type": "TextBlock", + "text": "Fri, October 18 9:50 PM", + "weight": "bolder", + "spacing": "none" + }, + { + "type": "ColumnSet", + "separator": true, + "columns": [ + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "text": "Amsterdam", + "isSubtle": true + }, + { + "type": "TextBlock", + "size": "extraLarge", + "color": "accent", + "text": "AMS", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": " " + }, + { + "type": "Image", + "url": "http://adaptivecards.io/content/airplane.png", + "size": "small", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "San Francisco", + "isSubtle": true + }, + { + "type": "TextBlock", + "horizontalAlignment": "right", + "size": "extraLarge", + "color": "accent", + "text": "SFO", + "spacing": "none" + } + ] + } + ] + }, + { + "type": "ColumnSet", + "spacing": "medium", + "columns": [ + { + "type": "Column", + "width": "1", + "items": [ + { + "type": "TextBlock", + "text": "Total", + "size": "medium", + "isSubtle": true + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "$4,032.54", + "size": "medium", + "weight": "bolder" + } + ] + } + ] + } + ] +} +``` \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/06.using-cards/resources/MainDialog.lg b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/MainDialog.lg new file mode 100644 index 0000000000..8a03c12dee --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/06.using-cards/resources/MainDialog.lg @@ -0,0 +1,12 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# CardChoice +- Select or type the card name you would like to see. +- What card would you like to see? You can click or type the card name + +# CardChoice.Invalid.Prompt +- That was not a valid choice, please select a card or number from 1 to 9. + +# CardStartOverResponse +- Type anything to see another card. \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/.eslintrc.js b/experimental/language-generation/javascript_nodejs/13.core-bot/.eslintrc.js new file mode 100644 index 0000000000..fcc7331e71 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + "extends": "standard", + "rules": { + "semi": [2, "always"], + "indent": [2, 4], + "no-return-await": 0, + "space-before-function-paren": [2, { + "named": "never", + "anonymous": "never", + "asyncArrow": "always" + }], + "template-curly-spacing": [2, "always"] + }, + "env": { + "commonjs": true, + "node": true, + "mocha": true + } +}; \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/.gitignore b/experimental/language-generation/javascript_nodejs/13.core-bot/.gitignore new file mode 100644 index 0000000000..faf84ac400 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.nyc_output/ +.vscode/ +.env \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/README.md b/experimental/language-generation/javascript_nodejs/13.core-bot/README.md new file mode 100644 index 0000000000..469eb1f84a --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/README.md @@ -0,0 +1,103 @@ +# core-bot + +Bot Framework v4 core bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: + +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities +- Implement a multi-turn conversation using Dialogs +- Handle user interruptions for such things as `Help` or `Cancel` +- Prompt for and validate requests for information from the user + +In this sample, we will demonstrate use of [Language Generation][41] to generate all responses from the bot. + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Overview + +This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. + +- [Node.js](https://nodejs.org) version 10.14 or higher + + ```bash + # determine node version + node --version + ``` + +### Create a LUIS Application to enable language understanding + +The LUIS model for this example can be found under `cognitiveModels/FlightBooking.json` and the LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=javascript). + +Once you created the LUIS model, update `.env` with your `LuisAppId`, `LuisAPIKey` and `LuisAPIHostName`. + +```text +LuisAppId = "Your LUIS App Id" +LuisAPIKey = "Your LUIS Subscription key here" +LuisAPIHostName = "Your LUIS App region here (i.e: westus.api.cognitive.microsoft.com)" +``` + +## To try this sample + +- Clone the repository + + ```bash + git clone https://github.com/microsoft/botbuilder-samples.git + ``` + +- In a terminal, navigate to `samples/javascript_nodejs/13.core-bot` + + ```bash + cd samples/javascript_nodejs/13.core-bot + ``` + +- Install modules + + ```bash + npm install + ``` + +- Setup LUIS + + The prerequisites outlined above contain the steps necessary to provision a language understanding model on www.luis.ai. Refer to _Create a LUIS Application to enable language understanding_ above for directions to setup and configure LUIS. + +- Run the sample + + ```bash + npm start + ``` + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Restify](https://www.npmjs.com/package/restify) +- [dotenv](https://www.npmjs.com/package/dotenv) + +[41]:../../README.md \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogAndWelcomeBot.js b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogAndWelcomeBot.js new file mode 100644 index 0000000000..bd46594d74 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogAndWelcomeBot.js @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { CardFactory } = require('botbuilder'); +const { DialogBot } = require('./dialogBot'); +const WelcomeCard = require('./resources/welcomeCard.json'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); +class DialogAndWelcomeBot extends DialogBot { + constructor(conversationState, userState, dialog) { + super(conversationState, userState, dialog); + + const templateEngine = new TemplateEngine().addFile('./resources/welcomeCard.lg'); + + // Actions to include in the welcome card. These are passed to LG and are then included in the generated Welcome card. + const actions = { + actions : [ + { + title : "Get an overview", + url : "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + title : "Ask a question", + url : "https://stackoverflow.com/questions/tagged/botframework" + }, + { + title : "Learn how to deploy", + url : "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] + }; + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id !== context.activity.recipient.id) { + const welcomeCard = ActivityFactory.createActivity(templateEngine.evaluateTemplate('WelcomeCard', actions)) + await context.sendActivity(welcomeCard); + await dialog.run(context, conversationState.createProperty('DialogState')); + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} + +module.exports.DialogAndWelcomeBot = DialogAndWelcomeBot; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogBot.js b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogBot.js new file mode 100644 index 0000000000..3203619948 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/dialogBot.js @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { ActivityHandler } = require('botbuilder'); + +class DialogBot extends ActivityHandler { + /** + * + * @param {ConversationState} conversationState + * @param {UserState} userState + * @param {Dialog} dialog + */ + constructor(conversationState, userState, dialog) { + super(); + if (!conversationState) throw new Error('[DialogBot]: Missing parameter. conversationState is required'); + if (!userState) throw new Error('[DialogBot]: Missing parameter. userState is required'); + if (!dialog) throw new Error('[DialogBot]: Missing parameter. dialog is required'); + + this.conversationState = conversationState; + this.userState = userState; + this.dialog = dialog; + this.dialogState = this.conversationState.createProperty('DialogState'); + + this.onMessage(async (context, next) => { + console.log('Running dialog with Message Activity.'); + + // Run the Dialog with the new message Activity. + await this.dialog.run(context, this.dialogState); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + this.onDialog(async (context, next) => { + // Save any state changes. The load happened during the execution of the Dialog. + await this.conversationState.saveChanges(context, false); + await this.userState.saveChanges(context, false); + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} + +module.exports.DialogBot = DialogBot; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/bots/resources/welcomeCard.json b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/resources/welcomeCard.json new file mode 100644 index 0000000000..100aa52876 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/bots/resources/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "Welcome to Bot Framework!", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": "true", + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/cognitiveModels/FlightBooking.json b/experimental/language-generation/javascript_nodejs/13.core-bot/cognitiveModels/FlightBooking.json new file mode 100644 index 0000000000..f0e4b97709 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/cognitiveModels/FlightBooking.json @@ -0,0 +1,339 @@ +{ + "luis_schema_version": "3.2.0", + "versionId": "0.1", + "name": "FlightBooking", + "desc": "Luis Model for CoreBot", + "culture": "en-us", + "tokenizerVersion": "1.0.0", + "intents": [ + { + "name": "BookFlight" + }, + { + "name": "Cancel" + }, + { + "name": "GetWeather" + }, + { + "name": "None" + } + ], + "entities": [], + "composites": [ + { + "name": "From", + "children": [ + "Airport" + ], + "roles": [] + }, + { + "name": "To", + "children": [ + "Airport" + ], + "roles": [] + } + ], + "closedLists": [ + { + "name": "Airport", + "subLists": [ + { + "canonicalForm": "Paris", + "list": [ + "paris", + "cdg" + ] + }, + { + "canonicalForm": "London", + "list": [ + "london", + "lhr" + ] + }, + { + "canonicalForm": "Berlin", + "list": [ + "berlin", + "txl" + ] + }, + { + "canonicalForm": "New York", + "list": [ + "new york", + "jfk" + ] + }, + { + "canonicalForm": "Seattle", + "list": [ + "seattle", + "sea" + ] + } + ], + "roles": [] + } + ], + "patternAnyEntities": [], + "regex_entities": [], + "prebuiltEntities": [ + { + "name": "datetimeV2", + "roles": [] + } + ], + "model_features": [], + "regex_features": [], + "patterns": [], + "utterances": [ + { + "text": "book a flight", + "intent": "BookFlight", + "entities": [] + }, + { + "text": "book a flight from new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 26 + } + ] + }, + { + "text": "book a flight from seattle", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 25 + } + ] + }, + { + "text": "book a hotel in new york", + "intent": "None", + "entities": [] + }, + { + "text": "book a restaurant", + "intent": "None", + "entities": [] + }, + { + "text": "book flight from london to paris on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 17, + "endPos": 22 + }, + { + "entity": "To", + "startPos": 27, + "endPos": 31 + } + ] + }, + { + "text": "book flight to berlin on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 15, + "endPos": 20 + } + ] + }, + { + "text": "book me a flight from london to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 22, + "endPos": 27 + }, + { + "entity": "To", + "startPos": 32, + "endPos": 36 + } + ] + }, + { + "text": "bye", + "intent": "Cancel", + "entities": [] + }, + { + "text": "cancel booking", + "intent": "Cancel", + "entities": [] + }, + { + "text": "exit", + "intent": "Cancel", + "entities": [] + }, + { + "text": "find an airport near me", + "intent": "None", + "entities": [] + }, + { + "text": "flight to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "flight to paris from london on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + }, + { + "entity": "From", + "startPos": 21, + "endPos": 26 + } + ] + }, + { + "text": "fly from berlin to paris on may 5th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 9, + "endPos": 14 + }, + { + "entity": "To", + "startPos": 19, + "endPos": 23 + } + ] + }, + { + "text": "go to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 6, + "endPos": 10 + } + ] + }, + { + "text": "going from paris to berlin", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 11, + "endPos": 15 + }, + { + "entity": "To", + "startPos": 20, + "endPos": 25 + } + ] + }, + { + "text": "i'd like to rent a car", + "intent": "None", + "entities": [] + }, + { + "text": "ignore", + "intent": "Cancel", + "entities": [] + }, + { + "text": "travel from new york to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 12, + "endPos": 19 + }, + { + "entity": "To", + "startPos": 24, + "endPos": 28 + } + ] + }, + { + "text": "travel to new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 17 + } + ] + }, + { + "text": "travel to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "what's the forecast for this friday?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like for tomorrow", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like in new york", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "winter is coming", + "intent": "None", + "entities": [] + } + ], + "settings": [] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/new-rg-parameters.json b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/new-rg-parameters.json new file mode 100644 index 0000000000..ead3390932 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/new-rg-parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "value": "" + }, + "groupName": { + "value": "" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/preexisting-rg-parameters.json b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/preexisting-rg-parameters.json new file mode 100644 index 0000000000..b6f5114fcc --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/preexisting-rg-parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appServicePlanLocation": { + "value": "" + }, + "existingAppServicePlan": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-new-rg.json b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 0000000000..06b8284158 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 0000000000..43943b6581 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('servicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/bookingDialog.js b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/bookingDialog.js new file mode 100644 index 0000000000..b47d75948e --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/bookingDialog.js @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { TimexProperty } = require('@microsoft/recognizers-text-data-types-timex-expression'); +const { InputHints, MessageFactory } = require('botbuilder'); +const { ConfirmPrompt, TextPrompt, WaterfallDialog } = require('botbuilder-dialogs'); +const { CancelAndHelpDialog } = require('./cancelAndHelpDialog'); +const { DateResolverDialog } = require('./dateResolverDialog'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); +const path = require('path'); +const CONFIRM_PROMPT = 'confirmPrompt'; +const DATE_RESOLVER_DIALOG = 'dateResolverDialog'; +const TEXT_PROMPT = 'textPrompt'; +const WATERFALL_DIALOG = 'waterfallDialog'; + +class BookingDialog extends CancelAndHelpDialog { + constructor(id) { + super(id || 'bookingDialog'); + + this.addDialog(new TextPrompt(TEXT_PROMPT)) + .addDialog(new ConfirmPrompt(CONFIRM_PROMPT)) + .addDialog(new DateResolverDialog(DATE_RESOLVER_DIALOG)) + .addDialog(new WaterfallDialog(WATERFALL_DIALOG, [ + this.destinationStep.bind(this), + this.originStep.bind(this), + this.travelDateStep.bind(this), + this.confirmStep.bind(this), + this.finalStep.bind(this) + ])); + + this.initialDialogId = WATERFALL_DIALOG; + + this.templateEngine = new TemplateEngine().addFile(path.join(__dirname, '../resources/BookingDialog.lg')); + } + + /** + * If a destination city has not been provided, prompt for one. + */ + async destinationStep(stepContext) { + const bookingDetails = stepContext.options; + + if (!bookingDetails.destination) { + const msg = ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('PromptForMissingInformation', bookingDetails)); + return await stepContext.prompt(TEXT_PROMPT, { prompt: msg }); + } + return await stepContext.next(bookingDetails.destination); + } + + /** + * If an origin city has not been provided, prompt for one. + */ + async originStep(stepContext) { + const bookingDetails = stepContext.options; + + // Capture the response to the previous step's prompt + bookingDetails.destination = stepContext.result; + if (!bookingDetails.origin) { + const msg = ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('PromptForMissingInformation', bookingDetails)); + return await stepContext.prompt(TEXT_PROMPT, { prompt: msg }); + } + return await stepContext.next(bookingDetails.origin); + } + + /** + * If a travel date has not been provided, prompt for one. + * This will use the DATE_RESOLVER_DIALOG. + */ + async travelDateStep(stepContext) { + const bookingDetails = stepContext.options; + + // Capture the results of the previous step + bookingDetails.origin = stepContext.result; + if (!bookingDetails.travelDate || this.isAmbiguous(bookingDetails.travelDate)) { + return await stepContext.beginDialog(DATE_RESOLVER_DIALOG, { date: bookingDetails.travelDate }); + } + return await stepContext.next(bookingDetails.travelDate); + } + + /** + * Confirm the information the user has provided. + */ + async confirmStep(stepContext) { + const bookingDetails = stepContext.options; + + // Capture the results of the previous step + bookingDetails.travelDate = stepContext.result; + const msg = ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('ConfirmBooking', bookingDetails)); + + // Offer a YES/NO prompt. + return await stepContext.prompt(CONFIRM_PROMPT, { prompt: msg }); + } + + /** + * Complete the interaction and end the dialog. + */ + async finalStep(stepContext) { + if (stepContext.result === true) { + const bookingDetails = stepContext.options; + return await stepContext.endDialog(bookingDetails); + } + return await stepContext.endDialog(); + } + + isAmbiguous(timex) { + const timexPropery = new TimexProperty(timex); + return !timexPropery.types.has('definite'); + } +} + +module.exports.BookingDialog = BookingDialog; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/cancelAndHelpDialog.js b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/cancelAndHelpDialog.js new file mode 100644 index 0000000000..74528df691 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/cancelAndHelpDialog.js @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { InputHints } = require('botbuilder'); +const { ComponentDialog, DialogTurnStatus } = require('botbuilder-dialogs'); + +/** + * This base class watches for common phrases like "help" and "cancel" and takes action on them + * BEFORE they reach the normal bot logic. + */ +class CancelAndHelpDialog extends ComponentDialog { + async onContinueDialog(innerDc) { + const result = await this.interrupt(innerDc); + if (result) { + return result; + } + return await super.onContinueDialog(innerDc); + } + + async interrupt(innerDc) { + if (innerDc.context.activity.text) { + const text = innerDc.context.activity.text.toLowerCase(); + + switch (text) { + case 'help': + case '?': { + const helpMessageText = 'Show help here'; + await innerDc.context.sendActivity(helpMessageText, helpMessageText, InputHints.ExpectingInput); + return { status: DialogTurnStatus.waiting }; + } + case 'cancel': + case 'quit': { + const cancelMessageText = 'Cancelling...'; + await innerDc.context.sendActivity(cancelMessageText, cancelMessageText, InputHints.IgnoringInput); + return await innerDc.cancelAllDialogs(); + } + } + } + } +} + +module.exports.CancelAndHelpDialog = CancelAndHelpDialog; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/dateResolverDialog.js b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/dateResolverDialog.js new file mode 100644 index 0000000000..1fba76113e --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/dateResolverDialog.js @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { InputHints, MessageFactory } = require('botbuilder'); +const { DateTimePrompt, WaterfallDialog } = require('botbuilder-dialogs'); +const { CancelAndHelpDialog } = require('./cancelAndHelpDialog'); +const { TimexProperty } = require('@microsoft/recognizers-text-data-types-timex-expression'); + +const DATETIME_PROMPT = 'datetimePrompt'; +const WATERFALL_DIALOG = 'waterfallDialog'; + +class DateResolverDialog extends CancelAndHelpDialog { + constructor(id) { + super(id || 'dateResolverDialog'); + this.addDialog(new DateTimePrompt(DATETIME_PROMPT, this.dateTimePromptValidator.bind(this))) + .addDialog(new WaterfallDialog(WATERFALL_DIALOG, [ + this.initialStep.bind(this), + this.finalStep.bind(this) + ])); + + this.initialDialogId = WATERFALL_DIALOG; + } + + async initialStep(stepContext) { + const timex = stepContext.options.date; + + const promptMessageText = 'On what date would you like to travel?'; + const promptMessage = MessageFactory.text(promptMessageText, promptMessageText, InputHints.ExpectingInput); + + const repromptMessageText = "I'm sorry, for best results, please enter your travel date including the month, day and year."; + const repromptMessage = MessageFactory.text(repromptMessageText, repromptMessageText, InputHints.ExpectingInput); + + if (!timex) { + // We were not given any date at all so prompt the user. + return await stepContext.prompt(DATETIME_PROMPT, + { + prompt: promptMessage, + retryPrompt: repromptMessage + }); + } + // We have a Date we just need to check it is unambiguous. + const timexProperty = new TimexProperty(timex); + if (!timexProperty.types.has('definite')) { + // This is essentially a "reprompt" of the data we were given up front. + return await stepContext.prompt(DATETIME_PROMPT, { prompt: repromptMessage }); + } + return await stepContext.next([{ timex: timex }]); + } + + async finalStep(stepContext) { + const timex = stepContext.result[0].timex; + return await stepContext.endDialog(timex); + } + + async dateTimePromptValidator(promptContext) { + if (promptContext.recognized.succeeded) { + // This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + // TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. + const timex = promptContext.recognized.value[0].timex.split('T')[0]; + + // If this is a definite Date including year, month and day we are good otherwise reprompt. + // A better solution might be to let the user know what part is actually missing. + return new TimexProperty(timex).types.has('definite'); + } + return false; + } +} + +module.exports.DateResolverDialog = DateResolverDialog; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/flightBookingRecognizer.js b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/flightBookingRecognizer.js new file mode 100644 index 0000000000..dc18fcf27f --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/flightBookingRecognizer.js @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { LuisRecognizer } = require('botbuilder-ai'); + +class FlightBookingRecognizer { + constructor(config) { + const luisIsConfigured = config && config.applicationId && config.endpointKey && config.endpoint; + if (luisIsConfigured) { + this.recognizer = new LuisRecognizer(config, {}, true); + } + } + + get isConfigured() { + return (this.recognizer !== undefined); + } + + /** + * Returns an object with preformatted LUIS results for the bot's dialogs to consume. + * @param {TurnContext} context + */ + async executeLuisQuery(context) { + return await this.recognizer.recognize(context); + } + + getFromEntities(result) { + let fromValue, fromAirportValue; + if (result.entities.$instance.From) { + fromValue = result.entities.$instance.From[0].text; + } + if (fromValue && result.entities.From[0].Airport) { + fromAirportValue = result.entities.From[0].Airport[0][0]; + } + + return { from: fromValue, airport: fromAirportValue }; + } + + getToEntities(result) { + let toValue, toAirportValue; + if (result.entities.$instance.To) { + toValue = result.entities.$instance.To[0].text; + } + if (toValue && result.entities.To[0].Airport) { + toAirportValue = result.entities.To[0].Airport[0][0]; + } + + return { to: toValue, airport: toAirportValue }; + } + + /** + * This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + * TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. + */ + getTravelDate(result) { + const datetimeEntity = result.entities.datetime; + if (!datetimeEntity || !datetimeEntity[0]) return undefined; + + const timex = datetimeEntity[0].timex; + if (!timex || !timex[0]) return undefined; + + const datetime = timex[0].split('T')[0]; + return datetime; + } +} + +module.exports.FlightBookingRecognizer = FlightBookingRecognizer; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/mainDialog.js b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/mainDialog.js new file mode 100644 index 0000000000..3830d0fa2e --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/dialogs/mainDialog.js @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { TimexProperty } = require('@microsoft/recognizers-text-data-types-timex-expression'); +const { MessageFactory, InputHints } = require('botbuilder'); +const { LuisRecognizer } = require('botbuilder-ai'); +const { ComponentDialog, DialogSet, DialogTurnStatus, TextPrompt, WaterfallDialog } = require('botbuilder-dialogs'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); +const path = require('path'); +const MAIN_WATERFALL_DIALOG = 'mainWaterfallDialog'; + +class MainDialog extends ComponentDialog { + constructor(luisRecognizer, bookingDialog) { + super('MainDialog'); + + if (!luisRecognizer) throw new Error('[MainDialog]: Missing parameter \'luisRecognizer\' is required'); + this.luisRecognizer = luisRecognizer; + + if (!bookingDialog) throw new Error('[MainDialog]: Missing parameter \'bookingDialog\' is required'); + + // Define the main dialog and its related components. + // This is a sample "book a flight" dialog. + this.addDialog(new TextPrompt('TextPrompt')) + .addDialog(bookingDialog) + .addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [ + this.introStep.bind(this), + this.actStep.bind(this), + this.finalStep.bind(this) + ])); + + this.initialDialogId = MAIN_WATERFALL_DIALOG; + + this.templateEngine = new TemplateEngine().addFile(path.join(__dirname, '../resources/MainDialog.lg')); + } + + /** + * The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system. + * If no dialog is active, it will start the default dialog. + * @param {*} turnContext + * @param {*} accessor + */ + async run(turnContext, accessor) { + const dialogSet = new DialogSet(accessor); + dialogSet.add(this); + + const dialogContext = await dialogSet.createContext(turnContext); + const results = await dialogContext.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog(this.id); + } + } + + /** + * First step in the waterfall dialog. Prompts the user for a command. + * Currently, this expects a booking request, like "book me a flight from Paris to Berlin on march 22" + * Note that the sample LUIS model will only recognize Paris, Berlin, New York and London as airport cities. + */ + async introStep(stepContext) { + if (!this.luisRecognizer.isConfigured) { + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('Sample.Missing.Configuration'))); + return await stepContext.next(); + } + + return await stepContext.prompt('TextPrompt', { prompt: ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('IntroPrompt', stepContext.options)) }); + } + + /** + * Second step in the waterfall. This will use LUIS to attempt to extract the origin, destination and travel dates. + * Then, it hands off to the bookingDialog child dialog to collect any remaining details. + */ + async actStep(stepContext) { + const bookingDetails = {}; + + if (!this.luisRecognizer.isConfigured) { + // LUIS is not configured, we just run the BookingDialog path. + return await stepContext.beginDialog('bookingDialog', bookingDetails); + } + + // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt) + const luisResult = await this.luisRecognizer.executeLuisQuery(stepContext.context); + switch (LuisRecognizer.topIntent(luisResult)) { + case 'BookFlight': { + // Extract the values for the composite entities from the LUIS result. + const fromEntities = this.luisRecognizer.getFromEntities(luisResult); + const toEntities = this.luisRecognizer.getToEntities(luisResult); + + // Show a warning for Origin and Destination if we can't resolve them. + await this.showWarningForUnsupportedCities(stepContext.context, fromEntities, toEntities); + + // Initialize BookingDetails with any entities we may have found in the response. + bookingDetails.destination = toEntities.airport; + bookingDetails.origin = fromEntities.airport; + bookingDetails.travelDate = this.luisRecognizer.getTravelDate(luisResult); + console.log('LUIS extracted these booking details:', JSON.stringify(bookingDetails)); + + // Run the BookingDialog passing in whatever details we have from the LUIS call, it will fill out the remainder. + return await stepContext.beginDialog('bookingDialog', bookingDetails); + } + + case 'GetWeather': { + // We haven't implemented the GetWeatherDialog so we just display a TODO message. + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('GetWeather'))); + break; + } + + default: { + // Catch all for unhandled intents + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('Unknown.intent', { + intent : `${ LuisRecognizer.topIntent(luisResult) }` + }))); + } + } + + return await stepContext.next(); + } + + /** + * Shows a warning if the requested From or To cities are recognized as entities but they are not in the Airport entity list. + * In some cases LUIS will recognize the From and To composite entities as a valid cities but the From and To Airport values + * will be empty if those entity values can't be mapped to a canonical item in the Airport. + */ + async showWarningForUnsupportedCities(context, fromEntities, toEntities) { + const unsupportedCities = []; + if (fromEntities.from && !fromEntities.airport) { + unsupportedCities.push(fromEntities.from); + } + + if (toEntities.to && !toEntities.airport) { + unsupportedCities.push(toEntities.to); + } + + if (unsupportedCities.length) { + await context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('UnsupportedCities', { + cities : unsupportedCities + }))); + } + } + + /** + * This is the final step in the main waterfall dialog. + * It wraps up the sample "book a flight" interaction with a simple confirmation. + */ + async finalStep(stepContext) { + // If the child dialog ("bookingDialog") was cancelled or the user failed to confirm, the Result here will be null. + if (stepContext.result) { + const result = stepContext.result; + // Now we have all the booking details. + + // This is where calls to the booking AOU service or database would go. + + // If the call to the booking service was successful tell the user. + const timeProperty = new TimexProperty(result.travelDate); + const travelDateMsg = timeProperty.toNaturalLanguage(new Date(Date.now())); + const msg = `I have you booked to ${ result.destination } from ${ result.origin } on ${ travelDateMsg }.`; + await stepContext.context.sendActivity(ActivityFactory.createActivity(this.templateEngine.evaluateTemplate('BookingConfirmation', { + Destination : result.destination, + Origin : result.origin, + DateMessage : travelDateMsg + }))); + } + + // Restart the main dialog with a different message the second time around + return await stepContext.replaceDialog(this.initialDialogId, { restartMsg: 'What else can I do for you?' }); + } +} + +module.exports.MainDialog = MainDialog; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/index.js b/experimental/language-generation/javascript_nodejs/13.core-bot/index.js new file mode 100644 index 0000000000..c6fb589587 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/index.js @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// index.js is used to setup and configure your bot + +// Import required packages +const path = require('path'); +const restify = require('restify'); +const { + ActivityFactory, + TemplateEngine +} = require('botbuilder-lg'); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { BotFrameworkAdapter, ConversationState, InputHints, MemoryStorage, UserState } = require('botbuilder'); + +const { FlightBookingRecognizer } = require('./dialogs/flightBookingRecognizer'); + +// This bot's main dialog. +const { DialogAndWelcomeBot } = require('./bots/dialogAndWelcomeBot'); +const { MainDialog } = require('./dialogs/mainDialog'); + +// the bot's booking dialog +const { BookingDialog } = require('./dialogs/bookingDialog'); +const BOOKING_DIALOG = 'bookingDialog'; + +// Note: Ensure you have a .env file and include LuisAppId, LuisAPIKey and LuisAPIHostName. +const ENV_FILE = path.join(__dirname, '.env'); +require('dotenv').config({ path: ENV_FILE }); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new BotFrameworkAdapter({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword +}); + +// Create template engine for language generation. +console.log(path.join(__dirname, './resources/AdapterWithErrorHandler.lg')) +const templateEngine = new TemplateEngine().addFile(path.join(__dirname, './resources/AdapterWithErrorHandler.lg')); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. + console.error(templateEngine.evaluateTemplate('SomethingWentWrong', { + message : `${error}` + })); + + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + 'OnTurnError Trace', + `${ error }`, + 'https://www.botframework.com/schemas/error', + 'TurnError' + ); + + // Send a message to the user + let onTurnErrorMessage = 'The bot encounted an error or bug.'; + await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.ExpectingInput); + onTurnErrorMessage = 'To continue to run this bot, please fix the bot source code.'; + await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.ExpectingInput); + // Clear out state + await conversationState.delete(context); +}; + +// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state store to persist the dialog and user state between messages. + +// For local development, in-memory storage is used. +// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot +// is restarted, anything stored in memory will be gone. +const memoryStorage = new MemoryStorage(); +const conversationState = new ConversationState(memoryStorage); +const userState = new UserState(memoryStorage); + +// If configured, pass in the FlightBookingRecognizer. (Defining it externally allows it to be mocked for tests) +const { LuisAppId, LuisAPIKey, LuisAPIHostName } = process.env; +const luisConfig = { applicationId: LuisAppId, endpointKey: LuisAPIKey, endpoint: `https://${ LuisAPIHostName }` }; + +const luisRecognizer = new FlightBookingRecognizer(luisConfig); + +// Create the main dialog. +const bookingDialog = new BookingDialog(BOOKING_DIALOG); +const dialog = new MainDialog(luisRecognizer, bookingDialog); +const bot = new DialogAndWelcomeBot(conversationState, userState, dialog); + +// Create HTTP server +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, function() { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator'); + console.log('\nTo talk to your bot, open the emulator select "Open Bot"'); +}); + +// Listen for incoming activities and route them to your bot main dialog. +server.post('/api/messages', (req, res) => { + // Route received a request to adapter for processing + adapter.processActivity(req, res, async (turnContext) => { + // route to bot activity handler. + await bot.run(turnContext); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/package.json b/experimental/language-generation/javascript_nodejs/13.core-bot/package.json new file mode 100644 index 0000000000..cc36ca72a2 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/package.json @@ -0,0 +1,39 @@ +{ + "name": "core-bot", + "version": "1.0.0", + "description": "A bot that demonstrates core AI capabilities", + "author": "Microsoft", + "license": "MIT", + "main": "index.js", + "scripts": { + "start": "node ./index.js", + "watch": "nodemon ./index.js", + "lint": "eslint .", + "test": "nyc mocha tests/**/*.test.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "@microsoft/recognizers-text-data-types-timex-expression": "^1.1.4", + "botbuilder": "~4.7.0", + "botbuilder-ai": "~4.7.0", + "botbuilder-dialogs": "~4.7.0", + "botbuilder-testing": "~4.7.0", + "botbuilder-lg": "4.7.0-preview", + "dotenv": "^8.2.0", + "restify": "~8.4.0" + }, + "devDependencies": { + "eslint": "^6.6.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^10.0.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "mocha": "^6.2.2", + "nodemon": "~1.19.4", + "nyc": "^14.1.1" + } +} diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/resources/AdapterWithErrorHandler.lg b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/AdapterWithErrorHandler.lg new file mode 100644 index 0000000000..02e179d5b9 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/AdapterWithErrorHandler.lg @@ -0,0 +1,15 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# SomethingWentWrong +- @{ErrorPrefix()}, @{ErrorSuffix()}\nError:@{Message} + +# ErrorSuffix +- it looks like something went wrong. +- I seem to have run into a snag. We need to start over. +- something is not right. We need to start over. + +# ErrorPrefix +- Oops +- Sorry +- I apologize \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/resources/BookingDialog.lg b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/BookingDialog.lg new file mode 100644 index 0000000000..ebc38f3d66 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/BookingDialog.lg @@ -0,0 +1,47 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# PromptForDestinationCity +- To what city would you like to travel? +- Where would you like to travel to? +- What is your destination city? + +# PromptForDepartureCity +- Where are you traveling from? +- What is your departure city? + +# ConfirmPrefix +- Please confirm, +- Can you please confirm this is right? +- Does this sound righ to you? + +# ConfirmMessage +- I have you traveling to: @{Destination} from: @{Origin} on: @{TravelDate} +- on @{TravelDate}, travelling from @{Origin} to @{Destination} + +# ConfirmBooking +- @{ConfirmPrefix()} @{ConfirmMessage()} + +# PromptForTravelDate +- When would you like to travel? +- What is your departure date? +- Can you please give me your intended date of departure? + +> This template uses inline expressions. Expressions are defined using the common expression language. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language to learn more. +# PromptForMissingInformation +- IF: @{Destination == null} + - @{PromptForDestinationCity()} +- ELSEIF: @{Origin == null} + - @{PromptForDepartureCity()} +- ELSEIF: @{TravelDate == null} + - @{PromptForTravelDate()} +- ELSE: + - @{ConfirmBooking()} + + # ApologyPrefix + - I'm sorry, + - Unfortunately that does not work. + + # InvalidDateReprompt + - @{ApologyPrefix()} to make your booking please enter a full travel date including Day Month and Year. diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/resources/MainDialog.lg b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/MainDialog.lg new file mode 100644 index 0000000000..96c3f46f1f --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/MainDialog.lg @@ -0,0 +1,20 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +# BookingConfirmation +- I have you booked to @{Destination} from @{Origin} on @{DateMessage}. + +# IntroPrompt +- @{if(restartMsg, restartMsg, 'What can I help you with today?\nSay something like "Book a flight from Paris to Berlin on March 22, 2020"')} + +# Sample.Missing.Configuration +- NOTE: LUIS is not configured. To enable all capabilities, add `LuisAppId`, `LuisAPIKey` and `LuisAPIHostName` to the .env file. + +# GetWeather +- TODO: get weather flow here + +# Unknown.intent +- Sorry, I didn't get that. Please try asking in a different way (intent was @{intent}) + +# UnsupportedCities +- Sorry but the following airports are not supported: @{join(cities, ', ')} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/resources/welcomeCard.lg b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/welcomeCard.lg new file mode 100644 index 0000000000..431ff3bfe8 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/resources/welcomeCard.lg @@ -0,0 +1,59 @@ +> Language Generation definition file. +> See https://github.com/Microsoft/BotBuilder-Samples/tree/master/experimental/language-generation to learn more + +> This shows an example of inline definition of a card. +> See the cardsBot sample to see more ways to define and manage cards. + +# HeaderText +- Welcome to Bot Framework! +- Welcome to core bot with Language Generation! + +# WelcomeCard +[Activity + Attachments = @{json(AdaptiveCard())} +] + +> This template is called by AdaptiveCard template to put together the list of buttons in the adaptive card based on the object passed in via the EvaluateTemplate call. +# cardActionTemplate(title, url, type) +- ```{ + "type" : "@{if(type == null, 'Action.OpenUrl', type)}", + "title" : "@{title}", + "url" : "@{url}" +}``` + +# AdaptiveCard +- ``` +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "@{HeaderText()}", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": "true", + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + @{join(foreach(actions, item, cardActionTemplate(item.title, item.url, item.type)), ',')} + ] +} +``` + diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/README.md b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/README.md new file mode 100644 index 0000000000..0455e1eade --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/README.md @@ -0,0 +1,58 @@ + +# core-bot tests + +Bot Framework v4 core bot sample tests. + +This project uses the [botbuilder-testing](https://www.npmjs.com/package/botbuilder-testing) package and [mocha](https://github.com/mochajs/mocha) to create unit tests for the bot. + +This project shows how to: + +- Create unit tests for dialogs and bots +- Create different types of data driven tests using mocha tests +- Create mock objects for the different dependencies of a dialog (i.e. LUIS recognizers, other dialogs, configuration, etc.) +- Assert the activities returned by a dialog turn against expected values +- Assert the results returned by a dialog + +## Overview + +In this sample, dialogs are unit tested through the `DialogTestClient` class which provides a mechanism for testing them in isolation outside of a bot and without having to deploy your code to a web service. + +This class is used to write unit tests for dialogs that test their responses on a turn-by-turn basis. Any dialog built using the botbuilder dialogs library should work. + +Here is a simple example on how a test that uses `DialogTestClient` looks like: + +```javascript +const sut = new BookingDialog(); +const testClient = new DialogTestClient('msteams', sut); + +let reply = await testClient.sendActivity('hi'); +assert.strictEqual(reply.text, 'Where would you like to travel to?'); + +reply = await testClient.sendActivity('Seattle'); +assert.strictEqual(reply.text, 'Where are you traveling from?'); + +reply = await testClient.sendActivity('New York'); +assert.strictEqual(reply.text, 'When would you like to travel?'); + +reply = await testClient.sendActivity('tomorrow'); +assert.strictEqual(reply.text, 'OK, I will book a flight from Seattle to New York for tomorrow, Is this Correct?'); + +reply = await testClient.sendActivity('yes'); +assert.strictEqual(reply.text, 'Sure thing, wait while I finalize your reservation...'); + +reply = testClient.getNextReply(); +assert.strictEqual(reply.text, 'All set, I have booked your flight to Seattle for tomorrow'); +``` + +The project includes several examples on how to test different bot components: + +- [cancelAndHelpDialog.test](dialogs/cancelAndHelpDialog.test.js) shows how to write a simple data driven test for `CancelAndHelpDialog` using a test case array. +- [bookingDialog.test](dialogs/bookingDialog.test.js) shows how to write a data driven test using a `bookingDialogTestCases` module to generate the test cases. +- [mainDialog.test](dialogs/mainDialog.test.js) showcases how to use mock objects to mock the dialog's LUIS and `BookingDialog` dependencies to test `MainDialog` in isolation. +- [dialogAndWelcomeBot.test](bots/dialogAndWelcomeBot.test.js) provides an example on how to write a test for the bot's `ActivityHandler` using `TestAdapter`. + +## Further reading + +- [How to unit test bots](https://aka.ms/js-unit-test-docs) +- [Mocha](https://github.com/mochajs/mocha) +- [Bot Testing](https://github.com/microsoft/botframework-sdk/blob/master/specs/testing/testing.md) diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/bots/dialogAndWelcomeBot.test.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/bots/dialogAndWelcomeBot.test.js new file mode 100644 index 0000000000..c74665d80f --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/bots/dialogAndWelcomeBot.test.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +const { TestAdapter, ActivityTypes, TurnContext, ConversationState, MemoryStorage, UserState } = require('botbuilder'); +const { DialogSet, DialogTurnStatus, Dialog } = require('botbuilder-dialogs'); +const { DialogAndWelcomeBot } = require('../../bots/dialogAndWelcomeBot'); +const assert = require('assert'); + +/** + * A simple mock for a root dialog that gets invoked by the bot. + */ +class MockRootDialog extends Dialog { + constructor() { + super('mockRootDialog'); + } + + async beginDialog(dc, options) { + await dc.context.sendActivity(`${ this.id } mock invoked`); + return await dc.endDialog(); + } + + async run(turnContext, accessor) { + const dialogSet = new DialogSet(accessor); + dialogSet.add(this); + + const dialogContext = await dialogSet.createContext(turnContext); + const results = await dialogContext.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog(this.id); + } + } +} + +describe('DialogAndWelcomeBot', () => { + const testAdapter = new TestAdapter(); + + async function processActivity(activity, bot) { + const context = new TurnContext(testAdapter, activity); + await bot.run(context); + } + + it('Shows welcome card on member added and starts main dialog', async () => { + const mockRootDialog = new MockRootDialog(); + const memoryStorage = new MemoryStorage(); + const sut = new DialogAndWelcomeBot(new ConversationState(memoryStorage), new UserState(memoryStorage), mockRootDialog, console); + + // Create conversationUpdate activity + const conversationUpdateActivity = { + type: ActivityTypes.ConversationUpdate, + channelId: 'test', + conversation: { + id: 'someId' + }, + membersAdded: [ + { id: 'theUser' } + ], + recipient: { id: 'theBot' } + }; + + // Send the conversation update activity to the bot. + await processActivity(conversationUpdateActivity, sut); + + // Assert we got the welcome card + let reply = testAdapter.activityBuffer.shift(); + assert.strictEqual(reply.attachments.length, 1); + assert.strictEqual(reply.attachments[0].contentType, 'application/vnd.microsoft.card.adaptive'); + + // Assert that we started the main dialog. + reply = testAdapter.activityBuffer.shift(); + assert.strictEqual(reply.text, 'mockRootDialog mock invoked'); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/bookingDialog.test.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/bookingDialog.test.js new file mode 100644 index 0000000000..55a1b1e6f0 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/bookingDialog.test.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-env node, mocha */ +const { DialogTestClient, DialogTestLogger } = require('botbuilder-testing'); +const { BookingDialog } = require('../../dialogs/bookingDialog'); +const assert = require('assert'); + +describe('BookingDialog', () => { + const testCases = require('./testData/bookingDialogTestCases.js'); + const sut = new BookingDialog('bookingDialog'); + + testCases.map(testData => { + it(testData.name, async () => { + const client = new DialogTestClient('test', sut, testData.initialData, [new DialogTestLogger()]); + + // Execute the test case + console.log(`Test Case: ${ testData.name }`); + console.log(`Dialog Input ${ JSON.stringify(testData.initialData) }`); + for (let i = 0; i < testData.steps.length; i++) { + const reply = await client.sendActivity(testData.steps[i][0]); + assert.strictEqual((reply ? reply.text : null), testData.steps[i][1], `${ reply ? reply.text : null } != ${ testData.steps[i][1] }`); + } + + assert.strictEqual(client.dialogTurnResult.status, testData.expectedStatus, `${ testData.expectedStatus } != ${ client.dialogTurnResult.status }`); + + console.log(`Dialog result: ${ JSON.stringify(client.dialogTurnResult.result) }`); + if (testData.expectedResult !== undefined) { + // Check dialog results + const result = client.dialogTurnResult.result; + assert.strictEqual(result.destination, testData.expectedResult.destination); + assert.strictEqual(result.origin, testData.expectedResult.origin); + assert.strictEqual(result.travelDate, testData.expectedResult.travelDate); + } else { + assert.strictEqual(client.dialogTurnResult.result, undefined); + } + }); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/cancelAndHelpDialog.test.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/cancelAndHelpDialog.test.js new file mode 100644 index 0000000000..a583e41e5b --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/cancelAndHelpDialog.test.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-env node, mocha */ +const { MessageFactory } = require('botbuilder'); +const { DialogTestClient, DialogTestLogger } = require('botbuilder-testing'); +const { TextPrompt, WaterfallDialog } = require('botbuilder-dialogs'); +const { CancelAndHelpDialog } = require('../../dialogs/cancelAndHelpDialog'); +const assert = require('assert'); + +/** + * An waterfall dialog derived from CancelAndHelpDialog for testing + */ +class TestCancelAndHelpDialog extends CancelAndHelpDialog { + constructor() { + super('TestCancelAndHelpDialog'); + + this.addDialog(new TextPrompt('TextPrompt')) + .addDialog(new WaterfallDialog('WaterfallDialog', [ + this.promptStep.bind(this), + this.finalStep.bind(this) + ])); + + this.initialDialogId = 'WaterfallDialog'; + } + + async promptStep(stepContext) { + return await stepContext.prompt('TextPrompt', { prompt: MessageFactory.text('Hi there') }); + } + + async finalStep(stepContext) { + return await stepContext.endDialog(); + } +} + +describe('CancelAndHelpDialog', () => { + describe('Should be able to cancel', () => { + const testCases = ['cancel', 'quit']; + + testCases.map(testData => { + it(testData, async () => { + const sut = new TestCancelAndHelpDialog(); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + // Execute the test case + let reply = await client.sendActivity('Hi'); + assert.strictEqual(reply.text, 'Hi there'); + assert.strictEqual(client.dialogTurnResult.status, 'waiting'); + + reply = await client.sendActivity(testData); + assert.strictEqual(reply.text, 'Cancelling...'); + assert.strictEqual(client.dialogTurnResult.status, 'complete'); + }); + }); + }); + + describe('Should be able to get help', () => { + const testCases = ['help', '?']; + + testCases.map(testData => { + it(testData, async () => { + const sut = new TestCancelAndHelpDialog(); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + // Execute the test case + let reply = await client.sendActivity('Hi'); + assert.strictEqual(reply.text, 'Hi there'); + assert.strictEqual(client.dialogTurnResult.status, 'waiting'); + + reply = await client.sendActivity(testData); + assert.strictEqual(reply.text, 'Show help here'); + assert.strictEqual(client.dialogTurnResult.status, 'waiting'); + }); + }); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/dateResolverDialog.test.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/dateResolverDialog.test.js new file mode 100644 index 0000000000..86b996d7fc --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/dateResolverDialog.test.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-env node, mocha */ +const { DialogTestClient, DialogTestLogger } = require('botbuilder-testing'); +const { DateResolverDialog } = require('../../dialogs/dateResolverDialog'); +const assert = require('assert'); + +describe('DateResolverDialog', () => { + const testCases = require('./testData/dateResolverTestCases.js'); + const sut = new DateResolverDialog('dateResolver'); + + testCases.map(testData => { + it(testData.name, async () => { + const client = new DialogTestClient('test', sut, testData.initialData, [new DialogTestLogger()]); + + // Execute the test case + console.log(`Test Case: ${ testData.name }`); + console.log(`Dialog Input ${ JSON.stringify(testData.initialData) }`); + for (let i = 0; i < testData.steps.length; i++) { + const reply = await client.sendActivity(testData.steps[i][0]); + assert.strictEqual((reply ? reply.text : null), testData.steps[i][1], `${ reply ? reply.text : null } != ${ testData.steps[i][1] }`); + } + console.log(`Dialog result: ${ client.dialogTurnResult.result }`); + assert.strictEqual(client.dialogTurnResult.result, testData.expectedResult, `${ testData.expectedResult } != ${ client.dialogTurnResult.result }`); + }); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/mainDialog.test.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/mainDialog.test.js new file mode 100644 index 0000000000..728c31b666 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/mainDialog.test.js @@ -0,0 +1,156 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-env node, mocha */ +const { TextPrompt } = require('botbuilder-dialogs'); +const { DialogTestClient, DialogTestLogger } = require('botbuilder-testing'); +const { FlightBookingRecognizer } = require('../../dialogs/flightBookingRecognizer'); +const { MainDialog } = require('../../dialogs/mainDialog'); +const { BookingDialog } = require('../../dialogs/bookingDialog'); +const assert = require('assert'); + +/** + * A mock FlightBookingRecognizer for our main dialog tests that takes + * a mock luis result and can set as isConfigured === false. + */ +class MockFlightBookingRecognizer extends FlightBookingRecognizer { + constructor(isConfigured, mockResult) { + super(isConfigured); + this.isLuisConfigured = isConfigured; + this.mockResult = mockResult; + } + + async executeLuisQuery(context) { + return this.mockResult; + } + + get isConfigured() { + return (this.isLuisConfigured); + } +} + +/** + * A simple mock for Booking dialog that just returns a preset booking info for tests. + */ +class MockBookingDialog extends BookingDialog { + constructor() { + super('bookingDialog'); + } + + async beginDialog(dc, options) { + const bookingDetails = { + origin: 'New York', + destination: 'Seattle', + travelDate: '2025-07-08' + }; + await dc.context.sendActivity(`${ this.id } mock invoked`); + return await dc.endDialog(bookingDetails); + } +} + +/** +* A specialized mock for BookingDialog that displays a dummy TextPrompt. +* The dummy prompt is used to prevent the MainDialog waterfall from moving to the next step +* and assert that the main dialog was called. +*/ +class MockBookingDialogWithPrompt extends BookingDialog { + constructor() { + super('bookingDialog'); + } + + async beginDialog(dc, options) { + dc.dialogs.add(new TextPrompt('MockDialog')); + return await dc.prompt('MockDialog', { prompt: `${ this.id } mock invoked` }); + } +}; + +describe('MainDialog', () => { + it('Shows message if LUIS is not configured and calls BookingDialogDirectly', async () => { + const mockRecognizer = new MockFlightBookingRecognizer(false); + const mockBookingDialog = new MockBookingDialogWithPrompt(); + const sut = new MainDialog(mockRecognizer, mockBookingDialog); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + const reply = await client.sendActivity('hi'); + assert.strictEqual(reply.text, 'NOTE: LUIS is not configured. To enable all capabilities, add `LuisAppId`, `LuisAPIKey` and `LuisAPIHostName` to the .env file.', 'Did not warn about missing luis'); + }); + + it('Shows prompt if LUIS is configured', async () => { + const mockRecognizer = new MockFlightBookingRecognizer(true); + const mockBookingDialog = new MockBookingDialog(); + const sut = new MainDialog(mockRecognizer, mockBookingDialog); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + const reply = await client.sendActivity('hi'); + assert.strictEqual(reply.text, 'What can I help you with today?\nSay something like "Book a flight from Paris to Berlin on March 22, 2020"', 'Did not show prompt'); + }); + + describe('Invokes tasks based on LUIS intent', () => { + // Create array with test case data. + const testCases = [ + { utterance: 'I want to book a flight', intent: 'BookFlight', invokedDialogResponse: 'bookingDialog mock invoked', taskConfirmationMessage: 'I have you booked to Seattle from New York' }, + { utterance: 'What\'s the weather like?', intent: 'GetWeather', invokedDialogResponse: 'TODO: get weather flow here', taskConfirmationMessage: undefined }, + { utterance: 'bananas', intent: 'None', invokedDialogResponse: 'Sorry, I didn\'t get that. Please try asking in a different way (intent was None)', taskConfirmationMessage: undefined } + ]; + + testCases.map(testData => { + it(testData.intent, async () => { + // Create LuisResult for the mock recognizer. + const mockLuisResult = JSON.parse(`{"intents": {"${ testData.intent }": {"score": 1}}, "entities": {"$instance": {}}}`); + const mockRecognizer = new MockFlightBookingRecognizer(true, mockLuisResult); + const bookingDialog = new MockBookingDialog(); + const sut = new MainDialog(mockRecognizer, bookingDialog); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + // Execute the test case + console.log(`Test Case: ${ testData.intent }`); + let reply = await client.sendActivity('Hi'); + assert.strictEqual(reply.text, 'What can I help you with today?\nSay something like "Book a flight from Paris to Berlin on March 22, 2020"'); + + reply = await client.sendActivity(testData.utterance); + assert.strictEqual(reply.text, testData.invokedDialogResponse); + + // The Booking dialog displays an additional confirmation message, assert that it is what we expect. + if (testData.taskConfirmationMessage) { + reply = client.getNextReply(); + assert(reply.text.startsWith(testData.taskConfirmationMessage)); + } + + // Validate that the MainDialog starts over once the task is completed. + reply = client.getNextReply(); + assert.strictEqual(reply.text, 'What else can I do for you?'); + }); + }); + }); + + describe('Shows unsupported cities warning', () => { + // Create array with test case data. + const testCases = [ + { jsonFile: 'FlightToMadrid.json', expectedMessage: 'Sorry but the following airports are not supported: madrid' }, + { jsonFile: 'FlightFromMadridToChicago.json', expectedMessage: 'Sorry but the following airports are not supported: madrid, chicago' }, + { jsonFile: 'FlightFromCdgToJfk.json', expectedMessage: 'Sorry but the following airports are not supported: cdg' }, + { jsonFile: 'FlightFromParisToNewYork.json', expectedMessage: 'bookingDialog mock invoked' } + ]; + + testCases.map(testData => { + it(testData.jsonFile, async () => { + // Create LuisResult for the mock recognizer. + const mockLuisResult = require(`./testData/${ testData.jsonFile }`); + const mockRecognizer = new MockFlightBookingRecognizer(true, mockLuisResult); + const bookingDialog = new MockBookingDialog(); + const sut = new MainDialog(mockRecognizer, bookingDialog); + const client = new DialogTestClient('test', sut, null, [new DialogTestLogger()]); + + // Execute the test case + console.log(`Test Case: ${ mockLuisResult.text }`); + let reply = await client.sendActivity('Hi'); + assert.strictEqual(reply.text, 'What can I help you with today?\nSay something like "Book a flight from Paris to Berlin on March 22, 2020"'); + + reply = await client.sendActivity(mockLuisResult.text); + assert.strictEqual(reply.text, testData.expectedMessage); + }); + }); + }); +}); diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromCdgToJfk.json b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromCdgToJfk.json new file mode 100644 index 0000000000..679604f79c --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromCdgToJfk.json @@ -0,0 +1,103 @@ +{ + "text": "flight from cdg to jfk", + "intents": { + "BookFlight": { + "score": 0.712058365 + } + }, + "entities": { + "$instance": { + "From": [ + { + "startIndex": 12, + "endIndex": 15, + "score": 0.9553623, + "text": "cdg", + "type": "From" + } + ], + "To": [ + { + "startIndex": 19, + "endIndex": 22, + "score": 0.8905674, + "text": "jfk", + "type": "To" + } + ] + }, + "From": [ { "$instance": {} } ], + + "To": [ + { + "$instance": { + "Airport": [ + { + "startIndex": 19, + "endIndex": 22, + "text": "jfk", + "type": "Airport" + } + ] + }, + "Airport": [ + [ + "New York" + ] + ] + } + ] + }, + "luisResult": { + "query": "flight from cdg to jfk", + "topScoringIntent": { + "intent": "BookFlight", + "score": 0.712058365 + }, + "entities": [ + { + "entity": "cdg", + "type": "From", + "startIndex": 12, + "endIndex": 14, + "score": 0.9553623 + }, + { + "entity": "jfk", + "type": "To", + "startIndex": 19, + "endIndex": 21, + "score": 0.8905674 + }, + { + "entity": "jfk", + "type": "Airport", + "startIndex": 19, + "endIndex": 21, + "resolution": { + "values": [ + "New York" + ] + } + } + ], + "compositeEntities": [ + { + "parentType": "From", + "value": "cdg", + "children": [] + + }, + { + "parentType": "To", + "value": "jfk", + "children": [ + { + "type": "Airport", + "value": "jfk" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromMadridToChicago.json b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromMadridToChicago.json new file mode 100644 index 0000000000..a001904c8f --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromMadridToChicago.json @@ -0,0 +1,63 @@ +{ + "text": "flight from madrid to chicago", + "intents": { "BookFlight": { "score": 0.8837919 } }, + "entities": { + "$instance": { + "From": [ + { + "startIndex": 12, + "endIndex": 18, + "score": 0.888236344, + "text": "madrid", + "type": "From" + } + ], + "To": [ + { + "startIndex": 22, + "endIndex": 29, + "score": 0.640484631, + "text": "chicago", + "type": "To" + } + ] + }, + "From": [ { "$instance": {} } ], + "To": [ { "$instance": {} } ] + }, + "luisResult": { + "query": "flight from madrid to chicago", + "topScoringIntent": { + "intent": "BookFlight", + "score": 0.8837919 + }, + "entities": [ + { + "entity": "madrid", + "type": "From", + "startIndex": 12, + "endIndex": 17, + "score": 0.888236344 + }, + { + "entity": "chicago", + "type": "To", + "startIndex": 22, + "endIndex": 28, + "score": 0.640484631 + } + ], + "compositeEntities": [ + { + "parentType": "From", + "value": "madrid", + "children": [] + }, + { + "parentType": "To", + "value": "chicago", + "children": [] + } + ] + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromParisToNewYork.json b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromParisToNewYork.json new file mode 100644 index 0000000000..be15e91eb6 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightFromParisToNewYork.json @@ -0,0 +1,115 @@ +{ + "text": "flight from paris to new york", + "intents": { "BookFlight": { "score": 0.9953049 } }, + "entities": { + "$instance": { + "From": [ + { + "startIndex": 12, + "endIndex": 17, + "score": 0.94712317, + "text": "paris", + "type": "From" + } + ], + "To": [ + { + "startIndex": 21, + "endIndex": 29, + "score": 0.8602996, + "text": "new york", + "type": "To" + } + ] + }, + "From": [ + { + "$instance": { + "Airport": [ + { + "startIndex": 12, + "endIndex": 17, + "text": "paris", + "type": "Airport" + } + ] + }, + "Airport": [ [ "Paris" ] ] + } + ], + "To": [ + { + "$instance": { + "Airport": [ + { + "startIndex": 21, + "endIndex": 29, + "text": "new york", + "type": "Airport" + } + ] + }, + "Airport": [ [ "New York" ] ] + } + ] + }, + "luisResult": { + "query": "flight from paris to new york", + "topScoringIntent": { + "intent": "BookFlight", + "score": 0.9953049 + }, + "entities": [ + { + "entity": "paris", + "type": "From", + "startIndex": 12, + "endIndex": 16, + "score": 0.94712317 + }, + { + "entity": "new york", + "type": "To", + "startIndex": 21, + "endIndex": 28, + "score": 0.8602996 + }, + { + "entity": "paris", + "type": "Airport", + "startIndex": 12, + "endIndex": 16, + "resolution": { "values": [ "Paris" ] } + }, + { + "entity": "new york", + "type": "Airport", + "startIndex": 21, + "endIndex": 28, + "resolution": { "values": [ "New York" ] } + } + ], + "compositeEntities": [ + { + "parentType": "From", + "value": "paris", + "children": [ + { + "type": "Airport", + "value": "paris" + } + ] + }, + { + "parentType": "To", + "value": "new york", + "children": [ + { + "type": "Airport", + "value": "new york" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightToMadrid.json b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightToMadrid.json new file mode 100644 index 0000000000..992d8f1ed9 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/FlightToMadrid.json @@ -0,0 +1,41 @@ +{ + "text": "flight to madrid", + "intents": { "BookFlight": { "score": 0.8476145 } }, + "entities": { + "$instance": { + "To": [ + { + "startIndex": 10, + "endIndex": 16, + "score": 0.7892059, + "text": "madrid", + "type": "To" + } + ] + }, + "To": [ { "$instance": {} } ] + }, + "luisResult": { + "query": "flight to madrid", + "topScoringIntent": { + "intent": "BookFlight", + "score": 0.8476145 + }, + "entities": [ + { + "entity": "madrid", + "type": "To", + "startIndex": 10, + "endIndex": 15, + "score": 0.7892059 + } + ], + "compositeEntities": [ + { + "parentType": "To", + "value": "madrid", + "children": [] + } + ] + } +} \ No newline at end of file diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/bookingDialogTestCases.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/bookingDialogTestCases.js new file mode 100644 index 0000000000..181cc955eb --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/bookingDialogTestCases.js @@ -0,0 +1,152 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +const now = new Date(); +const today = formatDate(new Date()); +const tomorrow = formatDate(new Date().setDate(now.getDate() + 1)); + +function formatDate(date) { + const d = new Date(date); + let month = '' + (d.getMonth() + 1); + let day = '' + d.getDate(); + const year = d.getFullYear(); + + if (month.length < 2) month = '0' + month; + if (day.length < 2) day = '0' + day; + + return [year, month, day].join('-'); +} + +module.exports = [ + { + name: 'Full flow', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['Seattle', 'From what city will you be travelling?'], + ['New York', 'On what date would you like to travel?'], + ['tomorrow', `Please confirm, I have you traveling to: Seattle from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], + ['yes', null] + ], + expectedStatus: 'complete', + expectedResult: { + destination: 'Seattle', + origin: 'New York', + travelDate: tomorrow + } + }, + { + name: 'Full flow with \'no\' at confirmation', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['Seattle', 'From what city will you be travelling?'], + ['New York', 'On what date would you like to travel?'], + ['tomorrow', `Please confirm, I have you traveling to: Seattle from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], + ['no', null] + ], + expectedStatus: 'complete', + expectedResult: undefined + }, + { + name: 'Destination given', + initialData: { + destination: 'Bahamas' + }, + steps: [ + ['hi', 'From what city will you be travelling?'], + ['New York', 'On what date would you like to travel?'], + ['tomorrow', `Please confirm, I have you traveling to: Bahamas from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], + ['yes', null] + ], + expectedStatus: 'complete', + expectedResult: { + origin: 'New York', + destination: 'Bahamas', + travelDate: tomorrow + } + }, + { + name: 'Destination and origin given', + initialData: { + destination: 'Seattle', + origin: 'New York' + }, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['tomorrow', `Please confirm, I have you traveling to: Seattle from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], + ['yes', null] + ], + expectedStatus: 'complete', + expectedResult: { + destination: 'Seattle', + origin: 'New York', + travelDate: tomorrow + } + }, + { + name: 'All booking details given for today', + initialData: { + destination: 'Seattle', + origin: 'Bahamas', + travelDate: today + }, + steps: [ + ['hi', `Please confirm, I have you traveling to: Seattle from: Bahamas on: ${ today }. Is this correct? (1) Yes or (2) No`], + ['yes', null] + ], + expectedStatus: 'complete', + expectedResult: { + destination: 'Seattle', + origin: 'Bahamas', + travelDate: today + } + }, + { + name: 'Cancel on origin prompt', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['cancel', 'Cancelling...'] + ], + expectedStatus: 'complete', + expectedResult: undefined + }, + { + name: 'Cancel on destination prompt', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['Seattle', 'From what city will you be travelling?'], + ['cancel', 'Cancelling...'] + ], + expectedStatus: 'complete', + expectedResult: undefined + }, + { + name: 'Cancel on date prompt', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['Seattle', 'From what city will you be travelling?'], + ['New York', 'On what date would you like to travel?'], + ['cancel', 'Cancelling...'] + ], + expectedStatus: 'complete', + expectedResult: undefined + }, + { + name: 'Cancel on confirm prompt', + initialData: {}, + steps: [ + ['hi', 'To what city would you like to travel?'], + ['Seattle', 'From what city will you be travelling?'], + ['New York', 'On what date would you like to travel?'], + ['tomorrow', `Please confirm, I have you traveling to: Seattle from: New York on: ${ tomorrow }. Is this correct? (1) Yes or (2) No`], + ['cancel', 'Cancelling...'] + ], + expectedStatus: 'complete', + expectedResult: undefined + } +]; diff --git a/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/dateResolverTestCases.js b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/dateResolverTestCases.js new file mode 100644 index 0000000000..6fc6d9a777 --- /dev/null +++ b/experimental/language-generation/javascript_nodejs/13.core-bot/tests/dialogs/testData/dateResolverTestCases.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +const now = new Date(); +const tomorrow = formatDate(new Date().setDate(now.getDate() + 1)); +const dayAfterTomorrow = formatDate(new Date().setDate(now.getDate() + 2)); + +function formatDate(date) { + const d = new Date(date); + let month = '' + (d.getMonth() + 1); + let day = '' + d.getDate(); + const year = d.getFullYear(); + + if (month.length < 2) month = '0' + month; + if (day.length < 2) day = '0' + day; + + return [year, month, day].join('-'); +} + +module.exports = [ + { + name: 'tomorrow', + initialData: null, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['tomorrow', null] + ], + expectedResult: tomorrow + }, + { + name: 'the day after tomorrow', + initialData: null, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['the day after tomorrow', null] + ], + expectedResult: dayAfterTomorrow + }, + { + name: 'two days from now', + initialData: null, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['two days from now', null] + ], + expectedResult: dayAfterTomorrow + }, + { + name: 'valid input given (tomorrow)', + initialData: { date: tomorrow }, + steps: [ + ['hi', null] + ], + expectedResult: tomorrow + }, + { + name: 'retry prompt', + initialData: {}, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['bananas', 'I\'m sorry, for best results, please enter your travel date including the month, day and year.'], + ['tomorrow', null] + ], + expectedResult: tomorrow + }, + { + name: 'fuzzy time', + initialData: {}, + steps: [ + ['hi', 'On what date would you like to travel?'], + ['may 5th', 'I\'m sorry, for best results, please enter your travel date including the month, day and year.'], + ['may 5th 2055', null] + ], + expectedResult: '2055-05-05' + } +];