Updated FN Test projects to match samples (#4236)

* Fixes issue with InvokeResponse<T> 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
This commit is contained in:
Gabo Gilabert 2020-07-07 16:53:30 -04:00 коммит произвёл GitHub
Родитель 09db52ba4b
Коммит 22f10761e1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
59 изменённых файлов: 1616 добавлений и 447 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\libraries\integration\Microsoft.Bot.Builder.Integration.AspNet.Core\Microsoft.Bot.Builder.Integration.AspNet.Core.csproj" />
<ProjectReference Include="..\..\..\..\libraries\Microsoft.Bot.Builder.Dialogs.Adaptive\Microsoft.Bot.Builder.Dialogs.Adaptive.csproj" />
<ProjectReference Include="..\..\..\..\libraries\Microsoft.Bot.Builder.AI.LUIS\Microsoft.Bot.Builder.AI.Luis.csproj" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -8,7 +8,7 @@ using Microsoft.Bot.Builder.Dialogs;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots
{
public class SkillBot<T> : IBot
public class SkillBot<T> : 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);
}
}

Просмотреть файл

@ -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
{
/// <summary>
/// A root dialog that can route activities sent to the skill to different dialogs.

Просмотреть файл

@ -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)

Просмотреть файл

@ -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);

Просмотреть файл

@ -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<RecognizerResult> RecognizeAsync(ITurnContext turnContext, CancellationToken cancellationToken)

Просмотреть файл

@ -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": [ "*" ]
}

Просмотреть файл

@ -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",

Просмотреть файл

@ -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<string[]>();
_allowedCallers = appsList != null ? new List<string>(appsList) : null;
if (appsList == null)
{
throw new ArgumentNullException($"\"{ConfigKey}\" not found in configuration.");
}
_allowedCallers = new List<string>(appsList);
}
public override Task ValidateClaimsAsync(IList<Claim> 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))
{

Просмотреть файл

@ -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<IEndOfConversationActivity> 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;
}
}
}

Просмотреть файл

@ -5,7 +5,6 @@
<LangVersion>latest</LangVersion>
<AssemblyName>Microsoft.BotBuilderSamples.DialogEchoSkillBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.DialogEchoSkillBot</RootNamespace>
<UserSecretsId>a6aa19d1-4134-48c1-8970-8404e694e003</UserSecretsId>
</PropertyGroup>
<ItemGroup>

Просмотреть файл

@ -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<Startup>();
});
}

Просмотреть файл

@ -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<BotFrameworkHttpAdapter> logger, ConversationState conversationState = null)
private readonly ILogger _logger;
public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger<BotFrameworkHttpAdapter> 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}");
}
}
}
}

Просмотреть файл

@ -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<ICredentialProvider, ConfigurationCredentialProvider>();
@ -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();
});
}
}
}

Просмотреть файл

@ -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": [ "*" ]
}

Просмотреть файл

@ -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;
}
}
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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
/// </summary>
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<BotFrameworkSkill> _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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="innerDc">The inner <see cref="DialogContext"/> for the current turn of conversation.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected override async Task<DialogTurnResult> 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<DialogTurnResult> 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<Choice> 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<Choice>();
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.

Просмотреть файл

@ -9,6 +9,9 @@ using Microsoft.Bot.Schema;
namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs
{
/// <summary>
/// A simple waterfall dialog used to test triggering tangents from <see cref="MainDialog"/>.
/// </summary>
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<DialogTurnResult> 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<DialogTurnResult> 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<DialogTurnResult> EndStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
}
}

Просмотреть файл

@ -20,10 +20,10 @@
"DialogRootBot": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"applicationUrl": "http://localhost:5000"
}
}
}

Просмотреть файл

@ -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<string> GetActions()
{
return new List<string>
{
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}\".");
}
}
}

Просмотреть файл

@ -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<string> GetActions()
{
return new List<string> { 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}\".");
}
}
}

Просмотреть файл

@ -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
{
/// <summary>
/// Extends <see cref="BotFrameworkSkill"/> and provides methods to return the actions and the begin activity to start a skill.
/// </summary>
/// <remarks>
/// This is just a temporary implementation, ideally, this should be replaced by logic that parses a manifest and creates
/// what's needed.
/// </remarks>
public class SkillDefinition : BotFrameworkSkill
{
public virtual IList<string> GetActions()
{
throw new NotImplementedException();
}
public virtual Activity CreateBeginActivity(string actionId)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -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<string> GetActions()
{
return new List<string>
{
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}\".");
}
}
}

