From 22f10761e1c329191a99e6b018f8698a9ea73f64 Mon Sep 17 00:00:00 2001 From: Gabo Gilabert Date: Tue, 7 Jul 2020 16:53:30 -0400 Subject: [PATCH] Updated FN Test projects to match samples (#4236) * Fixes issue with InvokeResponse hidding the underlying value for Body. Makes methods ins SkillHttpClient virtual so they can be mocked in tests. Updated BotFrameworkClient to use a deepclone of the activity rather than try/catch/finally to simply code and make sure we don't risk altering the original activity while the async request is being executed. Improved test coverage for BotFrameworkClient and SkillHttpClient. * Updated Skill test projects to match samples. * Deleted composer testing project --- .../Adaptive/AdaptiveRootBot/Program.cs | 5 +- .../AdaptiveSkillBot/AdaptiveSkillBot.csproj | 1 + .../AdaptiveSkillBot/Bots/SkillBot.cs | 6 +- .../{Bots => Dialogs}/ActivityRouterDialog.cs | 3 +- .../Dialogs/CancelAndHelpDialog.cs | 2 +- .../Dialogs/DateResolverDialog.cs | 8 +- .../Dialogs/DialogSkillBotRecognizer.cs | 2 +- .../AdaptiveSkillBot/appsettings.json | 9 +- .../manifest/dialogchildbot-manifest-1.0.json | 38 +- .../AllowedCallersClaimsValidator.cs | 22 +- .../DialogEchoSkillBot/Bots/EchoBot.cs | 10 +- .../DialogEchoSkillBot.csproj | 1 - .../DialogEchoSkillBot/Program.cs | 6 + .../SkillAdapterWithErrorHandler.cs | 67 +-- .../DialogEchoSkillBot/Startup.cs | 33 +- .../DialogEchoSkillBot/appsettings.json | 8 +- .../Controllers/BotController.cs | 15 +- .../DialogRootBot/Dialogs/BookingDetails.cs | 19 - .../DialogRootBot/Dialogs/Location.cs | 19 - .../DialogRootBot/Dialogs/MainDialog.cs | 133 ++---- .../DialogRootBot/Dialogs/TangentDialog.cs | 17 +- .../Properties/launchSettings.json | 4 +- .../DialogRootBot/Skills/DialogSkill.cs | 90 ++++ .../DialogRootBot/Skills/EchoSkill.cs | 32 ++ .../DialogRootBot/Skills/SkillDefinition.cs | 30 ++ .../DialogRootBot/Skills/TeamsSkill.cs | 45 ++ .../DialogRootBot/SkillsConfiguration.cs | 30 +- .../DialogRootBot/appsettings.json | 5 + .../CognitiveModels/FlightBookingEx.cs | 4 +- .../Dialogs/ActivityRouterDialog.cs | 31 ++ ....json => dialogskillbot-manifest-1.0.json} | 2 +- .../Skills/DialogToDialog/README.md | 74 --- .../AllowedCallersClaimsValidator.cs | 55 +++ .../TeamsSkillBot/Bots/TeamsBot.cs | 162 +++++++ .../Controllers/BotController.cs | 36 ++ .../Extensions/AdaptiveCardExtensions.cs | 66 +++ .../DialogToDialog/TeamsSkillBot/Program.cs | 31 ++ .../TeamsSkillBot/Resources/adaptiveCard.json | 29 ++ .../SkillAdapterWithErrorHandler.cs | 96 ++++ .../DialogToDialog/TeamsSkillBot/Startup.cs | 66 +++ .../TeamsSkillBot/TeamsSkillBot.csproj | 22 + .../TeamsSkillBot/appsettings.json | 6 + .../TeamsSkillBot/wwwroot/default.htm | 420 ++++++++++++++++++ .../manifest/teamskillbot-manifest-1.0.json | 37 ++ .../AllowedCallersClaimsValidator.cs | 22 +- .../EchoSkillBot/Bots/EchoBot.cs | 9 +- .../EchoSkillBot/EchoSkillBot.csproj | 7 +- .../SimpleBotToBot/EchoSkillBot/Program.cs | 6 + .../SkillAdapterWithErrorHandler.cs | 67 +-- .../SimpleBotToBot/EchoSkillBot/Startup.cs | 33 +- .../EchoSkillBot/appsettings.json | 8 +- .../manifest/echoskillbot-manifest-1.0.json | 2 +- .../SimpleRootBot/AdapterWithErrorHandler.cs | 27 +- .../SimpleRootBot/Bots/RootBot.cs | 5 +- .../SimpleBotToBot/SimpleRootBot/Program.cs | 6 + .../Properties/launchSettings.json | 5 +- .../SimpleRootBot/SimpleRootBot.csproj | 6 +- .../SimpleBotToBot/SimpleRootBot/Startup.cs | 32 +- Microsoft.Bot.Builder.Skills.sln | 31 +- 59 files changed, 1616 insertions(+), 447 deletions(-) rename FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/{Bots => Dialogs}/ActivityRouterDialog.cs (98%) delete mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/BookingDetails.cs delete mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/Location.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/DialogSkill.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/EchoSkill.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/SkillDefinition.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/TeamsSkill.cs rename FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/{dialogchildbot-manifest-1.0.json => dialogskillbot-manifest-1.0.json} (99%) delete mode 100644 FunctionalTests/Skills/DialogToDialog/README.md create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Authentication/AllowedCallersClaimsValidator.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Bots/TeamsBot.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Controllers/BotController.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Extensions/AdaptiveCardExtensions.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Program.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Resources/adaptiveCard.json create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/SkillAdapterWithErrorHandler.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Startup.cs create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/TeamsSkillBot.csproj create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/appsettings.json create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/default.htm create mode 100644 FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/manifest/teamskillbot-manifest-1.0.json diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveRootBot/Program.cs b/FunctionalTests/Skills/Adaptive/AdaptiveRootBot/Program.cs index 50823ccb9..5c3b55dd1 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveRootBot/Program.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveRootBot/Program.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Hosting; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace Microsoft.BotBuilderSamples.AdaptiveRootBot diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/AdaptiveSkillBot.csproj b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/AdaptiveSkillBot.csproj index b9df42efe..f4be7849c 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/AdaptiveSkillBot.csproj +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/AdaptiveSkillBot.csproj @@ -13,6 +13,7 @@ + diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/SkillBot.cs b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/SkillBot.cs index 97dd953da..a4d05e119 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/SkillBot.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/SkillBot.cs @@ -8,7 +8,7 @@ using Microsoft.Bot.Builder.Dialogs; namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots { - public class SkillBot : IBot + public class SkillBot : ActivityHandler where T : Dialog { private readonly ConversationState _conversationState; @@ -20,11 +20,11 @@ namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots _dialogManager = new DialogManager(mainDialog); } - public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await _dialogManager.OnTurnAsync(turnContext, cancellationToken); - // Save any state changes that might have occured during the turn. + // Save any state changes that might have occurred during the turn. await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); } } diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/ActivityRouterDialog.cs b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/ActivityRouterDialog.cs similarity index 98% rename from FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/ActivityRouterDialog.cs rename to FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/ActivityRouterDialog.cs index b60c246d8..7eee85c32 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Bots/ActivityRouterDialog.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/ActivityRouterDialog.cs @@ -10,11 +10,10 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.TraceExtensions; using Microsoft.Bot.Schema; -using Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; -namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots +namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs { /// /// A root dialog that can route activities sent to the skill to different dialogs. diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/CancelAndHelpDialog.cs b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/CancelAndHelpDialog.cs index 1a2fd44e3..4f595ce15 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/CancelAndHelpDialog.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/CancelAndHelpDialog.cs @@ -12,7 +12,7 @@ namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs public class CancelAndHelpDialog : ComponentDialog { private const string HelpMsgText = "Show help here"; - private const string CancelMsgText = "Cancelling..."; + private const string CancelMsgText = "Canceling..."; public CancelAndHelpDialog(string id) : base(id) diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DateResolverDialog.cs b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DateResolverDialog.cs index 380ab06ef..e5090e22d 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DateResolverDialog.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DateResolverDialog.cs @@ -14,7 +14,7 @@ namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs public class DateResolverDialog : CancelAndHelpDialog { private const string PromptMsgText = "When would you like to travel?"; - private const string RepromptMsgText = "I'm sorry, to make your booking please enter a full travel date including Day Month and Year."; + private const string RepromptMsgText = "I'm sorry, to make your booking please enter a full travel date, including Day, Month, and Year."; public DateResolverDialog(string id = null) : base(id ?? nameof(DateResolverDialog)) @@ -30,11 +30,11 @@ namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs { 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. + // This value will be a TIMEX. We are only interested in the Date part, so grab the first result and drop the Time part. + // TIMEX is a format that represents DateTime expressions that include some ambiguity, such as a missing Year. var 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. + // If this is a definite Date that includes year, month and day we are good; otherwise, reprompt. // A better solution might be to let the user know what part is actually missing. var isDefinite = new TimexProperty(timex).Types.Contains(Constants.TimexTypes.Definite); diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DialogSkillBotRecognizer.cs b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DialogSkillBotRecognizer.cs index 8b5d0ae7d..d1f598a4f 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DialogSkillBotRecognizer.cs +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/Dialogs/DialogSkillBotRecognizer.cs @@ -28,7 +28,7 @@ namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs } } - // Returns true if luis is configured in the appsettings.json and initialized. + // Returns true if LUIS is configured in the appsettings.json and initialized. public virtual bool IsConfigured => _recognizer != null; public virtual async Task RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken) diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/appsettings.json b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/appsettings.json index b2de4325f..1b9e450d5 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/appsettings.json +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/appsettings.json @@ -13,5 +13,12 @@ "LuisAppId": "", "LuisAPIKey": "", - "LuisAPIHostName": "" + "LuisAPIHostName": "", + + // This is a comma separate list with the App IDs that will have access to the skill. + // This setting is used in AllowedCallersClaimsValidator. + // Examples: + // [ "*" ] allows all callers. + // [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2". + "AllowedCallers": [ "*" ] } diff --git a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json index 251c3081c..a46c70f4c 100644 --- a/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json +++ b/FunctionalTests/Skills/Adaptive/AdaptiveSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json @@ -3,7 +3,7 @@ "$id": "DialogSkillBot", "name": "Skill bot with dialogs", "version": "1.0", - "description": "This is a sample skill definition for multiple activity types", + "description": "This is a sample skill definition for multiple activity types.", "publisherName": "Microsoft", "privacyUrl": "https://dialogskillbot.contoso.com/privacy.html", "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.", @@ -19,14 +19,14 @@ { "name": "default", "protocol": "BotFrameworkV3", - "description": "Default endpoint for the skill", - "endpointUrl": "https://ggdialogskillbot.azurewebsites.net/api/messages", - "msAppId": "f3fe8762-e50c-4688-b202-a040f522d916" + "description": "Default endpoint for the skill.", + "endpointUrl": "https://dialogskillbot.contoso.com/api/messages", + "msAppId": "00000000-0000-0000-0000-000000000000" } ], "activities": { "bookFlight": { - "description": "Books a flight (multi turn)", + "description": "Books a flight (multi turn).", "type": "event", "name": "BookFlight", "value": { @@ -36,9 +36,14 @@ "$ref": "#/definitions/bookingInfo" } }, + "oauthTest": { + "description": "Tests the OAuth prompt in a skill", + "type": "event", + "name": "OAuthTest" + }, "getWeather": { - "description": "Retrieves and returns the weather for the user's location (single turn, invoke)", - "type": "invoke", + "description": "Retrieves and returns the weather for the user's location.", + "type": "event", "name": "GetWeather", "value": { "$ref": "#/definitions/location" @@ -49,22 +54,13 @@ }, "passthroughMessage": { "type": "message", - "description": "Receives the user's utterance and attempts to resolve it using the skill's LUIS models", + "description": "Receives the user's utterance and attempts to resolve it using the skill's LUIS models.", "value": { "type": "object" } } }, "definitions": { - "localeValue": { - "type": "object", - "properties": { - "locale": { - "type": "string", - "description": "The current user's locale ISO code" - } - } - }, "bookingInfo": { "type": "object", "required": [ @@ -73,15 +69,15 @@ "properties": { "origin": { "type": "string", - "description": "this is the origin city for the flight" + "description": "This is the origin city for the flight." }, "destination": { "type": "string", - "description": "this is the destination city for the flight" + "description": "This is the destination city for the flight." }, "travelDate": { "type": "string", - "description": "The date for the flight in YYYY-MM-DD format" + "description": "The date for the flight in YYYY-MM-DD format." } } }, @@ -96,7 +92,7 @@ }, "location": { "type": "object", - "description": "Location metadata", + "description": "Location metadata.", "properties": { "latitude": { "type": "number", diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs index 1f59b79ce..3bb872e36 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs @@ -26,21 +26,27 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot.Authentication throw new ArgumentNullException(nameof(config)); } - // AllowedCallers is the setting in appsettings.json file - // that consists of the list of parent bot ids that are allowed to access the skill - // to add a new parent bot simply go to the AllowedCallers and add - // the parent bot's microsoft app id to the list + // AllowedCallers is the setting in the appsettings.json file + // that consists of the list of parent bot IDs that are allowed to access the skill. + // To add a new parent bot, simply edit the AllowedCallers and add + // the parent bot's Microsoft app ID to the list. + // In this sample, we allow all callers if AllowedCallers contains an "*". var section = config.GetSection(ConfigKey); var appsList = section.Get(); - _allowedCallers = appsList != null ? new List(appsList) : null; + if (appsList == null) + { + throw new ArgumentNullException($"\"{ConfigKey}\" not found in configuration."); + } + + _allowedCallers = new List(appsList); } public override Task ValidateClaimsAsync(IList claims) { - // if _allowedCallers is null we allow all calls - if (_allowedCallers != null && SkillValidation.IsSkillClaim(claims)) + // If _allowedCallers contains an "*", we allow all callers. + if (SkillValidation.IsSkillClaim(claims) && !_allowedCallers.Contains("*")) { - // Check that the appId claim in the skill request is in the list of skills configured for this bot. + // Check that the appId claim in the skill request is in the list of callers configured for this bot. var appId = JwtTokenValidation.GetAppIdFromClaims(claims); if (!_allowedCallers.Contains(appId)) { diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Bots/EchoBot.cs b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Bots/EchoBot.cs index a54b620af..c9d298e5a 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Bots/EchoBot.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Bots/EchoBot.cs @@ -23,11 +23,19 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot.Bots } else { - var messageText = $"Echo (dotnet core 3.1) : {turnContext.Activity.Text}"; + var messageText = $"Echo: {turnContext.Activity.Text}"; await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken); messageText = "Say \"end\" or \"stop\" and I'll end the conversation and back to the parent."; await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), cancellationToken); } } + + protected override Task OnEndOfConversationActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + // This will be called if the root bot is ending the conversation. Sending additional messages should be + // avoided as the conversation may have been deleted. + // Perform cleanup of resources if needed. + return Task.CompletedTask; + } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/DialogEchoSkillBot.csproj b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/DialogEchoSkillBot.csproj index 92e5b3ee7..bed0525f9 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/DialogEchoSkillBot.csproj +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/DialogEchoSkillBot.csproj @@ -5,7 +5,6 @@ latest Microsoft.BotBuilderSamples.DialogEchoSkillBot Microsoft.BotBuilderSamples.DialogEchoSkillBot - a6aa19d1-4134-48c1-8970-8404e694e003 diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Program.cs b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Program.cs index 1bdb7ef85..1e20ba9a8 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Program.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot { @@ -17,6 +18,11 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.ConfigureLogging((logging) => + { + logging.AddDebug(); + logging.AddConsole(); + }); webBuilder.UseStartup(); }); } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/SkillAdapterWithErrorHandler.cs b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/SkillAdapterWithErrorHandler.cs index 43a118f42..37956b231 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/SkillAdapterWithErrorHandler.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/SkillAdapterWithErrorHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Threading.Tasks; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.TraceExtensions; @@ -14,15 +15,29 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot { public class SkillAdapterWithErrorHandler : BotFrameworkHttpAdapter { - public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger logger, ConversationState conversationState = null) + private readonly ILogger _logger; + + public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger logger) : base(configuration, credentialProvider, authConfig, logger: logger) { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + OnTurnError = HandleTurnError; + } - // Send a message to the user + private async Task HandleTurnError(ITurnContext turnContext, Exception exception) + { + // Log any leaked exception from the application. + _logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + + await SendErrorMessageAsync(turnContext, exception); + await SendEoCToParentAsync(turnContext, exception); + } + + private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception) + { + try + { + // Send a message to the user. var errorMessageText = "The skill encountered an error or bug."; var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput); await turnContext.SendActivityAsync(errorMessage); @@ -31,32 +46,32 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput); await turnContext.SendActivityAsync(errorMessage); - if (conversationState != null) - { - try - { - // Delete the conversationState for the current conversation to prevent the - // bot from getting stuck in a error-loop caused by being in a bad state. - // ConversationState should be thought of as similar to "cookie-state" in a Web pages. - await conversationState.DeleteAsync(turnContext); - } - catch (Exception ex) - { - logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}"); - } - } + // Send a trace activity, which will be displayed in the Bot Framework Emulator. + // Note: we return the entire exception in the value property to help the developer; + // this should not be done in production. + await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}"); + } + } - // Send and EndOfConversation activity to the skill caller with the error to end the conversation + private async Task SendEoCToParentAsync(ITurnContext turnContext, Exception exception) + { + try + { + // Send an EndOfConversation activity to the skill caller with the error to end the conversation, // and let the caller decide what to do. var endOfConversation = Activity.CreateEndOfConversationActivity(); endOfConversation.Code = "SkillError"; endOfConversation.Text = exception.Message; await turnContext.SendActivityAsync(endOfConversation); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - // Note: we return the entire exception in the value property to help the developer, this should not be done in prod. - await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError"); - }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Exception caught in SendEoCToParentAsync : {ex}"); + } } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Startup.cs b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Startup.cs index 36fd44f7c..408361f55 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Startup.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/Startup.cs @@ -17,18 +17,10 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot { public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers() - .AddNewtonsoftJson(); + services.AddControllers().AddNewtonsoftJson(); // Configure credentials services.AddSingleton(); @@ -51,21 +43,14 @@ namespace Microsoft.BotBuilderSamples.DialogEchoSkillBot app.UseDeveloperExceptionPage(); } - app.UseDefaultFiles(); - app.UseStaticFiles(); - - //app.UseHttpsRedirection(); Enable this to support https - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + app.UseDefaultFiles() + .UseStaticFiles() + .UseRouting() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/appsettings.json b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/appsettings.json index 99f81e4cf..3ef33658a 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/appsettings.json +++ b/FunctionalTests/Skills/DialogToDialog/DialogEchoSkillBot/appsettings.json @@ -9,5 +9,11 @@ "AllowedHosts": "*", "MicrosoftAppId": "", "MicrosoftAppPassword": "", - "AllowedCallers": [] + + // This is a comma separate list with the App IDs that will have access to the skill. + // This setting is used in AllowedCallersClaimsValidator. + // Examples: + // [ "*" ] allows all callers. + // [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2". + "AllowedCallers": [ "*" ] } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Controllers/BotController.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Controllers/BotController.cs index 1ad12bdc1..b1d40c880 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Controllers/BotController.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Controllers/BotController.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Bot.Builder; @@ -28,9 +29,17 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Controllers [HttpGet] public async Task PostAsync() { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await _adapter.ProcessAsync(Request, Response, _bot); + try + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await _adapter.ProcessAsync(Request, Response, _bot); + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/BookingDetails.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/BookingDetails.cs deleted file mode 100644 index abfc96117..000000000 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/BookingDetails.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Newtonsoft.Json; - -namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs -{ - public class BookingDetails - { - [JsonProperty("destination")] - public string Destination { get; set; } - - [JsonProperty("origin")] - public string Origin { get; set; } - - [JsonProperty("travelDate")] - public string TravelDate { get; set; } - } -} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/Location.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/Location.cs deleted file mode 100644 index 04961e657..000000000 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/Location.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Newtonsoft.Json; - -namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs -{ - public class Location - { - [JsonProperty("latitude")] - public float? Latitude { get; set; } - - [JsonProperty("longitude")] - public float? Longitude { get; set; } - - [JsonProperty("postalCode")] - public string PostalCode { get; set; } - } -} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/MainDialog.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/MainDialog.cs index ecb50e376..240e5c056 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/MainDialog.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/MainDialog.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,7 +14,6 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs { @@ -24,15 +22,11 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs /// public class MainDialog : ComponentDialog { + // State property key that stores the active skill (used in AdapterWithErrorHandler to terminate the skills on error). public static readonly string ActiveSkillPropertyName = $"{typeof(MainDialog).FullName}.ActiveSkillProperty"; // Constants used for selecting actions on the skill. - private const string SkillActionBookFlight = "BookFlight"; - private const string SkillActionBookFlightWithInputParameters = "BookFlight with input parameters"; - private const string SkillActionGetWeather = "GetWeather"; - private const string SkillActionOAuthTest = "OAuthTest"; - private const string SkillActionEchoSkillBot = "EchoSkill"; - private const string SkillActionMessage = "Message"; + private const string JustForwardTheActivity = "JustForwardTurnContext.Activity"; private readonly IStatePropertyAccessor _activeSkillProperty; private readonly string _selectedSkillKey = $"{typeof(MainDialog).FullName}.SelectedSkillKey"; @@ -60,10 +54,10 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs throw new ArgumentNullException(nameof(conversationState)); } - // Register the tangent + // Register the tangent dialog for testing tangents and resume AddDialog(new TangentDialog()); - // Use helper method to add SkillDialog instances for the configured skills. + // Create and add SkillDialog instances for the configured skills. AddSkillDialogs(conversationState, conversationIdFactory, skillClient, skillsConfig, botId); // Add ChoicePrompt to render available skills. @@ -89,6 +83,14 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs InitialDialogId = nameof(WaterfallDialog); } + /// + /// This override is used to test the "abort" command to interrupt skills from the parent and + /// also to test the "tangent" command to start a tangent and resume a skill. + /// + /// The inner for the current turn of conversation. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A representing the asynchronous operation. protected override async Task OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default) { // This is an example on how to cancel a SkillDialog that is currently in progress from the parent bot. @@ -130,22 +132,22 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs return await stepContext.PromptAsync("SkillPrompt", options, cancellationToken); } - // Render a prompt to select the action for the skill. + // Render a prompt to select the begin action for the skill. private async Task SelectSkillActionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { // Get the skill info based on the selected skill. var selectedSkillId = ((FoundChoice)stepContext.Result).Value; - var selectedSkill = _skillsConfig.Skills.FirstOrDefault(s => s.Value.Id == selectedSkillId).Value; + var selectedSkill = _skillsConfig.Skills.FirstOrDefault(keyValuePair => keyValuePair.Value.Id == selectedSkillId).Value; // Remember the skill selected by the user. stepContext.Values[_selectedSkillKey] = selectedSkill; // Create the PromptOptions with the actions supported by the selected skill. - var messageText = $"Select an action # to send to **{selectedSkill.Id}** or just type in a message and it will be forwarded to the skill"; + var messageText = $"Select an action # to send to **{selectedSkill.Id}**.\n\nOr just type in a message and it will be forwarded to the skill as a message activity."; var options = new PromptOptions { Prompt = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), - Choices = GetSkillActions(selectedSkill) + Choices = selectedSkill.GetActions().Select(action => new Choice(action)).ToList() }; // Prompt the user to select a skill action. @@ -158,7 +160,7 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs if (!promptContext.Recognized.Succeeded) { // Assume the user wants to send a message if an item in the list is not selected. - promptContext.Recognized.Value = new FoundChoice { Value = SkillActionMessage }; + promptContext.Recognized.Value = new FoundChoice { Value = JustForwardTheActivity }; } return Task.FromResult(true); @@ -169,26 +171,13 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs { var selectedSkill = (BotFrameworkSkill)stepContext.Values[_selectedSkillKey]; - Activity skillActivity; - switch (selectedSkill.Id) - { - case "EchoSkillBot": - // Echo only takes messages - skillActivity = CreateDialogSkillBotActivity(SkillActionMessage, stepContext.Context); - - break; - case "DialogSkillBot": - skillActivity = CreateDialogSkillBotActivity(((FoundChoice)stepContext.Result).Value, stepContext.Context); - break; - default: - throw new Exception($"Unknown target skill id: {selectedSkill.Id}."); - } + var skillActivity = CreateBeginActivity(stepContext.Context, selectedSkill.Id, ((FoundChoice)stepContext.Result).Value); // Create the BeginSkillDialogOptions and assign the activity to send. var skillDialogArgs = new BeginSkillDialogOptions { Activity = skillActivity }; // Comment or uncomment this line if you need to enable or disabled buffered replies. - //skillDialogArgs.Activity.DeliveryMode = DeliveryModes.ExpectReplies; + // skillDialogArgs.Activity.DeliveryMode = DeliveryModes.ExpectReplies; // Save active skill in state. await _activeSkillProperty.SetAsync(stepContext.Context, selectedSkill, cancellationToken); @@ -241,90 +230,18 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs } } - // Helper method to create Choice elements for the actions supported by the skill. - private IList GetSkillActions(BotFrameworkSkill skill) - { - // Note: the bot would probably render this by reading the skill manifest. - // We are just using hardcoded skill actions here for simplicity. - - var choices = new List(); - switch (skill.Id) - { - case "EchoSkillBot": - choices.Add(new Choice(SkillActionMessage)); - break; - - case "DialogSkillBot": - choices.Add(new Choice(SkillActionBookFlight)); - choices.Add(new Choice(SkillActionBookFlightWithInputParameters)); - choices.Add(new Choice(SkillActionGetWeather)); - choices.Add(new Choice(SkillActionOAuthTest)); - choices.Add(new Choice(SkillActionEchoSkillBot)); - break; - } - - return choices; - } - // Helper method to create the activity to be sent to the DialogSkillBot using selected type and values. - private Activity CreateDialogSkillBotActivity(string selectedOption, ITurnContext turnContext) + private Activity CreateBeginActivity(ITurnContext turnContext, string skillId, string selectedOption) { - // Note: in a real bot, the dialogArgs will be created dynamically based on the conversation - // and what each action requires; here we hardcode the values to make things simpler. - - // Just forward the message activity to the skill with whatever the user said. - if (selectedOption.Equals(SkillActionMessage, StringComparison.CurrentCultureIgnoreCase)) + if (selectedOption.Equals(JustForwardTheActivity, StringComparison.CurrentCultureIgnoreCase)) { // Note message activities also support input parameters but we are not using them in this example. - return turnContext.Activity; + // Return a deep clone of the activity so we don't risk altering the original one + return ObjectPath.Clone(turnContext.Activity); } - Activity activity = null; - - // Send an event activity to the skill with "BookFlight" in the name. - if (selectedOption.Equals(SkillActionBookFlight, StringComparison.CurrentCultureIgnoreCase)) - { - activity = (Activity)Activity.CreateEventActivity(); - activity.Name = SkillActionBookFlight; - } - - // Send an event activity to the skill with "BookFlight" in the name and some testing values. - if (selectedOption.Equals(SkillActionBookFlightWithInputParameters, StringComparison.CurrentCultureIgnoreCase)) - { - activity = (Activity)Activity.CreateEventActivity(); - activity.Name = SkillActionBookFlight; - activity.Value = JObject.Parse("{ \"origin\": \"New York\", \"destination\": \"Seattle\"}"); - } - - // Send an event activity to the skill with "GetWeather" in the name and some testing values. - if (selectedOption.Equals(SkillActionGetWeather, StringComparison.CurrentCultureIgnoreCase)) - { - activity = (Activity)Activity.CreateEventActivity(); - activity.Name = SkillActionGetWeather; - activity.Value = JObject.Parse("{ \"latitude\": 47.614891, \"longitude\": -122.195801}"); - return activity; - } - - // Send an event activity to the skill with "OAuthTest" in the name. - if (selectedOption.Equals(SkillActionOAuthTest, StringComparison.CurrentCultureIgnoreCase)) - { - activity = (Activity)Activity.CreateEventActivity(); - activity.Name = SkillActionOAuthTest; - return activity; - } - - // Send an event activity to the skill with "EchoSkillBot" in the name. - if (selectedOption.Equals(SkillActionEchoSkillBot, StringComparison.CurrentCultureIgnoreCase)) - { - activity = (Activity)Activity.CreateEventActivity(); - activity.Name = SkillActionEchoSkillBot; - return activity; - } - - if (activity == null) - { - throw new Exception($"Unable to create dialogArgs for \"{selectedOption}\"."); - } + // Get the begin activity from the skill instance. + var activity = _skillsConfig.Skills[skillId].CreateBeginActivity(selectedOption); // We are manually creating the activity to send to the skill; ensure we add the ChannelData and Properties // from the original activity so the skill gets them. diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/TangentDialog.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/TangentDialog.cs index 12ca8cefd..8462f9601 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/TangentDialog.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/TangentDialog.cs @@ -9,6 +9,9 @@ using Microsoft.Bot.Schema; namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs { + /// + /// A simple waterfall dialog used to test triggering tangents from . + /// public class TangentDialog : ComponentDialog { public TangentDialog(string dialogId = nameof(TangentDialog)) @@ -18,7 +21,8 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs var waterfallSteps = new WaterfallStep[] { Step1Async, - Step2Async + Step2Async, + EndStepAsync }; AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps)); @@ -27,14 +31,21 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs private async Task Step1Async(WaterfallStepContext stepContext, CancellationToken cancellationToken) { - var promptMessage = MessageFactory.Text("Tangent step 1 of 2", InputHints.ExpectingInput); + var messageText = "Tangent step 1 of 2, say something."; + var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput); return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken); } private async Task Step2Async(WaterfallStepContext stepContext, CancellationToken cancellationToken) { - var promptMessage = MessageFactory.Text("Tangent step 2 of 2", InputHints.ExpectingInput); + var messageText = "Tangent step 2 of 2, say something."; + var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput); return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken); } + + private async Task EndStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); + } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Properties/launchSettings.json b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Properties/launchSettings.json index e13896491..5523cd7c0 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Properties/launchSettings.json +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Properties/launchSettings.json @@ -20,10 +20,10 @@ "DialogRootBot": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:5000" } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/DialogSkill.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/DialogSkill.cs new file mode 100644 index 000000000..5358cd14a --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/DialogSkill.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Schema; +using Newtonsoft.Json.Linq; + +namespace Microsoft.BotBuilderSamples.DialogRootBot.Skills +{ + public class DialogSkill : SkillDefinition + { + private const string SkillActionBookFlight = "BookFlight"; + private const string SkillActionBookFlightWithInputParameters = "BookFlight with input parameters"; + private const string SkillActionGetWeather = "GetWeather"; + private const string SkillActionOAuthTest = "OAuthTest"; + private const string SkillActionEchoSkillBot = "EchoSkill (Root->DialogSkill->EchoSkill)"; + private const string SkillActionMessage = "Message (sends 'Book a flight' as message)"; + + public override IList GetActions() + { + return new List + { + SkillActionBookFlight, + SkillActionBookFlightWithInputParameters, + SkillActionGetWeather, + SkillActionOAuthTest, + SkillActionEchoSkillBot, + SkillActionMessage + }; + } + + public override Activity CreateBeginActivity(string actionId) + { + Activity activity; + + // Send an event activity to the skill with "BookFlight" in the name. + if (actionId.Equals(SkillActionBookFlight, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateEventActivity(); + activity.Name = SkillActionBookFlight; + return activity; + } + + // Send an event activity to the skill with "BookFlight" in the name and some testing values. + if (actionId.Equals(SkillActionBookFlightWithInputParameters, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateEventActivity(); + activity.Name = SkillActionBookFlight; + activity.Value = JObject.Parse("{ \"origin\": \"New York\", \"destination\": \"Seattle\"}"); + return activity; + } + + // Send an event activity to the skill with "GetWeather" in the name and some testing values. + if (actionId.Equals(SkillActionGetWeather, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateEventActivity(); + activity.Name = SkillActionGetWeather; + activity.Value = JObject.Parse("{ \"latitude\": 47.614891, \"longitude\": -122.195801}"); + return activity; + } + + // Send an event activity to the skill with "OAuthTest" in the name. + if (actionId.Equals(SkillActionOAuthTest, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateEventActivity(); + activity.Name = SkillActionOAuthTest; + return activity; + } + + // Send an event activity to the skill with "EchoSkillBot" in the name. + if (actionId.Equals(SkillActionEchoSkillBot, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateEventActivity(); + activity.Name = "EchoSkill"; + return activity; + } + + if (actionId.Equals(SkillActionMessage, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateMessageActivity(); + activity.Name = SkillActionMessage; + activity.Text = "Book a flight"; + return activity; + } + + throw new InvalidOperationException($"Unable to create begin activity for \"{actionId}\"."); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/EchoSkill.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/EchoSkill.cs new file mode 100644 index 000000000..a434db613 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/EchoSkill.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Schema; + +namespace Microsoft.BotBuilderSamples.DialogRootBot.Skills +{ + public class EchoSkill : SkillDefinition + { + private const string SkillActionMessage = "Message"; + + public override IList GetActions() + { + return new List { SkillActionMessage }; + } + + public override Activity CreateBeginActivity(string actionId) + { + if (actionId.Equals(SkillActionMessage, StringComparison.CurrentCultureIgnoreCase)) + { + var activity = (Activity)Activity.CreateMessageActivity(); + activity.Name = SkillActionMessage; + activity.Text = "Begin the Echo Skill."; + return activity; + } + + throw new InvalidOperationException($"Unable to create begin activity for \"{actionId}\"."); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/SkillDefinition.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/SkillDefinition.cs new file mode 100644 index 000000000..7ede5d612 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/SkillDefinition.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Schema; + +namespace Microsoft.BotBuilderSamples.DialogRootBot.Skills +{ + /// + /// Extends and provides methods to return the actions and the begin activity to start a skill. + /// + /// + /// This is just a temporary implementation, ideally, this should be replaced by logic that parses a manifest and creates + /// what's needed. + /// + public class SkillDefinition : BotFrameworkSkill + { + public virtual IList GetActions() + { + throw new NotImplementedException(); + } + + public virtual Activity CreateBeginActivity(string actionId) + { + throw new NotImplementedException(); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/TeamsSkill.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/TeamsSkill.cs new file mode 100644 index 000000000..5b17fc6a3 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/Skills/TeamsSkill.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Schema; + +namespace Microsoft.BotBuilderSamples.DialogRootBot.Skills +{ + public class TeamsSkill : SkillDefinition + { + private const string SkillActionTeamsTaskModule = "TeamsTaskModule"; + private const string SkillActionTeamsCardAction = "TeamsCardAction"; + + public override IList GetActions() + { + return new List + { + SkillActionTeamsTaskModule, + SkillActionTeamsCardAction + }; + } + + public override Activity CreateBeginActivity(string actionId) + { + Activity activity; + + if (actionId.Equals(SkillActionTeamsTaskModule, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateInvokeActivity(); + activity.Name = SkillActionTeamsTaskModule; + return activity; + } + + if (actionId.Equals(SkillActionTeamsCardAction, StringComparison.CurrentCultureIgnoreCase)) + { + activity = (Activity)Activity.CreateInvokeActivity(); + activity.Name = SkillActionTeamsCardAction; + return activity; + } + + throw new InvalidOperationException($"Unable to create begin activity for \"{actionId}\"."); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/SkillsConfiguration.cs b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/SkillsConfiguration.cs index a82135a54..387b09fb4 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/SkillsConfiguration.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/SkillsConfiguration.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.Skills; +using Microsoft.BotBuilderSamples.DialogRootBot.Skills; using Microsoft.Extensions.Configuration; namespace Microsoft.BotBuilderSamples.DialogRootBot @@ -21,7 +23,7 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot { foreach (var skill in skills) { - Skills.Add(skill.Id, skill); + Skills.Add(skill.Id, CreateSkillDefinition(skill)); } } @@ -34,6 +36,30 @@ namespace Microsoft.BotBuilderSamples.DialogRootBot public Uri SkillHostEndpoint { get; } - public Dictionary Skills { get; } = new Dictionary(); + public Dictionary Skills { get; } = new Dictionary(); + + private static SkillDefinition CreateSkillDefinition(BotFrameworkSkill skill) + { + // Note: we hard code this for now, we should dynamically create instances based on the manifests. + // For now, this code creates a strong typed version of the SkillDefinition and copies the info from + // settings into it. + SkillDefinition skillDefinition; + switch (skill.Id) + { + case "EchoSkillBot": + skillDefinition = ObjectPath.Assign(new EchoSkill(), skill); + break; + case "DialogSkillBot": + skillDefinition = ObjectPath.Assign(new DialogSkill(), skill); + break; + case "TeamsSkillBot": + skillDefinition = ObjectPath.Assign(new TeamsSkill(), skill); + break; + default: + throw new Exception($"Unable to find definition class for {skill.Id}."); + } + + return skillDefinition; + } } } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/appsettings.json b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/appsettings.json index 3e35d4427..ab86d158f 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogRootBot/appsettings.json +++ b/FunctionalTests/Skills/DialogToDialog/DialogRootBot/appsettings.json @@ -13,6 +13,11 @@ "Id": "DialogSkillBot", "AppId": "TODO: Add here the App ID for the skill", "SkillEndpoint": "http://localhost:39783/api/messages" + }, + { + "Id": "TeamsSkillBot", + "AppId": "TODO: Add here the App ID for the skill", + "SkillEndpoint": "http://localhost:39773/api/messages" } ] } diff --git a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/CognitiveModels/FlightBookingEx.cs b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/CognitiveModels/FlightBookingEx.cs index 96a9d7d20..ed9b93498 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/CognitiveModels/FlightBookingEx.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/CognitiveModels/FlightBookingEx.cs @@ -5,7 +5,9 @@ using System.Linq; namespace Microsoft.BotBuilderSamples.DialogSkillBot.CognitiveModels { - // Extends the partial FlightBooking class with methods and properties that simplify accessing entities in the LUIS results. + /// + /// Extends the partial FlightBooking class with methods and properties that simplify accessing entities in the LUIS results. + /// public partial class FlightBooking { public (string From, string Airport) FromEntities diff --git a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/Dialogs/ActivityRouterDialog.cs b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/Dialogs/ActivityRouterDialog.cs index f49c63aba..1217bc05f 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/Dialogs/ActivityRouterDialog.cs +++ b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/Dialogs/ActivityRouterDialog.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -16,6 +17,7 @@ using Microsoft.Bot.Schema; using Microsoft.BotBuilderSamples.DialogSkillBot.CognitiveModels; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.BotBuilderSamples.DialogSkillBot.Dialogs { @@ -86,6 +88,9 @@ namespace Microsoft.BotBuilderSamples.DialogSkillBot.Dialogs case ActivityTypes.Message: return await OnMessageActivityAsync(stepContext, cancellationToken); + case ActivityTypes.Invoke: + return await OnInvokeActivityAsync(stepContext, cancellationToken); + default: // We didn't get an activity type we can handle. await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized ActivityType: \"{stepContext.Context.Activity.Type}\".", inputHint: InputHints.IgnoringInput), cancellationToken); @@ -169,6 +174,32 @@ namespace Microsoft.BotBuilderSamples.DialogSkillBot.Dialogs return new DialogTurnResult(DialogTurnStatus.Complete); } + + private async Task OnInvokeActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + { + var activity = stepContext.Context.Activity; + await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnMessageActivityAsync()", label: $"Text: \"{activity.Text}\". Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken); + + // Resolve what to execute based on the invoke activity name. + switch (activity.Name) + { + case "InvokeForAStartTest": + await stepContext.Context.SendActivityAsync("Maybe here I would start a dialog.", cancellationToken: cancellationToken); + var invokeResponseNoEoc = new InvokeResponse { Status = (int)HttpStatusCode.OK, Body = JObject.Parse("{ \"origin\": \"New York\", \"destination\": \"Seattle\"}") }; + await stepContext.Context.SendActivityAsync(new Activity { Value = invokeResponseNoEoc, Type = ActivityTypesEx.InvokeResponse }, cancellationToken).ConfigureAwait(false); + return EndOfTurn; + + case "InvokeWithEoc": + var invokeResponse = new InvokeResponse { Status = (int)HttpStatusCode.OK, Body = JObject.Parse("{ \"origin\": \"New York\", \"destination\": \"Seattle\"}") }; + await stepContext.Context.SendActivityAsync(new Activity { Value = invokeResponse, Type = ActivityTypesEx.InvokeResponse }, cancellationToken).ConfigureAwait(false); + return new DialogTurnResult(DialogTurnStatus.Complete); + + default: + // We didn't get an event name we can handle. + await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized InvokeName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken); + return new DialogTurnResult(DialogTurnStatus.Complete); + } + } private async Task BeginGetWeather(WaterfallStepContext stepContext, CancellationToken cancellationToken) { diff --git a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogskillbot-manifest-1.0.json similarity index 99% rename from FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json rename to FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogskillbot-manifest-1.0.json index 8de993649..eeef134d5 100644 --- a/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json +++ b/FunctionalTests/Skills/DialogToDialog/DialogSkillBot/wwwroot/manifest/dialogskillbot-manifest-1.0.json @@ -44,7 +44,7 @@ "dialogEchoSkillBot": { "description": "Calls the Echo Skill Bot from a skill", "type": "event", - "name": "EchoSkillBot" + "name": "EchoSkill" }, "getWeather": { "description": "Retrieves and returns the weather for the user's location.", diff --git a/FunctionalTests/Skills/DialogToDialog/README.md b/FunctionalTests/Skills/DialogToDialog/README.md deleted file mode 100644 index 61d28f1c7..000000000 --- a/FunctionalTests/Skills/DialogToDialog/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# SkillDialog - -Bot Framework v4 Skills with Dialogs sample. - -This bot has been created using the [Bot Framework](https://dev.botframework.com); it shows how to use a skill dialog from a root bot. - -## Prerequisites - -- [.NET Core SDK](https://dotnet.microsoft.com/download) version 3.1 - - ```bash - # determine dotnet version - dotnet --version - ``` - -## Key concepts in this sample - -The solution uses dialogs, within both a parent bot (`DialogRootBot`) and a skill bot (`DialogSkillBot`). -It demonstrates how to post activities from the parent bot to the skill bot and return the skill responses to the user. - -- `DialogRootBot`: this project shows how to consume a skill bot using a `SkillDialog`. It includes: - - A [root dialog](DialogRootBot/Dialogs/MainDialog.cs) that can call different actions on a skill using a `SkillDialog`: - - To send events activities. - - To send message activities. - - To cancel a `SkillDialog` using `CancelAllDialogsAsync` that automatically sends an `EndOfConversation` activity to remotely let a skill know that it needs to end a conversation. - - A sample [AdapterWithErrorHandler](DialogRootBot/AdapterWithErrorHandler.cs) adapter that shows how to handle errors, terminate skills and send traces back to the emulator to help debugging the bot. - - A sample [AllowedSkillsClaimsValidator](DialogRootBot/Authentication/AllowedSkillsClaimsValidator.cs) class that shows how to validate that responses sent to the bot are coming from the configured skills. - - A [Logger Middleware](DialogRootBot/Middleware/LoggerMiddleware.cs) that shows how to handle and log activities coming from a skill. - - A [SkillConversationIdFactory](DialogRootBot/SkillConversationIdFactory.cs) based on `IStorage` used to create and maintain conversation IDs to interact with a skill. - - A [SkillsConfiguration](DialogRootBot/SkillsConfiguration.cs) class that can load skill definitions from the appsettings.json file. - - A [startup](DialogRootBot/Startup.cs) class that shows how to register the different root bot components for dependency injection. - - A [SkillController](DialogRootBot/Controllers/SkillController.cs) that handles skill responses. - -- `DialogSkillBot`: this project shows a modified CoreBot that acts as a skill. It receives event and message activities from the parent bot and executes the requested tasks. This project includes: - - An [ActivityRouterDialog](DialogSkillBot/Dialogs/ActivityRouterDialog.cs) that handles Event and Message activities coming from a parent and performs different tasks. - - Event activities are routed to specific dialogs using the parameters provided in the `Values` property of the activity. - - Message activities are sent to LUIS if configured and trigger the desired tasks if the intent is recognized. - - A sample [ActivityHandler](DialogSkillBot/Bots/SkillBot.cs) that uses the `RunAsync` method on `ActivityRouterDialog`. - - Note: Starting in Bot Framework 4.8, the `RunAsync` method adds support to automatically send `EndOfConversation` with return values when the bot is running as a skill and the current dialog ends. It also handles reprompt messages to resume a skill where it left of. - - A sample [SkillAdapterWithErrorHandler](DialogSkillBot/SkillAdapterWithErrorHandler.cs) adapter that shows how to handle errors, terminate the skills, send traces back to the emulator to help debugging the bot and send `EndOfConversation` messages to the parent bot with details of the error. - - A sample [AllowedCallersClaimsValidator](DialogSkillBot/Authentication/AllowedCallersClaimsValidator.cs) that shows how to validate that the skill is only invoked from a list of allowed callers - - A [startup](DialogSkillBot/Startup.cs) class that shows how to register the different skill components for dependency injection. - - A [sample skill manifest](DialogSkillBot/wwwroot/manifest/dialogchildbot-manifest-1.0.json) that describes what the skill can do. - -## To try this sample - -- Clone the repository. - - ```bash - git clone https://github.com/microsoft/botbuilder-samples.git - ``` - -- Create a bot registration in the azure portal for the `DialogSkillBot` and update [DialogSkillBot/appsettings.json](DialogSkillBot/appsettings.json) with the AppId and password. -- Create a bot registration in the azure portal for the DialogRootBot and update [DialogRootBot/appsettings.json](DialogRootBot/appsettings.json) with the AppId and password. -- Update the BotFrameworkSkills section in [DialogRootBot/appsettings.json](DialogRootBot/appsettings.json) with the AppId for the skill you created in the previous step. -- (Optional) Configure the LuisAppId, LuisAPIKey and LuisAPIHostName section in the [DialogSkillBot/appsettings.json](DialogSkillBot/appsettings.json) if you want to run message activities through LUIS. -- Open the `Microsoft.Bot.Builder.Skills.sln` solution and configure it to [start debugging with multiple processes](https://docs.microsoft.com/en-us/visualstudio/debugger/debug-multiple-processes?view=vs-2019#start-debugging-with-multiple-processes). - -## 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.8.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`, the `MicrosoftAppId` and `MicrosoftAppPassword` for the `DialogRootBot` - -## Deploy the bots 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. diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Authentication/AllowedCallersClaimsValidator.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Authentication/AllowedCallersClaimsValidator.cs new file mode 100644 index 000000000..30e813556 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Authentication/AllowedCallersClaimsValidator.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoSkillBot v4.7.0 + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot.Authentication +{ + /// + /// Sample claims validator that loads an allowed list from configuration if present + /// and checks that requests are coming from allowed parent bots. + /// + public class AllowedCallersClaimsValidator : ClaimsValidator + { + private const string ConfigKey = "AllowedCallers"; + private readonly List _allowedCallers; + + public AllowedCallersClaimsValidator(IConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + // AllowedCallers is the setting in appsettings.json file + // that consists of the list of parent bot ids that are allowed to access the skill + // to add a new parent bot simply go to the AllowedCallers and add + // the parent bot's microsoft app id to the list + var section = config.GetSection(ConfigKey); + var appsList = section.Get(); + _allowedCallers = appsList != null ? new List(appsList) : null; + } + + public override Task ValidateClaimsAsync(IList claims) + { + // if _allowedCallers is null we allow all calls + if (_allowedCallers != null && _allowedCallers.Count > 0 && SkillValidation.IsSkillClaim(claims)) + { + // Check that the appId claim in the skill request is in the list of skills configured for this bot. + var appId = JwtTokenValidation.GetAppIdFromClaims(claims); + if (!_allowedCallers.Contains(appId)) + { + throw new UnauthorizedAccessException($"Received a request from a bot with an app ID of \"{appId}\". To enable requests from this caller, add the app ID to your configuration file."); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Bots/TeamsBot.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Bots/TeamsBot.cs new file mode 100644 index 000000000..142c4f412 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Bots/TeamsBot.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveCards; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Teams; +using Microsoft.Bot.Schema; +using Microsoft.Bot.Schema.Teams; +using Microsoft.BotBuilderSamples.TeamsSkillBot.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot.Bots +{ + public class TeamsBot : TeamsActivityHandler + { + protected override async Task OnInvokeActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + // Handle invoke triggers for the skill. + switch (turnContext.Activity.Name) + { + case "TeamsTaskModule": + { + var reply = MessageFactory.Attachment(GetTaskModuleHeroCard()); + await turnContext.SendActivityAsync(reply, cancellationToken); + + //var token = await (turnContext.Adapter as SkillAdapterWithErrorHandler).GetBotSkillToken(turnContext); + + return new InvokeResponse + { + Status = (int)HttpStatusCode.OK, + }; + } + + case "TeamsCardAction": + { + var reply = MessageFactory.Attachment(GetAdaptiveCardWithInvokeAction()); + await turnContext.SendActivityAsync(reply, cancellationToken); + + return new InvokeResponse + { + Status = (int)HttpStatusCode.OK, + }; + } + + default: + // Let the base handle it. + return await base.OnInvokeActivityAsync(turnContext, cancellationToken); + } + } + + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + var reply = MessageFactory.Attachment(GetTaskModuleHeroCard()); + await turnContext.SendActivityAsync(reply, cancellationToken); + } + + protected override async Task OnTeamsTaskModuleFetchAsync(ITurnContext turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken) + { + var reply = MessageFactory.Text("OnTeamsTaskModuleFetchAsync TaskModuleRequest: " + JsonConvert.SerializeObject(taskModuleRequest)); + + await turnContext.SendActivityAsync(reply, cancellationToken); + + return new TaskModuleResponse + { + Task = new TaskModuleContinueResponse + { + Value = new TaskModuleTaskInfo + { + Card = CreateAdaptiveCardAttachment(), + Height = 200, + Width = 400, + Title = "Adaptive Card: Inputs", + }, + }, + }; + } + + protected override async Task OnTeamsTaskModuleSubmitAsync(ITurnContext turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken) + { + var reply = MessageFactory.Text("OnTeamsTaskModuleSubmitAsync Value: " + JsonConvert.SerializeObject(taskModuleRequest)); + await turnContext.SendActivityAsync(reply, cancellationToken); + + // Send End of conversation at the end. + var activity = new Activity(ActivityTypes.EndOfConversation) + { + Value = taskModuleRequest.Data, + Locale = ((Activity)turnContext.Activity).Locale + }; + await turnContext.SendActivityAsync(activity, cancellationToken); + + return new TaskModuleResponse + { + Task = new TaskModuleMessageResponse + { + Value = "Thanks!", + }, + }; + } + + protected override async Task OnTeamsCardActionInvokeAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + await turnContext.SendActivityAsync(MessageFactory.Text("hello from OnTeamsCardActionInvokeAsync."), cancellationToken); + + // Send End of conversation at the end. + var activity = new Activity(ActivityTypes.EndOfConversation) + { + Locale = ((Activity)turnContext.Activity).Locale + }; + await turnContext.SendActivityAsync(activity, cancellationToken); + + return new InvokeResponse { Status = (int)HttpStatusCode.OK }; + } + + private Attachment GetTaskModuleHeroCard() + { + return new HeroCard + { + Title = "Task Module Invocation from Hero Card", + Subtitle = "This is a hero card with a Task Module Action button. Click the button to show an Adaptive Card within a Task Module.", + Buttons = new List + { + new TaskModuleAction("Adaptive Card", new { data = "adaptivecard" }), + }, + }.ToAttachment(); + } + + private Attachment GetAdaptiveCardWithInvokeAction() + { + var adaptiveCard = new AdaptiveCard(); + adaptiveCard.Body.Add(new AdaptiveTextBlock("Bot Builder Invoke Action")); + var action4 = new CardAction("invoke", "invoke", null, null, null, JObject.Parse(@"{ ""key"" : ""value"" }")); + adaptiveCard.Actions.Add(action4.ToAdaptiveCardAction()); + + return adaptiveCard.ToAttachment(); + } + + private Attachment CreateAdaptiveCardAttachment() + { + // combine path for cross platform support + string[] paths = + { + ".", + "Resources", + "adaptiveCard.json" + }; + var adaptiveCardJson = File.ReadAllText(Path.Combine(paths)); + + var adaptiveCardAttachment = new Attachment + { + ContentType = "application/vnd.microsoft.card.adaptive", + Content = JsonConvert.DeserializeObject(adaptiveCardJson), + }; + return adaptiveCardAttachment; + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Controllers/BotController.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Controllers/BotController.cs new file mode 100644 index 000000000..9af92bfe9 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Controllers/BotController.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot.Controllers +{ + // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot + // implementation at runtime. Multiple different IBot implementations running at different endpoints can be + // achieved by specifying a more specific type for the bot constructor argument. + [Route("api/messages")] + [ApiController] + public class BotController : ControllerBase + { + private readonly IBotFrameworkHttpAdapter _adapter; + private readonly IBot _bot; + + public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) + { + _adapter = adapter; + _bot = bot; + } + + [HttpPost] + [HttpGet] + public async Task PostAsync() + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await _adapter.ProcessAsync(Request, Response, _bot); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Extensions/AdaptiveCardExtensions.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Extensions/AdaptiveCardExtensions.cs new file mode 100644 index 000000000..1d748c6a7 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Extensions/AdaptiveCardExtensions.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AdaptiveCards; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot.Extensions +{ + //https://github.com/microsoft/botbuilder-dotnet/blob/master/tests/Teams/AdaptiveCards/AdaptiveCardExtensions.cs + public static class AdaptiveCardExtensions + { + /// + /// Creates a new attachment from AdaptiveCard. + /// + /// The instance of AdaptiveCard. + /// The generated attachment. + public static Attachment ToAttachment(this AdaptiveCard card) + { + return new Attachment + { + Content = card, + ContentType = AdaptiveCard.ContentType, + }; + } + + /// + /// Wrap BotBuilder action into AdaptiveCard submit action. + /// + /// The instance of adaptive card submit action. + /// Target action to be adapted. + public static void RepresentAsBotBuilderAction(this AdaptiveSubmitAction action, CardAction targetAction) + { + var wrappedAction = new CardAction + { + Type = targetAction.Type, + Value = targetAction.Value, + Text = targetAction.Text, + DisplayText = targetAction.DisplayText, + }; + + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.NullValueHandling = NullValueHandling.Ignore; + + var jsonStr = action.DataJson == null ? "{}" : action.DataJson; + JToken dataJson = JObject.Parse(jsonStr); + dataJson["msteams"] = JObject.FromObject(wrappedAction, JsonSerializer.Create(serializerSettings)); + + action.Title = targetAction.Title; + action.DataJson = dataJson.ToString(); + } + + /// + /// Wrap BotBuilder action into AdaptiveCard submit action. + /// + /// Target bot builder aciton to be adapted. + /// The wrapped adaptive card submit action. + public static AdaptiveSubmitAction ToAdaptiveCardAction(this CardAction action) + { + var adaptiveCardAction = new AdaptiveSubmitAction(); + adaptiveCardAction.RepresentAsBotBuilderAction(action); + return adaptiveCardAction; + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Program.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Program.cs new file mode 100644 index 000000000..52d0c1388 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Program.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoSkillBot v4.7.0 + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureLogging((logging) => + { + logging.AddDebug(); + logging.AddConsole(); + }); + webBuilder.UseStartup(); + }); + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Resources/adaptiveCard.json b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Resources/adaptiveCard.json new file mode 100644 index 000000000..6cf51dc8b --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Resources/adaptiveCard.json @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "text": "Enter Text Here", + "weight": "bolder", + "isSubtle": false + }, + { + "type": "Input.Text", + "id": "usertext", + "spacing": "none", + "isMultiLine": "true", + "placeholder": "add some text and submit" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Submit" + } + ] +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/SkillAdapterWithErrorHandler.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/SkillAdapterWithErrorHandler.cs new file mode 100644 index 000000000..ec66880bf --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/SkillAdapterWithErrorHandler.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.TraceExtensions; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot +{ + public class SkillAdapterWithErrorHandler : BotFrameworkHttpAdapter + { + private readonly IConfiguration _configuration; + + public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger logger, ConversationState conversationState = null) + : base(configuration, credentialProvider, authConfig, logger: logger) + { + _configuration = configuration; + OnTurnError = async (turnContext, exception) => + { + // Log any leaked exception from the application. + logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + + // Send a message to the user + var errorMessageText = "The skill encountered an error or bug."; + var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput); + await turnContext.SendActivityAsync(errorMessage); + + errorMessageText = "To continue to run this bot, please fix the bot source code."; + errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput); + await turnContext.SendActivityAsync(errorMessage); + + // Send a trace activity, which will be displayed in the Bot Framework Emulator + // Note: we return the entire exception in the value property to help the developer, this should not be done in prod. + await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError"); + + // Send and EndOfConversation activity to the skill caller with the error to end the conversation + // and let the caller decide what to do. + var endOfConversation = Activity.CreateEndOfConversationActivity(); + endOfConversation.Code = "SkillError"; + endOfConversation.Text = exception.Message; + await turnContext.SendActivityAsync(endOfConversation); + + if (conversationState != null) + { + try + { + // Delete the conversationState for the current conversation to prevent the + // bot from getting stuck in a error-loop caused by being in a bad state. + // ConversationState should be thought of as similar to "cookie-state" in a Web pages. + await conversationState.DeleteAsync(turnContext); + } + catch (Exception ex) + { + logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}"); + } + } + }; + } + + public async Task GetBotSkillToken(ITurnContext turnContext) + { + var appId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value; + var oAuthScope = turnContext.TurnState.Get(OAuthScopeKey); + var token = await (await GetAppCredentialsAsync(appId, oAuthScope).ConfigureAwait(false)).GetTokenAsync(); + return token; + } + + private async Task GetAppCredentialsAsync(string appId, string oAuthScope, CancellationToken cancellationToken = default) + { + if (appId == null) + { + return MicrosoftAppCredentials.Empty; + } + + var cacheKey = $"{appId}{oAuthScope}"; + if (AppCredentialMap.TryGetValue(cacheKey, out var appCredentials)) + { + return appCredentials; + } + + // Credentials not found in cache, build them + appCredentials = await BuildCredentialsAsync(appId, oAuthScope).ConfigureAwait(false); + + // Cache the credentials for later use + AppCredentialMap[cacheKey] = appCredentials; + return appCredentials; + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Startup.cs b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Startup.cs new file mode 100644 index 000000000..f6533230b --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/Startup.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.BotFramework; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.BotBuilderSamples.TeamsSkillBot.Authentication; +using Microsoft.BotBuilderSamples.TeamsSkillBot.Bots; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.BotBuilderSamples.TeamsSkillBot +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddNewtonsoftJson(); + + // Configure credentials + services.AddSingleton(); + + // Register AuthConfiguration to enable custom claim validation. + services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(sp.GetService()) }); + + // Create the Bot Framework Adapter with error handling enabled. + services.AddSingleton(); + + // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. + services.AddTransient(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseDefaultFiles() + .UseStaticFiles() + .UseWebSockets() + .UseRouting() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // app.UseHttpsRedirection(); + } + } +} diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/TeamsSkillBot.csproj b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/TeamsSkillBot.csproj new file mode 100644 index 000000000..ce2446816 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/TeamsSkillBot.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + latest + Microsoft.BotBuilderSamples.TeamsSkillBot + Microsoft.BotBuilderSamples.TeamsSkillBot + + + + + + + + + + + Always + + + + \ No newline at end of file diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/appsettings.json b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/appsettings.json new file mode 100644 index 000000000..87be97f33 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/appsettings.json @@ -0,0 +1,6 @@ +{ + "MicrosoftAppId": "", + "MicrosoftAppPassword": "", + "AllowedCallers": [] +} + diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/default.htm b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/default.htm new file mode 100644 index 000000000..88cf51b13 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/default.htm @@ -0,0 +1,420 @@ + + + + + + + TeamsSkillBot + + + + + +
+
+
+
TeamsSkillBot
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:39773/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/manifest/teamskillbot-manifest-1.0.json b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/manifest/teamskillbot-manifest-1.0.json new file mode 100644 index 000000000..1efb2d727 --- /dev/null +++ b/FunctionalTests/Skills/DialogToDialog/TeamsSkillBot/wwwroot/manifest/teamskillbot-manifest-1.0.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json", + "$id": "TeamsSkillBot", + "name": "Skill bot for Teams", + "version": "1.0", + "description": "This is a of a bot with skills for teams", + "publisherName": "Microsoft", + "privacyUrl": "https://teamsskillbot.contoso.com/privacy.html", + "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.", + "license": "", + "iconUrl": "https://teamsskillbot.contoso.com/icon.png", + "tags": [ + "sample", + "teams" + ], + "endpoints": [ + { + "name": "default", + "protocol": "BotFrameworkV3", + "description": "Default endpoint for the skill", + "endpointUrl": "http://teamsskillbot.contoso.com/api/messages", + "msAppId": "00000000-0000-0000-0000-000000000000" + } + ], + "activities": { + "taskModule": { + "description": "Task module sample.", + "type": "invoke", + "name": "TeamsTaskModule" + }, + "cardAction": { + "description": "Card action sample.", + "type": "invoke", + "name": "TeamsCardAction" + } + } +} \ No newline at end of file diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs index 139913c02..bada1de8b 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Authentication/AllowedCallersClaimsValidator.cs @@ -26,21 +26,27 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot.Authentication throw new ArgumentNullException(nameof(config)); } - // AllowedCallers is the setting in appsettings.json file - // that consists of the list of parent bot ids that are allowed to access the skill - // to add a new parent bot simply go to the AllowedCallers and add - // the parent bot's microsoft app id to the list + // AllowedCallers is the setting in the appsettings.json file + // that consists of the list of parent bot IDs that are allowed to access the skill. + // To add a new parent bot, simply edit the AllowedCallers and add + // the parent bot's Microsoft app ID to the list. + // In this sample, we allow all callers if AllowedCallers contains an "*". var section = config.GetSection(ConfigKey); var appsList = section.Get(); - _allowedCallers = appsList != null ? new List(appsList) : null; + if (appsList == null) + { + throw new ArgumentNullException($"\"{ConfigKey}\" not found in configuration."); + } + + _allowedCallers = new List(appsList); } public override Task ValidateClaimsAsync(IList claims) { - // if _allowedCallers is null we allow all calls - if (_allowedCallers != null && SkillValidation.IsSkillClaim(claims)) + // If _allowedCallers contains an "*", we allow all callers. + if (SkillValidation.IsSkillClaim(claims) && !_allowedCallers.Contains("*")) { - // Check that the appId claim in the skill request is in the list of skills configured for this bot. + // Check that the appId claim in the skill request is in the list of callers configured for this bot. var appId = JwtTokenValidation.GetAppIdFromClaims(claims); if (!_allowedCallers.Contains(appId)) { diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Bots/EchoBot.cs b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Bots/EchoBot.cs index 82de67b25..48ea0c520 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Bots/EchoBot.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Bots/EchoBot.cs @@ -15,15 +15,18 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot.Bots if (turnContext.Activity.Text.Contains("end") || turnContext.Activity.Text.Contains("stop")) { // Send End of conversation at the end. - await turnContext.SendActivityAsync(MessageFactory.Text($"ending conversation from the skill..."), cancellationToken); + var messageText = $"ending conversation from the skill..."; + await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken); var endOfConversation = Activity.CreateEndOfConversationActivity(); endOfConversation.Code = EndOfConversationCodes.CompletedSuccessfully; await turnContext.SendActivityAsync(endOfConversation, cancellationToken); } else { - await turnContext.SendActivityAsync(MessageFactory.Text($"Echo (dotnet core 3.1) : {turnContext.Activity.Text}"), cancellationToken); - await turnContext.SendActivityAsync(MessageFactory.Text("Say \"end\" or \"stop\" and I'll end the conversation and back to the parent."), cancellationToken); + var messageText = $"Echo: {turnContext.Activity.Text}"; + await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken); + messageText = "Say \"end\" or \"stop\" and I'll end the conversation and back to the parent."; + await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), cancellationToken); } } diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/EchoSkillBot.csproj b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/EchoSkillBot.csproj index ee2d049e4..b31c0defa 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/EchoSkillBot.csproj +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/EchoSkillBot.csproj @@ -5,7 +5,6 @@ latest Microsoft.BotBuilderSamples.EchoSkillBot Microsoft.BotBuilderSamples.EchoSkillBot - d3e58f1c-0841-4154-8a6e-c4dfc5ae3edf @@ -16,4 +15,10 @@ + + + Always + + + diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Program.cs b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Program.cs index 4acf713f5..4e8f4eaa3 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Program.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Microsoft.BotBuilderSamples.EchoSkillBot { @@ -17,6 +18,11 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.ConfigureLogging((logging) => + { + logging.AddDebug(); + logging.AddConsole(); + }); webBuilder.UseStartup(); }); } diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/SkillAdapterWithErrorHandler.cs b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/SkillAdapterWithErrorHandler.cs index 1efdc689d..b34c5c2de 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/SkillAdapterWithErrorHandler.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/SkillAdapterWithErrorHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Threading.Tasks; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.TraceExtensions; @@ -14,15 +15,29 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot { public class SkillAdapterWithErrorHandler : BotFrameworkHttpAdapter { - public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger logger, ConversationState conversationState = null) + private readonly ILogger _logger; + + public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger logger) : base(configuration, credentialProvider, authConfig, logger: logger) { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + OnTurnError = HandleTurnError; + } - // Send a message to the user + private async Task HandleTurnError(ITurnContext turnContext, Exception exception) + { + // Log any leaked exception from the application. + _logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + + await SendErrorMessageAsync(turnContext, exception); + await SendEoCToParentAsync(turnContext, exception); + } + + private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception) + { + try + { + // Send a message to the user. var errorMessageText = "The skill encountered an error or bug."; var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput); await turnContext.SendActivityAsync(errorMessage); @@ -31,32 +46,32 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput); await turnContext.SendActivityAsync(errorMessage); - if (conversationState != null) - { - try - { - // Delete the conversationState for the current conversation to prevent the - // bot from getting stuck in a error-loop caused by being in a bad state. - // ConversationState should be thought of as similar to "cookie-state" in a Web pages. - await conversationState.DeleteAsync(turnContext); - } - catch (Exception ex) - { - logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}"); - } - } + // Send a trace activity, which will be displayed in the Bot Framework Emulator. + // Note: we return the entire exception in the value property to help the developer; + // this should not be done in production. + await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}"); + } + } - // Send and EndOfConversation activity to the skill caller with the error to end the conversation + private async Task SendEoCToParentAsync(ITurnContext turnContext, Exception exception) + { + try + { + // Send an EndOfConversation activity to the skill caller with the error to end the conversation, // and let the caller decide what to do. var endOfConversation = Activity.CreateEndOfConversationActivity(); endOfConversation.Code = "SkillError"; endOfConversation.Text = exception.Message; await turnContext.SendActivityAsync(endOfConversation); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - // Note: we return the entire exception in the value property to help the developer, this should not be done in prod. - await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError"); - }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Exception caught in SendEoCToParentAsync : {ex}"); + } } } } diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Startup.cs b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Startup.cs index 8467d6c21..aa76a57c2 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Startup.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/Startup.cs @@ -17,18 +17,10 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot { public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers() - .AddNewtonsoftJson(); + services.AddControllers().AddNewtonsoftJson(); // Configure credentials services.AddSingleton(); @@ -51,21 +43,14 @@ namespace Microsoft.BotBuilderSamples.EchoSkillBot app.UseDeveloperExceptionPage(); } - app.UseDefaultFiles(); - app.UseStaticFiles(); - - //app.UseHttpsRedirection(); Enable this to support https - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + app.UseDefaultFiles() + .UseStaticFiles() + .UseRouting() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } } diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/appsettings.json b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/appsettings.json index 99f81e4cf..3ef33658a 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/appsettings.json +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/appsettings.json @@ -9,5 +9,11 @@ "AllowedHosts": "*", "MicrosoftAppId": "", "MicrosoftAppPassword": "", - "AllowedCallers": [] + + // This is a comma separate list with the App IDs that will have access to the skill. + // This setting is used in AllowedCallersClaimsValidator. + // Examples: + // [ "*" ] allows all callers. + // [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2". + "AllowedCallers": [ "*" ] } diff --git a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/wwwroot/manifest/echoskillbot-manifest-1.0.json b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/wwwroot/manifest/echoskillbot-manifest-1.0.json index 4af07e1c1..f1e7128f5 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/wwwroot/manifest/echoskillbot-manifest-1.0.json +++ b/FunctionalTests/Skills/SimpleBotToBot/EchoSkillBot/wwwroot/manifest/echoskillbot-manifest-1.0.json @@ -19,7 +19,7 @@ "protocol": "BotFrameworkV3", "description": "Default endpoint for the skill", "endpointUrl": "https://ggechoskillbot.azurewebsites.net/api/messages", - "msAppId": "0be01cfa-478e-4ec2-b2cc-9a4ec02f101b" + "msAppId": "f3fe8762-e50c-4688-b202-a040f522d916" } ] } \ No newline at end of file diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/AdapterWithErrorHandler.cs b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/AdapterWithErrorHandler.cs index d53c9a564..e044f2d31 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/AdapterWithErrorHandler.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/AdapterWithErrorHandler.cs @@ -25,11 +25,11 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot private readonly SkillHttpClient _skillClient; private readonly SkillsConfiguration _skillsConfig; - public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger, ConversationState conversationState = null, SkillHttpClient skillClient = null, SkillsConfiguration skillsConfig = null) + public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger, ConversationState conversationState, SkillHttpClient skillClient = null, SkillsConfiguration skillsConfig = null) : base(configuration, logger) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _conversationState = conversationState; + _conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _skillClient = skillClient; _skillsConfig = skillsConfig; @@ -71,7 +71,7 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot private async Task EndSkillConversationAsync(ITurnContext turnContext) { - if (_conversationState == null || _skillClient == null || _skillsConfig == null) + if (_skillClient == null || _skillsConfig == null) { return; } @@ -103,19 +103,16 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot private async Task ClearConversationStateAsync(ITurnContext turnContext) { - if (_conversationState != null) + try { - try - { - // Delete the conversationState for the current conversation to prevent the - // bot from getting stuck in a error-loop caused by being in a bad state. - // ConversationState should be thought of as similar to "cookie-state" in a Web pages. - await _conversationState.DeleteAsync(turnContext); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}"); - } + // Delete the conversationState for the current conversation to prevent the + // bot from getting stuck in a error-loop caused by being in a bad state. + // ConversationState should be thought of as similar to "cookie-state" in a Web pages. + await _conversationState.DeleteAsync(turnContext); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}"); } } } diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Bots/RootBot.cs b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Bots/RootBot.cs index ff2403866..531d9c03e 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Bots/RootBot.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Bots/RootBot.cs @@ -116,6 +116,9 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot.Bots // We are back at the root await turnContext.SendActivityAsync(MessageFactory.Text("Back in the root bot. Say \"skill\" and I'll patch you through"), cancellationToken); + + // Save conversation state + await _conversationState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken); } protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) @@ -124,7 +127,7 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot.Bots { if (member.Id != turnContext.Activity.Recipient.Id) { - await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome! (dotnet core 3.1)"), cancellationToken); + await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome!"), cancellationToken); } } } diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Program.cs b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Program.cs index ed999195f..19f5470de 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Program.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Microsoft.BotBuilderSamples.SimpleRootBot { @@ -17,6 +18,11 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.ConfigureLogging((logging) => + { + logging.AddDebug(); + logging.AddConsole(); + }); webBuilder.UseStartup(); }); } diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Properties/launchSettings.json b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Properties/launchSettings.json index cafe90fa4..0d93313e5 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Properties/launchSettings.json +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Properties/launchSettings.json @@ -12,15 +12,14 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "default.htm", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "SimpleRootBot31": { + "SimpleRootBot": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "http://localhost:3978", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/SimpleRootBot.csproj b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/SimpleRootBot.csproj index 6d59d392b..b1e159cc2 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/SimpleRootBot.csproj +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/SimpleRootBot.csproj @@ -5,7 +5,6 @@ latest Microsoft.BotBuilderSamples.SimpleRootBot Microsoft.BotBuilderSamples.SimpleRootBot - a52d1fa1-0d90-42d7-bbfc-2d776e8b7804 @@ -16,4 +15,9 @@ + + + Always + + diff --git a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Startup.cs b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Startup.cs index 243b4ec3a..7bce29803 100644 --- a/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Startup.cs +++ b/FunctionalTests/Skills/SimpleBotToBot/SimpleRootBot/Startup.cs @@ -11,7 +11,6 @@ using Microsoft.Bot.Builder.Skills; using Microsoft.Bot.Connector.Authentication; using Microsoft.BotBuilderSamples.SimpleRootBot.Authentication; using Microsoft.BotBuilderSamples.SimpleRootBot.Bots; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -19,18 +18,10 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot { public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers() - .AddNewtonsoftJson(); + services.AddControllers().AddNewtonsoftJson(); // Configure credentials services.AddSingleton(); @@ -72,19 +63,14 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot app.UseDeveloperExceptionPage(); } - app.UseDefaultFiles(); - app.UseStaticFiles(); - - //app.UseHttpsRedirection(); Enable this to support https - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + app.UseDefaultFiles() + .UseStaticFiles() + .UseRouting() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } } diff --git a/Microsoft.Bot.Builder.Skills.sln b/Microsoft.Bot.Builder.Skills.sln index 6b649a9be..23848004a 100644 --- a/Microsoft.Bot.Builder.Skills.sln +++ b/Microsoft.Bot.Builder.Skills.sln @@ -5,6 +5,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{4269F3C3-6B42-419B-B64A-3E6DC0F1574A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{AD743B78-D61F-4FBF-B620-FA83CE599A50}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Adaptive", "libraries\Microsoft.Bot.Builder.Dialogs.Adaptive\Microsoft.Bot.Builder.Dialogs.Adaptive.csproj", "{3CF175CF-1AF4-4109-96CB-221684DCED7D}" EndProject @@ -69,6 +72,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.TestBot", "tests\Microsoft.Bot.Builder.TestBot\Microsoft.Bot.Builder.TestBot.csproj", "{C113E0AE-5564-4389-BA39-183A8D574210}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FunctionalTests", "FunctionalTests", "{8667F820-8ADA-4498-91AE-AE95DEE5227E}" + ProjectSection(SolutionItems) = preProject + FunctionalTests\Directory.Build.props = FunctionalTests\Directory.Build.props + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Testing", "libraries\Microsoft.Bot.Builder.Testing\Microsoft.Bot.Builder.Testing.csproj", "{060F070A-BBFA-490E-BE89-3844C857B771}" EndProject @@ -94,6 +100,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Langu EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B6AFE9D-6FA5-4699-B0EB-62335FD431C8}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig bot.png = bot.png bot_icon.png = bot_icon.png BotBuilder-DotNet.ruleset = BotBuilder-DotNet.ruleset @@ -102,16 +109,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution tests\tests.schema = tests\tests.schema EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{EE56F2B6-4995-4E8F-ACFF-310AF0A4DA0F}" - ProjectSection(SolutionItems) = preProject - schemas\baseComponent.schema = schemas\baseComponent.schema - schemas\component.schema = schemas\component.schema - schemas\readme.md = schemas\readme.md - schemas\sdk.schema = schemas\sdk.schema - schemas\update.cmd = schemas\update.cmd - schemas\updateBranch.cmd = schemas\updateBranch.cmd - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.TestBot.Json", "tests\Microsoft.Bot.Builder.TestBot.Json\Microsoft.Bot.Builder.TestBot.Json.csproj", "{2454BBCD-77BC-4E3D-B5A6-3562BED898D6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.AI.Luis.TestUtils", "tests\Microsoft.Bot.Builder.AI.Luis.TestUtils\Microsoft.Bot.Builder.AI.Luis.TestUtils.csproj", "{685271A8-6C69-46E4-9B11-89AF9761CE0A}" @@ -152,9 +149,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoSkillBot", "FunctionalTests\Skills\SimpleBotToBot\EchoSkillBot\EchoSkillBot.csproj", "{C8F3C6E5-6A21-4F77-ADF7-30119D836A4D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DialogToDialog", "DialogToDialog", "{3E023AB7-FE1F-41B1-9EF4-1550BCE1DC37}" - ProjectSection(SolutionItems) = preProject - FunctionalTests\Skills\DialogToDialog\README.md = FunctionalTests\Skills\DialogToDialog\README.md - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DialogRootBot", "FunctionalTests\Skills\DialogToDialog\DialogRootBot\DialogRootBot.csproj", "{E65DC262-CA77-41F6-8439-02C1917874DD}" EndProject @@ -170,6 +164,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRootBot", "Function EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveSkillBot", "FunctionalTests\Skills\Adaptive\AdaptiveSkillBot\AdaptiveSkillBot.csproj", "{3EA28B80-440E-4919-9850-31236968BC04}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamsSkillBot", "FunctionalTests\Skills\DialogToDialog\TeamsSkillBot\TeamsSkillBot.csproj", "{9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -658,6 +654,14 @@ Global {3EA28B80-440E-4919-9850-31236968BC04}.Release|Any CPU.Build.0 = Release|Any CPU {3EA28B80-440E-4919-9850-31236968BC04}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU {3EA28B80-440E-4919-9850-31236968BC04}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Release|Any CPU.Build.0 = Release|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E}.Release-Windows|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -731,6 +735,7 @@ Global {AE7143FE-C460-449D-AA50-EFA4820EBADE} = {3C16F609-61D8-49C5-A243-9D32B9491D91} {1E638E89-43B9-4381-8CC7-B65548DB0070} = {AE7143FE-C460-449D-AA50-EFA4820EBADE} {3EA28B80-440E-4919-9850-31236968BC04} = {AE7143FE-C460-449D-AA50-EFA4820EBADE} + {9CE278DD-0560-4D4B-B6BB-D118FBA20D4E} = {3E023AB7-FE1F-41B1-9EF4-1550BCE1DC37} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}