Просмотреть файл

@ -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<string, BotFrameworkSkill> Skills { get; } = new Dictionary<string, BotFrameworkSkill>();
public Dictionary<string, SkillDefinition> Skills { get; } = new Dictionary<string, SkillDefinition>();
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<EchoSkill>(new EchoSkill(), skill);
break;
case "DialogSkillBot":
skillDefinition = ObjectPath.Assign<DialogSkill>(new DialogSkill(), skill);
break;
case "TeamsSkillBot":
skillDefinition = ObjectPath.Assign<TeamsSkill>(new TeamsSkill(), skill);
break;
default:
throw new Exception($"Unable to find definition class for {skill.Id}.");
}
return skillDefinition;
}
}
}

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -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.
/// <summary>
/// Extends the partial FlightBooking class with methods and properties that simplify accessing entities in the LUIS results.
/// </summary>
public partial class FlightBooking
{
public (string From, string Airport) FromEntities

Просмотреть файл

@ -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<DialogTurnResult> 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<DialogTurnResult> BeginGetWeather(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{

Просмотреть файл

@ -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.",

Просмотреть файл

@ -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.

Просмотреть файл

@ -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
{
/// <summary>
/// Sample claims validator that loads an allowed list from configuration if present
/// and checks that requests are coming from allowed parent bots.
/// </summary>
public class AllowedCallersClaimsValidator : ClaimsValidator
{
private const string ConfigKey = "AllowedCallers";
private readonly List<string> _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<string[]>();
_allowedCallers = appsList != null ? new List<string>(appsList) : null;
}
public override Task ValidateClaimsAsync(IList<Claim> 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;
}
}
}

Просмотреть файл

@ -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<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> 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<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var reply = MessageFactory.Attachment(GetTaskModuleHeroCard());
await turnContext.SendActivityAsync(reply, cancellationToken);
}
protected override async Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(ITurnContext<IInvokeActivity> 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<TaskModuleResponse> OnTeamsTaskModuleSubmitAsync(ITurnContext<IInvokeActivity> 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<InvokeResponse> OnTeamsCardActionInvokeAsync(ITurnContext<IInvokeActivity> 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<CardAction>
{
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;
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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
{
/// <summary>
/// Creates a new attachment from AdaptiveCard.
/// </summary>
/// <param name="card"> The instance of AdaptiveCard.</param>
/// <returns> The generated attachment.</returns>
public static Attachment ToAttachment(this AdaptiveCard card)
{
return new Attachment
{
Content = card,
ContentType = AdaptiveCard.ContentType,
};
}
/// <summary>
/// Wrap BotBuilder action into AdaptiveCard submit action.
/// </summary>
/// <param name="action"> The instance of adaptive card submit action.</param>
/// <param name="targetAction"> Target action to be adapted.</param>
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();
}
/// <summary>
/// Wrap BotBuilder action into AdaptiveCard submit action.
/// </summary>
/// <param name="action"> Target bot builder aciton to be adapted.</param>
/// <returns> The wrapped adaptive card submit action.</returns>
public static AdaptiveSubmitAction ToAdaptiveCardAction(this CardAction action)
{
var adaptiveCardAction = new AdaptiveSubmitAction();
adaptiveCardAction.RepresentAsBotBuilderAction(action);
return adaptiveCardAction;
}
}
}

Просмотреть файл

@ -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<Startup>();
});
}
}

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -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<BotFrameworkHttpAdapter> 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<string> GetBotSkillToken(ITurnContext turnContext)
{
var appId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;
var oAuthScope = turnContext.TurnState.Get<string>(OAuthScopeKey);
var token = await (await GetAppCredentialsAsync(appId, oAuthScope).ConfigureAwait(false)).GetTokenAsync();
return token;
}
private async Task<AppCredentials> 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;
}
}
}

Просмотреть файл

@ -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<ICredentialProvider, ConfigurationCredentialProvider>();
// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(sp.GetService<IConfiguration>()) });
// Create the Bot Framework Adapter with error handling enabled.
services.AddSingleton<IBotFrameworkHttpAdapter, SkillAdapterWithErrorHandler>();
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, TeamsBot>();
}
// 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();
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
<AssemblyName>Microsoft.BotBuilderSamples.TeamsSkillBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.TeamsSkillBot</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AdaptiveCards" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
<ProjectReference Include="..\..\..\..\libraries\integration\Microsoft.Bot.Builder.Integration.AspNet.Core\Microsoft.Bot.Builder.Integration.AspNet.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,6 @@
{
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"AllowedCallers": []
}

Просмотреть файл

@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TeamsSkillBot</title>
<style>
body {
margin: 0px;
padding: 0px;
font-family: Segoe UI;
}
html,
body {
height: 100%;
}
header {
background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 4638.9 651.6' style='enable-background:new 0 0 4638.9 651.6;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%2355A0E0;%7D .st1%7Bfill:none;%7D .st2%7Bfill:%230058A8;%7D .st3%7Bfill:%23328BD8;%7D .st4%7Bfill:%23B6DCF1;%7D .st5%7Bopacity:0.2;fill:url(%23SVGID_1_);enable-background:new ;%7D%0A%3C/style%3E%3Crect y='1.1' class='st0' width='4640' height='646.3'/%3E%3Cpath class='st1' d='M3987.8,323.6L4310.3,1.1h-65.6l-460.1,460.1c-17.5,17.5-46.1,17.5-63.6,0L3260.9,1.1H0v646.3h3660.3 L3889,418.7c17.5-17.5,46.1-17.5,63.6,0l228.7,228.7h66.6l-260.2-260.2C3970.3,369.8,3970.3,341.1,3987.8,323.6z'/%3E%3Cpath class='st2' d='M3784.6,461.2L4244.7,1.1h-983.9l460.1,460.1C3738.4,478.7,3767.1,478.7,3784.6,461.2z'/%3E%3Cpath class='st3' d='M4640,1.1h-329.8l-322.5,322.5c-17.5,17.5-17.5,46.1,0,63.6l260.2,260.2H4640L4640,1.1L4640,1.1z'/%3E%3Cpath class='st4' d='M3889,418.8l-228.7,228.7h521.1l-228.7-228.7C3935.2,401.3,3906.5,401.3,3889,418.8z'/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='3713.7576' y1='438.1175' x2='3911.4084' y2='14.2535' gradientTransform='matrix(1 0 0 -1 0 641.3969)'%3E%3Cstop offset='0' style='stop-color:%23FFFFFF;stop-opacity:0.5'/%3E%3Cstop offset='1' style='stop-color:%23FFFFFF'/%3E%3C/linearGradient%3E%3Cpath class='st5' d='M3952.7,124.5c-17.5-17.5-46.1-17.5-63.6,0l-523,523h1109.6L3952.7,124.5z'/%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-size: 100%;
background-position: right;
background-color: #55A0E0;
width: 100%;
font-size: 44px;
height: 120px;
color: white;
padding: 30px 0 40px 0px;
display: inline-block;
}
.header-icon {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20x%3D%220px%22%20y%3D%220px%22%0A%09%20viewBox%3D%220%200%20150.2%20125%22%20style%3D%22enable-background%3Anew%200%200%20150.2%20125%3B%22%20xml%3Aspace%3D%22preserve%22%3E%0A%3Cstyle%20type%3D%22text/css%22%3E%0A%09.st0%7Bfill%3Anone%3B%7D%0A%09.st1%7Bfill%3A%23FFFFFF%3B%7D%0A%3C/style%3E%0A%3Crect%20x%3D%220.5%22%20class%3D%22st0%22%20width%3D%22149.7%22%20height%3D%22125%22/%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M59%2C102.9L21.8%2C66c-3.5-3.5-3.5-9.1%2C0-12.5l37-36.5l2.9%2C3l-37%2C36.4c-1.8%2C1.8-1.8%2C4.7%2C0%2C6.6l37.2%2C37L59%2C102.9z%22%0A%09%09/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M92.5%2C102.9l-3-3l37.2-37c0.9-0.9%2C1.4-2%2C1.4-3.3c0-1.2-0.5-2.4-1.4-3.3L89.5%2C20l2.9-3l37.2%2C36.4%0A%09%09c1.7%2C1.7%2C2.6%2C3.9%2C2.6%2C6.3s-0.9%2C4.6-2.6%2C6.3L92.5%2C102.9z%22/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M90.1%2C68.4c-4.5%2C0-8-3.5-8-8.1c0-4.5%2C3.5-8.1%2C8-8.1c4.4%2C0%2C8%2C3.7%2C8%2C8.1C98.1%2C64.7%2C94.4%2C68.4%2C90.1%2C68.4z%0A%09%09%20M90.1%2C56.5c-2.2%2C0-3.8%2C1.7-3.8%2C3.9c0%2C2.2%2C1.7%2C3.9%2C3.8%2C3.9c1.9%2C0%2C3.8-1.6%2C3.8-3.9S91.9%2C56.5%2C90.1%2C56.5z%22/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M61.4%2C68.4c-4.5%2C0-8-3.5-8-8.1c0-4.5%2C3.5-8.1%2C8-8.1c4.4%2C0%2C8%2C3.7%2C8%2C8.1C69.5%2C64.7%2C65.8%2C68.4%2C61.4%2C68.4z%0A%09%09%20M61.4%2C56.5c-2.2%2C0-3.8%2C1.7-3.8%2C3.9c0%2C2.2%2C1.7%2C3.9%2C3.8%2C3.9c1.9%2C0%2C3.8-1.6%2C3.8-3.9S63.3%2C56.5%2C61.4%2C56.5z%22/%3E%0A%3C/g%3E%0A%3C/svg%3E%0A");
background-repeat: no-repeat;
float: left;
height: 140px;
width: 140px;
display: inline-block;
vertical-align: middle;
}
.header-text {
padding-left: 1%;
color: #FFFFFF;
font-family: "Segoe UI";
font-size: 72px;
font-weight: 300;
letter-spacing: 0.35px;
line-height: 96px;
display: inline-block;
vertical-align: middle;
}
.header-inner-container {
min-width: 480px;
max-width: 1366px;
margin-left: auto;
margin-right: auto;
vertical-align: middle;
}
.header-inner-container::after {
content: "";
clear: both;
display: table;
}
.main-content-area {
padding-left: 30px;
}
.content-title {
color: #000000;
font-family: "Segoe UI";
font-size: 46px;
font-weight: 300;
line-height: 62px;
}
.main-text {
color: #808080;
font-size: 24px;
font-family: "Segoe UI";
font-size: 24px;
font-weight: 200;
line-height: 32px;
}
.main-text-p1 {
padding-top: 48px;
padding-bottom: 28px;
}
.endpoint {
height: 32px;
width: 571px;
color: #808080;
font-family: "Segoe UI";
font-size: 24px;
font-weight: 200;
line-height: 32px;
padding-top: 28px;
}
.how-to-build-section {
padding-top: 20px;
padding-left: 30px;
}
.how-to-build-section>h3 {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.35px;
line-height: 22px;
margin: 0 0 24px 0;
text-transform: uppercase;
}
.step-container {
display: flex;
align-items: stretch;
position: relative;
}
.step-container dl {
border-left: 1px solid #A0A0A0;
display: block;
padding: 0 24px;
margin: 0;
}
.step-container dl>dt::before {
background-color: white;
border: 1px solid #A0A0A0;
border-radius: 100%;
content: '';
left: 47px;
height: 11px;
position: absolute;
width: 11px;
}
.step-container dl>.test-bullet::before {
background-color: blue;
}
.step-container dl>dt {
display: block;
font-size: inherit;
font-weight: bold;
line-height: 20px;
}
.step-container dl>dd {
font-size: inherit;
line-height: 20px;
margin-left: 0;
padding-bottom: 32px;
}
.step-container:last-child dl {
border-left: 1px solid transparent;
}
.ctaLink {
background-color: transparent;
border: 1px solid transparent;
color: #006AB1;
cursor: pointer;
font-weight: 600;
padding: 0;
white-space: normal;
}
.ctaLink:focus {
outline: 1px solid #00bcf2;
}
.ctaLink:hover {
text-decoration: underline;
}
.step-icon {
display: flex;
height: 38px;
margin-right: 15px;
width: 38px;
}
.step-icon>div {
height: 30px;
width: 30px;
background-repeat: no-repeat;
}
.ms-logo-container {
min-width: 580px;
max-width: 980px;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
transition: bottom 400ms;
}
.ms-logo {
float: right;
background-image: url("data:image/svg+xml;utf8,%0A%3Csvg%20version%3D%221.1%22%20id%3D%22MS-symbol%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20x%3D%220px%22%20y%3D%220px%22%0A%09%20viewBox%3D%220%200%20400%20120%22%20style%3D%22enable-background%3Anew%200%200%20400%20120%3B%22%20xml%3Aspace%3D%22preserve%22%3E%0A%3Cstyle%20type%3D%22text/css%22%3E%0A%09.st0%7Bfill%3Anone%3B%7D%0A%09.st1%7Bfill%3A%23737474%3B%7D%0A%09.st2%7Bfill%3A%23D63F26%3B%7D%0A%09.st3%7Bfill%3A%23167D3E%3B%7D%0A%09.st4%7Bfill%3A%232E76BC%3B%7D%0A%09.st5%7Bfill%3A%23FDB813%3B%7D%0A%3C/style%3E%0A%3Crect%20x%3D%220.6%22%20class%3D%22st0%22%20width%3D%22398.7%22%20height%3D%22119%22/%3E%0A%3Cpath%20class%3D%22st1%22%20d%3D%22M171.3%2C38.4v43.2h-7.5V47.7h-0.1l-13.4%2C33.9h-5l-13.7-33.9h-0.1v33.9h-6.9V38.4h10.8l12.4%2C32h0.2l13.1-32H171.3%0A%09z%20M177.6%2C41.7c0-1.2%2C0.4-2.2%2C1.3-3c0.9-0.8%2C1.9-1.2%2C3.1-1.2c1.3%2C0%2C2.4%2C0.4%2C3.2%2C1.3c0.8%2C0.8%2C1.3%2C1.8%2C1.3%2C3c0%2C1.2-0.4%2C2.2-1.3%2C3%0A%09c-0.9%2C0.8-1.9%2C1.2-3.2%2C1.2s-2.3-0.4-3.1-1.2C178%2C43.8%2C177.6%2C42.8%2C177.6%2C41.7z%20M185.7%2C50.6v31h-7.3v-31H185.7z%20M207.8%2C76.3%0A%09c1.1%2C0%2C2.3-0.3%2C3.6-0.8c1.3-0.5%2C2.5-1.2%2C3.6-2v6.8c-1.2%2C0.7-2.5%2C1.2-4%2C1.5c-1.5%2C0.3-3.1%2C0.5-4.9%2C0.5c-4.6%2C0-8.3-1.4-11.1-4.3%0A%09c-2.9-2.9-4.3-6.6-4.3-11c0-5%2C1.5-9.1%2C4.4-12.3c2.9-3.2%2C7-4.8%2C12.4-4.8c1.4%2C0%2C2.7%2C0.2%2C4.1%2C0.5c1.4%2C0.4%2C2.5%2C0.8%2C3.3%2C1.2v7%0A%09c-1.1-0.8-2.3-1.5-3.4-1.9c-1.2-0.5-2.4-0.7-3.6-0.7c-2.9%2C0-5.2%2C0.9-7%2C2.8c-1.8%2C1.9-2.7%2C4.4-2.7%2C7.6c0%2C3.1%2C0.8%2C5.6%2C2.5%2C7.3%0A%09C202.6%2C75.4%2C204.9%2C76.3%2C207.8%2C76.3z%20M235.7%2C50.1c0.6%2C0%2C1.1%2C0%2C1.6%2C0.1s0.9%2C0.2%2C1.2%2C0.3v7.4c-0.4-0.3-0.9-0.5-1.7-0.8%0A%09c-0.7-0.3-1.6-0.4-2.7-0.4c-1.8%2C0-3.3%2C0.8-4.5%2C2.3c-1.2%2C1.5-1.9%2C3.8-1.9%2C7v15.6h-7.3v-31h7.3v4.9h0.1c0.7-1.7%2C1.7-3%2C3-4%0A%09C232.2%2C50.6%2C233.8%2C50.1%2C235.7%2C50.1z%20M238.9%2C66.6c0-5.1%2C1.4-9.2%2C4.3-12.2c2.9-3%2C6.9-4.5%2C12.1-4.5c4.8%2C0%2C8.6%2C1.4%2C11.3%2C4.3%0A%09c2.7%2C2.9%2C4.1%2C6.8%2C4.1%2C11.7c0%2C5-1.4%2C9-4.3%2C12c-2.9%2C3-6.8%2C4.5-11.8%2C4.5c-4.8%2C0-8.6-1.4-11.4-4.2C240.3%2C75.3%2C238.9%2C71.4%2C238.9%2C66.6z%0A%09%20M246.5%2C66.3c0%2C3.2%2C0.7%2C5.7%2C2.2%2C7.4c1.5%2C1.7%2C3.6%2C2.6%2C6.3%2C2.6c2.7%2C0%2C4.7-0.9%2C6.1-2.6c1.4-1.7%2C2.1-4.2%2C2.1-7.6c0-3.3-0.7-5.8-2.2-7.5%0A%09c-1.4-1.7-3.4-2.5-6-2.5c-2.7%2C0-4.7%2C0.9-6.2%2C2.7C247.2%2C60.5%2C246.5%2C63%2C246.5%2C66.3z%20M281.5%2C58.8c0%2C1%2C0.3%2C1.9%2C1%2C2.5%0A%09c0.7%2C0.6%2C2.1%2C1.3%2C4.4%2C2.2c2.9%2C1.2%2C5%2C2.5%2C6.1%2C3.9c1.2%2C1.5%2C1.8%2C3.2%2C1.8%2C5.3c0%2C2.9-1.1%2C5.3-3.4%2C7c-2.2%2C1.8-5.3%2C2.7-9.1%2C2.7%0A%09c-1.3%2C0-2.7-0.2-4.3-0.5c-1.6-0.3-2.9-0.7-4-1.2v-7.2c1.3%2C0.9%2C2.8%2C1.7%2C4.3%2C2.2c1.5%2C0.5%2C2.9%2C0.8%2C4.2%2C0.8c1.6%2C0%2C2.9-0.2%2C3.6-0.7%0A%09c0.8-0.5%2C1.2-1.2%2C1.2-2.3c0-1-0.4-1.9-1.2-2.5c-0.8-0.7-2.4-1.5-4.6-2.4c-2.7-1.1-4.6-2.4-5.7-3.8c-1.1-1.4-1.7-3.2-1.7-5.4%0A%09c0-2.8%2C1.1-5.1%2C3.3-6.9c2.2-1.8%2C5.1-2.7%2C8.6-2.7c1.1%2C0%2C2.3%2C0.1%2C3.6%2C0.4c1.3%2C0.2%2C2.5%2C0.6%2C3.4%2C0.9v6.9c-1-0.6-2.1-1.2-3.4-1.7%0A%09c-1.3-0.5-2.6-0.7-3.8-0.7c-1.4%2C0-2.5%2C0.3-3.2%2C0.8C281.9%2C57.1%2C281.5%2C57.8%2C281.5%2C58.8z%20M297.9%2C66.6c0-5.1%2C1.4-9.2%2C4.3-12.2%0A%09c2.9-3%2C6.9-4.5%2C12.1-4.5c4.8%2C0%2C8.6%2C1.4%2C11.3%2C4.3c2.7%2C2.9%2C4.1%2C6.8%2C4.1%2C11.7c0%2C5-1.4%2C9-4.3%2C12c-2.9%2C3-6.8%2C4.5-11.8%2C4.5%0A%09c-4.8%2C0-8.6-1.4-11.4-4.2C299.4%2C75.3%2C297.9%2C71.4%2C297.9%2C66.6z%20M305.5%2C66.3c0%2C3.2%2C0.7%2C5.7%2C2.2%2C7.4c1.5%2C1.7%2C3.6%2C2.6%2C6.3%2C2.6%0A%09c2.7%2C0%2C4.7-0.9%2C6.1-2.6c1.4-1.7%2C2.1-4.2%2C2.1-7.6c0-3.3-0.7-5.8-2.2-7.5c-1.4-1.7-3.4-2.5-6-2.5c-2.7%2C0-4.7%2C0.9-6.2%2C2.7%0A%09C306.3%2C60.5%2C305.5%2C63%2C305.5%2C66.3z%20M353.9%2C56.6h-10.9v25h-7.4v-25h-5.2v-6h5.2v-4.3c0-3.3%2C1.1-5.9%2C3.2-8c2.1-2.1%2C4.8-3.1%2C8.1-3.1%0A%09c0.9%2C0%2C1.7%2C0%2C2.4%2C0.1c0.7%2C0.1%2C1.3%2C0.2%2C1.8%2C0.4V42c-0.2-0.1-0.7-0.3-1.3-0.5c-0.6-0.2-1.3-0.3-2.1-0.3c-1.5%2C0-2.7%2C0.5-3.5%2C1.4%0A%09s-1.2%2C2.4-1.2%2C4.2v3.7h10.9v-7l7.3-2.2v9.2h7.4v6h-7.4v14.5c0%2C1.9%2C0.3%2C3.3%2C1%2C4c0.7%2C0.8%2C1.8%2C1.2%2C3.3%2C1.2c0.4%2C0%2C0.9-0.1%2C1.5-0.3%0A%09c0.6-0.2%2C1.1-0.4%2C1.6-0.7v6c-0.5%2C0.3-1.2%2C0.5-2.3%2C0.7c-1.1%2C0.2-2.1%2C0.3-3.2%2C0.3c-3.1%2C0-5.4-0.8-6.9-2.5c-1.5-1.6-2.3-4.1-2.3-7.4%0A%09V56.6z%22/%3E%0A%3Cg%3E%0A%09%3Crect%20x%3D%2231%22%20y%3D%2224%22%20class%3D%22st2%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2268.8%22%20y%3D%2224%22%20class%3D%22st3%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2231%22%20y%3D%2261.8%22%20class%3D%22st4%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2268.8%22%20y%3D%2261.8%22%20class%3D%22st5%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%3C/g%3E%0A%3C/svg%3E%0A");
}
.ms-logo-container>div {
min-height: 60px;
width: 150px;
background-repeat: no-repeat;
}
.row {
padding: 90px 0px 0 20px;
min-width: 480px;
max-width: 1366px;
margin-left: auto;
margin-right: auto;
}
.column {
float: left;
width: 45%;
padding-right: 20px;
}
.row:after {
content: "";
display: table;
clear: both;
}
a {
text-decoration: none;
}
.download-the-emulator {
height: 20px;
color: #0063B1;
font-size: 15px;
line-height: 20px;
padding-bottom: 70px;
}
.how-to-iframe {
max-width: 700px !important;
min-width: 650px !important;
height: 700px !important;
}
.remove-frame-height {
height: 10px;
}
@media only screen and (max-width: 1300px) {
.ms-logo {
padding-top: 30px;
}
.header-text {
font-size: 40x;
}
.column {
float: none;
padding-top: 30px;
width: 100%;
}
.ms-logo-container {
padding-top: 30px;
min-width: 480px;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
.row {
padding: 20px 0px 0 20px;
min-width: 480px;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
}
@media only screen and (max-width: 1370px) {
header {
background-color: #55A0E0;
background-size: auto 200px;
}
}
@media only screen and (max-width: 1230px) {
header {
background-color: #55A0E0;
background-size: auto 200px;
}
.header-text {
font-size: 44px;
}
.header-icon {
height: 120px;
width: 120px;
}
}
@media only screen and (max-width: 1000px) {
header {
background-color: #55A0E0;
background-image: none;
}
}
@media only screen and (max-width: 632px) {
.header-text {
font-size: 32px;
}
.row {
padding: 10px 0px 0 10px;
max-width: 490px !important;
min-width: 410px !important;
}
.endpoint {
font-size: 25px;
}
.main-text {
font-size: 20px;
}
.step-container dl>dd {
font-size: 14px;
}
.column {
padding-right: 5px;
}
.header-icon {
height: 110px;
width: 110px;
}
.how-to-iframe {
max-width: 480px !important;
min-width: 400px !important;
height: 650px !important;
overflow: hidden;
}
}
.remove-frame-height {
max-height: 10px;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
loadFrame();
});
var loadFrame = function () {
var iframe = document.createElement('iframe');
iframe.setAttribute("id", "iframe");
var offLineHTMLContent = "";
var frameElement = document.getElementById("how-to-iframe");
if (window.navigator.onLine) {
iframe.src = 'https://docs.botframework.com/static/abs/pages/f5.htm';
iframe.setAttribute("scrolling", "no");
iframe.setAttribute("frameborder", "0");
iframe.setAttribute("width", "100%");
iframe.setAttribute("height", "100%");
var frameDiv = document.getElementById("how-to-iframe");
frameDiv.appendChild(iframe);
} else {
frameElement.classList.add("remove-frame-height");
}
};
</script>
</head>
<body>
<header class="header">
<div class="header-inner-container">
<div class="header-icon" style="display: inline-block"></div>
<div class="header-text" style="display: inline-block">TeamsSkillBot</div>
</div>
</header>
<div class="row">
<div class="column" class="main-content-area">
<div class="content-title">Your bot is ready!</div>
<div class="main-text main-text-p1">You can test your bot in the Bot Framework Emulator<br />
by connecting to http://localhost:39773/api/messages.</div>
<div class="main-text download-the-emulator"><a class="ctaLink"
href="https://aka.ms/bot-framework-F5-download-emulator" target="_blank">Download the Emulator</a>
</div>
<div class="main-text">Visit <a class="ctaLink" href="https://aka.ms/bot-framework-F5-abs-home"
target="_blank">Azure
Bot Service</a> to register your bot and add it to<br />
various channels. The bot's endpoint URL typically looks
like this:</div>
<div class="endpoint">https://<i>your_bots_hostname</i>/api/messages</div>
</div>
<div class="column how-to-iframe" id="how-to-iframe"></div>
</div>
<div class="ms-logo-container">
<div class="ms-logo"></div>
</div>
</body>
</html>

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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<string[]>();
_allowedCallers = appsList != null ? new List<string>(appsList) : null;
if (appsList == null)
{
throw new ArgumentNullException($"\"{ConfigKey}\" not found in configuration.");
}
_allowedCallers = new List<string>(appsList);
}
public override Task ValidateClaimsAsync(IList<Claim> 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))
{

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -5,7 +5,6 @@
<LangVersion>latest</LangVersion>
<AssemblyName>Microsoft.BotBuilderSamples.EchoSkillBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.EchoSkillBot</RootNamespace>
<UserSecretsId>d3e58f1c-0841-4154-8a6e-c4dfc5ae3edf</UserSecretsId>
</PropertyGroup>
<ItemGroup>
@ -16,4 +15,10 @@
<ProjectReference Include="..\..\..\..\libraries\integration\Microsoft.Bot.Builder.Integration.AspNet.Core\Microsoft.Bot.Builder.Integration.AspNet.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

Просмотреть файл

@ -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<Startup>();
});
}

Просмотреть файл

@ -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<BotFrameworkHttpAdapter> logger, ConversationState conversationState = null)
private readonly ILogger _logger;
public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger<BotFrameworkHttpAdapter> 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}");
}
}
}
}

Просмотреть файл

@ -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<ICredentialProvider, ConfigurationCredentialProvider>();
@ -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();
});
}
}
}

Просмотреть файл

@ -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": [ "*" ]
}

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -25,11 +25,11 @@ namespace Microsoft.BotBuilderSamples.SimpleRootBot
private readonly SkillHttpClient _skillClient;
private readonly SkillsConfiguration _skillsConfig;
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger, ConversationState conversationState = null, SkillHttpClient skillClient = null, SkillsConfiguration skillsConfig = null)
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> 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}");
}
}
}

Просмотреть файл

@ -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<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> 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);
}
}
}

Просмотреть файл

@ -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<Startup>();
});
}

Просмотреть файл

@ -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"
}

Просмотреть файл

@ -5,7 +5,6 @@
<LangVersion>latest</LangVersion>
<AssemblyName>Microsoft.BotBuilderSamples.SimpleRootBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.SimpleRootBot</RootNamespace>
<UserSecretsId>a52d1fa1-0d90-42d7-bbfc-2d776e8b7804</UserSecretsId>
</PropertyGroup>
<ItemGroup>
@ -16,4 +15,9 @@
<ProjectReference Include="..\..\..\..\libraries\integration\Microsoft.Bot.Builder.Integration.AspNet.Core\Microsoft.Bot.Builder.Integration.AspNet.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

Просмотреть файл

@ -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<ICredentialProvider, ConfigurationCredentialProvider>();
@ -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();
});
}
}
}

Просмотреть файл

@ -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}